diff --git a/draftlogs/6101_add.md b/draftlogs/6101_add.md new file mode 100644 index 00000000000..c3d17aad427 --- /dev/null +++ b/draftlogs/6101_add.md @@ -0,0 +1 @@ + - Add pattern fill for scatter filled area diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 6acdfac46a3..ca3ac9bca02 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -177,24 +177,42 @@ drawing.dashStyle = function(dash, lineWidth) { return dash; }; +function setFillStyle(sel, trace, gd) { + var markerPattern = trace.fillpattern; + var patternShape = markerPattern && drawing.getPatternAttr(markerPattern.shape, 0, ''); + if(patternShape) { + var patternBGColor = drawing.getPatternAttr(markerPattern.bgcolor, 0, null); + var patternFGColor = drawing.getPatternAttr(markerPattern.fgcolor, 0, null); + var patternFGOpacity = markerPattern.fgopacity; + var patternSize = drawing.getPatternAttr(markerPattern.size, 0, 8); + var patternSolidity = drawing.getPatternAttr(markerPattern.solidity, 0, 0.3); + var patternID = trace.uid; + drawing.pattern(sel, 'point', gd, patternID, + patternShape, patternSize, patternSolidity, + undefined, markerPattern.fillmode, + patternBGColor, patternFGColor, patternFGOpacity + ); + } else if(trace.fillcolor) { + sel.call(Color.fill, trace.fillcolor); + } +} + // Same as fillGroupStyle, except in this case the selection may be a transition -drawing.singleFillStyle = function(sel) { +drawing.singleFillStyle = function(sel, gd) { var node = d3.select(sel.node()); var data = node.data(); - var fillcolor = (((data[0] || [])[0] || {}).trace || {}).fillcolor; - if(fillcolor) { - sel.call(Color.fill, fillcolor); - } + var trace = ((data[0] || [])[0] || {}).trace || {}; + setFillStyle(sel, trace, gd); }; -drawing.fillGroupStyle = function(s) { +drawing.fillGroupStyle = function(s, gd) { 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) { - shape.call(Color.fill, d[0].trace.fillcolor); + setFillStyle(shape, d[0].trace, gd); } }); }; @@ -347,12 +365,7 @@ drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) { sel.style(prop, getFullUrl(fullID, gd)) .style(prop + '-opacity', null); - var className2query = function(s) { - return '.' + s.attr('class').replace(/\s/g, '.'); - }; - var k = className2query(d3.select(sel.node().parentNode)) + - '>' + className2query(sel); - fullLayout._gradientUrlQueryParts[k] = 1; + sel.classed('gradient_filled', true); }; /** @@ -559,11 +572,6 @@ drawing.pattern = function(sel, calledBy, gd, patternID, shape, size, solidity, .style('fill-opacity', null); sel.classed('pattern_filled', true); - var className2query = function(s) { - return '.' + s.attr('class').replace(/\s/g, '.'); - }; - var k = className2query(d3.select(sel.node().parentNode)) + '>.pattern_filled'; - fullLayout._patternUrlQueryParts[k] = 1; }; /* @@ -579,9 +587,7 @@ drawing.initGradients = function(gd) { var gradientsGroup = Lib.ensureSingle(fullLayout._defs, 'g', 'gradients'); gradientsGroup.selectAll('linearGradient,radialGradient').remove(); - // initialize stash of query parts filled in Drawing.gradient, - // used to fix URL strings during image exports - fullLayout._gradientUrlQueryParts = {}; + d3.select(gd).selectAll('.gradient_filled').classed('gradient_filled', false); }; drawing.initPatterns = function(gd) { @@ -590,9 +596,7 @@ drawing.initPatterns = function(gd) { var patternsGroup = Lib.ensureSingle(fullLayout._defs, 'g', 'patterns'); patternsGroup.selectAll('pattern').remove(); - // initialize stash of query parts filled in Drawing.pattern, - // used to fix URL strings during image exports - fullLayout._patternUrlQueryParts = {}; + d3.select(gd).selectAll('.pattern_filled').classed('pattern_filled', false); }; drawing.getPatternAttr = function(mp, i, dflt) { diff --git a/src/components/legend/style.js b/src/components/legend/style.js index 9ca9c194af1..72bc7f67c13 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -111,12 +111,16 @@ module.exports = function style(s, gd, legend) { var colorscale = cOpts.colorscale; var reversescale = cOpts.reversescale; - var fillGradient = function(s) { + var fillStyle = function(s) { if(s.size()) { - var gradientID = 'legendfill-' + trace.uid; - Drawing.gradient(s, gd, gradientID, - getGradientDirection(reversescale), - colorscale, 'fill'); + if(showFill) { + Drawing.fillGroupStyle(s, gd); + } else { + var gradientID = 'legendfill-' + trace.uid; + Drawing.gradient(s, gd, gradientID, + getGradientDirection(reversescale), + colorscale, 'fill'); + } } }; @@ -145,7 +149,7 @@ module.exports = function style(s, gd, legend) { fill.enter().append('path').classed('js-fill', true); fill.exit().remove(); fill.attr('d', pathStart + 'h' + itemWidth + 'v6h-' + itemWidth + 'z') - .call(showFill ? Drawing.fillGroupStyle : fillGradient); + .call(fillStyle); if(showLine || showGradientLine) { var lw = boundLineWidth(undefined, trace.line, MAX_LINE_WIDTH, CST_LINE_WIDTH); diff --git a/src/snapshot/tosvg.js b/src/snapshot/tosvg.js index 34ebfdc5f8d..aebd6814f6a 100644 --- a/src/snapshot/tosvg.js +++ b/src/snapshot/tosvg.js @@ -33,7 +33,7 @@ module.exports = function toSVG(gd, format, scale) { var toppaper = fullLayout._toppaper; var width = fullLayout.width; var height = fullLayout.height; - var i, k; + var i; // make background color a rect in the svg, then revert after scraping // all other alterations have been dealt with by properly preparing the svg @@ -106,32 +106,21 @@ module.exports = function toSVG(gd, format, scale) { } }); - var queryParts = []; - if(fullLayout._gradientUrlQueryParts) { - for(k in fullLayout._gradientUrlQueryParts) queryParts.push(k); - } - - if(fullLayout._patternUrlQueryParts) { - for(k in fullLayout._patternUrlQueryParts) queryParts.push(k); - } + svg.selectAll('.gradient_filled,.pattern_filled').each(function() { + var pt = d3.select(this); - if(queryParts.length) { - svg.selectAll(queryParts.join(',')).each(function() { - var pt = d3.select(this); - - // similar to font family styles above, - // we must remove " after the SVG DOM has been serialized - var fill = this.style.fill; - if(fill && fill.indexOf('url(') !== -1) { - pt.style('fill', fill.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB)); - } + // similar to font family styles above, + // we must remove " after the SVG DOM has been serialized + var fill = this.style.fill; + if(fill && fill.indexOf('url(') !== -1) { + pt.style('fill', fill.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB)); + } - var stroke = this.style.stroke; - if(stroke && stroke.indexOf('url(') !== -1) { - pt.style('stroke', stroke.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB)); - } - }); - } + var stroke = this.style.stroke; + if(stroke && stroke.indexOf('url(') !== -1) { + pt.style('stroke', stroke.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB)); + } + }); if(format === 'pdf' || format === 'eps') { // these formats make the extra line MathJax adds around symbols look super thick in some cases diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 634104692df..ad59a800fb1 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -6,6 +6,7 @@ var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplat var colorScaleAttrs = require('../../components/colorscale/attributes'); var fontAttrs = require('../../plots/font_attributes'); var dash = require('../../components/drawing/attributes').dash; +var pattern = require('../../components/drawing/attributes').pattern; var Drawing = require('../../components/drawing'); var constants = require('./constants'); @@ -363,6 +364,7 @@ module.exports = { 'marker color, or marker line color, whichever is available.' ].join(' ') }, + fillpattern: pattern, marker: extendFlat({ symbol: { valType: 'enumerated', diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index 0450df06fa9..0d47c52c216 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -14,6 +14,7 @@ var handleLineDefaults = require('./line_defaults'); var handleLineShapeDefaults = require('./line_shape_defaults'); var handleTextDefaults = require('./text_defaults'); var handleFillColorDefaults = require('./fillcolor_defaults'); +var coercePattern = require('../../lib').coercePattern; module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { @@ -67,6 +68,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(traceOut.fill !== 'none') { handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce); + coercePattern(coerce, 'fillpattern', traceOut.fillcolor, false); } var lineColor = (traceOut.line || {}).color; diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index 88671f53481..7fb3e6c3cc9 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -304,11 +304,11 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition // the points on the axes are the first two points. Otherwise // animations get a little crazy if the number of points changes. transition(ownFillEl3).attr('d', 'M' + pt1 + 'L' + pt0 + 'L' + fullpath.substr(1)) - .call(Drawing.singleFillStyle); + .call(Drawing.singleFillStyle, gd); } else { // fill to self: just join the path to itself transition(ownFillEl3).attr('d', fullpath + 'Z') - .call(Drawing.singleFillStyle); + .call(Drawing.singleFillStyle, gd); } } } else if(tonext) { @@ -320,7 +320,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition // This makes strange results if one path is *not* entirely // inside the other, but then that is a strange usage. transition(tonext).attr('d', fullpath + 'Z' + prevRevpath + 'Z') - .call(Drawing.singleFillStyle); + .call(Drawing.singleFillStyle, gd); } else { // tonextx/y: for now just connect endpoints with lines. This is // the correct behavior if the endpoints are at the same value of @@ -328,7 +328,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition // things depending on whether the new endpoint projects onto the // existing curve or off the end of it transition(tonext).attr('d', fullpath + 'L' + prevRevpath.substr(1) + 'Z') - .call(Drawing.singleFillStyle); + .call(Drawing.singleFillStyle, gd); } trace._polygons = trace._polygons.concat(prevPolygons); } else { diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js index 64bfac1416e..ad3fd5a217b 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); + .call(Drawing.fillGroupStyle, gd); Registry.getComponentMethod('errorbars', 'style')(s); } diff --git a/test/image/baselines/z-scatter_fill_pattern.png b/test/image/baselines/z-scatter_fill_pattern.png new file mode 100644 index 00000000000..36546821c82 Binary files /dev/null and b/test/image/baselines/z-scatter_fill_pattern.png differ diff --git a/test/image/mocks/z-scatter_fill_pattern.json b/test/image/mocks/z-scatter_fill_pattern.json new file mode 100644 index 00000000000..19bd339305c --- /dev/null +++ b/test/image/mocks/z-scatter_fill_pattern.json @@ -0,0 +1,69 @@ +{ + "data": [ + { + "x": [1, 2, 3, 4, 5], + "y": [0.1, 0.3, 0.2, 0.8, 0.7], + "stackgroup": "one", + "fillpattern": { + "fillmode": "overlay", + "shape": "/" + } + }, + { + "x": [1, 2, 3, 4, 5], + "y": [0.3, 0.2, 0.1, 0.1, 0.2], + "stackgroup": "one", + "fillpattern": { + "fillmode": "overlay", + "shape": "\\" + } + }, + { + "x": [1, 2, 3, 4, 5], + "y": [0.8, 0.8, 0.6, 0.7, 0.4], + "stackgroup": "one", + "fillpattern": { + "fillmode": "overlay", + "shape": "." + } + }, + + { + "xaxis": "x2", + "yaxis": "y2", + "x": [1, 2, 3, 4, 5, 6], + "y": [0.1, 0.3, 0.4, 1.1, 0.8, 0.3], + "fill": "tozeroy", + "fillpattern": { + "fillmode": "replace", + "solidity": 0.4, + "size": 12, + "shape": "-" + } + }, + { + "xaxis": "x2", + "yaxis": "y2", + "x": [1, 2, 3, 4, 5, 6], + "y": [0.8, 0.7, 0.1, 0.6, 0.7, 0.8], + "fill": "tonexty", + "fillpattern": { + "fillmode": "replace", + "solidity": 0.4, + "size": 12, + "shape": "|" + } + } + ], + "layout": { + "title": {"text": "Pattern fill for scatter"}, + "width": 800, + "height": 400, + + "grid": { + "rows": 1, + "columns": 2, + "pattern": "independent" + } + } +} diff --git a/test/plot-schema.json b/test/plot-schema.json index a811b800c7c..ddc3f619bef 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -42073,6 +42073,99 @@ "editType": "style", "valType": "color" }, + "fillpattern": { + "bgcolor": { + "arrayOk": true, + "description": "When there is no colorscale sets the color of background pattern fill. Defaults to a `marker.color` background when `fillmode` is *overlay*. Otherwise, defaults to a transparent background.", + "editType": "style", + "valType": "color" + }, + "bgcolorsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `bgcolor`.", + "editType": "none", + "valType": "string" + }, + "description": "Sets the pattern within the marker.", + "editType": "style", + "fgcolor": { + "arrayOk": true, + "description": "When there is no colorscale sets the color of foreground pattern fill. Defaults to a `marker.color` background when `fillmode` is *replace*. Otherwise, defaults to dark grey or white to increase contrast with the `bgcolor`.", + "editType": "style", + "valType": "color" + }, + "fgcolorsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `fgcolor`.", + "editType": "none", + "valType": "string" + }, + "fgopacity": { + "description": "Sets the opacity of the foreground pattern fill. Defaults to a 0.5 when `fillmode` is *overlay*. Otherwise, defaults to 1.", + "editType": "style", + "max": 1, + "min": 0, + "valType": "number" + }, + "fillmode": { + "description": "Determines whether `marker.color` should be used as a default to `bgcolor` or a `fgcolor`.", + "dflt": "replace", + "editType": "style", + "valType": "enumerated", + "values": [ + "replace", + "overlay" + ] + }, + "role": "object", + "shape": { + "arrayOk": true, + "description": "Sets the shape of the pattern fill. By default, no pattern is used for filling the area.", + "dflt": "", + "editType": "style", + "valType": "enumerated", + "values": [ + "", + "/", + "\\", + "x", + "-", + "|", + "+", + "." + ] + }, + "shapesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `shape`.", + "editType": "none", + "valType": "string" + }, + "size": { + "arrayOk": true, + "description": "Sets the size of unit squares of the pattern fill in pixels, which corresponds to the interval of repetition of the pattern.", + "dflt": 8, + "editType": "style", + "min": 0, + "valType": "number" + }, + "sizesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `size`.", + "editType": "none", + "valType": "string" + }, + "solidity": { + "arrayOk": true, + "description": "Sets the solidity of the pattern fill. Solidity is roughly the fraction of the area filled by the pattern. Solidity of 0 shows only the background color without pattern and solidty of 1 shows only the foreground color without pattern.", + "dflt": 0.3, + "editType": "style", + "max": 1, + "min": 0, + "valType": "number" + }, + "soliditysrc": { + "description": "Sets the source reference on Chart Studio Cloud for `solidity`.", + "editType": "none", + "valType": "string" + } + }, "groupnorm": { "description": "Only relevant when `stackgroup` is used, and only the first `groupnorm` found in the `stackgroup` will be used - including if `visible` is *legendonly* but not if it is `false`. Sets the normalization for the sum of this `stackgroup`. With *fraction*, the value of each trace at each location is divided by the sum of all trace values at that location. *percent* is the same but multiplied by 100 to show percentages. If there are multiple subplots, or multiple `stackgroup`s on one subplot, each will be normalized within its own set.", "dflt": "",