Skip to content
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 fill gradients for scatter traces #6905

Merged
merged 10 commits into from
Mar 6, 2024
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 @@ -114,7 +114,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 @@ -673,7 +673,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'],
archmoj marked this conversation as resolved.
Show resolved Hide resolved
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',
archmoj marked this conversation as resolved.
Show resolved Hide resolved
'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