Skip to content

Commit b0e7756

Browse files
committed
Merge pull request #462 from plotly/ternary-fill
Ternary (and scatter) fill
2 parents 2fdfc17 + aca5423 commit b0e7756

File tree

11 files changed

+164
-47
lines changed

11 files changed

+164
-47
lines changed

src/plots/ternary/layout/axis_defaults.js

+12-24
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ module.exports = function supplyLayoutDefaults(containerIn, containerOut, option
2424
return Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt);
2525
}
2626

27-
function coerce2(attr, dflt) {
28-
return Lib.coerce2(containerIn, containerOut, layoutAttributes, attr, dflt);
29-
}
30-
3127
containerOut.type = 'linear'; // no other types allowed for ternary
3228

3329
var dfltColor = coerce('color');
@@ -54,10 +50,8 @@ module.exports = function supplyLayoutDefaults(containerIn, containerOut, option
5450
handleTickValueDefaults(containerIn, containerOut, coerce, 'linear');
5551
handleTickLabelDefaults(containerIn, containerOut, coerce, 'linear',
5652
{ noHover: false });
57-
handleTickMarkDefaults(containerIn, containerOut, coerce, 'linear',
58-
{ outerticks: false });
59-
60-
// TODO - below is a bit repetitious from cartesian still...
53+
handleTickMarkDefaults(containerIn, containerOut, coerce,
54+
{ outerTicks: true });
6155

6256
var showTickLabels = coerce('showticklabels');
6357
if(showTickLabels) {
@@ -72,23 +66,17 @@ module.exports = function supplyLayoutDefaults(containerIn, containerOut, option
7266

7367
coerce('hoverformat');
7468

75-
var lineColor = coerce2('linecolor', dfltColor),
76-
lineWidth = coerce2('linewidth'),
77-
showLine = coerce('showline', !!lineColor || !!lineWidth);
78-
79-
if(!showLine) {
80-
delete containerOut.linecolor;
81-
delete containerOut.linewidth;
69+
var showLine = coerce('showline');
70+
if(showLine) {
71+
coerce('linecolor', dfltColor);
72+
coerce('linewidth');
8273
}
8374

84-
// default grid color is darker here (60%, vs cartesian default ~91%)
85-
// because the grid is not square so the eye needs heavier cues to follow
86-
var gridColor = coerce2('gridcolor', colorMix(dfltColor, options.bgColor, 60).toRgbString()),
87-
gridWidth = coerce2('gridwidth'),
88-
showGridLines = coerce('showgrid', !!gridColor || !!gridWidth);
89-
90-
if(!showGridLines) {
91-
delete containerOut.gridcolor;
92-
delete containerOut.gridwidth;
75+
var showGridLines = coerce('showgrid');
76+
if(showGridLines) {
77+
// default grid color is darker here (60%, vs cartesian default ~91%)
78+
// because the grid is not square so the eye needs heavier cues to follow
79+
coerce('gridcolor', colorMix(dfltColor, options.bgColor, 60).toRgbString());
80+
coerce('gridwidth');
9381
}
9482
};

src/traces/scatter/attributes.js

+19-3
Original file line numberDiff line numberDiff line change
@@ -152,18 +152,34 @@ module.exports = {
152152
},
153153
fill: {
154154
valType: 'enumerated',
155-
values: ['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx'],
155+
values: ['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx', 'toself', 'tonext'],
156156
dflt: 'none',
157157
role: 'style',
158158
description: [
159159
'Sets the area to fill with a solid color.',
160-
'Use with `fillcolor`.'
160+
'Use with `fillcolor` if not *none*.',
161+
'*tozerox* and *tozeroy* fill to x=0 and y=0 respectively.',
162+
'*tonextx* and *tonexty* fill between the endpoints of this',
163+
'trace and the endpoints of the trace before it, connecting those',
164+
'endpoints with straight lines (to make a stacked area graph);',
165+
'if there is no trace before it, they behave like *tozerox* and',
166+
'*tozeroy*.',
167+
'*toself* connects the endpoints of the trace (or each segment',
168+
'of the trace if it has gaps) into a closed shape.',
169+
'*tonext* fills the space between two traces if one completely',
170+
'encloses the other (eg consecutive contour lines), and behaves like',
171+
'*toself* if there is no trace before it. *tonext* should not be',
172+
'used if one trace does not enclose the other.'
161173
].join(' ')
162174
},
163175
fillcolor: {
164176
valType: 'color',
165177
role: 'style',
166-
description: 'Sets the fill color.'
178+
description: [
179+
'Sets the fill color.',
180+
'Defaults to a half-transparent variant of the line color,',
181+
'marker color, or marker line color, whichever is available.'
182+
].join(' ')
167183
},
168184
marker: {
169185
symbol: {

src/traces/scatter/plot.js

+60-17
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,25 @@ module.exports = function plot(gd, plotinfo, cdscatter) {
4141

4242
// BUILD LINES AND FILLS
4343
var prevpath = '',
44-
tozero, tonext, nexttonext;
44+
ownFillEl3, ownFillDir, tonext, nexttonext;
4545

4646
scattertraces.each(function(d) {
4747
var trace = d[0].trace,
4848
line = trace.line,
4949
tr = d3.select(this);
5050
if(trace.visible !== true) return;
5151

52+
ownFillDir = trace.fill.charAt(trace.fill.length - 1);
53+
if(ownFillDir !== 'x' && ownFillDir !== 'y') ownFillDir = '';
54+
5255
d[0].node3 = tr; // store node for tweaking by selectPoints
5356

5457
arraysToCalcdata(d);
5558

5659
if(!subTypes.hasLines(trace) && trace.fill === 'none') return;
5760

5861
var thispath,
62+
thisrevpath,
5963
// fullpath is all paths for this curve, joined together straight
6064
// across gaps, for filling
6165
fullpath = '',
@@ -67,12 +71,12 @@ module.exports = function plot(gd, plotinfo, cdscatter) {
6771
// make the fill-to-zero path now, so it shows behind the line
6872
// fill to next puts the fill associated with one trace
6973
// grouped with the previous
70-
if(trace.fill.substr(0, 6) === 'tozero' ||
74+
if(trace.fill.substr(0, 6) === 'tozero' || trace.fill === 'toself' ||
7175
(trace.fill.substr(0, 2) === 'to' && !prevpath)) {
72-
tozero = tr.append('path')
76+
ownFillEl3 = tr.append('path')
7377
.classed('js-fill', true);
7478
}
75-
else tozero = null;
79+
else ownFillEl3 = null;
7680

7781
// make the fill-to-next path now for the NEXT trace, so it shows
7882
// behind both lines.
@@ -91,7 +95,15 @@ module.exports = function plot(gd, plotinfo, cdscatter) {
9195
}
9296
else if(line.shape === 'spline') {
9397
pathfn = revpathbase = function(pts) {
94-
return Drawing.smoothopen(pts, line.smoothing);
98+
var pLast = pts[pts.length - 1];
99+
if(pts[0][0] === pLast[0] && pts[0][1] === pLast[1]) {
100+
// identical start and end points: treat it as a
101+
// closed curve so we don't get a kink
102+
return Drawing.smoothclosed(pts.slice(1), line.smoothing);
103+
}
104+
else {
105+
return Drawing.smoothopen(pts, line.smoothing);
106+
}
95107
};
96108
}
97109
else {
@@ -102,7 +114,7 @@ module.exports = function plot(gd, plotinfo, cdscatter) {
102114

103115
revpathfn = function(pts) {
104116
// note: this is destructive (reverses pts in place) so can't use pts after this
105-
return 'L' + revpathbase(pts.reverse()).substr(1);
117+
return revpathbase(pts.reverse());
106118
};
107119

108120
var segments = linePoints(d, {
@@ -121,27 +133,58 @@ module.exports = function plot(gd, plotinfo, cdscatter) {
121133
for(var i = 0; i < segments.length; i++) {
122134
var pts = segments[i];
123135
thispath = pathfn(pts);
124-
fullpath += fullpath ? ('L' + thispath.substr(1)) : thispath;
125-
revpath = revpathfn(pts) + revpath;
136+
thisrevpath = revpathfn(pts);
137+
if(!fullpath) {
138+
fullpath = thispath;
139+
revpath = thisrevpath;
140+
}
141+
else if(ownFillDir) {
142+
fullpath += 'L' + thispath.substr(1);
143+
revpath = thisrevpath + ('L' + revpath.substr(1));
144+
}
145+
else {
146+
fullpath += 'Z' + thispath;
147+
revpath = thisrevpath + 'Z' + revpath;
148+
}
126149
if(subTypes.hasLines(trace) && pts.length > 1) {
127150
tr.append('path').classed('js-line', true).attr('d', thispath);
128151
}
129152
}
130-
if(tozero) {
153+
if(ownFillEl3) {
131154
if(pt0 && pt1) {
132-
if(trace.fill.charAt(trace.fill.length - 1) === 'y') {
133-
pt0[1] = pt1[1] = ya.c2p(0, true);
155+
if(ownFillDir) {
156+
if(ownFillDir === 'y') {
157+
pt0[1] = pt1[1] = ya.c2p(0, true);
158+
}
159+
else if(ownFillDir === 'x') {
160+
pt0[0] = pt1[0] = xa.c2p(0, true);
161+
}
162+
163+
// fill to zero: full trace path, plus extension of
164+
// the endpoints to the appropriate axis
165+
ownFillEl3.attr('d', fullpath + 'L' + pt1 + 'L' + pt0 + 'Z');
134166
}
135-
else pt0[0] = pt1[0] = xa.c2p(0, true);
136-
137-
// fill to zero: full trace path, plus extension of
138-
// the endpoints to the appropriate axis
139-
tozero.attr('d', fullpath + 'L' + pt1 + 'L' + pt0 + 'Z');
167+
// fill to self: just join the path to itself
168+
else ownFillEl3.attr('d', fullpath + 'Z');
140169
}
141170
}
142171
else if(trace.fill.substr(0, 6) === 'tonext' && fullpath && prevpath) {
143172
// fill to next: full trace path, plus the previous path reversed
144-
tonext.attr('d', fullpath + prevpath + 'Z');
173+
if(trace.fill === 'tonext') {
174+
// tonext: for use by concentric shapes, like manually constructed
175+
// contours, we just add the two paths closed on themselves.
176+
// This makes strange results if one path is *not* entirely
177+
// inside the other, but then that is a strange usage.
178+
tonext.attr('d', fullpath + 'Z' + prevpath + 'Z');
179+
}
180+
else {
181+
// tonextx/y: for now just connect endpoints with lines. This is
182+
// the correct behavior if the endpoints are at the same value of
183+
// y/x, but if they *aren't*, we should ideally do more complicated
184+
// things depending on whether the new endpoint projects onto the
185+
// existing curve or off the end of it
186+
tonext.attr('d', fullpath + 'L' + prevpath.substr(1) + 'Z');
187+
}
145188
}
146189
prevpath = revpath;
147190
}

src/traces/scatterternary/attributes.js

+15
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ module.exports = {
8080
smoothing: scatterLineAttrs.smoothing
8181
},
8282
connectgaps: scatterAttrs.connectgaps,
83+
fill: extendFlat({}, scatterAttrs.fill, {
84+
values: ['none', 'toself', 'tonext'],
85+
description: [
86+
'Sets the area to fill with a solid color.',
87+
'Use with `fillcolor` if not *none*.',
88+
'scatterternary has a subset of the options available to scatter.',
89+
'*toself* connects the endpoints of the trace (or each segment',
90+
'of the trace if it has gaps) into a closed shape.',
91+
'*tonext* fills the space between two traces if one completely',
92+
'encloses the other (eg consecutive contour lines), and behaves like',
93+
'*toself* if there is no trace before it. *tonext* should not be',
94+
'used if one trace does not enclose the other.'
95+
].join(' ')
96+
}),
97+
fillcolor: scatterAttrs.fillcolor,
8398
marker: {
8499
symbol: scatterMarkerAttrs.symbol,
85100
opacity: scatterMarkerAttrs.opacity,

src/traces/scatterternary/defaults.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var handleMarkerDefaults = require('../scatter/marker_defaults');
1717
var handleLineDefaults = require('../scatter/line_defaults');
1818
var handleLineShapeDefaults = require('../scatter/line_shape_defaults');
1919
var handleTextDefaults = require('../scatter/text_defaults');
20+
var handleFillColorDefaults = require('../scatter/fillcolor_defaults');
2021

2122
var attributes = require('./attributes');
2223

@@ -84,8 +85,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
8485
coerce('marker.maxdisplayed');
8586
}
8687

87-
coerce('hoverinfo', (layout._dataLength === 1) ? 'a+b+c+text' : undefined);
88+
coerce('fill');
89+
if(traceOut.fill !== 'none') {
90+
handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce);
91+
if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce);
92+
}
8893

89-
// until 'fill' and 'fillcolor' are supported
90-
traceOut.fill = 'none';
94+
coerce('hoverinfo', (layout._dataLength === 1) ? 'a+b+c+text' : undefined);
9195
};

test/image/baselines/plot_types.png

10.3 KB
Loading
17.8 KB
Loading

test/image/baselines/ternary_fill.png

34.1 KB
Loading
15.8 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"data":[
3+
{
4+
"x": [1, 2, 3, 1, null, 4, 5, 6],
5+
"y": [2, 3, 2, 2, null, 3, 4, 3],
6+
"fill": "tonext",
7+
"line":{"shape": "spline"}
8+
},
9+
{
10+
"x": [-1, 4, 9, null, 0, 1, 2],
11+
"y": [1, 6, 1, null, 5, 6, 5],
12+
"fill": "tonext"
13+
},
14+
{
15+
"x": [6, 7, 8],
16+
"y": [5, 6, 5],
17+
"fill": "toself"
18+
}
19+
],
20+
"layout":{
21+
"title": "Fill toself and tonext",
22+
"width": 400,
23+
"height": 400
24+
}
25+
}

test/image/mocks/ternary_fill.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"data": [
3+
{
4+
"a": [0.4, 0.4, 0.2, 0.4],
5+
"b":[0.2, 0.4, 0.4, 0.2],
6+
"type": "scatterternary",
7+
"fill": "tonext"
8+
},
9+
{
10+
"a":[0.5, 0.5, 0, 0.5],
11+
"b":[0, 0.5, 0.5, 0],
12+
"type": "scatterternary",
13+
"fill": "tonext"
14+
},
15+
{
16+
"a": [0.8, 0.6, 0.6, 0.8, null, 0.1, 0.1, 0.3, 0.1, null, 0.1, 0.1, 0.3, 0.1],
17+
"b": [0.1, 0.1, 0.3, 0.1, null, 0.8, 0.6, 0.6, 0.8, null, 0.3, 0.1, 0.1, 0.3],
18+
"type": "scatterternary",
19+
"fill": "toself"
20+
}
21+
],
22+
"layout": {
23+
"height": 400,
24+
"width": 500
25+
}
26+
}

0 commit comments

Comments
 (0)