diff --git a/draftlogs/6905_add.md b/draftlogs/6905_add.md new file mode 100644 index 00000000000..94b10657d5c --- /dev/null +++ b/draftlogs/6905_add.md @@ -0,0 +1 @@ +- Add fill gradients for scatter traces \ No newline at end of file diff --git a/src/components/color/index.js b/src/components/color/index.js index e114ae3b1a4..50eccc4a73e 100644 --- a/src/components/color/index.js +++ b/src/components/color/index.js @@ -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. * diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index d02095aaa86..2784a2d4d73 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -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); @@ -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); } @@ -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); } }); }; @@ -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} }; /** @@ -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) { @@ -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 diff --git a/src/components/legend/style.js b/src/components/legend/style.js index e7d31f7d3fa..f953db454d1 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -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, @@ -674,7 +674,6 @@ function getStyleGuide(d) { showGradientFill = true; } } - return { showMarker: showMarker, showLine: showLine, diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index cda4fc672c5..63b0def141a 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -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'); @@ -386,7 +387,7 @@ module.exports = { editType: 'plot' }, - fillcolor: scatterAttrs.fillcolor, + fillcolor: makeFillcolorAttr(), whiskerwidth: { valType: 'number', diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index f6beafa16a4..7241fd5784d 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -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', @@ -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: { diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index ce6a5fab07f..31cae6f285c 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -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); } diff --git a/src/traces/scatter/fillcolor_attribute.js b/src/traces/scatter/fillcolor_attribute.js new file mode 100644 index 00000000000..9d79711d9dc --- /dev/null +++ b/src/traces/scatter/fillcolor_attribute.js @@ -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(' ') + }; +}; diff --git a/src/traces/scatter/fillcolor_defaults.js b/src/traces/scatter/fillcolor_defaults.js index bcb08a51825..0822ae8aad6 100644 --- a/src/traces/scatter/fillcolor_defaults.js +++ b/src/traces/scatter/fillcolor_defaults.js @@ -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) { @@ -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 )); }; diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js index ad3fd5a217b..734ecf9f9d0 100644 --- a/src/traces/scatter/style.js +++ b/src/traces/scatter/style.js @@ -27,7 +27,7 @@ function style(gd) { .call(Drawing.lineGroupStyle); s.selectAll('g.trace path.js-fill') - .call(Drawing.fillGroupStyle, gd); + .call(Drawing.fillGroupStyle, gd, false); Registry.getComponentMethod('errorbars', 'style')(s); } diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js index 6022878cc9a..af0e7d406bf 100644 --- a/src/traces/scattercarpet/attributes.js +++ b/src/traces/scattercarpet/attributes.js @@ -1,5 +1,6 @@ 'use strict'; +var makeFillcolorAttr = require('../scatter/fillcolor_attribute'); var scatterAttrs = require('../scatter/attributes'); var baseAttrs = require('../../plots/attributes'); var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; @@ -83,7 +84,7 @@ module.exports = { 'used if one trace does not enclose the other.' ].join(' ') }), - fillcolor: scatterAttrs.fillcolor, + fillcolor: makeFillcolorAttr(), marker: extendFlat({ symbol: scatterMarkerAttrs.symbol, opacity: scatterMarkerAttrs.opacity, diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js index d80b62c7735..76ccd70e204 100644 --- a/src/traces/scattergeo/attributes.js +++ b/src/traces/scattergeo/attributes.js @@ -2,6 +2,7 @@ var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; +var makeFillcolorAttr = require('../scatter/fillcolor_attribute'); var scatterAttrs = require('../scatter/attributes'); var baseAttrs = require('../../plots/attributes'); var colorAttributes = require('../../components/colorscale/attributes'); @@ -151,7 +152,7 @@ module.exports = overrideAll({ 'of the trace if it has gaps) into a closed shape.' ].join(' ') }, - fillcolor: scatterAttrs.fillcolor, + fillcolor: makeFillcolorAttr(), selected: scatterAttrs.selected, unselected: scatterAttrs.unselected, diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js index f458b27086b..bbfd23e13f2 100644 --- a/src/traces/scattergl/attributes.js +++ b/src/traces/scattergl/attributes.js @@ -1,6 +1,7 @@ 'use strict'; var baseAttrs = require('../../plots/attributes'); +var makeFillcolorAttr = require('../scatter/fillcolor_attribute'); var scatterAttrs = require('../scatter/attributes'); var axisHoverFormat = require('../../plots/cartesian/axis_format_attributes').axisHoverFormat; var colorScaleAttrs = require('../../components/colorscale/attributes'); @@ -80,7 +81,7 @@ var attrs = module.exports = overrideAll({ }), connectgaps: scatterAttrs.connectgaps, fill: extendFlat({}, scatterAttrs.fill, {dflt: 'none'}), - fillcolor: scatterAttrs.fillcolor, + fillcolor: makeFillcolorAttr(), // no hoveron diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js index 3404f2f9c35..7686403e253 100644 --- a/src/traces/scattermapbox/attributes.js +++ b/src/traces/scattermapbox/attributes.js @@ -2,6 +2,7 @@ var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; +var makeFillcolorAttr = require('../scatter/fillcolor_attribute'); var scatterGeoAttrs = require('../scattergeo/attributes'); var scatterAttrs = require('../scatter/attributes'); var mapboxAttrs = require('../../plots/mapbox/layout_attributes'); @@ -151,7 +152,7 @@ module.exports = overrideAll({ ), fill: scatterGeoAttrs.fill, - fillcolor: scatterAttrs.fillcolor, + fillcolor: makeFillcolorAttr(), textfont: mapboxAttrs.layers.symbol.textfont, textposition: mapboxAttrs.layers.symbol.textposition, diff --git a/src/traces/scatterpolar/attributes.js b/src/traces/scatterpolar/attributes.js index f5d0321c519..34a859df181 100644 --- a/src/traces/scatterpolar/attributes.js +++ b/src/traces/scatterpolar/attributes.js @@ -3,6 +3,7 @@ var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var extendFlat = require('../../lib/extend').extendFlat; +var makeFillcolorAttr = require('../scatter/fillcolor_attribute'); var scatterAttrs = require('../scatter/attributes'); var baseAttrs = require('../../plots/attributes'); var lineAttrs = scatterAttrs.line; @@ -112,7 +113,7 @@ module.exports = { 'used if one trace does not enclose the other.' ].join(' ') }), - fillcolor: scatterAttrs.fillcolor, + fillcolor: makeFillcolorAttr(), // TODO error bars // https://stackoverflow.com/a/26597487/4068492 diff --git a/src/traces/scattersmith/attributes.js b/src/traces/scattersmith/attributes.js index 2344a5fc2a5..5a8e931013d 100644 --- a/src/traces/scattersmith/attributes.js +++ b/src/traces/scattersmith/attributes.js @@ -3,6 +3,7 @@ var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var extendFlat = require('../../lib/extend').extendFlat; +var makeFillcolorAttr = require('../scatter/fillcolor_attribute'); var scatterAttrs = require('../scatter/attributes'); var baseAttrs = require('../../plots/attributes'); var lineAttrs = scatterAttrs.line; @@ -68,7 +69,7 @@ module.exports = { 'used if one trace does not enclose the other.' ].join(' ') }), - fillcolor: scatterAttrs.fillcolor, + fillcolor: makeFillcolorAttr(), hoverinfo: extendFlat({}, baseAttrs.hoverinfo, { flags: ['real', 'imag', 'text', 'name'] diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js index 23c0bba0621..67d5baf5feb 100644 --- a/src/traces/scatterternary/attributes.js +++ b/src/traces/scatterternary/attributes.js @@ -2,6 +2,7 @@ var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; +var makeFillcolorAttr = require('../scatter/fillcolor_attribute'); var scatterAttrs = require('../scatter/attributes'); var baseAttrs = require('../../plots/attributes'); var colorScaleAttrs = require('../../components/colorscale/attributes'); @@ -112,7 +113,7 @@ module.exports = { 'used if one trace does not enclose the other.' ].join(' ') }), - fillcolor: scatterAttrs.fillcolor, + fillcolor: makeFillcolorAttr(), marker: extendFlat({ symbol: scatterMarkerAttrs.symbol, opacity: scatterMarkerAttrs.opacity, diff --git a/test/image/baselines/zz-scatter_fill_gradient_tonext.png b/test/image/baselines/zz-scatter_fill_gradient_tonext.png new file mode 100644 index 00000000000..9e6939e28a0 Binary files /dev/null and b/test/image/baselines/zz-scatter_fill_gradient_tonext.png differ diff --git a/test/image/baselines/zz-scatter_fill_gradient_tonexty_toself.png b/test/image/baselines/zz-scatter_fill_gradient_tonexty_toself.png new file mode 100644 index 00000000000..54d176b74b5 Binary files /dev/null and b/test/image/baselines/zz-scatter_fill_gradient_tonexty_toself.png differ diff --git a/test/image/mocks/zz-scatter_fill_gradient_tonext.json b/test/image/mocks/zz-scatter_fill_gradient_tonext.json new file mode 100644 index 00000000000..3be1cd0b03e --- /dev/null +++ b/test/image/mocks/zz-scatter_fill_gradient_tonext.json @@ -0,0 +1,42 @@ +{ + "data": [ + { + "x": [1, 1, 2, 2], + "y": [1, 2, 2, 1], + "type": "scatter", + "mode": "none", + "fill": "tonext", + "hoveron": "points+fills", + "fillgradient": { + "type": "horizontal", + "colorscale": [ + [0.0, "rgba(0, 255, 0, 1)"], + [0.5, "rgba(0, 255, 0, 0)"], + [1.0, "rgba(0, 255, 0, 1)"] + ] + } + }, + { + "x": [0, 0, 3, 3], + "y": [0, 4, 4, 1], + "type": "scatter", + "mode": "none", + "fill": "tonext", + "hoveron": "fills+points", + "fillgradient": { + "type": "radial", + "colorscale": [ + [0.0, "rgba(255, 255, 0, 0.0)"], + [0.8, "rgba(255, 0, 0, 0.3)"], + [1.0, "rgba(255, 255, 0, 1.0)"] + ] + } + } + ], + + "layout": { + "autosize": true, + "title": { "text": "Scatter traces with radial color gradient fills" }, + "showlegend": true + } +} diff --git a/test/image/mocks/zz-scatter_fill_gradient_tonexty_toself.json b/test/image/mocks/zz-scatter_fill_gradient_tonexty_toself.json new file mode 100644 index 00000000000..c8399861792 --- /dev/null +++ b/test/image/mocks/zz-scatter_fill_gradient_tonexty_toself.json @@ -0,0 +1,81 @@ +{ + "data": [ + { + "x": [0, 2], + "y": [1, 2.5], + "type": "scatter", + "mode": "none", + "fill": "tonexty", + "fillcolor": "rgba(0, 255, 0, .5)", + "fillgradient": { + "type": "horizontal", + "colorscale": [ + [0.0, "rgba(0, 255, 0, 1)"], + [0.5, "rgba(0, 255, 0, 0)"], + [1.0, "rgba(0, 255, 0, 1)"] + ], + "start": 0, + "stop": 1 + }, + "name": "greenthing", + "hoveron": "fills+points" + }, + { + "x": [0, 0.75, 1.5, 2], + "y": [1, 2, 2.5, 2.8], + "type": "scatter", + "mode": "none", + "fill": "tonexty", + "hoveron": "fills+points", + "fillgradient": { + "type": "horizontal", + "colorscale": [ + [0.0, "rgba(255, 255, 0, 1.0)"], + [0.6, "rgba(255, 255, 0, 1.0)"], + [1.0, "rgba(255, 255, 0, 0.3)"] + ] + }, + "name": "yellow" + }, + { + "x": [0, 1, 1, 0.5, 0], + "y": [5, 5, 2, 1.5, 0], + "type": "scatter", + "mode": "none", + "fill": "toself", + "fillgradient": { + "type": "vertical", + "colorscale": [ + [0.0, "rgba(255, 0, 0, 0.8)"], + [1.0, "rgba(0, 0, 255, 0.9)"] + ] + }, + "fillcolor": "rgb(255, 0, 0)", + "hoveron": "fills" + }, + { + "x": [1.1, 2, 2, 1.1], + "y": [5, 5, 3, 2.1], + "type": "scatter", + "mode": "none", + "fill": "toself", + "fillgradient": { + "type": "vertical", + "colorscale": [ + [0.0, "rgba(255, 0, 0, 0.8)"], + [1.0, "rgba(0, 0, 255, 0.9)"] + ], + "start": 0, + "stop": 5 + }, + "fillcolor": "rgb(255, 0, 0)", + "hoveron": "fills" + } + ], + + "layout": { + "autosize": true, + "title": { "text": "Scatter traces with linear color gradient fills" }, + "showlegend": true + } +} diff --git a/test/plot-schema.json b/test/plot-schema.json index 35e00377ad9..9b3a8816729 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -45042,10 +45042,42 @@ }, "fillcolor": { "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.", + "description": "Sets the fill color. Defaults to a half-transparent variant of the line color, marker color, or marker line color, whichever is available. If fillgradient is specified, fillcolor is ignored except for setting the background color of the hover label, if any.", "editType": "style", "valType": "color" }, + "fillgradient": { + "colorscale": { + "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*.", + "editType": "style", + "valType": "colorscale" + }, + "description": "Sets a fill gradient. If not specified, the fillcolor is used instead.", + "editType": "calc", + "role": "object", + "start": { + "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*.", + "editType": "calc", + "valType": "number" + }, + "stop": { + "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*.", + "editType": "calc", + "valType": "number" + }, + "type": { + "description": "Sets the type/orientation of the color gradient for the fill. Defaults to *none*.", + "dflt": "none", + "editType": "calc", + "valType": "enumerated", + "values": [ + "radial", + "horizontal", + "vertical", + "none" + ] + } + }, "fillpattern": { "bgcolor": { "arrayOk": true,