Skip to content

Commit 7bca015

Browse files
authored
Merge pull request #6905 from lumip/scatter-gradient-fills
Add fill gradients for scatter traces
2 parents 1087f73 + a2400f1 commit 7bca015

22 files changed

+413
-36
lines changed

draftlogs/6905_add.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add fill gradients for scatter traces

src/components/color/index.js

+19
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ color.combine = function(front, back) {
5353
return tinycolor(fcflat).toRgbString();
5454
};
5555

56+
/*
57+
* Linearly interpolate between two colors at a normalized interpolation position (0 to 1).
58+
*
59+
* Ignores alpha channel values.
60+
* The resulting color is computed as: factor * first + (1 - factor) * second.
61+
*/
62+
color.interpolate = function(first, second, factor) {
63+
var fc = tinycolor(first).toRgb();
64+
var sc = tinycolor(second).toRgb();
65+
66+
var ic = {
67+
r: factor * fc.r + (1 - factor) * sc.r,
68+
g: factor * fc.g + (1 - factor) * sc.g,
69+
b: factor * fc.b + (1 - factor) * sc.b,
70+
};
71+
72+
return tinycolor(ic).toRgbString();
73+
};
74+
5675
/*
5776
* Create a color that contrasts with cstr.
5877
*

src/components/drawing/index.js

+111-14
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,9 @@ drawing.dashStyle = function(dash, lineWidth) {
177177
return dash;
178178
};
179179

180-
function setFillStyle(sel, trace, gd) {
180+
function setFillStyle(sel, trace, gd, forLegend) {
181181
var markerPattern = trace.fillpattern;
182+
var fillgradient = trace.fillgradient;
182183
var patternShape = markerPattern && drawing.getPatternAttr(markerPattern.shape, 0, '');
183184
if(patternShape) {
184185
var patternBGColor = drawing.getPatternAttr(markerPattern.bgcolor, 0, null);
@@ -192,6 +193,55 @@ function setFillStyle(sel, trace, gd) {
192193
undefined, markerPattern.fillmode,
193194
patternBGColor, patternFGColor, patternFGOpacity
194195
);
196+
} else if(fillgradient && fillgradient.type !== 'none') {
197+
var direction = fillgradient.type;
198+
var gradientID = 'scatterfill-' + trace.uid;
199+
if(forLegend) {
200+
gradientID = 'legendfill-' + trace.uid;
201+
}
202+
203+
if(!forLegend && (fillgradient.start !== undefined || fillgradient.stop !== undefined)) {
204+
var start, stop;
205+
if(direction === 'horizontal') {
206+
start = {
207+
x: fillgradient.start,
208+
y: 0,
209+
};
210+
stop = {
211+
x: fillgradient.stop,
212+
y: 0,
213+
};
214+
} else if(direction === 'vertical') {
215+
start = {
216+
x: 0,
217+
y: fillgradient.start,
218+
};
219+
stop = {
220+
x: 0,
221+
y: fillgradient.stop,
222+
};
223+
}
224+
225+
start.x = trace._xA.c2p(
226+
(start.x === undefined) ? trace._extremes.x.min[0].val : start.x, true
227+
);
228+
start.y = trace._yA.c2p(
229+
(start.y === undefined) ? trace._extremes.y.min[0].val : start.y, true
230+
);
231+
232+
stop.x = trace._xA.c2p(
233+
(stop.x === undefined) ? trace._extremes.x.max[0].val : stop.x, true
234+
);
235+
stop.y = trace._yA.c2p(
236+
(stop.y === undefined) ? trace._extremes.y.max[0].val : stop.y, true
237+
);
238+
sel.call(gradientWithBounds, gd, gradientID, 'linear', fillgradient.colorscale, 'fill', start, stop, true, false);
239+
} else {
240+
if(direction === 'horizontal') {
241+
direction = direction + 'reversed';
242+
}
243+
sel.call(drawing.gradient, gd, gradientID, direction, fillgradient.colorscale, 'fill');
244+
}
195245
} else if(trace.fillcolor) {
196246
sel.call(Color.fill, trace.fillcolor);
197247
}
@@ -202,17 +252,17 @@ drawing.singleFillStyle = function(sel, gd) {
202252
var node = d3.select(sel.node());
203253
var data = node.data();
204254
var trace = ((data[0] || [])[0] || {}).trace || {};
205-
setFillStyle(sel, trace, gd);
255+
setFillStyle(sel, trace, gd, false);
206256
};
207257

208-
drawing.fillGroupStyle = function(s, gd) {
258+
drawing.fillGroupStyle = function(s, gd, forLegend) {
209259
s.style('stroke-width', 0)
210260
.each(function(d) {
211261
var shape = d3.select(this);
212262
// N.B. 'd' won't be a calcdata item when
213263
// fill !== 'none' on a segment-less and marker-less trace
214264
if(d[0].trace) {
215-
setFillStyle(shape, d[0].trace, gd);
265+
setFillStyle(shape, d[0].trace, gd, forLegend);
216266
}
217267
});
218268
};
@@ -294,16 +344,14 @@ function makePointPath(symbolNumber, r, t, s) {
294344
return drawing.symbolFuncs[base](r, t, s) + (symbolNumber >= 200 ? DOTPATH : '');
295345
}
296346

297-
var HORZGRADIENT = {x1: 1, x2: 0, y1: 0, y2: 0};
298-
var VERTGRADIENT = {x1: 0, x2: 0, y1: 1, y2: 0};
299347
var stopFormatter = numberFormat('~f');
300348
var gradientInfo = {
301-
radial: {node: 'radialGradient'},
302-
radialreversed: {node: 'radialGradient', reversed: true},
303-
horizontal: {node: 'linearGradient', attrs: HORZGRADIENT},
304-
horizontalreversed: {node: 'linearGradient', attrs: HORZGRADIENT, reversed: true},
305-
vertical: {node: 'linearGradient', attrs: VERTGRADIENT},
306-
verticalreversed: {node: 'linearGradient', attrs: VERTGRADIENT, reversed: true}
349+
radial: {type: 'radial'},
350+
radialreversed: {type: 'radial', reversed: true},
351+
horizontal: {type: 'linear', start: {x: 1, y: 0}, stop: {x: 0, y: 0}},
352+
horizontalreversed: {type: 'linear', start: {x: 1, y: 0}, stop: {x: 0, y: 0}, reversed: true},
353+
vertical: {type: 'linear', start: {x: 0, y: 1}, stop: {x: 0, y: 0}},
354+
verticalreversed: {type: 'linear', start: {x: 0, y: 1}, stop: {x: 0, y: 0}, reversed: true}
307355
};
308356

309357
/**
@@ -321,8 +369,57 @@ var gradientInfo = {
321369
* @param {string} prop: the property to apply to, 'fill' or 'stroke'
322370
*/
323371
drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) {
324-
var len = colorscale.length;
325372
var info = gradientInfo[type];
373+
return gradientWithBounds(
374+
sel, gd, gradientID, info.type, colorscale, prop, info.start, info.stop, false, info.reversed
375+
);
376+
};
377+
378+
/**
379+
* gradient_with_bounds: create and apply a gradient fill for defined start and stop positions
380+
*
381+
* @param {object} sel: d3 selection to apply this gradient to
382+
* You can use `selection.call(Drawing.gradient, ...)`
383+
* @param {DOM element} gd: the graph div `sel` is part of
384+
* @param {string} gradientID: a unique (within this plot) identifier
385+
* for this gradient, so that we don't create unnecessary definitions
386+
* @param {string} type: 'radial' or 'linear'. Radial goes center to edge,
387+
* horizontal goes as defined by start and stop
388+
* @param {array} colorscale: as in attribute values, [[fraction, color], ...]
389+
* @param {string} prop: the property to apply to, 'fill' or 'stroke'
390+
* @param {object} start: start point for linear gradients, { x: number, y: number }.
391+
* Ignored if type is 'radial'.
392+
* @param {object} stop: stop point for linear gradients, { x: number, y: number }.
393+
* Ignored if type is 'radial'.
394+
* @param {boolean} inUserSpace: If true, start and stop give absolute values in the plot.
395+
* If false, start and stop are fractions of the traces extent along each axis.
396+
* @param {boolean} reversed: If true, the gradient is reversed between normal start and stop,
397+
* i.e., the colorscale is applied in order from stop to start for linear, from edge
398+
* to center for radial gradients.
399+
*/
400+
function gradientWithBounds(sel, gd, gradientID, type, colorscale, prop, start, stop, inUserSpace, reversed) {
401+
var len = colorscale.length;
402+
403+
var info;
404+
if(type === 'linear') {
405+
info = {
406+
node: 'linearGradient',
407+
attrs: {
408+
x1: start.x,
409+
y1: start.y,
410+
x2: stop.x,
411+
y2: stop.y,
412+
gradientUnits: inUserSpace ? 'userSpaceOnUse' : 'objectBoundingBox',
413+
},
414+
reversed: reversed,
415+
};
416+
} else if(type === 'radial') {
417+
info = {
418+
node: 'radialGradient',
419+
reversed: reversed,
420+
};
421+
}
422+
326423
var colorStops = new Array(len);
327424
for(var i = 0; i < len; i++) {
328425
if(info.reversed) {
@@ -368,7 +465,7 @@ drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) {
368465
.style(prop + '-opacity', null);
369466

370467
sel.classed('gradient_filled', true);
371-
};
468+
}
372469

373470
/**
374471
* pattern: create and apply a pattern fill

src/components/legend/style.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ module.exports = function style(s, gd, legend) {
115115
var fillStyle = function(s) {
116116
if(s.size()) {
117117
if(showFill) {
118-
Drawing.fillGroupStyle(s, gd);
118+
Drawing.fillGroupStyle(s, gd, true);
119119
} else {
120120
var gradientID = 'legendfill-' + trace.uid;
121121
Drawing.gradient(s, gd, gradientID,
@@ -674,7 +674,6 @@ function getStyleGuide(d) {
674674
showGradientFill = true;
675675
}
676676
}
677-
678677
return {
679678
showMarker: showMarker,
680679
showLine: showLine,

src/traces/box/attributes.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
var makeFillcolorAttr = require('../scatter/fillcolor_attribute');
34
var scatterAttrs = require('../scatter/attributes');
45
var barAttrs = require('../bar/attributes');
56
var colorAttrs = require('../../components/color/attributes');
@@ -386,7 +387,7 @@ module.exports = {
386387
editType: 'plot'
387388
},
388389

389-
fillcolor: scatterAttrs.fillcolor,
390+
fillcolor: makeFillcolorAttr(),
390391

391392
whiskerwidth: {
392393
valType: 'number',

src/traces/scatter/attributes.js

+56-8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ var constants = require('./constants');
1313

1414
var extendFlat = require('../../lib/extend').extendFlat;
1515

16+
var makeFillcolorAttr = require('./fillcolor_attribute');
17+
1618
function axisPeriod(axis) {
1719
return {
1820
valType: 'any',
@@ -391,16 +393,62 @@ module.exports = {
391393
'consecutive, the later ones will be pushed down in the drawing order.'
392394
].join(' ')
393395
},
394-
fillcolor: {
395-
valType: 'color',
396-
editType: 'style',
397-
anim: true,
396+
fillcolor: makeFillcolorAttr(true),
397+
fillgradient: extendFlat({
398+
type: {
399+
valType: 'enumerated',
400+
values: ['radial', 'horizontal', 'vertical', 'none'],
401+
dflt: 'none',
402+
editType: 'calc',
403+
description: [
404+
'Sets the type/orientation of the color gradient for the fill.',
405+
'Defaults to *none*.'
406+
].join(' ')
407+
},
408+
start: {
409+
valType: 'number',
410+
editType: 'calc',
411+
description: [
412+
'Sets the gradient start value.',
413+
'It is given as the absolute position on the axis determined by',
414+
'the orientiation. E.g., if orientation is *horizontal*, the',
415+
'gradient will be horizontal and start from the x-position',
416+
'given by start. If omitted, the gradient starts at the lowest',
417+
'value of the trace along the respective axis.',
418+
'Ignored if orientation is *radial*.'
419+
].join(' ')
420+
},
421+
stop: {
422+
valType: 'number',
423+
editType: 'calc',
424+
description: [
425+
'Sets the gradient end value.',
426+
'It is given as the absolute position on the axis determined by',
427+
'the orientiation. E.g., if orientation is *horizontal*, the',
428+
'gradient will be horizontal and end at the x-position',
429+
'given by end. If omitted, the gradient ends at the highest',
430+
'value of the trace along the respective axis.',
431+
'Ignored if orientation is *radial*.'
432+
].join(' ')
433+
},
434+
colorscale: {
435+
valType: 'colorscale',
436+
editType: 'style',
437+
description: [
438+
'Sets the fill gradient colors as a color scale.',
439+
'The color scale is interpreted as a gradient',
440+
'applied in the direction specified by *orientation*,',
441+
'from the lowest to the highest value of the scatter',
442+
'plot along that axis, or from the center to the most',
443+
'distant point from it, if orientation is *radial*.'
444+
].join(' ')
445+
},
446+
editType: 'calc',
398447
description: [
399-
'Sets the fill color.',
400-
'Defaults to a half-transparent variant of the line color,',
401-
'marker color, or marker line color, whichever is available.'
448+
'Sets a fill gradient.',
449+
'If not specified, the fillcolor is used instead.'
402450
].join(' ')
403-
},
451+
}),
404452
fillpattern: pattern,
405453
marker: extendFlat({
406454
symbol: {

src/traces/scatter/defaults.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
7272
// We handle that case in some hacky code inside handleStackDefaults.
7373
coerce('fill', stackGroupOpts ? stackGroupOpts.fillDflt : 'none');
7474
if(traceOut.fill !== 'none') {
75-
handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce);
75+
handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce, {
76+
moduleHasFillgradient: true
77+
});
7678
if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce);
7779
coercePattern(coerce, 'fillpattern', traceOut.fillcolor, false);
7880
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
module.exports = function makeFillcolorAttr(hasFillgradient) {
4+
return {
5+
valType: 'color',
6+
editType: 'style',
7+
anim: true,
8+
description: [
9+
'Sets the fill color.',
10+
'Defaults to a half-transparent variant of the line color,',
11+
'marker color, or marker line color, whichever is available.' + (
12+
hasFillgradient ?
13+
' If fillgradient is specified, fillcolor is ignored except for setting the background color of the hover label, if any.' :
14+
''
15+
)
16+
].join(' ')
17+
};
18+
};

src/traces/scatter/fillcolor_defaults.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@
33
var Color = require('../../components/color');
44
var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray;
55

6-
module.exports = function fillColorDefaults(traceIn, traceOut, defaultColor, coerce) {
6+
function averageColors(colorscale) {
7+
var color = Color.interpolate(colorscale[0][1], colorscale[1][1], 0.5);
8+
for(var i = 2; i < colorscale.length; i++) {
9+
var averageColorI = Color.interpolate(colorscale[i - 1][1], colorscale[i][1], 0.5);
10+
color = Color.interpolate(color, averageColorI, colorscale[i - 1][0] / colorscale[i][0]);
11+
}
12+
return color;
13+
}
14+
15+
module.exports = function fillColorDefaults(traceIn, traceOut, defaultColor, coerce, opts) {
16+
if(!opts) opts = {};
17+
718
var inheritColorFromMarker = false;
819

920
if(traceOut.marker) {
@@ -18,9 +29,28 @@ module.exports = function fillColorDefaults(traceIn, traceOut, defaultColor, coe
1829
}
1930
}
2031

32+
var averageGradientColor;
33+
if(opts.moduleHasFillgradient) {
34+
var gradientOrientation = coerce('fillgradient.type');
35+
if(gradientOrientation !== 'none') {
36+
coerce('fillgradient.start');
37+
coerce('fillgradient.stop');
38+
var gradientColorscale = coerce('fillgradient.colorscale');
39+
40+
// if a fillgradient is specified, we use the average gradient color
41+
// to specify fillcolor after all other more specific candidates
42+
// are considered, but before the global default color.
43+
// fillcolor affects the background color of the hoverlabel in this case.
44+
if(gradientColorscale) {
45+
averageGradientColor = averageColors(gradientColorscale);
46+
}
47+
}
48+
}
49+
2150
coerce('fillcolor', Color.addOpacity(
2251
(traceOut.line || {}).color ||
2352
inheritColorFromMarker ||
53+
averageGradientColor ||
2454
defaultColor, 0.5
2555
));
2656
};

0 commit comments

Comments
 (0)