-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Changes from 10 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 | ||
---|---|---|---|---|
|
@@ -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; | ||||
|
@@ -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 | ||||
|
@@ -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; | ||||
} | ||||
|
||||
|
@@ -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) { | ||||
|
@@ -332,6 +352,130 @@ 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; | ||||
|
||||
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') | ||||
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 = 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); | ||||
}); | ||||
} | ||||
}); | ||||
}; | ||||
|
||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
{ | ||
"data": [ | ||
{ | ||
"texttemplate": "%{z:.1f}", | ||
"textfont": {"size": 8}, | ||
"z": [ | ||
[ | ||
1, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
{ | ||
"data": [ | ||
{ | ||
"texttemplate": "%{z}", | ||
"z": [ | ||
[ | ||
1, | ||
|
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
When trying
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.
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.
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.