Skip to content

Commit fe709d0

Browse files
authoredAug 16, 2018
Merge pull request #2910 from plotly/gradient-colorbar
Use new multistop gradients for continuous colorbars
2 parents 7bba266 + c17b9bc commit fe709d0

12 files changed

+138
-110
lines changed
 

Diff for: ‎src/components/colorbar/connect.js

+2-14
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
'use strict';
1111

12-
var Colorscale = require('../colorscale');
1312
var drawColorbar = require('./draw');
1413

1514
/**
@@ -48,20 +47,9 @@ module.exports = function connectColorbar(gd, cd, moduleOpts) {
4847
gd._fullLayout._infolayer.selectAll('.' + cbId).remove();
4948
if(!container || !container.showscale) return;
5049

51-
var zmin = container[moduleOpts.min];
52-
var zmax = container[moduleOpts.max];
53-
5450
var cb = cd[0].t.cb = drawColorbar(gd, cbId);
55-
var sclFunc = Colorscale.makeColorScaleFunc(
56-
Colorscale.extractScale(
57-
container.colorscale,
58-
zmin,
59-
zmax
60-
),
61-
{ noNumericCheck: true }
62-
);
6351

64-
cb.fillcolor(sclFunc)
65-
.filllevels({start: zmin, end: zmax, size: (zmax - zmin) / 254})
52+
cb.fillgradient(container.colorscale)
53+
.zrange([container[moduleOpts.min], container[moduleOpts.max]])
6654
.options(container.colorbar)();
6755
};

Diff for: ‎src/components/colorbar/draw.js

+36-19
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ module.exports = function draw(gd, id) {
3939
// opts: options object, containing everything from attributes
4040
// plus a few others that are the equivalent of the colorbar "data"
4141
var opts = {};
42-
Object.keys(attributes).forEach(function(k) {
42+
for(var k in attributes) {
4343
opts[k] = null;
44-
});
44+
}
4545
// fillcolor can be a d3 scale, domain is z values, range is colors
4646
// or leave it out for no fill,
4747
// or set to a string constant for single-color fill
@@ -57,17 +57,23 @@ module.exports = function draw(gd, id) {
5757
// contour map) if this is omitted, fillcolors will be
5858
// evaluated halfway between levels
5959
opts.filllevels = null;
60+
// for continuous colorscales: fill with a gradient instead of explicit levels
61+
// value should be the colorscale [[0, c0], [v1, c1], ..., [1, cEnd]]
62+
opts.fillgradient = null;
63+
// when using a gradient, we need the data range specified separately
64+
opts.zrange = null;
6065

6166
function component() {
6267
var fullLayout = gd._fullLayout,
6368
gs = fullLayout._size;
6469
if((typeof opts.fillcolor !== 'function') &&
65-
(typeof opts.line.color !== 'function')) {
70+
(typeof opts.line.color !== 'function') &&
71+
!opts.fillgradient) {
6672
fullLayout._infolayer.selectAll('g.' + id).remove();
6773
return;
6874
}
69-
var zrange = d3.extent(((typeof opts.fillcolor === 'function') ?
70-
opts.fillcolor : opts.line.color).domain());
75+
var zrange = opts.zrange || (d3.extent(((typeof opts.fillcolor === 'function') ?
76+
opts.fillcolor : opts.line.color).domain()));
7177
var linelevels = [];
7278
var filllevels = [];
7379
var linecolormap = typeof opts.line.color === 'function' ?
@@ -87,7 +93,10 @@ module.exports = function draw(gd, id) {
8793
if(l > zr0 && l < zr1) linelevels.push(l);
8894
}
8995

90-
if(typeof opts.fillcolor === 'function') {
96+
if(opts.fillgradient) {
97+
filllevels = [0];
98+
}
99+
else if(typeof opts.fillcolor === 'function') {
91100
if(opts.filllevels) {
92101
l0 = opts.filllevels.end + opts.filllevels.size / 100;
93102
ls = opts.filllevels.size;
@@ -358,6 +367,12 @@ module.exports = function draw(gd, id) {
358367
.classed(cn.cbfill, true)
359368
.style('stroke', 'none');
360369
fills.exit().remove();
370+
371+
var zBounds = zrange
372+
.map(cbAxisOut.c2p)
373+
.map(Math.round)
374+
.sort(function(a, b) { return a - b; });
375+
361376
fills.each(function(d, i) {
362377
var z = [
363378
(i === 0) ? zrange[0] :
@@ -370,25 +385,27 @@ module.exports = function draw(gd, id) {
370385

371386
// offset the side adjoining the next rectangle so they
372387
// overlap, to prevent antialiasing gaps
373-
if(i !== filllevels.length - 1) {
374-
z[1] += (z[1] > z[0]) ? 1 : -1;
375-
}
376-
377-
378-
// Tinycolor can't handle exponents and
379-
// at this scale, removing it makes no difference.
380-
var colorString = fillcolormap(d).replace('e-', ''),
381-
opaqueColor = tinycolor(colorString).toHexString();
388+
z[1] = Lib.constrain(z[1] + (z[1] > z[0]) ? 1 : -1, zBounds[0], zBounds[1]);
382389

383390
// Colorbar cannot currently support opacities so we
384391
// use an opaque fill even when alpha channels present
385-
d3.select(this).attr({
392+
var fillEl = d3.select(this).attr({
386393
x: xLeft,
387394
width: Math.max(thickPx, 2),
388395
y: d3.min(z),
389396
height: Math.max(d3.max(z) - d3.min(z), 2),
390-
fill: opaqueColor
391397
});
398+
399+
if(opts.fillgradient) {
400+
Drawing.gradient(fillEl, gd, id, 'vertical',
401+
opts.fillgradient, 'fill');
402+
}
403+
else {
404+
// Tinycolor can't handle exponents and
405+
// at this scale, removing it makes no difference.
406+
var colorString = fillcolormap(d).replace('e-', '');
407+
fillEl.attr('fill', tinycolor(colorString).toHexString());
408+
}
392409
});
393410

394411
var lines = container.select('.cblines')
@@ -650,13 +667,13 @@ module.exports = function draw(gd, id) {
650667

651668
// or use .options to set multiple options at once via a dictionary
652669
component.options = function(o) {
653-
Object.keys(o).forEach(function(name) {
670+
for(var name in o) {
654671
// in case something random comes through
655672
// that's not an option, ignore it
656673
if(typeof component[name] === 'function') {
657674
component[name](o[name]);
658675
}
659-
});
676+
}
660677
return component;
661678
};
662679

Diff for: ‎src/traces/contour/colorbar.js

+10-17
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,25 @@ var endPlus = require('./end_plus');
1616

1717

1818
module.exports = function colorbar(gd, cd) {
19-
var trace = cd[0].trace,
20-
cbId = 'cb' + trace.uid;
19+
var trace = cd[0].trace;
20+
var cbId = 'cb' + trace.uid;
2121

2222
gd._fullLayout._infolayer.selectAll('.' + cbId).remove();
2323

2424
if(!trace.showscale) return;
2525

26-
var cb = drawColorbar(gd, cbId);
27-
cd[0].t.cb = cb;
26+
var cb = cd[0].t.cb = drawColorbar(gd, cbId);
2827

29-
var contours = trace.contours,
30-
line = trace.line,
31-
cs = contours.size || 1,
32-
coloring = contours.coloring;
28+
var contours = trace.contours;
29+
var line = trace.line;
30+
var cs = contours.size || 1;
31+
var coloring = contours.coloring;
3332

3433
var colorMap = makeColorMap(trace, {isColorbar: true});
3534

36-
if(coloring === 'heatmap') {
37-
cb.filllevels({
38-
start: trace.zmin,
39-
end: trace.zmax,
40-
size: (trace.zmax - trace.zmin) / 254
41-
});
42-
}
43-
44-
cb.fillcolor((coloring === 'fill' || coloring === 'heatmap') ? colorMap : '')
35+
cb.fillgradient(coloring === 'heatmap' ? trace.colorscale : '')
36+
.zrange(coloring === 'heatmap' ? [trace.zmin, trace.zmax] : '')
37+
.fillcolor((coloring === 'fill') ? colorMap : '')
4538
.line({
4639
color: coloring === 'lines' ? colorMap : line.color,
4740
width: contours.showlines !== false ? line.width : 0,

Diff for: ‎test/image/baselines/16.png

45 Bytes
Loading

Diff for: ‎test/image/baselines/colorbar_enumerated_ticks.png

4 Bytes
Loading

Diff for: ‎test/image/baselines/colorscale_constraint.png

-17 Bytes
Loading
20 Bytes
Loading

Diff for: ‎test/image/baselines/gl2d_parcoords_1.png

30 Bytes
Loading

Diff for: ‎test/image/baselines/gl2d_parcoords_2.png

469 Bytes
Loading

Diff for: ‎test/image/baselines/hist2d_summed.png

-13 Bytes
Loading

Diff for: ‎test/image/baselines/zsmooth_methods.png

79 Bytes
Loading

Diff for: ‎test/jasmine/tests/colorbar_test.js

+90-60
Original file line numberDiff line numberDiff line change
@@ -62,37 +62,47 @@ describe('Test colorbar:', function() {
6262
.then(done);
6363
});
6464

65-
// let heatmap stand in for all traces with trace.{showscale, colorbar}
66-
// also test impliedEdits for colorbars at the trace root
67-
it('can show and hide heatmap colorbars and sizes correctly with automargin', function(done) {
68-
function assertCB(msg, present, expandedMarginR, expandedMarginT, cbTop, cbHeight) {
69-
var colorbars = d3.select(gd).selectAll('.colorbar');
70-
expect(colorbars.size()).toBe(present ? 1 : 0);
71-
72-
var cbbg = colorbars.selectAll('.colorbar .cbbg');
73-
74-
// check that the displayed object has the right size,
75-
// not just that fullLayout._size changed
76-
var plotSizeTest = {};
77-
if(expandedMarginR) plotSizeTest.widthLessThan = 400;
78-
else plotSizeTest.width = 400;
79-
80-
if(expandedMarginT) plotSizeTest.heightLessThan = 400;
81-
else plotSizeTest.height = 400;
82-
83-
assertPlotSize(plotSizeTest);
84-
85-
if(present) {
86-
if(!cbHeight) cbHeight = 400;
87-
var bgHeight = +cbbg.attr('height');
88-
if(expandedMarginT) expect(bgHeight).toBeLessThan(cbHeight - 2);
89-
else expect(bgHeight).toBeWithin(cbHeight, 2);
90-
65+
function assertCB(msg, present, opts) {
66+
var expandedMarginR = opts.expandedMarginR;
67+
var expandedMarginT = opts.expandedMarginT;
68+
var cbTop = opts.top;
69+
var cbHeight = opts.height;
70+
var multiFill = opts.multiFill;
71+
var colorbars = d3.select(gd).selectAll('.colorbar');
72+
expect(colorbars.size()).toBe(present ? 1 : 0);
73+
74+
// check that the displayed object has the right size,
75+
// not just that fullLayout._size changed
76+
var plotSizeTest = {};
77+
if(expandedMarginR) plotSizeTest.widthLessThan = 400;
78+
else plotSizeTest.width = 400;
79+
80+
if(expandedMarginT) plotSizeTest.heightLessThan = 400;
81+
else plotSizeTest.height = 400;
82+
83+
assertPlotSize(plotSizeTest);
84+
85+
if(present) {
86+
var cbbg = colorbars.selectAll('.cbbg');
87+
var cbfills = colorbars.selectAll('.cbfill');
88+
89+
expect(cbfills.size()).negateIf(multiFill).toBe(1);
90+
91+
if(!cbHeight) cbHeight = 400;
92+
var bgHeight = +cbbg.attr('height');
93+
if(expandedMarginT) expect(bgHeight).toBeLessThan(cbHeight - 2);
94+
else expect(bgHeight).toBeWithin(cbHeight, 2);
95+
96+
if(cbTop !== undefined) {
9197
var topShift = cbbg.node().getBoundingClientRect().top - gd.getBoundingClientRect().top;
9298
expect(topShift).toBeWithin(cbTop, 2);
9399
}
94100
}
101+
}
95102

103+
// let heatmap stand in for all traces with trace.{showscale, colorbar}
104+
// also test impliedEdits for colorbars at the trace root
105+
it('can show and hide heatmap colorbars and sizes correctly with automargin', function(done) {
96106
var thickPx, lenFrac;
97107

98108
Plotly.newPlot(gd, [{
@@ -104,22 +114,22 @@ describe('Test colorbar:', function() {
104114
margin: {l: 50, r: 50, t: 50, b: 50}
105115
})
106116
.then(function() {
107-
assertCB('initial', true, true, false, 50);
117+
assertCB('initial', true, {expandedMarginR: true, expandedMarginT: false, top: 50});
108118

109119
return Plotly.restyle(gd, {showscale: false});
110120
})
111121
.then(function() {
112-
assertCB('hidden', false, false, false);
122+
assertCB('hidden', false, {expandedMarginR: false, expandedMarginT: false});
113123

114124
return Plotly.restyle(gd, {showscale: true, 'colorbar.y': 0.8});
115125
})
116126
.then(function() {
117-
assertCB('up high', true, true, true, 12);
127+
assertCB('up high', true, {expandedMarginR: true, expandedMarginT: true, top: 12});
118128

119129
return Plotly.restyle(gd, {'colorbar.y': 0.7});
120130
})
121131
.then(function() {
122-
assertCB('a little lower', true, true, true, 12);
132+
assertCB('a little lower', true, {expandedMarginR: true, expandedMarginT: true, top: 12});
123133

124134
return Plotly.restyle(gd, {
125135
'colorbar.x': 0.7,
@@ -129,7 +139,7 @@ describe('Test colorbar:', function() {
129139
});
130140
})
131141
.then(function() {
132-
assertCB('mid-plot', true, false, false, 150, 200);
142+
assertCB('mid-plot', true, {expandedMarginR: false, expandedMarginT: false, top: 150, height: 200});
133143

134144
thickPx = gd._fullData[0].colorbar.thickness;
135145
lenFrac = gd._fullData[0].colorbar.len;
@@ -146,21 +156,14 @@ describe('Test colorbar:', function() {
146156
expect(gd._fullData[0].colorbar.len)
147157
.toBeCloseTo(lenFrac * 400, 3);
148158

149-
assertCB('changed size modes', true, true, false, 150, 200);
159+
assertCB('changed size modes', true, {expandedMarginR: true, expandedMarginT: false, top: 150, height: 200});
150160
})
151161
.catch(failTest)
152162
.then(done);
153163
});
154164

155165
// scatter has trace.marker.{showscale, colorbar}
156166
it('can show and hide scatter colorbars', function(done) {
157-
function assertCB(present, expandedMargin) {
158-
var colorbars = d3.select(gd).selectAll('.colorbar');
159-
expect(colorbars.size()).toBe(present ? 1 : 0);
160-
161-
assertPlotSize(expandedMargin ? {widthLessThan: 400} : {width: 400});
162-
}
163-
164167
Plotly.newPlot(gd, [{
165168
y: [1, 2, 3],
166169
marker: {color: [1, 2, 3], showscale: true}
@@ -170,36 +173,29 @@ describe('Test colorbar:', function() {
170173
margin: {l: 50, r: 50, t: 50, b: 50}
171174
})
172175
.then(function() {
173-
assertCB(true, true);
176+
assertCB('initial', true, {expandedMarginR: true});
174177

175178
return Plotly.restyle(gd, {'marker.showscale': false});
176179
})
177180
.then(function() {
178-
assertCB(false, false);
181+
assertCB('hidden', false, {expandedMarginR: false});
179182

180183
return Plotly.restyle(gd, {'marker.showscale': true, 'marker.colorbar.x': 0.7});
181184
})
182185
.then(function() {
183-
assertCB(true, false);
186+
assertCB('mid-plot', true, {expandedMarginR: false});
184187

185188
return Plotly.restyle(gd, {'marker.colorbar.x': 1.1});
186189
})
187190
.then(function() {
188-
assertCB(true, true);
191+
assertCB('far right', true, {expandedMarginR: true});
189192
})
190193
.catch(failTest)
191194
.then(done);
192195
});
193196

194197
// histogram colorbars could not be edited before
195-
it('can show and hide scatter colorbars', function(done) {
196-
function assertCB(present, expandedMargin) {
197-
var colorbars = d3.select(gd).selectAll('.colorbar');
198-
expect(colorbars.size()).toBe(present ? 1 : 0);
199-
200-
assertPlotSize(expandedMargin ? {widthLessThan: 400} : {width: 400});
201-
}
202-
198+
it('can show and hide histogram colorbars', function(done) {
203199
Plotly.newPlot(gd, [{
204200
type: 'histogram',
205201
x: [0, 1, 1, 2, 2, 2, 3, 3, 4],
@@ -211,22 +207,53 @@ describe('Test colorbar:', function() {
211207
margin: {l: 50, r: 50, t: 50, b: 50}
212208
})
213209
.then(function() {
214-
assertCB(true, true);
210+
assertCB('initial', true, {expandedMarginR: true});
215211

216212
return Plotly.restyle(gd, {'marker.showscale': false});
217213
})
218214
.then(function() {
219-
assertCB(false, false);
215+
assertCB('hidden', false, {expandedMarginR: false});
220216

221217
return Plotly.restyle(gd, {'marker.showscale': true, 'marker.colorbar.x': 0.7});
222218
})
223219
.then(function() {
224-
assertCB(true, false);
220+
assertCB('mid-plot', true, {expandedMarginR: false});
225221

226222
return Plotly.restyle(gd, {'marker.colorbar.x': 1.1});
227223
})
228224
.then(function() {
229-
assertCB(true, true);
225+
assertCB('far right', true, {expandedMarginR: true});
226+
})
227+
.catch(failTest)
228+
.then(done);
229+
});
230+
231+
it('creates multiple fills for contour colorbars', function(done) {
232+
Plotly.newPlot(gd, [{
233+
type: 'contour',
234+
z: [[1, 10], [100, 1000]]
235+
}], {
236+
height: 500,
237+
width: 500,
238+
margin: {l: 50, r: 50, t: 50, b: 50}
239+
})
240+
.then(function() {
241+
assertCB('initial', true, {expandedMarginR: true, expandedMarginT: false, top: 50, multiFill: true});
242+
243+
return Plotly.restyle(gd, {showscale: false});
244+
})
245+
.then(function() {
246+
assertCB('hidden', false, {expandedMarginR: false, expandedMarginT: false});
247+
248+
return Plotly.restyle(gd, {showscale: true, 'colorbar.y': 0.8});
249+
})
250+
.then(function() {
251+
assertCB('up high', true, {expandedMarginR: true, expandedMarginT: true, top: 12, multiFill: true});
252+
253+
return Plotly.restyle(gd, {'contours.coloring': 'heatmap'});
254+
})
255+
.then(function() {
256+
assertCB('up high', true, {expandedMarginR: true, expandedMarginT: true, top: 12, multiFill: false});
230257
})
231258
.catch(failTest)
232259
.then(done);
@@ -235,7 +262,7 @@ describe('Test colorbar:', function() {
235262
// parcoords has trace.marker.{showscale, colorbar}
236263
// also tests impliedEdits for colorbars in containers
237264
it('can show and hide parcoords colorbars', function(done) {
238-
function assertCB(present, expandedMargin) {
265+
function assertParcoordsCB(present, expandedMargin) {
239266
var colorbars = d3.select(gd).selectAll('.colorbar');
240267
expect(colorbars.size()).toBe(present ? 1 : 0);
241268

@@ -244,6 +271,9 @@ describe('Test colorbar:', function() {
244271
var transform = yAxes[0][1].getAttribute('transform');
245272
if(expandedMargin) expect(transform).not.toBe('translate(400, 0)');
246273
else expect(transform).toBe('translate(400, 0)');
274+
275+
var cbfills = colorbars.selectAll('.cbfill');
276+
expect(cbfills.size()).toBe(present ? 1 : 0);
247277
}
248278

249279
var thickPx, lenFrac;
@@ -258,20 +288,20 @@ describe('Test colorbar:', function() {
258288
margin: {l: 50, r: 50, t: 50, b: 50}
259289
})
260290
.then(function() {
261-
assertCB(true, true);
291+
assertParcoordsCB(true, true);
262292

263293
return Plotly.restyle(gd, {'line.showscale': false});
264294
})
265295
.then(function() {
266-
assertCB(false, false);
296+
assertParcoordsCB(false, false);
267297

268298
return Plotly.restyle(gd, {
269299
'line.showscale': true,
270300
'line.colorbar.x': 0.7
271301
});
272302
})
273303
.then(function() {
274-
assertCB(true, false);
304+
assertParcoordsCB(true, false);
275305

276306
thickPx = gd._fullData[0].line.colorbar.thickness;
277307
lenFrac = gd._fullData[0].line.colorbar.len;
@@ -288,7 +318,7 @@ describe('Test colorbar:', function() {
288318
expect(gd._fullData[0].line.colorbar.len)
289319
.toBeCloseTo(lenFrac * 400, 3);
290320

291-
assertCB(true, true);
321+
assertParcoordsCB(true, true);
292322
})
293323
.catch(failTest)
294324
.then(done);

0 commit comments

Comments
 (0)
Please sign in to comment.