Skip to content

Commit c80fda4

Browse files
authored
Merge pull request #5520 from s417-lama/patternfill
Add pattern fill for histogram, bar and barpolar traces
2 parents c288f57 + 5161478 commit c80fda4

File tree

9 files changed

+439
-26
lines changed

9 files changed

+439
-26
lines changed

src/components/drawing/index.js

+214-1
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,185 @@ drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) {
354354
fullLayout._gradientUrlQueryParts[k] = 1;
355355
};
356356

357+
/**
358+
* pattern: create and apply a pattern fill
359+
*
360+
* @param {object} sel: d3 selection to apply this pattern to
361+
* You can use `selection.call(Drawing.pattern, ...)`
362+
* @param {DOM element} gd: the graph div `sel` is part of
363+
* @param {string} patternID: a unique (within this plot) identifier
364+
* for this pattern, so that we don't create unnecessary definitions
365+
* @param {string} bgcolor: background color for this pattern
366+
* @param {string} fgcolor: foreground color for this pattern
367+
* @param {number} size: size of unit squares for repetition of this pattern
368+
* @param {number} solidity: how solid lines of this pattern are
369+
* @param {string} prop: the property to apply to, 'fill' or 'stroke'
370+
*/
371+
drawing.pattern = function(sel, gd, patternID, shape, bgcolor, fgcolor, size, solidity, prop) {
372+
var fullLayout = gd._fullLayout;
373+
var fullID = 'p' + fullLayout._uid + '-' + patternID;
374+
var width, height;
375+
376+
// linear interpolation
377+
var linearFn = function(x, x0, x1, y0, y1) {
378+
return y0 + (y1 - y0) * (x - x0) / (x1 - x0);
379+
};
380+
381+
var path, linewidth, radius;
382+
var patternTag;
383+
var patternAttrs = {};
384+
switch(shape) {
385+
case '/':
386+
width = size * Math.sqrt(2);
387+
height = size * Math.sqrt(2);
388+
path = 'M-' + (width / 4) + ',' + (height / 4) + 'l' + (width / 2) + ',-' + (height / 2) +
389+
'M0,' + height + 'L' + width + ',0' +
390+
'M' + (width / 4 * 3) + ',' + (height / 4 * 5) + 'l' + (width / 2) + ',-' + (height / 2);
391+
linewidth = solidity * size;
392+
patternTag = 'path';
393+
patternAttrs = {
394+
'd': path,
395+
'stroke': fgcolor,
396+
'stroke-width': linewidth + 'px'
397+
};
398+
break;
399+
case '\\':
400+
width = size * Math.sqrt(2);
401+
height = size * Math.sqrt(2);
402+
path = 'M' + (width / 4 * 3) + ',-' + (height / 4) + 'l' + (width / 2) + ',' + (height / 2) +
403+
'M0,0L' + width + ',' + height +
404+
'M-' + (width / 4) + ',' + (height / 4 * 3) + 'l' + (width / 2) + ',' + (height / 2);
405+
linewidth = solidity * size;
406+
patternTag = 'path';
407+
patternAttrs = {
408+
'd': path,
409+
'stroke': fgcolor,
410+
'stroke-width': linewidth + 'px'
411+
};
412+
break;
413+
case 'x':
414+
width = size * Math.sqrt(2);
415+
height = size * Math.sqrt(2);
416+
path = 'M-' + (width / 4) + ',' + (height / 4) + 'l' + (width / 2) + ',-' + (height / 2) +
417+
'M0,' + height + 'L' + width + ',0' +
418+
'M' + (width / 4 * 3) + ',' + (height / 4 * 5) + 'l' + (width / 2) + ',-' + (height / 2) +
419+
'M' + (width / 4 * 3) + ',-' + (height / 4) + 'l' + (width / 2) + ',' + (height / 2) +
420+
'M0,0L' + width + ',' + height +
421+
'M-' + (width / 4) + ',' + (height / 4 * 3) + 'l' + (width / 2) + ',' + (height / 2);
422+
linewidth = size - size * Math.sqrt(1.0 - solidity);
423+
patternTag = 'path';
424+
patternAttrs = {
425+
'd': path,
426+
'stroke': fgcolor,
427+
'stroke-width': linewidth + 'px'
428+
};
429+
break;
430+
case '|':
431+
width = size;
432+
height = size;
433+
patternTag = 'path';
434+
path = 'M' + (width / 2) + ',0L' + (width / 2) + ',' + height;
435+
linewidth = solidity * size;
436+
patternTag = 'path';
437+
patternAttrs = {
438+
'd': path,
439+
'stroke': fgcolor,
440+
'stroke-width': linewidth + 'px'
441+
};
442+
break;
443+
case '-':
444+
width = size;
445+
height = size;
446+
patternTag = 'path';
447+
path = 'M0,' + (height / 2) + 'L' + width + ',' + (height / 2);
448+
linewidth = solidity * size;
449+
patternTag = 'path';
450+
patternAttrs = {
451+
'd': path,
452+
'stroke': fgcolor,
453+
'stroke-width': linewidth + 'px'
454+
};
455+
break;
456+
case '+':
457+
width = size;
458+
height = size;
459+
patternTag = 'path';
460+
path = 'M' + (width / 2) + ',0L' + (width / 2) + ',' + height +
461+
'M0,' + (height / 2) + 'L' + width + ',' + (height / 2);
462+
linewidth = size - size * Math.sqrt(1.0 - solidity);
463+
patternTag = 'path';
464+
patternAttrs = {
465+
'd': path,
466+
'stroke': fgcolor,
467+
'stroke-width': linewidth + 'px'
468+
};
469+
break;
470+
case '.':
471+
width = size;
472+
height = size;
473+
if(solidity < Math.PI / 4) {
474+
radius = Math.sqrt(solidity * size * size / Math.PI);
475+
} else {
476+
radius = linearFn(solidity, Math.PI / 4, 1.0, size / 2, size / Math.sqrt(2));
477+
}
478+
patternTag = 'circle';
479+
patternAttrs = {
480+
'cx': width / 2,
481+
'cy': height / 2,
482+
'r': radius,
483+
'fill': fgcolor
484+
};
485+
break;
486+
}
487+
488+
var pattern = fullLayout._defs.select('.patterns')
489+
.selectAll('#' + fullID)
490+
.data([shape + ';' + bgcolor + ';' + fgcolor + ';' + size + ';' + solidity], Lib.identity);
491+
492+
pattern.exit().remove();
493+
494+
pattern.enter()
495+
.append('pattern')
496+
.each(function() {
497+
var el = d3.select(this);
498+
499+
el.attr({
500+
'id': fullID,
501+
'width': width + 'px',
502+
'height': height + 'px',
503+
'patternUnits': 'userSpaceOnUse'
504+
});
505+
506+
if(bgcolor) {
507+
var rects = el.selectAll('rect').data([0]);
508+
rects.exit().remove();
509+
rects.enter()
510+
.append('rect')
511+
.attr({
512+
'width': width + 'px',
513+
'height': height + 'px',
514+
'fill': bgcolor
515+
});
516+
}
517+
518+
var patterns = el.selectAll(patternTag).data([0]);
519+
patterns.exit().remove();
520+
patterns.enter()
521+
.append(patternTag)
522+
.attr(patternAttrs);
523+
});
524+
525+
sel.style(prop, getFullUrl(fullID, gd))
526+
.style(prop + '-opacity', null);
527+
528+
sel.classed('pattern_filled', true);
529+
var className2query = function(s) {
530+
return '.' + s.attr('class').replace(/\s/g, '.');
531+
};
532+
var k = className2query(d3.select(sel.node().parentNode)) + '>.pattern_filled';
533+
fullLayout._patternUrlQueryParts[k] = 1;
534+
};
535+
357536
/*
358537
* Make the gradients container and clear out any previous gradients.
359538
* We never collect all the gradients we need in one place,
@@ -372,6 +551,23 @@ drawing.initGradients = function(gd) {
372551
fullLayout._gradientUrlQueryParts = {};
373552
};
374553

554+
drawing.initPatterns = function(gd) {
555+
var fullLayout = gd._fullLayout;
556+
557+
var patternsGroup = Lib.ensureSingle(fullLayout._defs, 'g', 'patterns');
558+
patternsGroup.selectAll('pattern').remove();
559+
560+
// initialize stash of query parts filled in Drawing.pattern,
561+
// used to fix URL strings during image exports
562+
fullLayout._patternUrlQueryParts = {};
563+
};
564+
565+
drawing.getPatternAttr = function(mp, i, dflt) {
566+
if(mp && Lib.isArrayOrTypedArray(mp)) {
567+
return i < mp.length ? mp[i] : dflt;
568+
}
569+
return mp;
570+
};
375571

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

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

681+
var markerPattern = marker.pattern;
682+
var patternShape = markerPattern && drawing.getPatternAttr(markerPattern.shape, d.i, '');
683+
485684
if(gradientType && gradientType !== 'none') {
486685
var gradientColor = d.mgc;
487686
if(gradientColor) perPointGradient = true;
@@ -492,6 +691,20 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) {
492691

493692
drawing.gradient(sel, gd, gradientID, gradientType,
494693
[[0, gradientColor], [1, fillColor]], 'fill');
694+
} else if(patternShape) {
695+
var patternBGColor = drawing.getPatternAttr(markerPattern.bgcolor, d.i, null);
696+
var patternSize = drawing.getPatternAttr(markerPattern.size, d.i, 8);
697+
var patternSolidity = drawing.getPatternAttr(markerPattern.solidity, d.i, 0.3);
698+
var perPointPattern = Lib.isArrayOrTypedArray(markerPattern.shape) ||
699+
Lib.isArrayOrTypedArray(markerPattern.bgcolor) ||
700+
Lib.isArrayOrTypedArray(markerPattern.size) ||
701+
Lib.isArrayOrTypedArray(markerPattern.solidity);
702+
703+
var patternID = trace.uid;
704+
if(perPointPattern) patternID += '-' + d.i;
705+
706+
drawing.pattern(sel, gd, patternID, patternShape, patternBGColor, fillColor,
707+
patternSize, patternSolidity, 'fill');
495708
} else {
496709
Color.fill(sel, fillColor);
497710
}

src/components/legend/style.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,23 @@ module.exports = function style(s, gd, legend) {
357357
var d0 = d[0];
358358
var w = boundLineWidth(d0.mlw, marker.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH);
359359

360-
p.style('stroke-width', w + 'px')
361-
.call(Color.fill, d0.mc || marker.color);
360+
p.style('stroke-width', w + 'px');
361+
362+
var fillColor = d0.mc || marker.color;
363+
364+
var markerPattern = marker.pattern;
365+
var patternShape = markerPattern && Drawing.getPatternAttr(markerPattern.shape, 0, '');
366+
367+
if(patternShape) {
368+
var patternBGColor = Drawing.getPatternAttr(markerPattern.bgcolor, 0, null);
369+
var patternSize = Math.min(12, Drawing.getPatternAttr(markerPattern.size, 0, 8));
370+
var patternSolidity = Drawing.getPatternAttr(markerPattern.solidity, 0, 0.3);
371+
var patternID = 'legend-' + trace.uid;
372+
p.call(Drawing.pattern, gd, patternID, patternShape, patternBGColor,
373+
fillColor, patternSize, patternSolidity, 'fill');
374+
} else {
375+
p.call(Color.fill, fillColor);
376+
}
362377

363378
if(w) Color.stroke(p, d0.mlc || markerLine.color);
364379
});

src/plot_api/plot_api.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,9 @@ function _doPlot(gd, data, layout, config) {
144144
}
145145
}
146146

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

150151
// save initial show spikes once per graph
151152
if(graphWasEmpty) Axes.saveShowSpikeInitial(gd);

src/snapshot/tosvg.js

+25-22
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ module.exports = function toSVG(gd, format, scale) {
3333
var toppaper = fullLayout._toppaper;
3434
var width = fullLayout.width;
3535
var height = fullLayout.height;
36-
var i;
36+
var i, k;
3737

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

109-
109+
var queryParts = [];
110110
if(fullLayout._gradientUrlQueryParts) {
111-
var queryParts = [];
112-
for(var k in fullLayout._gradientUrlQueryParts) queryParts.push(k);
113-
114-
if(queryParts.length) {
115-
svg.selectAll(queryParts.join(',')).each(function() {
116-
var pt = d3.select(this);
117-
118-
// similar to font family styles above,
119-
// we must remove " after the SVG DOM has been serialized
120-
var fill = this.style.fill;
121-
if(fill && fill.indexOf('url(') !== -1) {
122-
pt.style('fill', fill.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB));
123-
}
124-
125-
var stroke = this.style.stroke;
126-
if(stroke && stroke.indexOf('url(') !== -1) {
127-
pt.style('stroke', stroke.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB));
128-
}
129-
});
130-
}
111+
for(k in fullLayout._gradientUrlQueryParts) queryParts.push(k);
112+
}
113+
114+
if(fullLayout._patternUrlQueryParts) {
115+
for(k in fullLayout._patternUrlQueryParts) queryParts.push(k);
116+
}
117+
118+
if(queryParts.length) {
119+
svg.selectAll(queryParts.join(',')).each(function() {
120+
var pt = d3.select(this);
121+
122+
// similar to font family styles above,
123+
// we must remove " after the SVG DOM has been serialized
124+
var fill = this.style.fill;
125+
if(fill && fill.indexOf('url(') !== -1) {
126+
pt.style('fill', fill.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB));
127+
}
128+
129+
var stroke = this.style.stroke;
130+
if(stroke && stroke.indexOf('url(') !== -1) {
131+
pt.style('stroke', stroke.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB));
132+
}
133+
});
131134
}
132135

133136
if(format === 'pdf' || format === 'eps') {

0 commit comments

Comments
 (0)