Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options to display text over heatmaps & histogram2d #6028

Merged
merged 16 commits into from
Dec 6, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)]
2 changes: 2 additions & 0 deletions src/traces/contour/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ module.exports = extendFlat({
yhoverformat: axisHoverFormat('y'),
zhoverformat: axisHoverFormat('z', 1),
hovertemplate: heatmapAttrs.hovertemplate,
texttemplate: heatmapAttrs.texttemplate,
textfont: heatmapAttrs.textfont,
hoverongaps: heatmapAttrs.hoverongaps,
connectgaps: extendFlat({}, heatmapAttrs.connectgaps, {
description: [
Expand Down
13 changes: 12 additions & 1 deletion src/traces/contour/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,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 +43,15 @@ 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'
) {
coerce('texttemplate');

var fontDflt = Lib.extendFlat({}, layout.font);
fontDflt.color = undefined; // color contrast by default
Lib.coerceFont(coerce, 'textfont', fontDflt);
}
};
11 changes: 11 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,15 @@ module.exports = extendFlat({
zhoverformat: axisHoverFormat('z', 1),

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

showlegend: extendFlat({}, baseAttrs.showlegend, {dflt: false})
}, {
transforms: undefined
Expand Down
5 changes: 5 additions & 0 deletions src/traces/heatmap/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('text');
coerce('hovertext');
coerce('hovertemplate');
coerce('texttemplate');

var fontDflt = Lib.extendFlat({}, layout.font);
fontDflt.color = undefined; // color contrast by default
Lib.coerceFont(coerce, 'textfont', fontDflt);

handleStyleDefaults(traceIn, traceOut, coerce, layout);

Expand Down
148 changes: 146 additions & 2 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 = '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 @@ -31,7 +49,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;

// 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 +130,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 +187,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 @@ -332,6 +352,130 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
y: top,
'xlink:href': canvas.toDataURL('image/png')
});

removeLabels(plotGroup);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle we should be able to do this with the d3 add/remove/update idiom, rather than removing and re-adding everything. The performance seems fine with all the mocks you added text to here, but I worry about it bogging down during interactions in more intense cases, like if we update https://dash.plotly.com/dash-bio/alignmentchart with this feature (try dragging the rangeslider there right now, it's terrible)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When trying

Plotly.relayout(gd, 'xaxis.rangeslider', {});
Plotly.restyle(gd, 'texttemplate', '%{z}');

e.g. on earth_heatmap the performance seems good.

So I suggest we move forward with this feature and come back to further optimize it if we hit a performance problem.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK - it is a little fiddly to get the add/remove/update idiom working properly, and anyway perhaps the higher-value addition to this feature would be a way to disable showing labels at all beyond a certain scale as you zoom out, which we have already decided not to do in this PR.


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;

textData.push({
t: _t,
x: _x,
y: _y,
z: zVal
});
}
}

var font = trace.textfont;
var xFn = function(d) { return d.x; };
var yFn = function(d) {
var nlines = d.t.split('<br>').length;
return d.y - font.size * ((nlines * LINE_SPACING) / 2 - 1);
};

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

labels
.enter()
.append('g')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need each text element wrapped in a separate group? Could we get away with one group with all the text elements inside it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This quite similar to the way we draw ticklabels:

tickLabels.enter().append('g')

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I think this is important for MathJax purposes, so I guess if we think supporting that in heatmap text is something we want to do in the future we can leave it.

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

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

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

Expand Down
3 changes: 3 additions & 0 deletions src/traces/histogram2d/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var heatmapAttrs = require('../heatmap/attributes');
var baseAttrs = require('../../plots/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 @@ -69,6 +70,8 @@ module.exports = extendFlat(
yhoverformat: axisHoverFormat('y'),
zhoverformat: axisHoverFormat('z', 1),
hovertemplate: hovertemplateAttrs({}, {keys: 'z'}),
texttemplate: texttemplateAttrs({editType: 'plot'}, {keys: 'z'}),
textfont: heatmapAttrs.textfont,
showlegend: extendFlat({}, baseAttrs.showlegend, {dflt: false})
},
colorScaleAttrs('', {cLetter: 'z', autoColorDflt: false})
Expand Down
6 changes: 6 additions & 0 deletions src/traces/histogram2d/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
handleStyleDefaults(traceIn, traceOut, coerce, layout);
colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'});
coerce('hovertemplate');
coerce('texttemplate');

var fontDflt = Lib.extendFlat({}, layout.font);
fontDflt.color = undefined; // color contrast by default
Lib.coerceFont(coerce, 'textfont', fontDflt);

coerce('xhoverformat');
coerce('yhoverformat');
};
4 changes: 3 additions & 1 deletion src/traces/histogram2dcontour/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ module.exports = extendFlat({
xhoverformat: axisHoverFormat('x'),
yhoverformat: axisHoverFormat('y'),
zhoverformat: axisHoverFormat('z', 1),
hovertemplate: histogram2dAttrs.hovertemplate
hovertemplate: histogram2dAttrs.hovertemplate,
texttemplate: histogram2dAttrs.texttemplate,
textfont: histogram2dAttrs.textfont
},
colorScaleAttrs('', {
cLetter: 'z',
Expand Down
12 changes: 11 additions & 1 deletion src/traces/histogram2dcontour/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout

handleContoursDefaults(traceIn, traceOut, coerce, coerce2);
handleStyleDefaults(traceIn, traceOut, coerce, layout);
coerce('hovertemplate');
coerce('xhoverformat');
coerce('yhoverformat');
coerce('hovertemplate');
if(
traceOut.contours &&
traceOut.contours.coloring === 'heatmap'
) {
coerce('texttemplate');

var fontDflt = Lib.extendFlat({}, layout.font);
fontDflt.color = undefined; // color contrast by default
Lib.coerceFont(coerce, 'textfont', fontDflt);
}
};
Binary file modified test/image/baselines/13.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/contour_heatmap_coloring_reversescale.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/contour_legend-colorscale.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/heatmap_categoryorder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/heatmap_columnar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/heatmap_xyz-gaps-on-sides.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/hist2d_summed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/histogram2d_bingroup-coloraxis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/histogram2d_bingroup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/histogram2d_legend.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/zsmooth_methods.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions test/image/mocks/13.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"data": [
{
"texttemplate": "%{z:.1f}",
"textfont": {"size": 8},
"z": [
[
1,
Expand Down
1 change: 1 addition & 0 deletions test/image/mocks/4.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"data": [
{
"texttemplate": "%{z}",
"z": [
[
1,
Expand Down
2 changes: 2 additions & 0 deletions test/image/mocks/contour_heatmap_coloring_reversescale.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"x": [ -9, -6, -5, -3, -1 ],
"y": [ 0, 1, 4, 5, 7 ],
"type": "contour",
"texttemplate": "%{z}",
"contours": {
"coloring": "heatmap"
},
Expand All @@ -33,6 +34,7 @@
"x": [ -9, -6, -5, -3, -1 ],
"y": [ 0, 1, 4, 5, 7 ],
"type": "contour",
"texttemplate": "%{z}",
"contours": {
"coloring": "heatmap"
},
Expand Down
7 changes: 7 additions & 0 deletions test/image/mocks/contour_legend-colorscale.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@
}
],
"layout": {
"template": {
"data": {
"contour": [{
"texttemplate": "%{z:.2s}"
}]
}
},
"title": {"text": "<b>contour legends with heatmap coloring</b><br>red-blue should be equal to reversed blue-red.<br>i.e. display identical legends for identical graphs"},
"margin": {
"t": 125,
Expand Down
4 changes: 4 additions & 0 deletions test/image/mocks/heatmap_categoryorder.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
},
"data": [{
"type": "heatmap",
"texttemplate": "x: %{x}<br>y: %{y}<br>z: %{z}",
"textfont": {
"size": 14
},

"x": ["z", "y", "x", "w"],
"y": ["d", "c", "b", "a"],
Expand Down
8 changes: 7 additions & 1 deletion test/image/mocks/heatmap_columnar.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
"type": "heatmap",
"x": ["a", "a", "a", "b", "b", "b", "c", "c", "c"],
"y": ["A", "B", "C", "A", "B", "C", "A", "B", "C"],
"z": [0, 50, 100, 50, 0, 255, 100, 510, 1010]
"z": [0, 50, 100, 50, 0, 255, 100, 510, 1010],
"text": ["zero", "fifty", "one hundred", "fifty", "zero", "two hundred<br>and fifty-five", "one hundred", "five hundred<br>and ten", "one thousand ten"],
"texttemplate": "%{text}",
"textfont": {
"size": 20,
"color": "rgba(255,255,255,0.5)"
}
}],
"layout": {
"xaxis": {
Expand Down
Loading