Skip to content
Merged
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
11 changes: 10 additions & 1 deletion src/EditorControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class EditorControls extends Component {
return {
advancedTraceTypeSelector: this.props.advancedTraceTypeSelector,
config: gd._context,
srcConverters: this.props.srcConverters,
data: gd.data,
dataSources: this.props.dataSources,
dataSourceOptions: this.props.dataSourceOptions,
Expand Down Expand Up @@ -280,6 +281,10 @@ EditorControls.propTypes = {
beforeUpdateTraces: PropTypes.func,
children: PropTypes.node,
className: PropTypes.string,
srcConverters: PropTypes.shape({
toSrc: PropTypes.func.isRequired,
fromSrc: PropTypes.func.isRequired,
}),
dataSourceOptionRenderer: PropTypes.func,
dataSourceOptions: PropTypes.array,
dataSources: PropTypes.object,
Expand All @@ -289,8 +294,8 @@ EditorControls.propTypes = {
locale: PropTypes.string,
onUpdate: PropTypes.func,
plotly: PropTypes.object,
traceTypesConfig: PropTypes.object,
showFieldTooltips: PropTypes.bool,
traceTypesConfig: PropTypes.object,
};

EditorControls.defaultProps = {
Expand All @@ -306,6 +311,10 @@ EditorControls.defaultProps = {
EditorControls.childContextTypes = {
advancedTraceTypeSelector: PropTypes.bool,
config: PropTypes.object,
srcConverters: PropTypes.shape({
toSrc: PropTypes.func.isRequired,
fromSrc: PropTypes.func.isRequired,
}),
data: PropTypes.array,
dataSourceOptionRenderer: PropTypes.func,
dataSourceOptions: PropTypes.array,
Expand Down
5 changes: 5 additions & 0 deletions src/PlotlyEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class PlotlyEditor extends Component {
traceTypesConfig={this.props.traceTypesConfig}
dictionaries={this.props.dictionaries}
showFieldTooltips={this.props.showFieldTooltips}
srcConverters={this.props.srcConverters}
>
{this.props.children}
</EditorControls>
Expand Down Expand Up @@ -70,6 +71,10 @@ PlotlyEditor.propTypes = {
divId: PropTypes.string,
hideControls: PropTypes.bool,
showFieldTooltips: PropTypes.bool,
srcConverters: PropTypes.shape({
toSrc: PropTypes.func.isRequired,
fromSrc: PropTypes.func.isRequired,
}),
};

PlotlyEditor.defaultProps = {
Expand Down
68 changes: 37 additions & 31 deletions src/components/fields/DataSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import React, {Component} from 'react';
import Field from './Field';
import nestedProperty from 'plotly.js/src/lib/nested_property';
import {connectToContainer} from 'lib';
import {connectToContainer, maybeAdjustSrc, maybeTransposeData} from 'lib';

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

this.srcAttr = props.attr + 'src';
this.srcProperty = nestedProperty(props.container, this.srcAttr);
this.fullValue = this.srcProperty.get();
this.srcProperty = nestedProperty(props.container, this.srcAttr).get();
this.fullValue = this.context.srcConverters
? this.context.srcConverters.toSrc(this.srcProperty, props.container.type)
: this.srcProperty;

this.is2D = false;
if (props.container) {
this.is2D =
(props.attr === 'z' &&
['contour', 'heatmap', 'surface', 'heatmapgl'].includes(
props.container.type
)) ||
[
'contour',
'contourgl',
'heatmap',
'heatmapgl',
'surface',
'carpet',
'contourcarpet',
].includes(props.container.type)) ||
(props.container.type === 'table' && props.attr !== 'columnorder');
}
}
Expand All @@ -44,39 +52,33 @@ export class UnconnectedDataSelector extends Component {
if (!this.props.updateContainer) {
return;
}

const update = {};
let data;

if (Array.isArray(value)) {
update[this.props.attr] = value
data = value
.filter(v => Array.isArray(this.dataSources[v]))
.map(v => this.dataSources[v]);

// Table traces have many configuration options,
// The below attributes can be 2d or 1d and will affect the plot differently
// EX:
// header.values = ['Jan', 'Feb', 'Mar'] => will put data in a row
// header.values = [['Jan', 1], ['Feb', 2], ['Mar', 3]] => will create 3 columns
// 1d arrays affect columns
// 2d arrays affect rows within each column

if (
this.props.container.type === 'table' &&
value.length === 1 &&
[
'header.values',
'header.font.color',
'header.font.size',
'header.fill.color',
'columnwidth',
].includes(this.props.attr)
) {
update[this.props.attr] = update[this.props.attr][0];
}
} else {
update[this.props.attr] = this.dataSources[value] || null;
data = this.dataSources[value] || null;
}
update[this.srcAttr] = value;

update[this.props.attr] = maybeTransposeData(
data,
this.srcAttr,
this.props.container.type
);
update[this.srcAttr] = maybeAdjustSrc(
value,
this.srcAttr,
this.props.container.type,
{
fromSrc: this.context.srcConverters
? this.context.srcConverters.fromSrc
: null,
}
);
this.props.updateContainer(update);
}

Expand Down Expand Up @@ -122,6 +124,10 @@ UnconnectedDataSelector.contextTypes = {
dataSourceOptions: PropTypes.array,
dataSourceValueRenderer: PropTypes.func,
dataSourceOptionRenderer: PropTypes.func,
srcConverters: PropTypes.shape({
toSrc: PropTypes.func.isRequired,
fromSrc: PropTypes.func.isRequired,
}),
};

function modifyPlotProps(props, context, plotProps) {
Expand Down
53 changes: 53 additions & 0 deletions src/lib/__tests__/dereference-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,57 @@ describe('dereference', () => {
expect(Array.isArray(container[0].transforms[0].y)).toBe(true);
expect(Array.isArray(container[0].x)).toBe(true);
});

it('handles multidimensional srcs correctly', () => {
const container = [{zsrc: ['z1', 'z2'], type: 'heatmap'}];
dereference(container, {z1: [1, 2, 3], z2: [2, 2, 2]});

// contents should have been transposed
expect(Array.isArray(container[0].z[0])).toBe(true);
expect(Array.isArray(container[0].z[1])).toBe(true);
expect(Array.isArray(container[0].z[2])).toBe(true);

expect(container[0].z[0][0]).toBe(1);
expect(container[0].z[0][1]).toBe(2);
expect(container[0].z[1][0]).toBe(2);
expect(container[0].z[1][1]).toBe(2);
expect(container[0].z[2][0]).toBe(3);
expect(container[0].z[2][1]).toBe(2);
});

it('handles ambiguous 2d srcs correctly', () => {
const container = [{zsrc: ['z1'], type: 'heatmap'}];
dereference(container, {z1: [1, 2, 3]});

// contents should have been transposed
expect(Array.isArray(container[0].z[0])).toBe(true);
expect(Array.isArray(container[0].z[1])).toBe(true);
expect(Array.isArray(container[0].z[2])).toBe(true);

expect(container[0].z[0][0]).toBe(1);
expect(container[0].z[1][0]).toBe(2);
expect(container[0].z[2][0]).toBe(3);
});

it('uses custom function if provided in config', () => {
const customParsing = src => src.split(',');
const container = [{zsrc: 'z1,z2', type: 'heatmap'}];
dereference(
container,
{z1: [1, 2, 3], z2: [2, 2, 2]},
{toSrc: customParsing}
);

// contents should have been transposed
expect(Array.isArray(container[0].z[0])).toBe(true);
expect(Array.isArray(container[0].z[1])).toBe(true);
expect(Array.isArray(container[0].z[2])).toBe(true);

expect(container[0].z[0][0]).toBe(1);
expect(container[0].z[0][1]).toBe(2);
expect(container[0].z[1][0]).toBe(2);
expect(container[0].z[1][1]).toBe(2);
expect(container[0].z[2][0]).toBe(3);
expect(container[0].z[2][1]).toBe(2);
});
});
16 changes: 16 additions & 0 deletions src/lib/__tests__/maybeAdjustSrc-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {maybeAdjustSrc} from '../index';
/* eslint-disable no-magic-numbers */
describe('maybeAdjustSrc', () => {
it('uses custom parsing function if one is provided', () => {
const custom = srcs => srcs.join('$');
const adjusted = maybeAdjustSrc(['z1', 'z2'], 'zsrc', 'heatmap', {
fromSrc: custom,
});
expect(adjusted).toBe('z1$z2');
});

it('reduces src to string for special table case', () => {
const adjusted = maybeAdjustSrc(['z1'], 'header.valuessrc', 'table');
expect(adjusted).toBe('z1');
});
});
44 changes: 44 additions & 0 deletions src/lib/__tests__/maybeTransposeData-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {maybeTransposeData} from '../index';
/* eslint-disable no-magic-numbers */
describe('maybeTransposeData', () => {
it('transposes 2d data for row based traceTypes', () => {
const transposed = maybeTransposeData(
[[1, 2, 3], [4, 5, 6]],
'zsrc',
'heatmap'
);

// [[1, 4], [2, 5], [3, 6]]
expect(transposed.length).toBe(3);
});

it('transposes 1d data for row based traceTypes', () => {
const transposed = maybeTransposeData([1, 2, 3], 'zsrc', 'heatmap');

// [[1], [2], [3]]
expect(transposed.length).toBe(3);
});

it('does not transpose data for column based traceTypes', () => {
const transposed = maybeTransposeData(
[[1, 2, 3], [4, 5, 6]],
'header.valuessrc',
'table'
);

// [[1, 2, 3], [4, 5, 6]]
expect(transposed.length).toBe(2);
});

it('removes extra array wrapping for special cases in tables', () => {
const transposed = maybeTransposeData(
[[1, 2, 3]],
'header.valuessrc',
'table'
);

// [1, 2, 3]
expect(Array.isArray(transposed[0])).toBe(false);
expect(transposed.length).toBe(3);
});
});
55 changes: 55 additions & 0 deletions src/lib/__tests__/transpose-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {transpose} from '../index';
/* eslint-disable no-magic-numbers */
describe('transpose', () => {
it('correctly transposes 1d arrays', () => {
const originalArray = [1, 2, 3];
const transposedArray = transpose(originalArray);

expect(transposedArray.length).toBe(3);

transposedArray.forEach(subArray => {
expect(Array.isArray(subArray)).toBe(true);
expect(subArray.length).toBe(1);
});

expect(transposedArray[0][0]).toBe(1);
expect(transposedArray[1][0]).toBe(2);
expect(transposedArray[2][0]).toBe(3);
});

it('correctly transposes 2d arrays', () => {
const originalArray = [[1, 2, 3], [9, 8, 7]];
const transposedArray = transpose(originalArray);

expect(transposedArray.length).toBe(3);
transposedArray.forEach(subArray => {
expect(Array.isArray(subArray)).toBe(true);
expect(subArray.length).toBe(2);
});

expect(transposedArray[0][0]).toBe(1);
expect(transposedArray[0][1]).toBe(9);
expect(transposedArray[1][0]).toBe(2);
expect(transposedArray[1][1]).toBe(8);
expect(transposedArray[2][0]).toBe(3);
expect(transposedArray[2][1]).toBe(7);
});

it('correctly fills non symmetrical 2d arrays', () => {
const originalArray = [[1, 2], [9, 8, 7]];
const transposedArray = transpose(originalArray);

expect(transposedArray.length).toBe(3);
transposedArray.forEach(subArray => {
expect(Array.isArray(subArray)).toBe(true);
expect(subArray.length).toBe(2);
});

expect(transposedArray[0][0]).toBe(1);
expect(transposedArray[0][1]).toBe(9);
expect(transposedArray[1][0]).toBe(2);
expect(transposedArray[1][1]).toBe(8);
expect(transposedArray[2][0]).toBe(null);
expect(transposedArray[2][1]).toBe(7);
});
});
29 changes: 22 additions & 7 deletions src/lib/dereference.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import walkObject from './walkObject';
import {maybeTransposeData} from './index';

const SRC_ATTR_PATTERN = /src$/;

Expand All @@ -7,28 +8,42 @@ export default function dereference(
dataSources,
config = {deleteKeys: false}
) {
const replacer = (key, parent) => {
const replacer = (key, parent, srcPath) => {
if (!SRC_ATTR_PATTERN.test(key)) {
return;
}

const srcRef = parent[key];
const data = dataSources[srcRef];
const dataKey = key.replace(SRC_ATTR_PATTERN, '');
const traceType = parent.type;

if (config.deleteKeys && !(srcRef in dataSources)) {
delete parent[dataKey];
return;
let srcRef = config.toSrc ? config.toSrc(parent[key]) : parent[key];

// making this into an array to more easily lookup 1d and 2d srcs in dataSourceOptions
if (!Array.isArray(srcRef)) {
srcRef = [srcRef];
}

let data = srcRef.map(ref => {
if (config.deleteKeys && !(ref in dataSources)) {
delete parent[dataKey];
}
return dataSources[ref];
});

// remove extra data wrapping
if (srcRef.length === 1) {
data = data[0];
}

if (!Array.isArray(data)) {
return;
}

parent[dataKey] = data;
parent[dataKey] = maybeTransposeData(data, srcPath, traceType);
};

walkObject(container, replacer, {
walkArraysMatchingKeys: ['data', 'transforms'],
pathType: 'nestedProperty',
});
}
Loading