Skip to content

Commit 8f53e11

Browse files
committed
Add pattern fill for bar plots
1 parent d0ddf03 commit 8f53e11

File tree

9 files changed

+397
-22
lines changed

9 files changed

+397
-22
lines changed

src/components/drawing/index.js

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,145 @@ 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} scale: scale 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, scale, solidity, prop) {
372+
var fullLayout = gd._fullLayout;
373+
var fullID = 'p' + fullLayout._uid + '-' + patternID;
374+
var baseSize = 8 * scale;
375+
var width, height;
376+
377+
var path = '';
378+
switch(shape) {
379+
case '/':
380+
width = baseSize * Math.sqrt(2);
381+
height = baseSize * Math.sqrt(2);
382+
path = 'M-1,1l2,-2' +
383+
'M0,' + height + 'L' + width + ',0' +
384+
'M' + (width - 1) + ',' + (height + 1) + 'l2,-2';
385+
break;
386+
case '\\':
387+
width = baseSize * Math.sqrt(2);
388+
height = baseSize * Math.sqrt(2);
389+
path = 'M' + (width - 1) + ',-1l2,2' +
390+
'M0,0L' + width + ',' + height +
391+
'M-1,' + (height - 1) + 'l2,2';
392+
break;
393+
case 'x':
394+
width = baseSize * Math.sqrt(2);
395+
height = baseSize * Math.sqrt(2);
396+
path = 'M-1,1l2,-2' +
397+
'M0,' + height + 'L' + width + ',0' +
398+
'M' + (width - 1) + ',' + (height + 1) + 'l2,-2' +
399+
'M' + (width - 1) + ',-1l2,2' +
400+
'M0,0L' + width + ',' + height +
401+
'M-1,' + (height - 1) + 'l2,2';
402+
break;
403+
case '|':
404+
width = baseSize;
405+
height = baseSize;
406+
path = 'M' + (width / 2) + ',0L' + (width / 2) + ',' + height;
407+
break;
408+
case '-':
409+
width = baseSize;
410+
height = baseSize;
411+
path = 'M0,' + (height / 2) + 'L' + width + ',' + (height / 2);
412+
break;
413+
case '+':
414+
width = baseSize;
415+
height = baseSize;
416+
path = 'M' + (width / 2) + ',0L' + (width / 2) + ',' + height +
417+
'M0,' + (height / 2) + 'L' + width + ',' + (height / 2);
418+
break;
419+
case '+':
420+
width = baseSize;
421+
height = baseSize;
422+
path = 'M' + (width / 2) + ',0L' + (width / 2) + ',' + height +
423+
'M0,' + (height / 2) + 'L' + width + ',' + (height / 2);
424+
break;
425+
case '.':
426+
width = baseSize;
427+
height = baseSize;
428+
break;
429+
}
430+
431+
var pattern = fullLayout._defs.select('.patterns')
432+
.selectAll('#' + fullID)
433+
.data([0]);
434+
435+
pattern.exit().remove();
436+
437+
pattern.enter()
438+
.append('pattern')
439+
.each(function() {
440+
var el = d3.select(this);
441+
442+
el.attr({
443+
'id' : fullID,
444+
'width' : width + 'px',
445+
'height' : height + 'px',
446+
'patternUnits' : 'userSpaceOnUse'
447+
});
448+
449+
var rects = el.selectAll('rect').data([0]);
450+
rects.exit().remove();
451+
if(bgcolor) {
452+
rects.enter()
453+
.append('rect')
454+
.attr({
455+
'width' : width + 'px',
456+
'height' : height + 'px',
457+
'fill' : bgcolor
458+
});
459+
}
460+
461+
if(shape == '.') {
462+
var circles = el.selectAll('circle').data([0]);
463+
circles.exit().remove();
464+
circles.enter()
465+
.append('circle')
466+
.attr({
467+
'cx' : width / 2,
468+
'cy' : height / 2,
469+
'r' : solidity,
470+
'fill' : fgcolor
471+
});
472+
} else {
473+
var paths = el.selectAll('path').data([0]);
474+
paths.exit().remove();
475+
paths.enter()
476+
.append('path')
477+
.attr({
478+
'd' : path,
479+
'stroke' : fgcolor,
480+
'stroke-width' : solidity + 'px'
481+
});
482+
}
483+
});
484+
485+
sel.style(prop, getFullUrl(fullID, gd))
486+
.style(prop + '-opacity', null);
487+
488+
sel.classed("pattern_filled", true);
489+
var className2query = function(s) {
490+
return '.' + s.attr('class').replace(/\s/g, '.');
491+
};
492+
var k = className2query(d3.select(sel.node().parentNode)) + '>.pattern_filled';
493+
fullLayout._patternUrlQueryParts[k] = 1;
494+
};
495+
357496
/*
358497
* Make the gradients container and clear out any previous gradients.
359498
* We never collect all the gradients we need in one place,
@@ -372,6 +511,16 @@ drawing.initGradients = function(gd) {
372511
fullLayout._gradientUrlQueryParts = {};
373512
};
374513

514+
drawing.initPatterns = function(gd) {
515+
var fullLayout = gd._fullLayout;
516+
517+
var patternsGroup = Lib.ensureSingle(fullLayout._defs, 'g', 'patterns');
518+
patternsGroup.selectAll('pattern').remove();
519+
520+
// initialize stash of query parts filled in Drawing.pattern,
521+
// used to fix URL strings during image exports
522+
fullLayout._patternUrlQueryParts = {};
523+
};
375524

376525
drawing.pointStyle = function(s, trace, gd) {
377526
if(!s.size()) return;
@@ -482,6 +631,16 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) {
482631
if(!gradientInfo[gradientType]) gradientType = 0;
483632
}
484633

634+
var getPatternAttr = function(mp, i, dflt) {
635+
if (mp && Array.isArray(mp)) {
636+
if (i < mp.length) return mp[i];
637+
else return dflt;
638+
}
639+
return mp;
640+
};
641+
var markerPattern = marker.patternfill;
642+
var patternShape = markerPattern && getPatternAttr(markerPattern.shape, d.i, '');
643+
485644
if(gradientType && gradientType !== 'none') {
486645
var gradientColor = d.mgc;
487646
if(gradientColor) perPointGradient = true;
@@ -492,6 +651,20 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) {
492651

493652
drawing.gradient(sel, gd, gradientID, gradientType,
494653
[[0, gradientColor], [1, fillColor]], 'fill');
654+
} else if(patternShape) {
655+
var patternBGColor = getPatternAttr(markerPattern.bgcolor , d.i, null);
656+
var patternScale = getPatternAttr(markerPattern.scale , d.i, 1);
657+
var patternSolidity = getPatternAttr(markerPattern.solidity, d.i, 1);
658+
var perPointPattern = Array.isArray(markerPattern.shape) ||
659+
Array.isArray(markerPattern.bgcolor) ||
660+
Array.isArray(markerPattern.scale) ||
661+
Array.isArray(markerPattern.solidity);
662+
663+
var patternID = trace.uid;
664+
if(perPointPattern) patternID += '-' + d.i;
665+
666+
drawing.pattern(sel, gd, patternID, patternShape, patternBGColor, fillColor,
667+
patternScale, patternSolidity, 'fill');
495668
} else {
496669
Color.fill(sel, fillColor);
497670
}

src/components/legend/style.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,30 @@ 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 getPatternAttr = function(mp, dflt) {
365+
if (mp && Array.isArray(mp)) {
366+
if (mp.length > 0) return mp[0];
367+
else return dflt;
368+
}
369+
return mp;
370+
};
371+
var markerPattern = marker.patternfill;
372+
var patternShape = markerPattern && getPatternAttr(markerPattern.shape, '');
373+
374+
if(patternShape) {
375+
var patternBGColor = getPatternAttr(markerPattern.bgcolor , null);
376+
var patternScale = getPatternAttr(markerPattern.scale , 1);
377+
var patternSolidity = getPatternAttr(markerPattern.solidity, 1);
378+
var patternID = trace.uid;
379+
p.call(Drawing.pattern, gd, patternID, patternShape, patternBGColor,
380+
fillColor, patternScale, patternSolidity, 'fill');
381+
} else {
382+
p.call(Color.fill, fillColor);
383+
}
362384

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

src/plot_api/plot_api.js

Lines changed: 2 additions & 1 deletion
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

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = [];
112111
for(var k in fullLayout._gradientUrlQueryParts) queryParts.push(k);
112+
}
113113

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-
}
114+
if(fullLayout._patternUrlQueryParts) {
115+
for(var 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') {

src/traces/bar/attributes.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,51 @@ var marker = extendFlat({
3939
max: 1,
4040
editType: 'style',
4141
description: 'Sets the opacity of the bars.'
42+
},
43+
patternfill: {
44+
shape: {
45+
valType: 'enumerated',
46+
values: ['', '/', '\\', 'x', '-', '|', '+', '.'],
47+
dflt: '',
48+
arrayOk: true,
49+
editType: 'style',
50+
description: [
51+
'Sets the shape of the pattern fill.',
52+
'By default, no pattern is used for filling the area.',
53+
].join(' ')
54+
},
55+
bgcolor: {
56+
valType: 'color',
57+
arrayOk: true,
58+
editType: 'style',
59+
description: [
60+
'Sets the background color of the pattern fill.',
61+
'Defaults to a transparent background.',
62+
].join(' ')
63+
},
64+
scale: {
65+
valType: 'number',
66+
min: 0,
67+
dflt: 1,
68+
arrayOk: true,
69+
editType: 'style',
70+
description: [
71+
'Sets the scale of the pattern fill.',
72+
'Defaults to 1.',
73+
].join(' ')
74+
},
75+
solidity: {
76+
valType: 'number',
77+
min: 0,
78+
dflt: 1,
79+
arrayOk: true,
80+
editType: 'style',
81+
description: [
82+
'Sets the solidity of the pattern fill.',
83+
'Defaults to 1.',
84+
].join(' ')
85+
},
86+
editType: 'style'
4287
}
4388
});
4489

src/traces/bar/style_defaults.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, default
2323

2424
coerce('marker.line.width');
2525
coerce('marker.opacity');
26+
coerce('marker.patternfill.shape');
27+
coerce('marker.patternfill.bgcolor');
28+
coerce('marker.patternfill.scale');
29+
coerce('marker.patternfill.solidity');
2630
coerce('selected.marker.color');
2731
coerce('unselected.marker.color');
2832
};
119 KB
Loading

0 commit comments

Comments
 (0)