Skip to content

Commit

Permalink
Merge pull request #6905 from lumip/scatter-gradient-fills
Browse files Browse the repository at this point in the history
Add fill gradients for scatter traces
  • Loading branch information
archmoj authored Mar 6, 2024
2 parents 1087f73 + a2400f1 commit 7bca015
Show file tree
Hide file tree
Showing 22 changed files with 413 additions and 36 deletions.
1 change: 1 addition & 0 deletions draftlogs/6905_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add fill gradients for scatter traces
19 changes: 19 additions & 0 deletions src/components/color/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ color.combine = function(front, back) {
return tinycolor(fcflat).toRgbString();
};

/*
* Linearly interpolate between two colors at a normalized interpolation position (0 to 1).
*
* Ignores alpha channel values.
* The resulting color is computed as: factor * first + (1 - factor) * second.
*/
color.interpolate = function(first, second, factor) {
var fc = tinycolor(first).toRgb();
var sc = tinycolor(second).toRgb();

var ic = {
r: factor * fc.r + (1 - factor) * sc.r,
g: factor * fc.g + (1 - factor) * sc.g,
b: factor * fc.b + (1 - factor) * sc.b,
};

return tinycolor(ic).toRgbString();
};

/*
* Create a color that contrasts with cstr.
*
Expand Down
125 changes: 111 additions & 14 deletions src/components/drawing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,9 @@ drawing.dashStyle = function(dash, lineWidth) {
return dash;
};

function setFillStyle(sel, trace, gd) {
function setFillStyle(sel, trace, gd, forLegend) {
var markerPattern = trace.fillpattern;
var fillgradient = trace.fillgradient;
var patternShape = markerPattern && drawing.getPatternAttr(markerPattern.shape, 0, '');
if(patternShape) {
var patternBGColor = drawing.getPatternAttr(markerPattern.bgcolor, 0, null);
Expand All @@ -192,6 +193,55 @@ function setFillStyle(sel, trace, gd) {
undefined, markerPattern.fillmode,
patternBGColor, patternFGColor, patternFGOpacity
);
} else if(fillgradient && fillgradient.type !== 'none') {
var direction = fillgradient.type;
var gradientID = 'scatterfill-' + trace.uid;
if(forLegend) {
gradientID = 'legendfill-' + trace.uid;
}

if(!forLegend && (fillgradient.start !== undefined || fillgradient.stop !== undefined)) {
var start, stop;
if(direction === 'horizontal') {
start = {
x: fillgradient.start,
y: 0,
};
stop = {
x: fillgradient.stop,
y: 0,
};
} else if(direction === 'vertical') {
start = {
x: 0,
y: fillgradient.start,
};
stop = {
x: 0,
y: fillgradient.stop,
};
}

start.x = trace._xA.c2p(
(start.x === undefined) ? trace._extremes.x.min[0].val : start.x, true
);
start.y = trace._yA.c2p(
(start.y === undefined) ? trace._extremes.y.min[0].val : start.y, true
);

stop.x = trace._xA.c2p(
(stop.x === undefined) ? trace._extremes.x.max[0].val : stop.x, true
);
stop.y = trace._yA.c2p(
(stop.y === undefined) ? trace._extremes.y.max[0].val : stop.y, true
);
sel.call(gradientWithBounds, gd, gradientID, 'linear', fillgradient.colorscale, 'fill', start, stop, true, false);
} else {
if(direction === 'horizontal') {
direction = direction + 'reversed';
}
sel.call(drawing.gradient, gd, gradientID, direction, fillgradient.colorscale, 'fill');
}
} else if(trace.fillcolor) {
sel.call(Color.fill, trace.fillcolor);
}
Expand All @@ -202,17 +252,17 @@ drawing.singleFillStyle = function(sel, gd) {
var node = d3.select(sel.node());
var data = node.data();
var trace = ((data[0] || [])[0] || {}).trace || {};
setFillStyle(sel, trace, gd);
setFillStyle(sel, trace, gd, false);
};

drawing.fillGroupStyle = function(s, gd) {
drawing.fillGroupStyle = function(s, gd, forLegend) {
s.style('stroke-width', 0)
.each(function(d) {
var shape = d3.select(this);
// N.B. 'd' won't be a calcdata item when
// fill !== 'none' on a segment-less and marker-less trace
if(d[0].trace) {
setFillStyle(shape, d[0].trace, gd);
setFillStyle(shape, d[0].trace, gd, forLegend);
}
});
};
Expand Down Expand Up @@ -294,16 +344,14 @@ function makePointPath(symbolNumber, r, t, s) {
return drawing.symbolFuncs[base](r, t, s) + (symbolNumber >= 200 ? DOTPATH : '');
}

var HORZGRADIENT = {x1: 1, x2: 0, y1: 0, y2: 0};
var VERTGRADIENT = {x1: 0, x2: 0, y1: 1, y2: 0};
var stopFormatter = numberFormat('~f');
var gradientInfo = {
radial: {node: 'radialGradient'},
radialreversed: {node: 'radialGradient', reversed: true},
horizontal: {node: 'linearGradient', attrs: HORZGRADIENT},
horizontalreversed: {node: 'linearGradient', attrs: HORZGRADIENT, reversed: true},
vertical: {node: 'linearGradient', attrs: VERTGRADIENT},
verticalreversed: {node: 'linearGradient', attrs: VERTGRADIENT, reversed: true}
radial: {type: 'radial'},
radialreversed: {type: 'radial', reversed: true},
horizontal: {type: 'linear', start: {x: 1, y: 0}, stop: {x: 0, y: 0}},
horizontalreversed: {type: 'linear', start: {x: 1, y: 0}, stop: {x: 0, y: 0}, reversed: true},
vertical: {type: 'linear', start: {x: 0, y: 1}, stop: {x: 0, y: 0}},
verticalreversed: {type: 'linear', start: {x: 0, y: 1}, stop: {x: 0, y: 0}, reversed: true}
};

/**
Expand All @@ -321,8 +369,57 @@ var gradientInfo = {
* @param {string} prop: the property to apply to, 'fill' or 'stroke'
*/
drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) {
var len = colorscale.length;
var info = gradientInfo[type];
return gradientWithBounds(
sel, gd, gradientID, info.type, colorscale, prop, info.start, info.stop, false, info.reversed
);
};

/**
* gradient_with_bounds: create and apply a gradient fill for defined start and stop positions
*
* @param {object} sel: d3 selection to apply this gradient to
* You can use `selection.call(Drawing.gradient, ...)`
* @param {DOM element} gd: the graph div `sel` is part of
* @param {string} gradientID: a unique (within this plot) identifier
* for this gradient, so that we don't create unnecessary definitions
* @param {string} type: 'radial' or 'linear'. Radial goes center to edge,
* horizontal goes as defined by start and stop
* @param {array} colorscale: as in attribute values, [[fraction, color], ...]
* @param {string} prop: the property to apply to, 'fill' or 'stroke'
* @param {object} start: start point for linear gradients, { x: number, y: number }.
* Ignored if type is 'radial'.
* @param {object} stop: stop point for linear gradients, { x: number, y: number }.
* Ignored if type is 'radial'.
* @param {boolean} inUserSpace: If true, start and stop give absolute values in the plot.
* If false, start and stop are fractions of the traces extent along each axis.
* @param {boolean} reversed: If true, the gradient is reversed between normal start and stop,
* i.e., the colorscale is applied in order from stop to start for linear, from edge
* to center for radial gradients.
*/
function gradientWithBounds(sel, gd, gradientID, type, colorscale, prop, start, stop, inUserSpace, reversed) {
var len = colorscale.length;

var info;
if(type === 'linear') {
info = {
node: 'linearGradient',
attrs: {
x1: start.x,
y1: start.y,
x2: stop.x,
y2: stop.y,
gradientUnits: inUserSpace ? 'userSpaceOnUse' : 'objectBoundingBox',
},
reversed: reversed,
};
} else if(type === 'radial') {
info = {
node: 'radialGradient',
reversed: reversed,
};
}

var colorStops = new Array(len);
for(var i = 0; i < len; i++) {
if(info.reversed) {
Expand Down Expand Up @@ -368,7 +465,7 @@ drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) {
.style(prop + '-opacity', null);

sel.classed('gradient_filled', true);
};
}

/**
* pattern: create and apply a pattern fill
Expand Down
3 changes: 1 addition & 2 deletions src/components/legend/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ module.exports = function style(s, gd, legend) {
var fillStyle = function(s) {
if(s.size()) {
if(showFill) {
Drawing.fillGroupStyle(s, gd);
Drawing.fillGroupStyle(s, gd, true);
} else {
var gradientID = 'legendfill-' + trace.uid;
Drawing.gradient(s, gd, gradientID,
Expand Down Expand Up @@ -674,7 +674,6 @@ function getStyleGuide(d) {
showGradientFill = true;
}
}

return {
showMarker: showMarker,
showLine: showLine,
Expand Down
3 changes: 2 additions & 1 deletion src/traces/box/attributes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

var makeFillcolorAttr = require('../scatter/fillcolor_attribute');
var scatterAttrs = require('../scatter/attributes');
var barAttrs = require('../bar/attributes');
var colorAttrs = require('../../components/color/attributes');
Expand Down Expand Up @@ -386,7 +387,7 @@ module.exports = {
editType: 'plot'
},

fillcolor: scatterAttrs.fillcolor,
fillcolor: makeFillcolorAttr(),

whiskerwidth: {
valType: 'number',
Expand Down
64 changes: 56 additions & 8 deletions src/traces/scatter/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ var constants = require('./constants');

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

var makeFillcolorAttr = require('./fillcolor_attribute');

function axisPeriod(axis) {
return {
valType: 'any',
Expand Down Expand Up @@ -391,16 +393,62 @@ module.exports = {
'consecutive, the later ones will be pushed down in the drawing order.'
].join(' ')
},
fillcolor: {
valType: 'color',
editType: 'style',
anim: true,
fillcolor: makeFillcolorAttr(true),
fillgradient: extendFlat({
type: {
valType: 'enumerated',
values: ['radial', 'horizontal', 'vertical', 'none'],
dflt: 'none',
editType: 'calc',
description: [
'Sets the type/orientation of the color gradient for the fill.',
'Defaults to *none*.'
].join(' ')
},
start: {
valType: 'number',
editType: 'calc',
description: [
'Sets the gradient start value.',
'It is given as the absolute position on the axis determined by',
'the orientiation. E.g., if orientation is *horizontal*, the',
'gradient will be horizontal and start from the x-position',
'given by start. If omitted, the gradient starts at the lowest',
'value of the trace along the respective axis.',
'Ignored if orientation is *radial*.'
].join(' ')
},
stop: {
valType: 'number',
editType: 'calc',
description: [
'Sets the gradient end value.',
'It is given as the absolute position on the axis determined by',
'the orientiation. E.g., if orientation is *horizontal*, the',
'gradient will be horizontal and end at the x-position',
'given by end. If omitted, the gradient ends at the highest',
'value of the trace along the respective axis.',
'Ignored if orientation is *radial*.'
].join(' ')
},
colorscale: {
valType: 'colorscale',
editType: 'style',
description: [
'Sets the fill gradient colors as a color scale.',
'The color scale is interpreted as a gradient',
'applied in the direction specified by *orientation*,',
'from the lowest to the highest value of the scatter',
'plot along that axis, or from the center to the most',
'distant point from it, if orientation is *radial*.'
].join(' ')
},
editType: 'calc',
description: [
'Sets the fill color.',
'Defaults to a half-transparent variant of the line color,',
'marker color, or marker line color, whichever is available.'
'Sets a fill gradient.',
'If not specified, the fillcolor is used instead.'
].join(' ')
},
}),
fillpattern: pattern,
marker: extendFlat({
symbol: {
Expand Down
4 changes: 3 additions & 1 deletion src/traces/scatter/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
// We handle that case in some hacky code inside handleStackDefaults.
coerce('fill', stackGroupOpts ? stackGroupOpts.fillDflt : 'none');
if(traceOut.fill !== 'none') {
handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce);
handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce, {
moduleHasFillgradient: true
});
if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce);
coercePattern(coerce, 'fillpattern', traceOut.fillcolor, false);
}
Expand Down
18 changes: 18 additions & 0 deletions src/traces/scatter/fillcolor_attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

module.exports = function makeFillcolorAttr(hasFillgradient) {
return {
valType: 'color',
editType: 'style',
anim: true,
description: [
'Sets the fill color.',
'Defaults to a half-transparent variant of the line color,',
'marker color, or marker line color, whichever is available.' + (
hasFillgradient ?
' If fillgradient is specified, fillcolor is ignored except for setting the background color of the hover label, if any.' :
''
)
].join(' ')
};
};
32 changes: 31 additions & 1 deletion src/traces/scatter/fillcolor_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
var Color = require('../../components/color');
var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray;

module.exports = function fillColorDefaults(traceIn, traceOut, defaultColor, coerce) {
function averageColors(colorscale) {
var color = Color.interpolate(colorscale[0][1], colorscale[1][1], 0.5);
for(var i = 2; i < colorscale.length; i++) {
var averageColorI = Color.interpolate(colorscale[i - 1][1], colorscale[i][1], 0.5);
color = Color.interpolate(color, averageColorI, colorscale[i - 1][0] / colorscale[i][0]);
}
return color;
}

module.exports = function fillColorDefaults(traceIn, traceOut, defaultColor, coerce, opts) {
if(!opts) opts = {};

var inheritColorFromMarker = false;

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

var averageGradientColor;
if(opts.moduleHasFillgradient) {
var gradientOrientation = coerce('fillgradient.type');
if(gradientOrientation !== 'none') {
coerce('fillgradient.start');
coerce('fillgradient.stop');
var gradientColorscale = coerce('fillgradient.colorscale');

// if a fillgradient is specified, we use the average gradient color
// to specify fillcolor after all other more specific candidates
// are considered, but before the global default color.
// fillcolor affects the background color of the hoverlabel in this case.
if(gradientColorscale) {
averageGradientColor = averageColors(gradientColorscale);
}
}
}

coerce('fillcolor', Color.addOpacity(
(traceOut.line || {}).color ||
inheritColorFromMarker ||
averageGradientColor ||
defaultColor, 0.5
));
};
Loading

0 comments on commit 7bca015

Please sign in to comment.