Skip to content

Commit fbfadbe

Browse files
authored
Merge pull request #4307 from plotly/image-scaleanchor
image: improve layout defaults
2 parents 518fc61 + 408b46b commit fbfadbe

16 files changed

+133
-22
lines changed

src/plots/cartesian/constraints.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ var concatExtremes = require('./autorange').concatExtremes;
1717
var ALMOST_EQUAL = require('../../constants/numerical').ALMOST_EQUAL;
1818
var FROM_BL = require('../../constants/alignment').FROM_BL;
1919

20-
exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, allAxisIds, layoutOut) {
20+
exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, opts) {
21+
var allAxisIds = opts.allAxisIds;
22+
var layoutOut = opts.layoutOut;
23+
var scaleanchorDflt = opts.scaleanchorDflt;
24+
var constrainDflt = opts.constrainDflt;
2125
var constraintGroups = layoutOut._axisConstraintGroups;
2226
var matchGroups = layoutOut._axisMatchGroups;
2327
var axId = containerOut._id;
@@ -28,7 +32,7 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a
2832

2933
// coerce the constraint mechanics even if this axis has no scaleanchor
3034
// because it may be the anchor of another axis.
31-
var constrain = coerce('constrain');
35+
var constrain = coerce('constrain', constrainDflt);
3236
Lib.coerce(containerIn, containerOut, {
3337
constraintoward: {
3438
valType: 'enumerated',
@@ -53,14 +57,17 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a
5357
// 'matches' wins over 'scaleanchor' (for now)
5458
var scaleanchor, scaleOpts;
5559

56-
if(!matches && containerIn.scaleanchor && !(containerOut.fixedrange && constrain !== 'domain')) {
60+
if(!matches &&
61+
!(containerOut.fixedrange && constrain !== 'domain') &&
62+
(containerIn.scaleanchor || scaleanchorDflt)
63+
) {
5764
scaleOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut, constrain);
5865
scaleanchor = Lib.coerce(containerIn, containerOut, {
5966
scaleanchor: {
6067
valType: 'enumerated',
6168
values: scaleOpts.linkableAxes || []
6269
}
63-
}, 'scaleanchor');
70+
}, 'scaleanchor', scaleanchorDflt);
6471
}
6572

6673
if(matches) {

src/plots/cartesian/layout_defaults.js

+29-11
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
3939
var yaMayHide = {};
4040
var xaMustDisplay = {};
4141
var yaMustDisplay = {};
42-
var yaMustForward = {};
43-
var yaMayBackward = {};
42+
var yaMustNotReverse = {};
43+
var yaMayReverse = {};
44+
var axHasImage = {};
4445
var outerTicks = {};
4546
var noGrids = {};
4647
var i, j;
@@ -74,24 +75,22 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
7475
if(trace.type === 'funnel') {
7576
if(trace.orientation === 'h') {
7677
if(xaName) xaMayHide[xaName] = true;
77-
if(yaName) yaMayBackward[yaName] = true;
78+
if(yaName) yaMayReverse[yaName] = true;
7879
} else {
7980
if(yaName) yaMayHide[yaName] = true;
8081
}
82+
} else if(trace.type === 'image') {
83+
if(yaName) axHasImage[yaName] = true;
84+
if(xaName) axHasImage[xaName] = true;
8185
} else {
8286
if(yaName) {
8387
yaMustDisplay[yaName] = true;
84-
yaMustForward[yaName] = true;
88+
yaMustNotReverse[yaName] = true;
8589
}
8690

8791
if(!traceIs(trace, 'carpet') || (trace.type === 'carpet' && !trace._cheater)) {
8892
if(xaName) xaMustDisplay[xaName] = true;
8993
}
90-
91-
if(trace.type === 'image') {
92-
if(yaName) yaMustForward[yaName] = false;
93-
if(yaName) yaMayBackward[yaName] = true;
94-
}
9594
}
9695

9796
// Two things trigger axis visibility:
@@ -197,7 +196,11 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
197196
(axLetter === 'y' && !yaMustDisplay[axName] && yaMayHide[axName]);
198197

199198
var reverseDflt =
200-
(axLetter === 'y' && !yaMustForward[axName] && yaMayBackward[axName]);
199+
(axLetter === 'y' &&
200+
(
201+
(!yaMustNotReverse[axName] && yaMayReverse[axName]) ||
202+
axHasImage[axName]
203+
));
201204

202205
var defaultOptions = {
203206
letter: axLetter,
@@ -299,7 +302,22 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
299302
axLayoutIn = layoutIn[axName];
300303
axLayoutOut = layoutOut[axName];
301304

302-
handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, allAxisIds, layoutOut);
305+
var scaleanchorDflt;
306+
if(axLetter === 'y' && !axLayoutIn.hasOwnProperty('scaleanchor') && axHasImage[axName]) {
307+
scaleanchorDflt = axLayoutOut.anchor;
308+
} else {scaleanchorDflt = undefined;}
309+
310+
var constrainDflt;
311+
if(!axLayoutIn.hasOwnProperty('constrain') && axHasImage[axName]) {
312+
constrainDflt = 'domain';
313+
} else {constrainDflt = undefined;}
314+
315+
handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, {
316+
allAxisIds: allAxisIds,
317+
layoutOut: layoutOut,
318+
scaleanchorDflt: scaleanchorDflt,
319+
constrainDflt: constrainDflt
320+
});
303321
}
304322

305323
for(i = 0; i < matchGroups.length; i++) {

src/traces/image/index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ module.exports = {
2424
animatable: false,
2525
meta: {
2626
description: [
27-
'Display an image, i.e. data on a 2D regular raster.'
27+
'Display an image, i.e. data on a 2D regular raster.',
28+
'By default, when an image is displayed in a subplot,',
29+
'its y axis will be reversed (ie. `autorange: \'reversed\'`),',
30+
'constrained to the domain (ie. `constrain: \'domain\'`)',
31+
'and it will have the same scale as its x axis (ie. `scaleanchor: \'x\,`)',
32+
'in order for pixels to be rendered as squares.'
2833
].join(' ')
2934
}
3035
};
1.94 KB
Loading
-1.42 KB
Loading

test/image/baselines/image_cat.png

2 Bytes
Loading
-847 Bytes
Loading
-2.12 KB
Loading
583 Bytes
Loading
-117 Bytes
Loading
-4.36 KB
Loading

test/image/mocks/image_adventurer.json

+2-1
Large diffs are not rendered by default.

test/image/mocks/image_cat.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"layout": {
3-
"width": 400, "height": 400, "yaxis": {"scaleanchor": "x"}, "margin": {"t":25, "b": 25, "r": 25, "l": 40}
3+
"width": 400, "height": 400, "yaxis": {"scaleanchor": "x", "autorange": "reversed"}, "margin": {"t":25, "b": 25, "r": 25, "l": 40}
44
},
55
"data": [
66
{"x":[50, 150, 350, 50], "y":[350, 23, 100, 350]},

test/image/mocks/image_colormodel.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
"data": [{
33
"type": "image",
44
"colormodel": "rgb",
5-
"dy": 10,
5+
"hoverinfo": "all",
6+
"dy": 1,
67
"dx": 0.5,
78
"z": [[[255, 0, 0], [0, 255, 0], [0, 0, 255]]]
89
}, {
910
"type": "image",
1011
"colormodel": "hsl",
12+
"hoverinfo": "all",
1113
"z": [
1214
[[0, 33, 50], [0, 66, 50], [0, 100, 50]],
1315
[[90, 33, 50], [90, 66, 50], [90, 100, 50]],
@@ -18,7 +20,7 @@
1820
"yaxis": "y2"
1921
}],
2022
"layout": {
21-
"grid": {"rows": 2, "columns": 1, "pattern": "independent"},
23+
"grid": {"rows": 1, "columns": 2, "pattern": "independent"},
2224
"width": 600,
2325
"height": 400
2426
}

test/jasmine/assets/mock_lists.js

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var svgMockList = [
2323
['finance_style', require('@mocks/finance_style.json')],
2424
['geo_first', require('@mocks/geo_first.json')],
2525
['indicator_bignumber', require('@mocks/indicator_bignumber.json')],
26+
['image_adventurer', require('@mocks/image_adventurer.json')],
2627
['layout_image', require('@mocks/layout_image.json')],
2728
['layout-colorway', require('@mocks/layout-colorway.json')],
2829
['multicategory', require('@mocks/multicategory.json')],

test/jasmine/tests/image_test.js

+79-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var failTest = require('../assets/fail_test');
1111

1212
var customAssertions = require('../assets/custom_assertions');
1313
var assertHoverLabelContent = customAssertions.assertHoverLabelContent;
14+
var supplyAllDefaults = require('../assets/supply_defaults');
1415
var Fx = require('@src/components/fx');
1516

1617
describe('image supplyDefaults', function() {
@@ -100,6 +101,82 @@ describe('image supplyDefaults', function() {
100101
});
101102
});
102103

104+
describe('image smart layout defaults', function() {
105+
var gd;
106+
beforeEach(function() {
107+
gd = createGraphDiv();
108+
});
109+
110+
afterEach(destroyGraphDiv);
111+
112+
it('should reverse yaxis if images are present', function() {
113+
gd = {};
114+
gd.data = [{type: 'image', z: [[[255, 0, 0]]]}];
115+
supplyAllDefaults(gd);
116+
expect(gd._fullLayout.yaxis.autorange).toBe('reversed');
117+
});
118+
119+
it('should reverse yaxis even if another trace is present', function() {
120+
gd = {};
121+
gd.data = [{type: 'image', z: [[[255, 0, 0]]]}, {type: 'scatter', y: [5, 3, 2]}];
122+
supplyAllDefaults(gd);
123+
expect(gd._fullLayout.yaxis.autorange).toBe('reversed');
124+
});
125+
126+
it('should NOT reverse yaxis if it\'s already defined', function() {
127+
gd = {};
128+
gd.data = [{type: 'image', z: [[[255, 0, 0]]]}];
129+
gd.layout = {yaxis: {autorange: false}};
130+
supplyAllDefaults(gd);
131+
expect(gd._fullLayout.yaxis.autorange).toBe(false);
132+
});
133+
134+
it('should set scaleanchor to make square pixels if images are present', function() {
135+
gd = {};
136+
gd.data = [{type: 'image', z: [[[255, 0, 0]]]}];
137+
supplyAllDefaults(gd);
138+
expect(gd._fullLayout.yaxis.scaleanchor).toBe('x');
139+
});
140+
141+
it('should set scaleanchor even if another trace is present', function() {
142+
gd = {};
143+
gd.data = [{type: 'image', z: [[[255, 0, 0]]]}, {type: 'scatter', y: [5, 3, 2]}];
144+
supplyAllDefaults(gd);
145+
expect(gd._fullLayout.yaxis.scaleanchor).toBe('x');
146+
});
147+
148+
it('should NOT set scaleanchor if it\'s already defined', function() {
149+
gd.data = [{type: 'image', z: [[[255, 0, 0]]]}];
150+
gd.layout = {yaxis: {scaleanchor: 'x3'}};
151+
supplyAllDefaults(gd);
152+
expect(gd._fullLayout.yaxis.scaleanchor).toBe(undefined);
153+
});
154+
155+
it('should constrain axes to domain if images are present', function() {
156+
gd = {};
157+
gd.data = [{type: 'image', z: [[[255, 0, 0]]]}];
158+
supplyAllDefaults(gd);
159+
expect(gd._fullLayout.xaxis.constrain).toBe('domain');
160+
expect(gd._fullLayout.yaxis.constrain).toBe('domain');
161+
});
162+
163+
it('should constrain axes to domain even if another trace is present', function() {
164+
gd = {};
165+
gd.data = [{type: 'image', z: [[[255, 0, 0]]]}, {type: 'scatter', y: [5, 3, 2]}];
166+
supplyAllDefaults(gd);
167+
expect(gd._fullLayout.xaxis.constrain).toBe('domain');
168+
expect(gd._fullLayout.yaxis.constrain).toBe('domain');
169+
});
170+
171+
it('should NOT constrain axes to domain if it\'s already defined', function() {
172+
gd.data = [{type: 'image', z: [[[255, 0, 0]]]}];
173+
gd.layout = {yaxis: {constrain: false}, xaxis: {constrain: false}};
174+
supplyAllDefaults(gd);
175+
expect(gd._fullLayout.xaxis.constrain).toBe('range');
176+
expect(gd._fullLayout.yaxis.constrain).toBe('range');
177+
});
178+
});
179+
103180
describe('image plot', function() {
104181
'use strict';
105182

@@ -444,10 +521,10 @@ describe('image hover:', function() {
444521
zmax: [1, 1, 1],
445522
text: [['A', 'B', 'C'], ['D', 'E', 'F']],
446523
hovertemplate: '%{text}<extra></extra>'
447-
}], layout: {width: 400, height: 400}};
524+
}], layout: {width: 400, height: 400, yaxis: {constrain: 'range'}}};
448525

449526
Plotly.newPlot(gd, mockCopy)
450-
.then(function() {_hover(140, 200);})
527+
.then(function() {_hover(140, 180);})
451528
.then(function() {
452529
assertHoverLabelContent({
453530
nums: 'E',

0 commit comments

Comments
 (0)