Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7894_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add native log-scale colorbars, with new 'type': 'log' attribute [[#7894](https://github.com/plotly/plotly.js/pull/7894)]
10 changes: 10 additions & 0 deletions src/components/colorbar/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ module.exports = overrideAll({
'This measure excludes the size of the padding, ticks and labels.'
].join(' ')
},
type: {
valType: 'enumerated',
values: ['linear', 'log'],
dflt: 'linear',
editType: 'calc',
description: [
'Sets the colorbar axis type.',
'If `"log"`, the colorscale and colorbar will be configured for logarithmic data.'
].join(' ')
},
lenmode: {
valType: 'enumerated',
values: ['fraction', 'pixels'],
Expand Down
1 change: 1 addition & 0 deletions src/components/colorbar/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = function colorbarDefaults(containerIn, containerOut, layout) {
var w = layout.width - margin.l - margin.r;
var h = layout.height - margin.t - margin.b;

coerce('type');
var orientation = coerce('orientation');
var isVertical = orientation === 'v';

Expand Down
7 changes: 4 additions & 3 deletions src/components/colorbar/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -956,8 +956,9 @@ function mockColorBarAxis(gd, opts, zrange) {
var isVertical = opts.orientation === 'v';

var cbAxisIn = {
type: 'linear',
range: zrange,
type: opts.type || 'linear',
range: opts.type === 'log' ?
[Math.log10(zrange[0]), Math.log10(zrange[1])] : zrange,
tickmode: opts.tickmode,
nticks: opts.nticks,
tick0: opts.tick0,
Expand Down Expand Up @@ -994,7 +995,7 @@ function mockColorBarAxis(gd, opts, zrange) {
var letter = isVertical ? 'y' : 'x';

var cbAxisOut = {
type: 'linear',
type: cbAxisIn.type,
_id: letter + opts._id
};

Expand Down
36 changes: 34 additions & 2 deletions src/components/colorscale/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ function extractScale(cont) {
var cmin = cOpts.min;
var cmax = cOpts.max;

// Check if a log type is defined on the colorbar
var type = (cOpts.colorbar && cOpts.colorbar.type) || 'linear';

// Convert domain boundaries to base-10 log if required
if(type === 'log') {
if(cmin > 0) cmin = Math.log10(cmin);
if(cmax > 0) cmax = Math.log10(cmax);
}

var scl = cOpts.reversescale ?
flipScale(cOpts.colorscale) :
cOpts.colorscale;
Expand Down Expand Up @@ -208,7 +217,30 @@ function makeColorScaleFunc(specs, opts) {
}

function makeColorScaleFuncFromTrace(trace, opts) {
return makeColorScaleFunc(extractScale(trace), opts);
var baseColorFn = makeColorScaleFunc(extractScale(trace), opts);
var cOpts = extractOpts(trace);
var type = (cOpts.colorbar && cOpts.colorbar.type) || 'linear';

// Wrap the base generator if we need to dynamically apply log transformations
if (type === 'log') {
var wrappedFunc = function(v) {
if (isNumeric(v)) {
if (v <= 0) return 'rgba(0,0,0,0)'; // Logarithm of zero/negative is undefined
return baseColorFn(Math.log10(v));
} else if (tinycolor(v).isValid()) {
return v;
} else {
return Color.defaultLine;
}
};

// Preserve native domain and range methods for the draw components
wrappedFunc.domain = baseColorFn.domain;
wrappedFunc.range = baseColorFn.range;
return wrappedFunc;
}

return baseColorFn;
}

function colorArray2rbga(colorArray) {
Expand All @@ -229,4 +261,4 @@ module.exports = {
flipScale: flipScale,
makeColorScaleFunc: makeColorScaleFunc,
makeColorScaleFuncFromTrace: makeColorScaleFuncFromTrace
};
};
40 changes: 40 additions & 0 deletions test/jasmine/tests/colorbar_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -550,4 +550,44 @@ describe('Test colorbar:', function() {
.then(done, done.fail);
});
});

describe('log ticks', function() {
var gd;

beforeEach(function() {
gd = createGraphDiv();
});

afterEach(destroyGraphDiv);

it('should generate logarithmic ticks when type is log', function() {
Plotly.newPlot(gd, [{
type: 'heatmap',
z: [[1, 10, 100], [10,100,1000]],
cmin: 1,
cmax: 1000,
colorbar: {
type: 'log',
tickmode: 'auto'
}
}]);

// Extract the tick text elements
var tickTexts = [];
d3Select(gd).selectAll('.cbaxis text').each(function(t) {
tickTexts.push(t.text);
});

// The mock axis should have automatically generated exponents for 1, 10, 100, 1000
expect(tickTexts).toContain('1');
expect(tickTexts).toContain('2');
expect(tickTexts).toContain('5');
expect(tickTexts).toContain('10');
expect(tickTexts).toContain('100');
expect(tickTexts).toContain('1000');

// // Verify a linear tick like '500' was NOT generated
expect(tickTexts).not.toContain('500');
});
});
});
68 changes: 68 additions & 0 deletions test/jasmine/tests/colorscale_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1198,4 +1198,72 @@ describe('Test colorscale restyle calls:', function() {
})
.then(done, done.fail);
});

describe('helpers log mapping', function() {
var makeColorScaleFuncFromTrace = Colorscale.makeColorScaleFuncFromTrace;

it('should map domain values logarithmically to the color gradient when type is log', function() {
var trace = {
cmin: 1,
cmax: 100,
colorscale: [
[0, 'rgb(0, 0, 0)'], // 0%
[1, 'rgb(100, 100, 100)'] // 100%
],
colorbar: {
type: 'log'
}
};

var colorFn = makeColorScaleFuncFromTrace(trace);

// log10(1) = 0 -> maps to 0% of the scale
expect(colorFn(1)).toEqual('rgb(0, 0, 0)');

// log10(10) = 1 -> maps to 50% of the scale (halfway between log10(1) and log10(100))
expect(colorFn(10)).toEqual('rgb(50, 50, 50)');

// log10(100) = 2 -> maps to 100% of the scale
expect(colorFn(100)).toEqual('rgb(100, 100, 100)');
});

it('should return a transparent color for zero or negative values on a log scale', function() {
var trace = {
cmin: 1,
cmax: 100,
colorscale: [
[0, 'rgb(0, 0, 0)'],
[1, 'rgb(255, 255, 255)']
],
colorbar: {
type: 'log'
}
};

var colorFn = makeColorScaleFuncFromTrace(trace);

// The logarithm of zero or a negative number is undefined,
// so our wrapper should catch it and return transparent black.
expect(colorFn(0)).toEqual('rgba(0,0,0,0)');
expect(colorFn(-10)).toEqual('rgba(0,0,0,0)');
});

it('should safely fall back to linear if type is undefined or linear', function() {
var trace = {
cmin: 1,
cmax: 100,
colorscale: [
[0, 'rgb(0, 0, 0)'],
[1, 'rgb(100, 100, 100)']
],
// Omitting colorbar.type to simulate default behavior
};

var colorFn = makeColorScaleFuncFromTrace(trace);

// Linear midpoint between 1 and 100 is 50.5
expect(colorFn(50.5)).toEqual('rgb(50, 50, 50)');
});
});

});
30 changes: 30 additions & 0 deletions test/jasmine/tests/validate_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -574,4 +574,34 @@ describe('Plotly.validate', function() {
}]);
expect(out).toBeUndefined();
});

describe('colorbar type validation', function() {
it('should accept valid colorbar types', function() {
var trace = {
type: 'heatmap',
z: [[1, 10], [100, 1000]],
colorbar: {
type: 'log'
}
};

// validate returns an array of errors. An empty array means success.
var out = Plotly.validate([trace]);
expect(out).toBeUndefined();
});

it('should reject invalid colorbar types', function() {
var trace = {
type: 'heatmap',
z: [[1, 2], [3, 4]],
colorbar: {
type: 'exponential' // not in our enumerated list
}
};

var out = Plotly.validate([trace]);
expect(out.length).toEqual(1);
expect(out[0].msg).toEqual('In data trace 0, key colorbar.type is set to an invalid value (exponential)');
});
});
});
Loading