Skip to content

Commit

Permalink
Merge pull request #5520 from s417-lama/patternfill
Browse files Browse the repository at this point in the history
Add pattern fill for histogram, bar and barpolar traces
  • Loading branch information
archmoj committed Mar 8, 2021
2 parents c288f57 + 5161478 commit c80fda4
Show file tree
Hide file tree
Showing 9 changed files with 439 additions and 26 deletions.
215 changes: 214 additions & 1 deletion src/components/drawing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,185 @@ drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) {
fullLayout._gradientUrlQueryParts[k] = 1;
};

/**
* pattern: create and apply a pattern fill
*
* @param {object} sel: d3 selection to apply this pattern to
* You can use `selection.call(Drawing.pattern, ...)`
* @param {DOM element} gd: the graph div `sel` is part of
* @param {string} patternID: a unique (within this plot) identifier
* for this pattern, so that we don't create unnecessary definitions
* @param {string} bgcolor: background color for this pattern
* @param {string} fgcolor: foreground color for this pattern
* @param {number} size: size of unit squares for repetition of this pattern
* @param {number} solidity: how solid lines of this pattern are
* @param {string} prop: the property to apply to, 'fill' or 'stroke'
*/
drawing.pattern = function(sel, gd, patternID, shape, bgcolor, fgcolor, size, solidity, prop) {
var fullLayout = gd._fullLayout;
var fullID = 'p' + fullLayout._uid + '-' + patternID;
var width, height;

// linear interpolation
var linearFn = function(x, x0, x1, y0, y1) {
return y0 + (y1 - y0) * (x - x0) / (x1 - x0);
};

var path, linewidth, radius;
var patternTag;
var patternAttrs = {};
switch(shape) {
case '/':
width = size * Math.sqrt(2);
height = size * Math.sqrt(2);
path = 'M-' + (width / 4) + ',' + (height / 4) + 'l' + (width / 2) + ',-' + (height / 2) +
'M0,' + height + 'L' + width + ',0' +
'M' + (width / 4 * 3) + ',' + (height / 4 * 5) + 'l' + (width / 2) + ',-' + (height / 2);
linewidth = solidity * size;
patternTag = 'path';
patternAttrs = {
'd': path,
'stroke': fgcolor,
'stroke-width': linewidth + 'px'
};
break;
case '\\':
width = size * Math.sqrt(2);
height = size * Math.sqrt(2);
path = 'M' + (width / 4 * 3) + ',-' + (height / 4) + 'l' + (width / 2) + ',' + (height / 2) +
'M0,0L' + width + ',' + height +
'M-' + (width / 4) + ',' + (height / 4 * 3) + 'l' + (width / 2) + ',' + (height / 2);
linewidth = solidity * size;
patternTag = 'path';
patternAttrs = {
'd': path,
'stroke': fgcolor,
'stroke-width': linewidth + 'px'
};
break;
case 'x':
width = size * Math.sqrt(2);
height = size * Math.sqrt(2);
path = 'M-' + (width / 4) + ',' + (height / 4) + 'l' + (width / 2) + ',-' + (height / 2) +
'M0,' + height + 'L' + width + ',0' +
'M' + (width / 4 * 3) + ',' + (height / 4 * 5) + 'l' + (width / 2) + ',-' + (height / 2) +
'M' + (width / 4 * 3) + ',-' + (height / 4) + 'l' + (width / 2) + ',' + (height / 2) +
'M0,0L' + width + ',' + height +
'M-' + (width / 4) + ',' + (height / 4 * 3) + 'l' + (width / 2) + ',' + (height / 2);
linewidth = size - size * Math.sqrt(1.0 - solidity);
patternTag = 'path';
patternAttrs = {
'd': path,
'stroke': fgcolor,
'stroke-width': linewidth + 'px'
};
break;
case '|':
width = size;
height = size;
patternTag = 'path';
path = 'M' + (width / 2) + ',0L' + (width / 2) + ',' + height;
linewidth = solidity * size;
patternTag = 'path';
patternAttrs = {
'd': path,
'stroke': fgcolor,
'stroke-width': linewidth + 'px'
};
break;
case '-':
width = size;
height = size;
patternTag = 'path';
path = 'M0,' + (height / 2) + 'L' + width + ',' + (height / 2);
linewidth = solidity * size;
patternTag = 'path';
patternAttrs = {
'd': path,
'stroke': fgcolor,
'stroke-width': linewidth + 'px'
};
break;
case '+':
width = size;
height = size;
patternTag = 'path';
path = 'M' + (width / 2) + ',0L' + (width / 2) + ',' + height +
'M0,' + (height / 2) + 'L' + width + ',' + (height / 2);
linewidth = size - size * Math.sqrt(1.0 - solidity);
patternTag = 'path';
patternAttrs = {
'd': path,
'stroke': fgcolor,
'stroke-width': linewidth + 'px'
};
break;
case '.':
width = size;
height = size;
if(solidity < Math.PI / 4) {
radius = Math.sqrt(solidity * size * size / Math.PI);
} else {
radius = linearFn(solidity, Math.PI / 4, 1.0, size / 2, size / Math.sqrt(2));
}
patternTag = 'circle';
patternAttrs = {
'cx': width / 2,
'cy': height / 2,
'r': radius,
'fill': fgcolor
};
break;
}

var pattern = fullLayout._defs.select('.patterns')
.selectAll('#' + fullID)
.data([shape + ';' + bgcolor + ';' + fgcolor + ';' + size + ';' + solidity], Lib.identity);

pattern.exit().remove();

pattern.enter()
.append('pattern')
.each(function() {
var el = d3.select(this);

el.attr({
'id': fullID,
'width': width + 'px',
'height': height + 'px',
'patternUnits': 'userSpaceOnUse'
});

if(bgcolor) {
var rects = el.selectAll('rect').data([0]);
rects.exit().remove();
rects.enter()
.append('rect')
.attr({
'width': width + 'px',
'height': height + 'px',
'fill': bgcolor
});
}

var patterns = el.selectAll(patternTag).data([0]);
patterns.exit().remove();
patterns.enter()
.append(patternTag)
.attr(patternAttrs);
});

sel.style(prop, getFullUrl(fullID, gd))
.style(prop + '-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;
};

/*
* Make the gradients container and clear out any previous gradients.
* We never collect all the gradients we need in one place,
Expand All @@ -372,6 +551,23 @@ drawing.initGradients = function(gd) {
fullLayout._gradientUrlQueryParts = {};
};

drawing.initPatterns = function(gd) {
var fullLayout = gd._fullLayout;

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 = {};
};

drawing.getPatternAttr = function(mp, i, dflt) {
if(mp && Lib.isArrayOrTypedArray(mp)) {
return i < mp.length ? mp[i] : dflt;
}
return mp;
};

drawing.pointStyle = function(s, trace, gd) {
if(!s.size()) return;
Expand Down Expand Up @@ -477,11 +673,14 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) {

// for legend - arrays will propagate through here, but we don't need
// to treat it as per-point.
if(Array.isArray(gradientType)) {
if(Lib.isArrayOrTypedArray(gradientType)) {
gradientType = gradientType[0];
if(!gradientInfo[gradientType]) gradientType = 0;
}

var markerPattern = marker.pattern;
var patternShape = markerPattern && drawing.getPatternAttr(markerPattern.shape, d.i, '');

if(gradientType && gradientType !== 'none') {
var gradientColor = d.mgc;
if(gradientColor) perPointGradient = true;
Expand All @@ -492,6 +691,20 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) {

drawing.gradient(sel, gd, gradientID, gradientType,
[[0, gradientColor], [1, fillColor]], 'fill');
} else if(patternShape) {
var patternBGColor = drawing.getPatternAttr(markerPattern.bgcolor, d.i, null);
var patternSize = drawing.getPatternAttr(markerPattern.size, d.i, 8);
var patternSolidity = drawing.getPatternAttr(markerPattern.solidity, d.i, 0.3);
var perPointPattern = Lib.isArrayOrTypedArray(markerPattern.shape) ||
Lib.isArrayOrTypedArray(markerPattern.bgcolor) ||
Lib.isArrayOrTypedArray(markerPattern.size) ||
Lib.isArrayOrTypedArray(markerPattern.solidity);

var patternID = trace.uid;
if(perPointPattern) patternID += '-' + d.i;

drawing.pattern(sel, gd, patternID, patternShape, patternBGColor, fillColor,
patternSize, patternSolidity, 'fill');
} else {
Color.fill(sel, fillColor);
}
Expand Down
19 changes: 17 additions & 2 deletions src/components/legend/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,23 @@ module.exports = function style(s, gd, legend) {
var d0 = d[0];
var w = boundLineWidth(d0.mlw, marker.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH);

p.style('stroke-width', w + 'px')
.call(Color.fill, d0.mc || marker.color);
p.style('stroke-width', w + 'px');

var fillColor = d0.mc || marker.color;

var markerPattern = marker.pattern;
var patternShape = markerPattern && Drawing.getPatternAttr(markerPattern.shape, 0, '');

if(patternShape) {
var patternBGColor = Drawing.getPatternAttr(markerPattern.bgcolor, 0, null);
var patternSize = Math.min(12, Drawing.getPatternAttr(markerPattern.size, 0, 8));
var patternSolidity = Drawing.getPatternAttr(markerPattern.solidity, 0, 0.3);
var patternID = 'legend-' + trace.uid;
p.call(Drawing.pattern, gd, patternID, patternShape, patternBGColor,
fillColor, patternSize, patternSolidity, 'fill');
} else {
p.call(Color.fill, fillColor);
}

if(w) Color.stroke(p, d0.mlc || markerLine.color);
});
Expand Down
3 changes: 2 additions & 1 deletion src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,9 @@ function _doPlot(gd, data, layout, config) {
}
}

// clear gradient defs on each .plot call, because we know we'll loop through all traces
// clear gradient and pattern defs on each .plot call, because we know we'll loop through all traces
Drawing.initGradients(gd);
Drawing.initPatterns(gd);

// save initial show spikes once per graph
if(graphWasEmpty) Axes.saveShowSpikeInitial(gd);
Expand Down
47 changes: 25 additions & 22 deletions src/snapshot/tosvg.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
var i, k;

// make background color a rect in the svg, then revert after scraping
// all other alterations have been dealt with by properly preparing the svg
Expand Down Expand Up @@ -106,28 +106,31 @@ module.exports = function toSVG(gd, format, scale) {
}
});


var queryParts = [];
if(fullLayout._gradientUrlQueryParts) {
var queryParts = [];
for(var k in fullLayout._gradientUrlQueryParts) queryParts.push(k);

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));
}

var stroke = this.style.stroke;
if(stroke && stroke.indexOf('url(') !== -1) {
pt.style('stroke', stroke.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB));
}
});
}
for(k in fullLayout._gradientUrlQueryParts) queryParts.push(k);
}

if(fullLayout._patternUrlQueryParts) {
for(k in fullLayout._patternUrlQueryParts) queryParts.push(k);
}

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));
}

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') {
Expand Down
Loading

0 comments on commit c80fda4

Please sign in to comment.