Skip to content

Commit

Permalink
Merge pull request #6028 from plotly/heatmap-with-text
Browse files Browse the repository at this point in the history
Add options to display text over heatmaps & histogram2d
  • Loading branch information
archmoj authored Dec 6, 2021
2 parents 10d3930 + e301f5b commit 285b923
Show file tree
Hide file tree
Showing 39 changed files with 445 additions and 8 deletions.
2 changes: 2 additions & 0 deletions draftlogs/6028_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Add `texttemplate` and `textfont` to `heatmap` and `histogram2d` traces as well as
`histogram2dcontour` and `contour` traces when `coloring` is set "heatmap" [[#6028](https://github.com/plotly/plotly.js/pull/6028)]
3 changes: 3 additions & 0 deletions src/plots/font_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ module.exports = function(opts) {
description: '' + (opts.description || '') + ''
};

if(opts.autoSize) attrs.size.dflt = 'auto';
if(opts.autoColor) attrs.color.dflt = 'auto';

if(opts.arrayOk) {
attrs.family.arrayOk = true;
attrs.size.arrayOk = true;
Expand Down
12 changes: 12 additions & 0 deletions src/traces/contour/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ module.exports = extendFlat({
yhoverformat: axisHoverFormat('y'),
zhoverformat: axisHoverFormat('z', 1),
hovertemplate: heatmapAttrs.hovertemplate,
texttemplate: extendFlat({}, heatmapAttrs.texttemplate, {
description: [
'For this trace it only has an effect if `coloring` is set to *heatmap*.',
heatmapAttrs.texttemplate.description
].join(' ')
}),
textfont: extendFlat({}, heatmapAttrs.textfont, {
description: [
'For this trace it only has an effect if `coloring` is set to *heatmap*.',
heatmapAttrs.textfont.description
].join(' ')
}),
hoverongaps: heatmapAttrs.hoverongaps,
connectgaps: extendFlat({}, heatmapAttrs.connectgaps, {
description: [
Expand Down
10 changes: 9 additions & 1 deletion src/traces/contour/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var handlePeriodDefaults = require('../scatter/period_defaults');
var handleConstraintDefaults = require('./constraint_defaults');
var handleContoursDefaults = require('./contours_defaults');
var handleStyleDefaults = require('./style_defaults');
var handleHeatmapLabelDefaults = require('../heatmap/label_defaults');
var attributes = require('./attributes');


Expand All @@ -31,8 +32,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout

coerce('text');
coerce('hovertext');
coerce('hovertemplate');
coerce('hoverongaps');
coerce('hovertemplate');

var isConstraint = (coerce('contours.type') === 'constraint');
coerce('connectgaps', Lib.isArray1D(traceOut.z));
Expand All @@ -43,4 +44,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
handleContoursDefaults(traceIn, traceOut, coerce, coerce2);
handleStyleDefaults(traceIn, traceOut, coerce, layout);
}

if(
traceOut.contours &&
traceOut.contours.coloring === 'heatmap'
) {
handleHeatmapLabelDefaults(coerce, layout);
}
};
16 changes: 16 additions & 0 deletions src/traces/heatmap/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

var scatterAttrs = require('../scatter/attributes');
var baseAttrs = require('../../plots/attributes');
var fontAttrs = require('../../plots/font_attributes');
var axisHoverFormat = require('../../plots/cartesian/axis_format_attributes').axisHoverFormat;
var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs;
var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs;
var colorScaleAttrs = require('../../components/colorscale/attributes');

var extendFlat = require('../../lib/extend').extendFlat;
Expand Down Expand Up @@ -116,6 +118,20 @@ module.exports = extendFlat({
zhoverformat: axisHoverFormat('z', 1),

hovertemplate: hovertemplateAttrs(),
texttemplate: texttemplateAttrs({
arrayOk: false,
editType: 'plot'
}, {
keys: ['x', 'y', 'z', 'text']
}),
textfont: fontAttrs({
editType: 'plot',
autoSize: true,
autoColor: true,
colorEditType: 'style',
description: 'Sets the text font.'
}),

showlegend: extendFlat({}, baseAttrs.showlegend, {dflt: false})
}, {
transforms: undefined
Expand Down
2 changes: 2 additions & 0 deletions src/traces/heatmap/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
var Lib = require('../../lib');

var handleXYZDefaults = require('./xyz_defaults');
var handleHeatmapLabelDefaults = require('./label_defaults');
var handlePeriodDefaults = require('../scatter/period_defaults');
var handleStyleDefaults = require('./style_defaults');
var colorscaleDefaults = require('../../components/colorscale/defaults');
Expand All @@ -28,6 +29,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('hovertext');
coerce('hovertemplate');

handleHeatmapLabelDefaults(coerce, layout);
handleStyleDefaults(traceIn, traceOut, coerce, layout);

coerce('hoverongaps');
Expand Down
13 changes: 13 additions & 0 deletions src/traces/heatmap/label_defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';

var Lib = require('../../lib');

module.exports = function handleHeatmapLabelDefaults(coerce, layout) {
coerce('texttemplate');

var fontDflt = Lib.extendFlat({}, layout.font, {
color: 'auto',
size: 'auto'
});
Lib.coerceFont(coerce, 'textfont', fontDflt);
};
207 changes: 203 additions & 4 deletions src/traces/heatmap/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,27 @@ var d3 = require('@plotly/d3');
var tinycolor = require('tinycolor2');

var Registry = require('../../registry');
var Drawing = require('../../components/drawing');
var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
var svgTextUtils = require('../../lib/svg_text_utils');
var formatLabels = require('../scatter/format_labels');
var Color = require('../../components/color');
var extractOpts = require('../../components/colorscale').extractOpts;
var makeColorScaleFuncFromTrace = require('../../components/colorscale').makeColorScaleFuncFromTrace;
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
var alignmentConstants = require('../../constants/alignment');
var LINE_SPACING = alignmentConstants.LINE_SPACING;

var labelClass = 'heatmap-label';

function selectLabels(plotGroup) {
return plotGroup.selectAll('g.' + labelClass);
}

function removeLabels(plotGroup) {
selectLabels(plotGroup).remove();
}

module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
var xa = plotinfo.xaxis;
Expand All @@ -16,6 +34,8 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
var plotGroup = d3.select(this);
var cd0 = cd[0];
var trace = cd0.trace;
var xGap = trace.xgap || 0;
var yGap = trace.ygap || 0;

var z = cd0.z;
var x = cd0.x;
Expand All @@ -31,7 +51,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
var xrev = false;
var yrev = false;

var left, right, temp, top, bottom, i;
var left, right, temp, top, bottom, i, j, k;

// TODO: if there are multiple overlapping categorical heatmaps,
// or if we allow category sorting, then the categories may not be
Expand Down Expand Up @@ -112,6 +132,8 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
if(isOffScreen) {
var noImage = plotGroup.selectAll('image').data([]);
noImage.exit().remove();

removeLabels(plotGroup);
return;
}

Expand Down Expand Up @@ -167,7 +189,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
var gcount = 0;
var bcount = 0;

var xb, j, xi, v, row, c;
var xb, xi, v, row, c;

function setColor(v, pixsize) {
if(v !== undefined) {
Expand Down Expand Up @@ -278,8 +300,6 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
} else { // zsmooth = false -> filling potentially large bricks works fastest with fillRect
// gaps do not need to be exact integers, but if they *are* we will get
// cleaner edges by rounding at least one edge
var xGap = trace.xgap;
var yGap = trace.ygap;
var xGapLeft = Math.floor(xGap / 2);
var yGapTop = Math.floor(yGap / 2);

Expand Down Expand Up @@ -332,6 +352,185 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
y: top,
'xlink:href': canvas.toDataURL('image/png')
});

removeLabels(plotGroup);

var texttemplate = trace.texttemplate;
if(texttemplate) {
// dummy axis for formatting the z value
var cOpts = extractOpts(trace);
var dummyAx = {
type: 'linear',
range: [cOpts.min, cOpts.max],
_separators: xa._separators,
_numFormat: xa._numFormat
};

var aHistogram2dContour = trace.type === 'histogram2dcontour';
var aContour = trace.type === 'contour';
var iStart = aContour ? 1 : 0;
var iStop = aContour ? m - 1 : m;
var jStart = aContour ? 1 : 0;
var jStop = aContour ? n - 1 : n;

var textData = [];
for(i = iStart; i < iStop; i++) {
var yVal;
if(aContour) {
yVal = cd0.y[i];
} else if(aHistogram2dContour) {
if(i === 0 || i === m - 1) continue;
yVal = cd0.y[i];
} else if(cd0.yCenter) {
yVal = cd0.yCenter[i];
} else {
if(i + 1 === m && cd0.y[i + 1] === undefined) continue;
yVal = (cd0.y[i] + cd0.y[i + 1]) / 2;
}

var _y = Math.round(ya.c2p(yVal));
if(0 > _y || _y > ya._length) continue;

for(j = jStart; j < jStop; j++) {
var xVal;
if(aContour) {
xVal = cd0.x[j];
} else if(aHistogram2dContour) {
if(j === 0 || j === n - 1) continue;
xVal = cd0.x[j];
} else if(cd0.xCenter) {
xVal = cd0.xCenter[j];
} else {
if(j + 1 === n && cd0.x[j + 1] === undefined) continue;
xVal = (cd0.x[j] + cd0.x[j + 1]) / 2;
}

var _x = Math.round(xa.c2p(xVal));
if(0 > _x || _x > xa._length) continue;

var obj = formatLabels({
x: xVal,
y: yVal
}, trace, gd._fullLayout);

obj.x = xVal;
obj.y = yVal;

var zVal = cd0.z[i][j];
if(zVal === undefined) {
obj.z = '';
obj.zLabel = '';
} else {
obj.z = zVal;
obj.zLabel = Axes.tickText(dummyAx, zVal, 'hover').text;
}

var theText = cd0.text && cd0.text[i] && cd0.text[i][j];
if(theText === undefined || theText === false) theText = '';
obj.text = theText;

var _t = Lib.texttemplateString(texttemplate, obj, gd._fullLayout._d3locale, obj, trace._meta || {});
if(!_t) continue;

var lines = _t.split('<br>');
var nL = lines.length;
var nC = 0;
for(k = 0; k < nL; k++) {
nC = Math.max(nC, lines[k].length);
}

textData.push({
l: nL, // number of lines
c: nC, // maximum number of chars in a line
t: _t, // text
x: _x,
y: _y,
z: zVal
});
}
}

var font = trace.textfont;
var fontFamily = font.family;
var fontSize = font.size;

if(!fontSize || fontSize === 'auto') {
var minW = Infinity;
var minH = Infinity;
var maxL = 0;
var maxC = 0;

for(k = 0; k < textData.length; k++) {
var d = textData[k];
maxL = Math.max(maxL, d.l);
maxC = Math.max(maxC, d.c);

if(k < textData.length - 1) {
var nextD = textData[k + 1];
var dx = Math.abs(nextD.x - d.x);
var dy = Math.abs(nextD.y - d.y);

if(dx) minW = Math.min(minW, dx);
if(dy) minH = Math.min(minH, dy);
}
}

if(
!isFinite(minW) ||
!isFinite(minH)
) {
fontSize = 12;
} else {
minW -= xGap;
minH -= yGap;

minW /= maxC;
minH /= maxL;

minW /= LINE_SPACING / 2;
minH /= LINE_SPACING;

fontSize = Math.min(
Math.floor(minW),
Math.floor(minH)
);
}
}
if(fontSize <= 0 || !isFinite(fontSize)) return;

var xFn = function(d) { return d.x; };
var yFn = function(d) {
return d.y - fontSize * ((d.l * LINE_SPACING) / 2 - 1);
};

var labels = selectLabels(plotGroup).data(textData);

labels
.enter()
.append('g')
.classed(labelClass, 1)
.append('text')
.attr('text-anchor', 'middle')
.each(function(d) {
var thisLabel = d3.select(this);

var fontColor = font.color;
if(!fontColor || fontColor === 'auto') {
fontColor = Color.contrast(
'rgba(' +
sclFunc(d.z).join() +
')'
);
}

thisLabel
.attr('data-notex', 1)
.call(svgTextUtils.positionText, xFn(d), yFn(d))
.call(Drawing.font, fontFamily, fontSize, fontColor)
.text(d.t)
.call(svgTextUtils.convertToTspans, gd);
});
}
});
};

Expand Down
Loading

0 comments on commit 285b923

Please sign in to comment.