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 pattern fill for histogram, bar and barpolar traces #5520

Merged
merged 6 commits into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions src/components/drawing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,184 @@ 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;

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 {
// linear interpolation
var linearFn = function(x, x0, x1, y0, y1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you declare this function outside the switch case please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5161478

return y0 + (y1 - y0) * (x - x0) / (x1 - x0);
};
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]);
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved

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 +550,16 @@ 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.pointStyle = function(s, trace, gd) {
if(!s.size()) return;
Expand Down Expand Up @@ -482,6 +670,16 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) {
if(!gradientInfo[gradientType]) gradientType = 0;
}

var getPatternAttr = function(mp, i, dflt) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking:
It looks like you could also move this function above drawing.pointStyle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5161478

if(mp && Array.isArray(mp)) {
if(i < mp.length) return mp[i];
else return dflt;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about simply

return i < mp.length ? mp[i] : dflt;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5161478

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

if(gradientType && gradientType !== 'none') {
var gradientColor = d.mgc;
if(gradientColor) perPointGradient = true;
Expand All @@ -492,6 +690,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 = getPatternAttr(markerPattern.bgcolor, d.i, null);
var patternSize = getPatternAttr(markerPattern.size, d.i, 1);
var patternSolidity = getPatternAttr(markerPattern.solidity, d.i, 1);
var perPointPattern = Array.isArray(markerPattern.shape) ||
Array.isArray(markerPattern.bgcolor) ||
Array.isArray(markerPattern.size) ||
Array.isArray(markerPattern.solidity);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use Lib.isArrayOrTypedArray here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5161478


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
26 changes: 24 additions & 2 deletions src/components/legend/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,30 @@ 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 getPatternAttr = function(mp, dflt) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function looks quite similar to getPatternAttr in src/components/drawing/index.js.
Could you explain the difference between two?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getPatternAttr in legends does not require any index because the first element of the array is always used, but the same getPatternAttr function can be used for both. If we unify them, which location do you think is appropriate for putting the common getPatternAttr fn?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could go in drawing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! Fixed in 5161478

if(mp && Array.isArray(mp)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we handle typedArrays here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5161478

if(mp.length > 0) return mp[0];
else return dflt;
}
return mp;
};
var markerPattern = marker.pattern;
var patternShape = markerPattern && getPatternAttr(markerPattern.shape, '');

if(patternShape) {
var patternBGColor = getPatternAttr(markerPattern.bgcolor, null);
var patternSize = getPatternAttr(markerPattern.size, 1);
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved
var patternSolidity = getPatternAttr(markerPattern.solidity, 1);
var patternID = 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use slice instead of the for loop here and below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this regard, I couldn't see the point in storing query parts as a dict (_gradientUrlQueryParts) in the first place. Seems like only keys are used and values are not. Why don't we make it just an array?

Also, I suspect that it makes things more complicated to store query parts globally. How about just putting a dedicated class name for pattern fill (and gradient) and collecting them later? Actually, I already did the similar thing (below).

in components/drawing/index.js:

    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;

I think it is enough to put .pattern_filled class and collect them by using a query for .pattern_filled when exporting svg. Maybe I am missing some points; is there any problem with it?

}

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