diff --git a/src/components/colorbar/connect.js b/src/components/colorbar/connect.js index 59ce044a7f2..f551b32b511 100644 --- a/src/components/colorbar/connect.js +++ b/src/components/colorbar/connect.js @@ -9,7 +9,6 @@ 'use strict'; -var Colorscale = require('../colorscale'); var drawColorbar = require('./draw'); /** @@ -48,20 +47,9 @@ module.exports = function connectColorbar(gd, cd, moduleOpts) { gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); if(!container || !container.showscale) return; - var zmin = container[moduleOpts.min]; - var zmax = container[moduleOpts.max]; - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - container.colorscale, - zmin, - zmax - ), - { noNumericCheck: true } - ); - cb.fillcolor(sclFunc) - .filllevels({start: zmin, end: zmax, size: (zmax - zmin) / 254}) + cb.fillgradient(container.colorscale) + .zrange([container[moduleOpts.min], container[moduleOpts.max]]) .options(container.colorbar)(); }; diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index 8614333873d..18d776a2873 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -39,9 +39,9 @@ module.exports = function draw(gd, id) { // opts: options object, containing everything from attributes // plus a few others that are the equivalent of the colorbar "data" var opts = {}; - Object.keys(attributes).forEach(function(k) { + for(var k in attributes) { opts[k] = null; - }); + } // fillcolor can be a d3 scale, domain is z values, range is colors // or leave it out for no fill, // or set to a string constant for single-color fill @@ -57,17 +57,23 @@ module.exports = function draw(gd, id) { // contour map) if this is omitted, fillcolors will be // evaluated halfway between levels opts.filllevels = null; + // for continuous colorscales: fill with a gradient instead of explicit levels + // value should be the colorscale [[0, c0], [v1, c1], ..., [1, cEnd]] + opts.fillgradient = null; + // when using a gradient, we need the data range specified separately + opts.zrange = null; function component() { var fullLayout = gd._fullLayout, gs = fullLayout._size; if((typeof opts.fillcolor !== 'function') && - (typeof opts.line.color !== 'function')) { + (typeof opts.line.color !== 'function') && + !opts.fillgradient) { fullLayout._infolayer.selectAll('g.' + id).remove(); return; } - var zrange = d3.extent(((typeof opts.fillcolor === 'function') ? - opts.fillcolor : opts.line.color).domain()); + var zrange = opts.zrange || (d3.extent(((typeof opts.fillcolor === 'function') ? + opts.fillcolor : opts.line.color).domain())); var linelevels = []; var filllevels = []; var linecolormap = typeof opts.line.color === 'function' ? @@ -87,7 +93,10 @@ module.exports = function draw(gd, id) { if(l > zr0 && l < zr1) linelevels.push(l); } - if(typeof opts.fillcolor === 'function') { + if(opts.fillgradient) { + filllevels = [0]; + } + else if(typeof opts.fillcolor === 'function') { if(opts.filllevels) { l0 = opts.filllevels.end + opts.filllevels.size / 100; ls = opts.filllevels.size; @@ -358,6 +367,12 @@ module.exports = function draw(gd, id) { .classed(cn.cbfill, true) .style('stroke', 'none'); fills.exit().remove(); + + var zBounds = zrange + .map(cbAxisOut.c2p) + .map(Math.round) + .sort(function(a, b) { return a - b; }); + fills.each(function(d, i) { var z = [ (i === 0) ? zrange[0] : @@ -370,25 +385,27 @@ module.exports = function draw(gd, id) { // offset the side adjoining the next rectangle so they // overlap, to prevent antialiasing gaps - if(i !== filllevels.length - 1) { - z[1] += (z[1] > z[0]) ? 1 : -1; - } - - - // Tinycolor can't handle exponents and - // at this scale, removing it makes no difference. - var colorString = fillcolormap(d).replace('e-', ''), - opaqueColor = tinycolor(colorString).toHexString(); + z[1] = Lib.constrain(z[1] + (z[1] > z[0]) ? 1 : -1, zBounds[0], zBounds[1]); // Colorbar cannot currently support opacities so we // use an opaque fill even when alpha channels present - d3.select(this).attr({ + var fillEl = d3.select(this).attr({ x: xLeft, width: Math.max(thickPx, 2), y: d3.min(z), height: Math.max(d3.max(z) - d3.min(z), 2), - fill: opaqueColor }); + + if(opts.fillgradient) { + Drawing.gradient(fillEl, gd, id, 'vertical', + opts.fillgradient, 'fill'); + } + else { + // Tinycolor can't handle exponents and + // at this scale, removing it makes no difference. + var colorString = fillcolormap(d).replace('e-', ''); + fillEl.attr('fill', tinycolor(colorString).toHexString()); + } }); var lines = container.select('.cblines') @@ -650,13 +667,13 @@ module.exports = function draw(gd, id) { // or use .options to set multiple options at once via a dictionary component.options = function(o) { - Object.keys(o).forEach(function(name) { + for(var name in o) { // in case something random comes through // that's not an option, ignore it if(typeof component[name] === 'function') { component[name](o[name]); } - }); + } return component; }; diff --git a/src/traces/contour/colorbar.js b/src/traces/contour/colorbar.js index 9b800339d5d..317760182ca 100644 --- a/src/traces/contour/colorbar.js +++ b/src/traces/contour/colorbar.js @@ -16,32 +16,25 @@ var endPlus = require('./end_plus'); module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - cbId = 'cb' + trace.uid; + var trace = cd[0].trace; + var cbId = 'cb' + trace.uid; gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); if(!trace.showscale) return; - var cb = drawColorbar(gd, cbId); - cd[0].t.cb = cb; + var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var contours = trace.contours, - line = trace.line, - cs = contours.size || 1, - coloring = contours.coloring; + var contours = trace.contours; + var line = trace.line; + var cs = contours.size || 1; + var coloring = contours.coloring; var colorMap = makeColorMap(trace, {isColorbar: true}); - if(coloring === 'heatmap') { - cb.filllevels({ - start: trace.zmin, - end: trace.zmax, - size: (trace.zmax - trace.zmin) / 254 - }); - } - - cb.fillcolor((coloring === 'fill' || coloring === 'heatmap') ? colorMap : '') + cb.fillgradient(coloring === 'heatmap' ? trace.colorscale : '') + .zrange(coloring === 'heatmap' ? [trace.zmin, trace.zmax] : '') + .fillcolor((coloring === 'fill') ? colorMap : '') .line({ color: coloring === 'lines' ? colorMap : line.color, width: contours.showlines !== false ? line.width : 0, diff --git a/test/image/baselines/16.png b/test/image/baselines/16.png index cfa602db6b7..4c6af3dd5e7 100644 Binary files a/test/image/baselines/16.png and b/test/image/baselines/16.png differ diff --git a/test/image/baselines/colorbar_enumerated_ticks.png b/test/image/baselines/colorbar_enumerated_ticks.png index 5e9b2dcc789..cc1c34ff699 100644 Binary files a/test/image/baselines/colorbar_enumerated_ticks.png and b/test/image/baselines/colorbar_enumerated_ticks.png differ diff --git a/test/image/baselines/colorscale_constraint.png b/test/image/baselines/colorscale_constraint.png index b83fbb1635e..63c18c23f96 100644 Binary files a/test/image/baselines/colorscale_constraint.png and b/test/image/baselines/colorscale_constraint.png differ diff --git a/test/image/baselines/geo_multiple-usa-choropleths.png b/test/image/baselines/geo_multiple-usa-choropleths.png index b90c2d32de0..1361df824ae 100644 Binary files a/test/image/baselines/geo_multiple-usa-choropleths.png and b/test/image/baselines/geo_multiple-usa-choropleths.png differ diff --git a/test/image/baselines/gl2d_parcoords_1.png b/test/image/baselines/gl2d_parcoords_1.png index 0d24cf48e80..78d4b4b92a8 100644 Binary files a/test/image/baselines/gl2d_parcoords_1.png and b/test/image/baselines/gl2d_parcoords_1.png differ diff --git a/test/image/baselines/gl2d_parcoords_2.png b/test/image/baselines/gl2d_parcoords_2.png index 921a5f5bd64..4b55d71b025 100644 Binary files a/test/image/baselines/gl2d_parcoords_2.png and b/test/image/baselines/gl2d_parcoords_2.png differ diff --git a/test/image/baselines/hist2d_summed.png b/test/image/baselines/hist2d_summed.png index c8a24409300..6d65d5d57a5 100644 Binary files a/test/image/baselines/hist2d_summed.png and b/test/image/baselines/hist2d_summed.png differ diff --git a/test/image/baselines/zsmooth_methods.png b/test/image/baselines/zsmooth_methods.png index baf8c7e3da8..17507ac5f1b 100644 Binary files a/test/image/baselines/zsmooth_methods.png and b/test/image/baselines/zsmooth_methods.png differ diff --git a/test/jasmine/tests/colorbar_test.js b/test/jasmine/tests/colorbar_test.js index a4ff4de3c31..48ab85137d3 100644 --- a/test/jasmine/tests/colorbar_test.js +++ b/test/jasmine/tests/colorbar_test.js @@ -62,37 +62,47 @@ describe('Test colorbar:', function() { .then(done); }); - // let heatmap stand in for all traces with trace.{showscale, colorbar} - // also test impliedEdits for colorbars at the trace root - it('can show and hide heatmap colorbars and sizes correctly with automargin', function(done) { - function assertCB(msg, present, expandedMarginR, expandedMarginT, cbTop, cbHeight) { - var colorbars = d3.select(gd).selectAll('.colorbar'); - expect(colorbars.size()).toBe(present ? 1 : 0); - - var cbbg = colorbars.selectAll('.colorbar .cbbg'); - - // check that the displayed object has the right size, - // not just that fullLayout._size changed - var plotSizeTest = {}; - if(expandedMarginR) plotSizeTest.widthLessThan = 400; - else plotSizeTest.width = 400; - - if(expandedMarginT) plotSizeTest.heightLessThan = 400; - else plotSizeTest.height = 400; - - assertPlotSize(plotSizeTest); - - if(present) { - if(!cbHeight) cbHeight = 400; - var bgHeight = +cbbg.attr('height'); - if(expandedMarginT) expect(bgHeight).toBeLessThan(cbHeight - 2); - else expect(bgHeight).toBeWithin(cbHeight, 2); - + function assertCB(msg, present, opts) { + var expandedMarginR = opts.expandedMarginR; + var expandedMarginT = opts.expandedMarginT; + var cbTop = opts.top; + var cbHeight = opts.height; + var multiFill = opts.multiFill; + var colorbars = d3.select(gd).selectAll('.colorbar'); + expect(colorbars.size()).toBe(present ? 1 : 0); + + // check that the displayed object has the right size, + // not just that fullLayout._size changed + var plotSizeTest = {}; + if(expandedMarginR) plotSizeTest.widthLessThan = 400; + else plotSizeTest.width = 400; + + if(expandedMarginT) plotSizeTest.heightLessThan = 400; + else plotSizeTest.height = 400; + + assertPlotSize(plotSizeTest); + + if(present) { + var cbbg = colorbars.selectAll('.cbbg'); + var cbfills = colorbars.selectAll('.cbfill'); + + expect(cbfills.size()).negateIf(multiFill).toBe(1); + + if(!cbHeight) cbHeight = 400; + var bgHeight = +cbbg.attr('height'); + if(expandedMarginT) expect(bgHeight).toBeLessThan(cbHeight - 2); + else expect(bgHeight).toBeWithin(cbHeight, 2); + + if(cbTop !== undefined) { var topShift = cbbg.node().getBoundingClientRect().top - gd.getBoundingClientRect().top; expect(topShift).toBeWithin(cbTop, 2); } } + } + // let heatmap stand in for all traces with trace.{showscale, colorbar} + // also test impliedEdits for colorbars at the trace root + it('can show and hide heatmap colorbars and sizes correctly with automargin', function(done) { var thickPx, lenFrac; Plotly.newPlot(gd, [{ @@ -104,22 +114,22 @@ describe('Test colorbar:', function() { margin: {l: 50, r: 50, t: 50, b: 50} }) .then(function() { - assertCB('initial', true, true, false, 50); + assertCB('initial', true, {expandedMarginR: true, expandedMarginT: false, top: 50}); return Plotly.restyle(gd, {showscale: false}); }) .then(function() { - assertCB('hidden', false, false, false); + assertCB('hidden', false, {expandedMarginR: false, expandedMarginT: false}); return Plotly.restyle(gd, {showscale: true, 'colorbar.y': 0.8}); }) .then(function() { - assertCB('up high', true, true, true, 12); + assertCB('up high', true, {expandedMarginR: true, expandedMarginT: true, top: 12}); return Plotly.restyle(gd, {'colorbar.y': 0.7}); }) .then(function() { - assertCB('a little lower', true, true, true, 12); + assertCB('a little lower', true, {expandedMarginR: true, expandedMarginT: true, top: 12}); return Plotly.restyle(gd, { 'colorbar.x': 0.7, @@ -129,7 +139,7 @@ describe('Test colorbar:', function() { }); }) .then(function() { - assertCB('mid-plot', true, false, false, 150, 200); + assertCB('mid-plot', true, {expandedMarginR: false, expandedMarginT: false, top: 150, height: 200}); thickPx = gd._fullData[0].colorbar.thickness; lenFrac = gd._fullData[0].colorbar.len; @@ -146,7 +156,7 @@ describe('Test colorbar:', function() { expect(gd._fullData[0].colorbar.len) .toBeCloseTo(lenFrac * 400, 3); - assertCB('changed size modes', true, true, false, 150, 200); + assertCB('changed size modes', true, {expandedMarginR: true, expandedMarginT: false, top: 150, height: 200}); }) .catch(failTest) .then(done); @@ -154,13 +164,6 @@ describe('Test colorbar:', function() { // scatter has trace.marker.{showscale, colorbar} it('can show and hide scatter colorbars', function(done) { - function assertCB(present, expandedMargin) { - var colorbars = d3.select(gd).selectAll('.colorbar'); - expect(colorbars.size()).toBe(present ? 1 : 0); - - assertPlotSize(expandedMargin ? {widthLessThan: 400} : {width: 400}); - } - Plotly.newPlot(gd, [{ y: [1, 2, 3], marker: {color: [1, 2, 3], showscale: true} @@ -170,36 +173,29 @@ describe('Test colorbar:', function() { margin: {l: 50, r: 50, t: 50, b: 50} }) .then(function() { - assertCB(true, true); + assertCB('initial', true, {expandedMarginR: true}); return Plotly.restyle(gd, {'marker.showscale': false}); }) .then(function() { - assertCB(false, false); + assertCB('hidden', false, {expandedMarginR: false}); return Plotly.restyle(gd, {'marker.showscale': true, 'marker.colorbar.x': 0.7}); }) .then(function() { - assertCB(true, false); + assertCB('mid-plot', true, {expandedMarginR: false}); return Plotly.restyle(gd, {'marker.colorbar.x': 1.1}); }) .then(function() { - assertCB(true, true); + assertCB('far right', true, {expandedMarginR: true}); }) .catch(failTest) .then(done); }); // histogram colorbars could not be edited before - it('can show and hide scatter colorbars', function(done) { - function assertCB(present, expandedMargin) { - var colorbars = d3.select(gd).selectAll('.colorbar'); - expect(colorbars.size()).toBe(present ? 1 : 0); - - assertPlotSize(expandedMargin ? {widthLessThan: 400} : {width: 400}); - } - + it('can show and hide histogram colorbars', function(done) { Plotly.newPlot(gd, [{ type: 'histogram', x: [0, 1, 1, 2, 2, 2, 3, 3, 4], @@ -211,22 +207,53 @@ describe('Test colorbar:', function() { margin: {l: 50, r: 50, t: 50, b: 50} }) .then(function() { - assertCB(true, true); + assertCB('initial', true, {expandedMarginR: true}); return Plotly.restyle(gd, {'marker.showscale': false}); }) .then(function() { - assertCB(false, false); + assertCB('hidden', false, {expandedMarginR: false}); return Plotly.restyle(gd, {'marker.showscale': true, 'marker.colorbar.x': 0.7}); }) .then(function() { - assertCB(true, false); + assertCB('mid-plot', true, {expandedMarginR: false}); return Plotly.restyle(gd, {'marker.colorbar.x': 1.1}); }) .then(function() { - assertCB(true, true); + assertCB('far right', true, {expandedMarginR: true}); + }) + .catch(failTest) + .then(done); + }); + + it('creates multiple fills for contour colorbars', function(done) { + Plotly.newPlot(gd, [{ + type: 'contour', + z: [[1, 10], [100, 1000]] + }], { + height: 500, + width: 500, + margin: {l: 50, r: 50, t: 50, b: 50} + }) + .then(function() { + assertCB('initial', true, {expandedMarginR: true, expandedMarginT: false, top: 50, multiFill: true}); + + return Plotly.restyle(gd, {showscale: false}); + }) + .then(function() { + assertCB('hidden', false, {expandedMarginR: false, expandedMarginT: false}); + + return Plotly.restyle(gd, {showscale: true, 'colorbar.y': 0.8}); + }) + .then(function() { + assertCB('up high', true, {expandedMarginR: true, expandedMarginT: true, top: 12, multiFill: true}); + + return Plotly.restyle(gd, {'contours.coloring': 'heatmap'}); + }) + .then(function() { + assertCB('up high', true, {expandedMarginR: true, expandedMarginT: true, top: 12, multiFill: false}); }) .catch(failTest) .then(done); @@ -235,7 +262,7 @@ describe('Test colorbar:', function() { // parcoords has trace.marker.{showscale, colorbar} // also tests impliedEdits for colorbars in containers it('can show and hide parcoords colorbars', function(done) { - function assertCB(present, expandedMargin) { + function assertParcoordsCB(present, expandedMargin) { var colorbars = d3.select(gd).selectAll('.colorbar'); expect(colorbars.size()).toBe(present ? 1 : 0); @@ -244,6 +271,9 @@ describe('Test colorbar:', function() { var transform = yAxes[0][1].getAttribute('transform'); if(expandedMargin) expect(transform).not.toBe('translate(400, 0)'); else expect(transform).toBe('translate(400, 0)'); + + var cbfills = colorbars.selectAll('.cbfill'); + expect(cbfills.size()).toBe(present ? 1 : 0); } var thickPx, lenFrac; @@ -258,12 +288,12 @@ describe('Test colorbar:', function() { margin: {l: 50, r: 50, t: 50, b: 50} }) .then(function() { - assertCB(true, true); + assertParcoordsCB(true, true); return Plotly.restyle(gd, {'line.showscale': false}); }) .then(function() { - assertCB(false, false); + assertParcoordsCB(false, false); return Plotly.restyle(gd, { 'line.showscale': true, @@ -271,7 +301,7 @@ describe('Test colorbar:', function() { }); }) .then(function() { - assertCB(true, false); + assertParcoordsCB(true, false); thickPx = gd._fullData[0].line.colorbar.thickness; lenFrac = gd._fullData[0].line.colorbar.len; @@ -288,7 +318,7 @@ describe('Test colorbar:', function() { expect(gd._fullData[0].line.colorbar.len) .toBeCloseTo(lenFrac * 400, 3); - assertCB(true, true); + assertParcoordsCB(true, true); }) .catch(failTest) .then(done);