-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add options to display text over heatmaps & histogram2d #6028
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
Changes from all commits
222ed47
810563f
b8a4f3a
b52adf2
7aa3e04
ce90ced
184d7e7
5e9520d
db7ecc9
5562418
2406789
e8f3287
304e214
6e1560b
51de82b
e301f5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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)] |
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); | ||
}; |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -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; | ||||
|
@@ -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; | ||||
|
@@ -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 | ||||
|
@@ -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; | ||||
} | ||||
|
||||
|
@@ -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) { | ||||
|
@@ -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); | ||||
|
||||
|
@@ -332,6 +352,185 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) { | |||
y: top, | ||||
'xlink:href': canvas.toDataURL('image/png') | ||||
}); | ||||
|
||||
removeLabels(plotGroup); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 So I suggest we move forward with this feature and come back to further optimize it if we hit a performance problem. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||
|
||||
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') | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This quite similar to the way we draw plotly.js/src/plots/cartesian/axes.js Line 2980 in 10d3930
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 === '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); | ||||
}); | ||||
} | ||||
}); | ||||
}; | ||||
|
||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this restriction? Not a big deal, probably won't get a lot of use in contour maps anyway. But the only issue I can think of is what autocolor to give the text - for
coloring='fill'
it should work pretty well with the same logic as heatmap, and for othercoloring
values we'd just contrast with theplot_bgcolor
. No?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree we may try to add this option for other
coloring
values in another PR.But that would require extra work as those use different
plot
paths.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A contour plot with a
coloring
other than heatmap has different plotting path. So enabling this feature requires extra work. So it could potentially be added later on the road.