Skip to content

Commit 9c2dafb

Browse files
authored
Merge pull request #471 from plotly/adjust-src-format
Adjust array src format + add transposition logic
2 parents e2f3d32 + cf5c799 commit 9c2dafb

File tree

9 files changed

+352
-39
lines changed

9 files changed

+352
-39
lines changed

src/EditorControls.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class EditorControls extends Component {
2828
return {
2929
advancedTraceTypeSelector: this.props.advancedTraceTypeSelector,
3030
config: gd._context,
31+
srcConverters: this.props.srcConverters,
3132
data: gd.data,
3233
dataSources: this.props.dataSources,
3334
dataSourceOptions: this.props.dataSourceOptions,
@@ -280,6 +281,10 @@ EditorControls.propTypes = {
280281
beforeUpdateTraces: PropTypes.func,
281282
children: PropTypes.node,
282283
className: PropTypes.string,
284+
srcConverters: PropTypes.shape({
285+
toSrc: PropTypes.func.isRequired,
286+
fromSrc: PropTypes.func.isRequired,
287+
}),
283288
dataSourceOptionRenderer: PropTypes.func,
284289
dataSourceOptions: PropTypes.array,
285290
dataSources: PropTypes.object,
@@ -289,8 +294,8 @@ EditorControls.propTypes = {
289294
locale: PropTypes.string,
290295
onUpdate: PropTypes.func,
291296
plotly: PropTypes.object,
292-
traceTypesConfig: PropTypes.object,
293297
showFieldTooltips: PropTypes.bool,
298+
traceTypesConfig: PropTypes.object,
294299
};
295300

296301
EditorControls.defaultProps = {
@@ -306,6 +311,10 @@ EditorControls.defaultProps = {
306311
EditorControls.childContextTypes = {
307312
advancedTraceTypeSelector: PropTypes.bool,
308313
config: PropTypes.object,
314+
srcConverters: PropTypes.shape({
315+
toSrc: PropTypes.func.isRequired,
316+
fromSrc: PropTypes.func.isRequired,
317+
}),
309318
data: PropTypes.array,
310319
dataSourceOptionRenderer: PropTypes.func,
311320
dataSourceOptions: PropTypes.array,

src/PlotlyEditor.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class PlotlyEditor extends Component {
2525
traceTypesConfig={this.props.traceTypesConfig}
2626
dictionaries={this.props.dictionaries}
2727
showFieldTooltips={this.props.showFieldTooltips}
28+
srcConverters={this.props.srcConverters}
2829
>
2930
{this.props.children}
3031
</EditorControls>
@@ -70,6 +71,10 @@ PlotlyEditor.propTypes = {
7071
divId: PropTypes.string,
7172
hideControls: PropTypes.bool,
7273
showFieldTooltips: PropTypes.bool,
74+
srcConverters: PropTypes.shape({
75+
toSrc: PropTypes.func.isRequired,
76+
fromSrc: PropTypes.func.isRequired,
77+
}),
7378
};
7479

7580
PlotlyEditor.defaultProps = {

src/components/fields/DataSelector.js

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
33
import React, {Component} from 'react';
44
import Field from './Field';
55
import nestedProperty from 'plotly.js/src/lib/nested_property';
6-
import {connectToContainer} from 'lib';
6+
import {connectToContainer, maybeAdjustSrc, maybeTransposeData} from 'lib';
77

88
export function attributeIsData(meta = {}) {
99
return meta.valType === 'data_array' || meta.arrayOk;
@@ -26,16 +26,24 @@ export class UnconnectedDataSelector extends Component {
2626
this.dataSourceOptions = context.dataSourceOptions || [];
2727

2828
this.srcAttr = props.attr + 'src';
29-
this.srcProperty = nestedProperty(props.container, this.srcAttr);
30-
this.fullValue = this.srcProperty.get();
29+
this.srcProperty = nestedProperty(props.container, this.srcAttr).get();
30+
this.fullValue = this.context.srcConverters
31+
? this.context.srcConverters.toSrc(this.srcProperty, props.container.type)
32+
: this.srcProperty;
3133

3234
this.is2D = false;
3335
if (props.container) {
3436
this.is2D =
3537
(props.attr === 'z' &&
36-
['contour', 'heatmap', 'surface', 'heatmapgl'].includes(
37-
props.container.type
38-
)) ||
38+
[
39+
'contour',
40+
'contourgl',
41+
'heatmap',
42+
'heatmapgl',
43+
'surface',
44+
'carpet',
45+
'contourcarpet',
46+
].includes(props.container.type)) ||
3947
(props.container.type === 'table' && props.attr !== 'columnorder');
4048
}
4149
}
@@ -44,39 +52,33 @@ export class UnconnectedDataSelector extends Component {
4452
if (!this.props.updateContainer) {
4553
return;
4654
}
55+
4756
const update = {};
57+
let data;
4858

4959
if (Array.isArray(value)) {
50-
update[this.props.attr] = value
60+
data = value
5161
.filter(v => Array.isArray(this.dataSources[v]))
5262
.map(v => this.dataSources[v]);
53-
54-
// Table traces have many configuration options,
55-
// The below attributes can be 2d or 1d and will affect the plot differently
56-
// EX:
57-
// header.values = ['Jan', 'Feb', 'Mar'] => will put data in a row
58-
// header.values = [['Jan', 1], ['Feb', 2], ['Mar', 3]] => will create 3 columns
59-
// 1d arrays affect columns
60-
// 2d arrays affect rows within each column
61-
62-
if (
63-
this.props.container.type === 'table' &&
64-
value.length === 1 &&
65-
[
66-
'header.values',
67-
'header.font.color',
68-
'header.font.size',
69-
'header.fill.color',
70-
'columnwidth',
71-
].includes(this.props.attr)
72-
) {
73-
update[this.props.attr] = update[this.props.attr][0];
74-
}
7563
} else {
76-
update[this.props.attr] = this.dataSources[value] || null;
64+
data = this.dataSources[value] || null;
7765
}
78-
update[this.srcAttr] = value;
7966

67+
update[this.props.attr] = maybeTransposeData(
68+
data,
69+
this.srcAttr,
70+
this.props.container.type
71+
);
72+
update[this.srcAttr] = maybeAdjustSrc(
73+
value,
74+
this.srcAttr,
75+
this.props.container.type,
76+
{
77+
fromSrc: this.context.srcConverters
78+
? this.context.srcConverters.fromSrc
79+
: null,
80+
}
81+
);
8082
this.props.updateContainer(update);
8183
}
8284

@@ -122,6 +124,10 @@ UnconnectedDataSelector.contextTypes = {
122124
dataSourceOptions: PropTypes.array,
123125
dataSourceValueRenderer: PropTypes.func,
124126
dataSourceOptionRenderer: PropTypes.func,
127+
srcConverters: PropTypes.shape({
128+
toSrc: PropTypes.func.isRequired,
129+
fromSrc: PropTypes.func.isRequired,
130+
}),
125131
};
126132

127133
function modifyPlotProps(props, context, plotProps) {

src/lib/__tests__/dereference-test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,57 @@ describe('dereference', () => {
1616
expect(Array.isArray(container[0].transforms[0].y)).toBe(true);
1717
expect(Array.isArray(container[0].x)).toBe(true);
1818
});
19+
20+
it('handles multidimensional srcs correctly', () => {
21+
const container = [{zsrc: ['z1', 'z2'], type: 'heatmap'}];
22+
dereference(container, {z1: [1, 2, 3], z2: [2, 2, 2]});
23+
24+
// contents should have been transposed
25+
expect(Array.isArray(container[0].z[0])).toBe(true);
26+
expect(Array.isArray(container[0].z[1])).toBe(true);
27+
expect(Array.isArray(container[0].z[2])).toBe(true);
28+
29+
expect(container[0].z[0][0]).toBe(1);
30+
expect(container[0].z[0][1]).toBe(2);
31+
expect(container[0].z[1][0]).toBe(2);
32+
expect(container[0].z[1][1]).toBe(2);
33+
expect(container[0].z[2][0]).toBe(3);
34+
expect(container[0].z[2][1]).toBe(2);
35+
});
36+
37+
it('handles ambiguous 2d srcs correctly', () => {
38+
const container = [{zsrc: ['z1'], type: 'heatmap'}];
39+
dereference(container, {z1: [1, 2, 3]});
40+
41+
// contents should have been transposed
42+
expect(Array.isArray(container[0].z[0])).toBe(true);
43+
expect(Array.isArray(container[0].z[1])).toBe(true);
44+
expect(Array.isArray(container[0].z[2])).toBe(true);
45+
46+
expect(container[0].z[0][0]).toBe(1);
47+
expect(container[0].z[1][0]).toBe(2);
48+
expect(container[0].z[2][0]).toBe(3);
49+
});
50+
51+
it('uses custom function if provided in config', () => {
52+
const customParsing = src => src.split(',');
53+
const container = [{zsrc: 'z1,z2', type: 'heatmap'}];
54+
dereference(
55+
container,
56+
{z1: [1, 2, 3], z2: [2, 2, 2]},
57+
{toSrc: customParsing}
58+
);
59+
60+
// contents should have been transposed
61+
expect(Array.isArray(container[0].z[0])).toBe(true);
62+
expect(Array.isArray(container[0].z[1])).toBe(true);
63+
expect(Array.isArray(container[0].z[2])).toBe(true);
64+
65+
expect(container[0].z[0][0]).toBe(1);
66+
expect(container[0].z[0][1]).toBe(2);
67+
expect(container[0].z[1][0]).toBe(2);
68+
expect(container[0].z[1][1]).toBe(2);
69+
expect(container[0].z[2][0]).toBe(3);
70+
expect(container[0].z[2][1]).toBe(2);
71+
});
1972
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {maybeAdjustSrc} from '../index';
2+
/* eslint-disable no-magic-numbers */
3+
describe('maybeAdjustSrc', () => {
4+
it('uses custom parsing function if one is provided', () => {
5+
const custom = srcs => srcs.join('$');
6+
const adjusted = maybeAdjustSrc(['z1', 'z2'], 'zsrc', 'heatmap', {
7+
fromSrc: custom,
8+
});
9+
expect(adjusted).toBe('z1$z2');
10+
});
11+
12+
it('reduces src to string for special table case', () => {
13+
const adjusted = maybeAdjustSrc(['z1'], 'header.valuessrc', 'table');
14+
expect(adjusted).toBe('z1');
15+
});
16+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {maybeTransposeData} from '../index';
2+
/* eslint-disable no-magic-numbers */
3+
describe('maybeTransposeData', () => {
4+
it('transposes 2d data for row based traceTypes', () => {
5+
const transposed = maybeTransposeData(
6+
[[1, 2, 3], [4, 5, 6]],
7+
'zsrc',
8+
'heatmap'
9+
);
10+
11+
// [[1, 4], [2, 5], [3, 6]]
12+
expect(transposed.length).toBe(3);
13+
});
14+
15+
it('transposes 1d data for row based traceTypes', () => {
16+
const transposed = maybeTransposeData([1, 2, 3], 'zsrc', 'heatmap');
17+
18+
// [[1], [2], [3]]
19+
expect(transposed.length).toBe(3);
20+
});
21+
22+
it('does not transpose data for column based traceTypes', () => {
23+
const transposed = maybeTransposeData(
24+
[[1, 2, 3], [4, 5, 6]],
25+
'header.valuessrc',
26+
'table'
27+
);
28+
29+
// [[1, 2, 3], [4, 5, 6]]
30+
expect(transposed.length).toBe(2);
31+
});
32+
33+
it('removes extra array wrapping for special cases in tables', () => {
34+
const transposed = maybeTransposeData(
35+
[[1, 2, 3]],
36+
'header.valuessrc',
37+
'table'
38+
);
39+
40+
// [1, 2, 3]
41+
expect(Array.isArray(transposed[0])).toBe(false);
42+
expect(transposed.length).toBe(3);
43+
});
44+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {transpose} from '../index';
2+
/* eslint-disable no-magic-numbers */
3+
describe('transpose', () => {
4+
it('correctly transposes 1d arrays', () => {
5+
const originalArray = [1, 2, 3];
6+
const transposedArray = transpose(originalArray);
7+
8+
expect(transposedArray.length).toBe(3);
9+
10+
transposedArray.forEach(subArray => {
11+
expect(Array.isArray(subArray)).toBe(true);
12+
expect(subArray.length).toBe(1);
13+
});
14+
15+
expect(transposedArray[0][0]).toBe(1);
16+
expect(transposedArray[1][0]).toBe(2);
17+
expect(transposedArray[2][0]).toBe(3);
18+
});
19+
20+
it('correctly transposes 2d arrays', () => {
21+
const originalArray = [[1, 2, 3], [9, 8, 7]];
22+
const transposedArray = transpose(originalArray);
23+
24+
expect(transposedArray.length).toBe(3);
25+
transposedArray.forEach(subArray => {
26+
expect(Array.isArray(subArray)).toBe(true);
27+
expect(subArray.length).toBe(2);
28+
});
29+
30+
expect(transposedArray[0][0]).toBe(1);
31+
expect(transposedArray[0][1]).toBe(9);
32+
expect(transposedArray[1][0]).toBe(2);
33+
expect(transposedArray[1][1]).toBe(8);
34+
expect(transposedArray[2][0]).toBe(3);
35+
expect(transposedArray[2][1]).toBe(7);
36+
});
37+
38+
it('correctly fills non symmetrical 2d arrays', () => {
39+
const originalArray = [[1, 2], [9, 8, 7]];
40+
const transposedArray = transpose(originalArray);
41+
42+
expect(transposedArray.length).toBe(3);
43+
transposedArray.forEach(subArray => {
44+
expect(Array.isArray(subArray)).toBe(true);
45+
expect(subArray.length).toBe(2);
46+
});
47+
48+
expect(transposedArray[0][0]).toBe(1);
49+
expect(transposedArray[0][1]).toBe(9);
50+
expect(transposedArray[1][0]).toBe(2);
51+
expect(transposedArray[1][1]).toBe(8);
52+
expect(transposedArray[2][0]).toBe(null);
53+
expect(transposedArray[2][1]).toBe(7);
54+
});
55+
});

src/lib/dereference.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import walkObject from './walkObject';
2+
import {maybeTransposeData} from './index';
23

34
const SRC_ATTR_PATTERN = /src$/;
45

@@ -7,28 +8,42 @@ export default function dereference(
78
dataSources,
89
config = {deleteKeys: false}
910
) {
10-
const replacer = (key, parent) => {
11+
const replacer = (key, parent, srcPath) => {
1112
if (!SRC_ATTR_PATTERN.test(key)) {
1213
return;
1314
}
1415

15-
const srcRef = parent[key];
16-
const data = dataSources[srcRef];
1716
const dataKey = key.replace(SRC_ATTR_PATTERN, '');
17+
const traceType = parent.type;
1818

19-
if (config.deleteKeys && !(srcRef in dataSources)) {
20-
delete parent[dataKey];
21-
return;
19+
let srcRef = config.toSrc ? config.toSrc(parent[key]) : parent[key];
20+
21+
// making this into an array to more easily lookup 1d and 2d srcs in dataSourceOptions
22+
if (!Array.isArray(srcRef)) {
23+
srcRef = [srcRef];
24+
}
25+
26+
let data = srcRef.map(ref => {
27+
if (config.deleteKeys && !(ref in dataSources)) {
28+
delete parent[dataKey];
29+
}
30+
return dataSources[ref];
31+
});
32+
33+
// remove extra data wrapping
34+
if (srcRef.length === 1) {
35+
data = data[0];
2236
}
2337

2438
if (!Array.isArray(data)) {
2539
return;
2640
}
2741

28-
parent[dataKey] = data;
42+
parent[dataKey] = maybeTransposeData(data, srcPath, traceType);
2943
};
3044

3145
walkObject(container, replacer, {
3246
walkArraysMatchingKeys: ['data', 'transforms'],
47+
pathType: 'nestedProperty',
3348
});
3449
}

0 commit comments

Comments
 (0)