Skip to content

Commit 285b923

Browse files
authored
Merge pull request #6028 from plotly/heatmap-with-text
Add options to display text over heatmaps & histogram2d
2 parents 10d3930 + e301f5b commit 285b923

39 files changed

+445
-8
lines changed

Diff for: draftlogs/6028_add.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Add `texttemplate` and `textfont` to `heatmap` and `histogram2d` traces as well as
2+
`histogram2dcontour` and `contour` traces when `coloring` is set "heatmap" [[#6028](https://github.com/plotly/plotly.js/pull/6028)]

Diff for: src/plots/font_attributes.js

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ module.exports = function(opts) {
5353
description: '' + (opts.description || '') + ''
5454
};
5555

56+
if(opts.autoSize) attrs.size.dflt = 'auto';
57+
if(opts.autoColor) attrs.color.dflt = 'auto';
58+
5659
if(opts.arrayOk) {
5760
attrs.family.arrayOk = true;
5861
attrs.size.arrayOk = true;

Diff for: src/traces/contour/attributes.js

+12
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ module.exports = extendFlat({
4242
yhoverformat: axisHoverFormat('y'),
4343
zhoverformat: axisHoverFormat('z', 1),
4444
hovertemplate: heatmapAttrs.hovertemplate,
45+
texttemplate: extendFlat({}, heatmapAttrs.texttemplate, {
46+
description: [
47+
'For this trace it only has an effect if `coloring` is set to *heatmap*.',
48+
heatmapAttrs.texttemplate.description
49+
].join(' ')
50+
}),
51+
textfont: extendFlat({}, heatmapAttrs.textfont, {
52+
description: [
53+
'For this trace it only has an effect if `coloring` is set to *heatmap*.',
54+
heatmapAttrs.textfont.description
55+
].join(' ')
56+
}),
4557
hoverongaps: heatmapAttrs.hoverongaps,
4658
connectgaps: extendFlat({}, heatmapAttrs.connectgaps, {
4759
description: [

Diff for: src/traces/contour/defaults.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var handlePeriodDefaults = require('../scatter/period_defaults');
77
var handleConstraintDefaults = require('./constraint_defaults');
88
var handleContoursDefaults = require('./contours_defaults');
99
var handleStyleDefaults = require('./style_defaults');
10+
var handleHeatmapLabelDefaults = require('../heatmap/label_defaults');
1011
var attributes = require('./attributes');
1112

1213

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

3233
coerce('text');
3334
coerce('hovertext');
34-
coerce('hovertemplate');
3535
coerce('hoverongaps');
36+
coerce('hovertemplate');
3637

3738
var isConstraint = (coerce('contours.type') === 'constraint');
3839
coerce('connectgaps', Lib.isArray1D(traceOut.z));
@@ -43,4 +44,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
4344
handleContoursDefaults(traceIn, traceOut, coerce, coerce2);
4445
handleStyleDefaults(traceIn, traceOut, coerce, layout);
4546
}
47+
48+
if(
49+
traceOut.contours &&
50+
traceOut.contours.coloring === 'heatmap'
51+
) {
52+
handleHeatmapLabelDefaults(coerce, layout);
53+
}
4654
};

Diff for: src/traces/heatmap/attributes.js

+16
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
var scatterAttrs = require('../scatter/attributes');
44
var baseAttrs = require('../../plots/attributes');
5+
var fontAttrs = require('../../plots/font_attributes');
56
var axisHoverFormat = require('../../plots/cartesian/axis_format_attributes').axisHoverFormat;
67
var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs;
8+
var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs;
79
var colorScaleAttrs = require('../../components/colorscale/attributes');
810

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

118120
hovertemplate: hovertemplateAttrs(),
121+
texttemplate: texttemplateAttrs({
122+
arrayOk: false,
123+
editType: 'plot'
124+
}, {
125+
keys: ['x', 'y', 'z', 'text']
126+
}),
127+
textfont: fontAttrs({
128+
editType: 'plot',
129+
autoSize: true,
130+
autoColor: true,
131+
colorEditType: 'style',
132+
description: 'Sets the text font.'
133+
}),
134+
119135
showlegend: extendFlat({}, baseAttrs.showlegend, {dflt: false})
120136
}, {
121137
transforms: undefined

Diff for: src/traces/heatmap/defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
var Lib = require('../../lib');
44

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

32+
handleHeatmapLabelDefaults(coerce, layout);
3133
handleStyleDefaults(traceIn, traceOut, coerce, layout);
3234

3335
coerce('hoverongaps');

Diff for: src/traces/heatmap/label_defaults.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
var Lib = require('../../lib');
4+
5+
module.exports = function handleHeatmapLabelDefaults(coerce, layout) {
6+
coerce('texttemplate');
7+
8+
var fontDflt = Lib.extendFlat({}, layout.font, {
9+
color: 'auto',
10+
size: 'auto'
11+
});
12+
Lib.coerceFont(coerce, 'textfont', fontDflt);
13+
};

Diff for: src/traces/heatmap/plot.js

+203-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,27 @@ var d3 = require('@plotly/d3');
44
var tinycolor = require('tinycolor2');
55

66
var Registry = require('../../registry');
7+
var Drawing = require('../../components/drawing');
8+
var Axes = require('../../plots/cartesian/axes');
79
var Lib = require('../../lib');
10+
var svgTextUtils = require('../../lib/svg_text_utils');
11+
var formatLabels = require('../scatter/format_labels');
12+
var Color = require('../../components/color');
13+
var extractOpts = require('../../components/colorscale').extractOpts;
814
var makeColorScaleFuncFromTrace = require('../../components/colorscale').makeColorScaleFuncFromTrace;
915
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
16+
var alignmentConstants = require('../../constants/alignment');
17+
var LINE_SPACING = alignmentConstants.LINE_SPACING;
18+
19+
var labelClass = 'heatmap-label';
20+
21+
function selectLabels(plotGroup) {
22+
return plotGroup.selectAll('g.' + labelClass);
23+
}
24+
25+
function removeLabels(plotGroup) {
26+
selectLabels(plotGroup).remove();
27+
}
1028

1129
module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
1230
var xa = plotinfo.xaxis;
@@ -16,6 +34,8 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
1634
var plotGroup = d3.select(this);
1735
var cd0 = cd[0];
1836
var trace = cd0.trace;
37+
var xGap = trace.xgap || 0;
38+
var yGap = trace.ygap || 0;
1939

2040
var z = cd0.z;
2141
var x = cd0.x;
@@ -31,7 +51,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
3151
var xrev = false;
3252
var yrev = false;
3353

34-
var left, right, temp, top, bottom, i;
54+
var left, right, temp, top, bottom, i, j, k;
3555

3656
// TODO: if there are multiple overlapping categorical heatmaps,
3757
// or if we allow category sorting, then the categories may not be
@@ -112,6 +132,8 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
112132
if(isOffScreen) {
113133
var noImage = plotGroup.selectAll('image').data([]);
114134
noImage.exit().remove();
135+
136+
removeLabels(plotGroup);
115137
return;
116138
}
117139

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

170-
var xb, j, xi, v, row, c;
192+
var xb, xi, v, row, c;
171193

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

@@ -332,6 +352,185 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
332352
y: top,
333353
'xlink:href': canvas.toDataURL('image/png')
334354
});
355+
356+
removeLabels(plotGroup);
357+
358+
var texttemplate = trace.texttemplate;
359+
if(texttemplate) {
360+
// dummy axis for formatting the z value
361+
var cOpts = extractOpts(trace);
362+
var dummyAx = {
363+
type: 'linear',
364+
range: [cOpts.min, cOpts.max],
365+
_separators: xa._separators,
366+
_numFormat: xa._numFormat
367+
};
368+
369+
var aHistogram2dContour = trace.type === 'histogram2dcontour';
370+
var aContour = trace.type === 'contour';
371+
var iStart = aContour ? 1 : 0;
372+
var iStop = aContour ? m - 1 : m;
373+
var jStart = aContour ? 1 : 0;
374+
var jStop = aContour ? n - 1 : n;
375+
376+
var textData = [];
377+
for(i = iStart; i < iStop; i++) {
378+
var yVal;
379+
if(aContour) {
380+
yVal = cd0.y[i];
381+
} else if(aHistogram2dContour) {
382+
if(i === 0 || i === m - 1) continue;
383+
yVal = cd0.y[i];
384+
} else if(cd0.yCenter) {
385+
yVal = cd0.yCenter[i];
386+
} else {
387+
if(i + 1 === m && cd0.y[i + 1] === undefined) continue;
388+
yVal = (cd0.y[i] + cd0.y[i + 1]) / 2;
389+
}
390+
391+
var _y = Math.round(ya.c2p(yVal));
392+
if(0 > _y || _y > ya._length) continue;
393+
394+
for(j = jStart; j < jStop; j++) {
395+
var xVal;
396+
if(aContour) {
397+
xVal = cd0.x[j];
398+
} else if(aHistogram2dContour) {
399+
if(j === 0 || j === n - 1) continue;
400+
xVal = cd0.x[j];
401+
} else if(cd0.xCenter) {
402+
xVal = cd0.xCenter[j];
403+
} else {
404+
if(j + 1 === n && cd0.x[j + 1] === undefined) continue;
405+
xVal = (cd0.x[j] + cd0.x[j + 1]) / 2;
406+
}
407+
408+
var _x = Math.round(xa.c2p(xVal));
409+
if(0 > _x || _x > xa._length) continue;
410+
411+
var obj = formatLabels({
412+
x: xVal,
413+
y: yVal
414+
}, trace, gd._fullLayout);
415+
416+
obj.x = xVal;
417+
obj.y = yVal;
418+
419+
var zVal = cd0.z[i][j];
420+
if(zVal === undefined) {
421+
obj.z = '';
422+
obj.zLabel = '';
423+
} else {
424+
obj.z = zVal;
425+
obj.zLabel = Axes.tickText(dummyAx, zVal, 'hover').text;
426+
}
427+
428+
var theText = cd0.text && cd0.text[i] && cd0.text[i][j];
429+
if(theText === undefined || theText === false) theText = '';
430+
obj.text = theText;
431+
432+
var _t = Lib.texttemplateString(texttemplate, obj, gd._fullLayout._d3locale, obj, trace._meta || {});
433+
if(!_t) continue;
434+
435+
var lines = _t.split('<br>');
436+
var nL = lines.length;
437+
var nC = 0;
438+
for(k = 0; k < nL; k++) {
439+
nC = Math.max(nC, lines[k].length);
440+
}
441+
442+
textData.push({
443+
l: nL, // number of lines
444+
c: nC, // maximum number of chars in a line
445+
t: _t, // text
446+
x: _x,
447+
y: _y,
448+
z: zVal
449+
});
450+
}
451+
}
452+
453+
var font = trace.textfont;
454+
var fontFamily = font.family;
455+
var fontSize = font.size;
456+
457+
if(!fontSize || fontSize === 'auto') {
458+
var minW = Infinity;
459+
var minH = Infinity;
460+
var maxL = 0;
461+
var maxC = 0;
462+
463+
for(k = 0; k < textData.length; k++) {
464+
var d = textData[k];
465+
maxL = Math.max(maxL, d.l);
466+
maxC = Math.max(maxC, d.c);
467+
468+
if(k < textData.length - 1) {
469+
var nextD = textData[k + 1];
470+
var dx = Math.abs(nextD.x - d.x);
471+
var dy = Math.abs(nextD.y - d.y);
472+
473+
if(dx) minW = Math.min(minW, dx);
474+
if(dy) minH = Math.min(minH, dy);
475+
}
476+
}
477+
478+
if(
479+
!isFinite(minW) ||
480+
!isFinite(minH)
481+
) {
482+
fontSize = 12;
483+
} else {
484+
minW -= xGap;
485+
minH -= yGap;
486+
487+
minW /= maxC;
488+
minH /= maxL;
489+
490+
minW /= LINE_SPACING / 2;
491+
minH /= LINE_SPACING;
492+
493+
fontSize = Math.min(
494+
Math.floor(minW),
495+
Math.floor(minH)
496+
);
497+
}
498+
}
499+
if(fontSize <= 0 || !isFinite(fontSize)) return;
500+
501+
var xFn = function(d) { return d.x; };
502+
var yFn = function(d) {
503+
return d.y - fontSize * ((d.l * LINE_SPACING) / 2 - 1);
504+
};
505+
506+
var labels = selectLabels(plotGroup).data(textData);
507+
508+
labels
509+
.enter()
510+
.append('g')
511+
.classed(labelClass, 1)
512+
.append('text')
513+
.attr('text-anchor', 'middle')
514+
.each(function(d) {
515+
var thisLabel = d3.select(this);
516+
517+
var fontColor = font.color;
518+
if(!fontColor || fontColor === 'auto') {
519+
fontColor = Color.contrast(
520+
'rgba(' +
521+
sclFunc(d.z).join() +
522+
')'
523+
);
524+
}
525+
526+
thisLabel
527+
.attr('data-notex', 1)
528+
.call(svgTextUtils.positionText, xFn(d), yFn(d))
529+
.call(Drawing.font, fontFamily, fontSize, fontColor)
530+
.text(d.t)
531+
.call(svgTextUtils.convertToTspans, gd);
532+
});
533+
}
335534
});
336535
};
337536

0 commit comments

Comments
 (0)