Skip to content

Commit 44aa70e

Browse files
committed
extend Drawing.gradient and use it for contour legends
1 parent 41d870b commit 44aa70e

File tree

7 files changed

+157
-47
lines changed

7 files changed

+157
-47
lines changed

src/components/drawing/index.js

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -266,43 +266,76 @@ function makePointPath(symbolNumber, r) {
266266

267267
var HORZGRADIENT = {x1: 1, x2: 0, y1: 0, y2: 0};
268268
var VERTGRADIENT = {x1: 0, x2: 0, y1: 1, y2: 0};
269+
var stopFormatter = d3.format('~.1f');
270+
var gradientInfo = {
271+
radial: {node: 'radialGradient'},
272+
radialreversed: {node: 'radialGradient', reversed: true},
273+
horizontal: {node: 'linearGradient', attrs: HORZGRADIENT},
274+
horizontalreversed: {node: 'linearGradient', attrs: HORZGRADIENT, reversed: true},
275+
vertical: {node: 'linearGradient', attrs: VERTGRADIENT},
276+
verticalreversed: {node: 'linearGradient', attrs: VERTGRADIENT, reversed: true}
277+
};
278+
279+
/**
280+
* gradient: create and apply a gradient fill
281+
*
282+
* @param {object} sel: d3 selection to apply this gradient to
283+
* You can use `selection.call(Drawing.gradient, ...)`
284+
* @param {DOM element} gd: the graph div `sel` is part of
285+
* @param {string} gradientID: a unique (within this plot) identifier
286+
* for this gradient, so that we don't create unnecessary definitions
287+
* @param {string} type: 'radial', 'horizontal', or 'vertical', optionally with
288+
* 'reversed' at the end. Normally radial goes center to edge,
289+
* horizontal goes right to left, and vertical goes bottom to top
290+
* @param {array} colorscale: as in attribute values, [[fraction, color], ...]
291+
* @param {string} prop: the property to apply to, 'fill' or 'stroke'
292+
*/
293+
drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) {
294+
var len = colorscale.length;
295+
var info = gradientInfo[type];
296+
var colorStops = new Array(len);
297+
for(var i = 0; i < len; i++) {
298+
if(info.reversed) {
299+
colorStops[len - 1 - i] = [stopFormatter((1 - colorscale[i][0]) * 100), colorscale[i][1]];
300+
}
301+
else {
302+
colorStops[i] = [stopFormatter(colorscale[i][0] * 100), colorscale[i][1]];
303+
}
304+
}
305+
306+
var fullID = 'g' + gd._fullLayout._uid + '-' + gradientID;
269307

270-
drawing.gradient = function(sel, gd, gradientID, type, color1, color2) {
271308
var gradient = gd._fullLayout._defs.select('.gradients')
272-
.selectAll('#' + gradientID)
273-
.data([type + color1 + color2], Lib.identity);
309+
.selectAll('#' + fullID)
310+
.data([type + colorStops.join(';')], Lib.identity);
274311

275312
gradient.exit().remove();
276313

277314
gradient.enter()
278-
.append(type === 'radial' ? 'radialGradient' : 'linearGradient')
315+
.append(info.node)
279316
.each(function() {
280317
var el = d3.select(this);
281-
if(type === 'horizontal') el.attr(HORZGRADIENT);
282-
else if(type === 'vertical') el.attr(VERTGRADIENT);
283-
284-
el.attr('id', gradientID);
285-
286-
var tc1 = tinycolor(color1);
287-
var tc2 = tinycolor(color2);
288-
289-
el.append('stop').attr({
290-
offset: '0%',
291-
'stop-color': Color.tinyRGB(tc2),
292-
'stop-opacity': tc2.getAlpha()
293-
});
294-
295-
el.append('stop').attr({
296-
offset: '100%',
297-
'stop-color': Color.tinyRGB(tc1),
298-
'stop-opacity': tc1.getAlpha()
318+
if(info.attrs) el.attr(info.attrs);
319+
320+
el.attr('id', fullID);
321+
322+
var stops = el.selectAll('stop')
323+
.data(colorStops);
324+
stops.exit().remove();
325+
stops.enter().append('stop');
326+
327+
stops.each(function(d) {
328+
var tc = tinycolor(d[1]);
329+
d3.select(this).attr({
330+
offset: d[0] + '%',
331+
'stop-color': Color.tinyRGB(tc),
332+
'stop-opacity': tc.getAlpha()
333+
});
299334
});
300335
});
301336

302-
sel.style({
303-
fill: 'url(#' + gradientID + ')',
304-
'fill-opacity': null
305-
});
337+
sel.style(prop, 'url(#' + fullID + ')')
338+
.style(prop + '-opacity', null);
306339
};
307340

308341
/*
@@ -425,16 +458,17 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) {
425458
if(gradientColor) perPointGradient = true;
426459
else gradientColor = markerGradient.color;
427460

428-
var gradientID = 'g' + gd._fullLayout._uid + '-' + trace.uid;
461+
var gradientID = trace.uid;
429462
if(perPointGradient) gradientID += '-' + d.i;
430463

431-
sel.call(drawing.gradient, gd, gradientID, gradientType, fillColor, gradientColor);
464+
drawing.gradient(sel, gd, gradientID, gradientType,
465+
[[0, gradientColor], [1, fillColor]], 'fill');
432466
} else {
433-
sel.call(Color.fill, fillColor);
467+
Color.fill(sel, fillColor);
434468
}
435469

436470
if(lineWidth) {
437-
sel.call(Color.stroke, lineColor);
471+
Color.stroke(sel, lineColor);
438472
}
439473
}
440474
};

src/components/legend/style.js

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,67 @@ module.exports = function style(s, gd) {
6161
var showFill = trace.visible && trace.fill && trace.fill !== 'none';
6262
var showLine = subTypes.hasLines(trace);
6363
var contours = trace.contours;
64+
var showGradientLine = false;
65+
var showGradientFill = false;
6466

65-
if(contours && contours.type === 'constraint') {
66-
showLine = contours.showlines;
67-
showFill = contours._operation !== '=';
67+
if(contours) {
68+
var coloring = contours.coloring;
69+
70+
if(coloring === 'lines') {
71+
showGradientLine = true;
72+
}
73+
else {
74+
showLine = coloring === 'none' || contours.showlines;
75+
}
76+
77+
if(contours.type === 'constraint') {
78+
showFill = contours._operation !== '=';
79+
}
80+
else if(coloring === 'fill' || coloring === 'heatmap') {
81+
showGradientFill = true;
82+
}
6883
}
6984

70-
var fill = d3.select(this).select('.legendfill').selectAll('path')
71-
.data(showFill ? [d] : []);
85+
var this3 = d3.select(this);
86+
87+
var fill = this3.select('.legendfill').selectAll('path')
88+
.data(showFill || showGradientFill ? [d] : []);
7289
fill.enter().append('path').classed('js-fill', true);
7390
fill.exit().remove();
7491
fill.attr('d', 'M5,0h30v6h-30z')
75-
.call(Drawing.fillGroupStyle);
92+
.call(showFill ? Drawing.fillGroupStyle : fillGradient);
7693

77-
var line = d3.select(this).select('.legendlines').selectAll('path')
78-
.data(showLine ? [d] : []);
79-
line.enter().append('path').classed('js-line', true)
80-
.attr('d', 'M5,0h30');
94+
var line = this3.select('.legendlines').selectAll('path')
95+
.data(showLine || showGradientLine ? [d] : []);
96+
line.enter().append('path').classed('js-line', true);
8197
line.exit().remove();
82-
line.call(Drawing.lineGroupStyle);
98+
99+
// this is ugly... but you can't apply a gradient to a perfectly
100+
// horizontal or vertical line. Presumably because then
101+
// the system doesn't know how to scale vertical variation, even
102+
// though there *is* no vertical variation in this case.
103+
// so add an invisibly small angle to the line
104+
// This issue (and workaround) exist across (Mac) Chrome, FF, and Safari
105+
line.attr('d', showGradientLine ? 'M5,0l30,0.0001' : 'M5,0h30')
106+
.call(showLine ? Drawing.lineGroupStyle : lineGradient);
107+
108+
function fillGradient(s) {
109+
if(s.size()) {
110+
var gradientID = 'legendfill-' + trace.uid;
111+
Drawing.gradient(s, gd, gradientID, 'horizontalreversed',
112+
trace.colorscale, 'fill');
113+
}
114+
}
115+
116+
function lineGradient(s) {
117+
if(s.size()) {
118+
var gradientID = 'legendline-' + trace.uid;
119+
Drawing.lineGroupStyle(s);
120+
Drawing.gradient(s, gd, gradientID, 'horizontalreversed',
121+
trace.colorscale, 'stroke');
122+
}
123+
}
124+
83125
}
84126

85127
function stylePoints(d) {

src/traces/contour/defaults.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
3737
var isConstraint = (coerce('contours.type') === 'constraint');
3838
coerce('connectgaps', Lib.isArray1D(traceOut.z));
3939

40-
// trace-level showlegend has already been set, but is only allowed if this is a constraint
41-
if(!isConstraint) delete traceOut.showlegend;
42-
4340
if(isConstraint) {
4441
handleConstraintDefaults(traceIn, traceOut, coerce, layout, defaultColor);
4542
}

src/traces/contour/style_defaults.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout,
2727
}
2828

2929
if(coloring !== 'none') {
30+
// plots/plots always coerces showlegend to true, but in this case
31+
// we default to false and (by default) show a colorbar instead
32+
if(traceIn.showlegend !== true) traceOut.showlegend = false;
33+
3034
colorscaleDefaults(
3135
traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'}
3236
);

src/traces/contourcarpet/defaults.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
5555
coerce('text');
5656
var isConstraint = (coerce('contours.type') === 'constraint');
5757

58-
// trace-level showlegend has already been set, but is only allowed if this is a constraint
59-
if(!isConstraint) delete traceOut.showlegend;
60-
6158
if(isConstraint) {
6259
handleConstraintDefaults(traceIn, traceOut, coerce, layout, defaultColor, {hasHover: false});
6360
} else {
44 KB
Loading

test/image/mocks/contour_legend.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"data":[{
3+
"type": "contour",
4+
"contours":{"coloring":"fills", "showlabels": true},
5+
"z":[[1, 2, 1], [2, 4, 1], [3, 4, 4]],
6+
"colorscale": "Greys",
7+
"showscale": false,
8+
"showlegend": true,
9+
"name": "fills"
10+
}, {
11+
"type": "contour",
12+
"contours":{"coloring":"lines", "showlabels": true},
13+
"z":[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
14+
"showscale": false,
15+
"showlegend": true,
16+
"line": {"width": 2},
17+
"name": "lines"
18+
}, {
19+
"type": "contour",
20+
"contours":{"coloring":"none", "showlabels": true},
21+
"z":[[1, 4, 7], [2, 5, 8], [3, 6, 9]],
22+
"line": {"color": "#0f0"},
23+
"name": "none"
24+
}, {
25+
"type": "contour",
26+
"contours": {"type": "constraint", "operation": "[]", "value": [3, 7], "showlabels": true},
27+
"z": [[3, 2, 1], [6, 5, 4], [9, 8, 7]],
28+
"line": {"color": "#f88"},
29+
"name": "constraint"
30+
}],
31+
"layout":{
32+
"autosize":false,
33+
"height":400,
34+
"width":500
35+
}
36+
}

0 commit comments

Comments
 (0)