diff --git a/src/EditorControls.js b/src/EditorControls.js
index 4a6e47efc..e21ae8f33 100644
--- a/src/EditorControls.js
+++ b/src/EditorControls.js
@@ -7,6 +7,8 @@ import {
shamefullyAdjustAxisRef,
shamefullyAdjustGeo,
shamefullyAddTableColumns,
+ shamefullyCreateSplitStyleProps,
+ shamefullyAdjustSplitStyleTargetContainers,
} from './shame';
import {EDITOR_ACTIONS} from './lib/constants';
import isNumeric from 'fast-isnumeric';
@@ -66,16 +68,32 @@ class EditorControls extends Component {
shamefullyClearAxisTypes(graphDiv, payload);
shamefullyAdjustAxisRef(graphDiv, payload);
shamefullyAddTableColumns(graphDiv, payload);
+ shamefullyAdjustSplitStyleTargetContainers(graphDiv, payload);
for (let i = 0; i < payload.traceIndexes.length; i++) {
for (const attr in payload.update) {
const traceIndex = payload.traceIndexes[i];
- const prop = nestedProperty(graphDiv.data[traceIndex], attr);
+ const splitTraceGroup = payload.splitTraceGroup
+ ? payload.splitTraceGroup.toString()
+ : null;
+
+ let props = [nestedProperty(graphDiv.data[traceIndex], attr)];
const value = payload.update[attr];
- if (value !== void 0) {
- prop.set(value);
+ if (splitTraceGroup) {
+ props = shamefullyCreateSplitStyleProps(
+ graphDiv,
+ attr,
+ traceIndex,
+ splitTraceGroup
+ );
}
+
+ props.forEach(p => {
+ if (value !== void 0) {
+ p.set(value);
+ }
+ });
}
}
diff --git a/src/components/containers/TraceAccordion.js b/src/components/containers/TraceAccordion.js
index 9b7f863fe..faad5d797 100644
--- a/src/components/containers/TraceAccordion.js
+++ b/src/components/containers/TraceAccordion.js
@@ -10,38 +10,109 @@ import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
const TraceFold = connectTraceToPlot(PlotlyFold);
class TraceAccordion extends Component {
- render() {
- const {data = [], localize: _} = this.context;
- const {
- canAdd,
- canGroup,
- children,
- messageIfEmptyFold,
- excludeFits,
- } = this.props;
+ constructor(props, context) {
+ super(props, context);
+ this.setLocals(props, context);
+ }
+ componentWillReceiveProps(nextProps, nextContext) {
+ this.setLocals(nextProps, nextContext);
+ }
+
+ setLocals(props, context) {
// we don't want to include analysis transforms when we're in the create panel
- const filteredData = data.filter(t => {
- if (excludeFits) {
+ const base = props.canGroup ? context.fullData : context.data;
+
+ this.filteredTracesFullDataPositions = [];
+ this.filteredTraces = base.filter((t, i) => {
+ if (props.excludeFits) {
return !(t.transforms && t.transforms.every(tr => tr.type === 'fit'));
}
+ this.filteredTracesFullDataPositions.push(i);
return true;
});
+ }
- const individualTraces =
- filteredData.length &&
- filteredData.map((d, i) => {
- return (
-
- {children}
-
- );
- });
+ renderGroupedTraceFolds() {
+ if (!this.filteredTraces.length || this.filteredTraces.length < 2) {
+ return null;
+ }
+
+ const dataArrayPositionsByTraceType = {};
+ const fullDataArrayPositionsByTraceType = {};
+
+ this.filteredTraces.forEach((trace, index) => {
+ const traceType = plotlyTraceToCustomTrace(trace);
+ if (!dataArrayPositionsByTraceType[traceType]) {
+ dataArrayPositionsByTraceType[traceType] = [];
+ }
+
+ if (!fullDataArrayPositionsByTraceType[traceType]) {
+ fullDataArrayPositionsByTraceType[traceType] = [];
+ }
+
+ dataArrayPositionsByTraceType[traceType].push(trace.index);
+ fullDataArrayPositionsByTraceType[traceType].push(
+ this.filteredTracesFullDataPositions[index]
+ );
+ });
+
+ return Object.keys(fullDataArrayPositionsByTraceType).map((type, index) => {
+ return (
+
+ {this.props.children}
+
+ );
+ });
+ }
+
+ renderUngroupedTraceFolds() {
+ if (!this.filteredTraces.length) {
+ return null;
+ }
+
+ return this.filteredTraces.map((d, i) => {
+ return (
+
+ {this.props.children}
+
+ );
+ });
+ }
+
+ renderTraceFolds() {
+ if (!this.filteredTraces.length) {
+ return null;
+ }
+
+ return this.filteredTraces.map((d, i) => {
+ return (
+
+ {this.props.children}
+
+ );
+ });
+ }
+
+ render() {
+ const {canAdd, canGroup} = this.props;
+ const _ = this.context.localize;
if (canAdd) {
const addAction = {
@@ -54,58 +125,44 @@ class TraceAccordion extends Component {
}
},
};
+
return (
- {individualTraces ? individualTraces : null}
+ {this.renderTraceFolds()}
);
}
- const tracesByGroup = filteredData.reduce((allTraces, nextTrace, index) => {
- const traceType = plotlyTraceToCustomTrace(nextTrace);
- if (!allTraces[traceType]) {
- allTraces[traceType] = [];
- }
- allTraces[traceType].push(index);
- return allTraces;
- }, {});
- const groupedTraces = Object.keys(tracesByGroup).map((traceType, index) => {
- return (
-
- {this.props.children}
-
- );
- });
+ if (canGroup) {
+ if (this.filteredTraces.length === 1) {
+ return (
+
+ {this.renderUngroupedTraceFolds()}
+
+ );
+ }
- if (canGroup && filteredData.length > 1 && groupedTraces.length > 0) {
- return (
-
-
-
- {_('Individually')}
- {_('By Type')}
-
-
-
- {individualTraces ? individualTraces : null}
-
-
-
- {groupedTraces ? groupedTraces : null}
-
-
-
- );
+ if (this.filteredTraces.length > 1) {
+ return (
+
+
+
+ {_('Individually')}
+ {_('By Type')}
+
+
+ {this.renderUngroupedTraceFolds()}
+
+
+ {this.renderGroupedTraceFolds()}
+
+
+
+ );
+ }
}
- return (
-
- {individualTraces ? individualTraces : null}
-
- );
+
+ return {this.renderTraceFolds()};
}
}
diff --git a/src/components/containers/TransformAccordion.js b/src/components/containers/TransformAccordion.js
index f1129c120..6211bc54e 100644
--- a/src/components/containers/TransformAccordion.js
+++ b/src/components/containers/TransformAccordion.js
@@ -83,9 +83,17 @@ class TransformAccordion extends Component {
));
+ // cannot have 2 Split transforms on one trace:
+ // https://github.com/plotly/plotly.js/issues/1742
+ const addActionOptions =
+ container.transforms &&
+ container.transforms.some(t => t.type === 'groupby')
+ ? transformTypes.filter(t => t.type !== 'groupby')
+ : transformTypes;
+
const addAction = {
label: _('Transform'),
- handler: transformTypes.map(({label, type}) => {
+ handler: addActionOptions.map(({label, type}) => {
return {
label,
handler: context => {
@@ -104,6 +112,10 @@ class TransformAccordion extends Component {
payload.groups = null;
}
+ if (type === 'groupby') {
+ payload.styles = [];
+ }
+
updateContainer({[key]: payload});
}
},
diff --git a/src/components/fields/DataSelector.js b/src/components/fields/DataSelector.js
index 57a8e551e..29e928477 100644
--- a/src/components/fields/DataSelector.js
+++ b/src/components/fields/DataSelector.js
@@ -49,7 +49,7 @@ export class UnconnectedDataSelector extends Component {
Array.isArray(this.fullValue);
}
- this.hasData = props.attr in props.container;
+ this.hasData = props.container ? props.attr in props.container : false;
}
updatePlot(value) {
@@ -83,6 +83,7 @@ export class UnconnectedDataSelector extends Component {
: null,
}
);
+
this.props.updateContainer(update);
}
@@ -136,6 +137,7 @@ UnconnectedDataSelector.contextTypes = {
toSrc: PropTypes.func.isRequired,
fromSrc: PropTypes.func.isRequired,
}),
+ container: PropTypes.object,
};
function modifyPlotProps(props, context, plotProps) {
diff --git a/src/components/fields/MarkerColor.js b/src/components/fields/MarkerColor.js
index b1e2018a4..30fd0a5b2 100644
--- a/src/components/fields/MarkerColor.js
+++ b/src/components/fields/MarkerColor.js
@@ -91,8 +91,10 @@ class UnconnectedMarkerColor extends Component {
(Array.isArray(this.props.fullValue) &&
this.props.fullValue.includes(MULTI_VALUED)) ||
(this.props.container.marker &&
+ this.props.container.marker.colorscale &&
this.props.container.marker.colorscale === MULTI_VALUED) ||
(this.props.container.marker &&
+ this.props.container.marker.colorsrc &&
this.props.container.marker.colorsrc === MULTI_VALUED) ||
(this.props.container.marker &&
this.props.container.marker.color &&
@@ -130,12 +132,12 @@ class UnconnectedMarkerColor extends Component {
renderVariableControls() {
const multiValued =
- (this.props.container &&
- this.props.container.marker &&
- (this.props.container.marker.colorscale &&
- this.props.container.marker.colorscale === MULTI_VALUED)) ||
- (this.props.container.marker.colorsrc &&
- this.props.container.marker.colorsrc === MULTI_VALUED);
+ this.props.container &&
+ this.props.container.marker &&
+ ((this.props.container.marker.colorscale &&
+ this.props.container.marker.colorscale === MULTI_VALUED) ||
+ (this.props.container.marker.colorsrc &&
+ this.props.container.marker.colorsrc === MULTI_VALUED));
return (
@@ -154,72 +156,87 @@ class UnconnectedMarkerColor extends Component {
render() {
const {attr} = this.props;
- const {localize: _} = this.context;
- const {type} = this.state;
- const options = [
- {label: _('Constant'), value: 'constant'},
- {label: _('Variable'), value: 'variable'},
- ];
+ const {localize: _, container} = this.context;
- return (
-
-
-
-
-
- {!type ? null : (
-
- {type === 'constant'
- ? _('All points in a trace are colored in the same color.')
- : _('Each point in a trace is colored according to data.')}
-
- )}
+ // TO DO: https://github.com/plotly/react-chart-editor/issues/654
+ const noSplitsPresent =
+ container &&
+ (!container.transforms ||
+ !container.transforms.filter(t => t.type === 'groupby').length);
+
+ if (noSplitsPresent) {
+ const {type} = this.state;
+ const options = [
+ {label: _('Constant'), value: 'constant'},
+ {label: _('Variable'), value: 'variable'},
+ ];
+
+ return (
+
+
+
+
+
+ {!type ? null : (
+
+ {type === 'constant'
+ ? _('All points in a trace are colored in the same color.')
+ : _('Each point in a trace is colored according to data.')}
+
+ )}
+
+
+ {!type
+ ? null
+ : type === 'constant'
+ ? this.renderConstantControls()
+ : this.renderVariableControls()}
+ {type === 'constant' ? null : (
+
+
+
+
+
+
+
+
+ )}
+
+ );
+ }
- {!type
- ? null
- : type === 'constant'
- ? this.renderConstantControls()
- : this.renderVariableControls()}
-
- {type === 'constant' ? null : (
-
-
-
-
-
-
-
-
- )}
-
+ return (
+
+ {this.renderConstantControls()}
+
);
}
}
@@ -234,6 +251,7 @@ UnconnectedMarkerColor.contextTypes = {
localize: PropTypes.func,
updateContainer: PropTypes.func,
traceIndexes: PropTypes.array,
+ container: PropTypes.object,
};
export default connectToContainer(UnconnectedMarkerColor);
diff --git a/src/components/fields/MultiColorPicker.js b/src/components/fields/MultiColorPicker.js
index 9d60f0899..9fb1aee0c 100644
--- a/src/components/fields/MultiColorPicker.js
+++ b/src/components/fields/MultiColorPicker.js
@@ -1,10 +1,11 @@
-import React, {Component} from 'react';
import Color from './Color';
import Colorscale from './Colorscale';
-import Info from './Info';
import Field from './Field';
-import RadioBlocks from '../widgets/RadioBlocks';
+import Info from './Info';
import PropTypes from 'prop-types';
+import RadioBlocks from '../widgets/RadioBlocks';
+import React, {Component} from 'react';
+import nestedProperty from 'plotly.js/src/lib/nested_property';
import {adjustColorscale, connectToContainer} from 'lib';
class UnconnectedMultiColorPicker extends Component {
@@ -26,7 +27,7 @@ class UnconnectedMultiColorPicker extends Component {
}
setColors(colorscale, colorscaleType) {
- const numberOfTraces = this.context.traceIndexes.length;
+ const numberOfTraces = this.props.tracesToColor.length;
const colors = colorscale.map(c => c[1]);
let adjustedColors = colors;
@@ -133,6 +134,7 @@ UnconnectedMultiColorPicker.propTypes = {
onConstantColorOptionChange: PropTypes.func,
messageKeyWordSingle: PropTypes.string,
messageKeyWordPlural: PropTypes.string,
+ tracesToColor: PropTypes.array,
...Field.propTypes,
};
@@ -146,22 +148,32 @@ UnconnectedMultiColorPicker.contextTypes = {
export default connectToContainer(UnconnectedMultiColorPicker, {
modifyPlotProps(props, context, plotProps) {
if (plotProps.isVisible) {
- plotProps.fullValue = context.traceIndexes
- .map(index => {
- const trace = context.fullData.filter(
- trace => trace.index === index
- )[0];
-
- const properties = props.attr.split('.');
- let value = trace;
-
- properties.forEach(prop => {
- value = value[prop];
- });
-
- return value;
- })
- .map(c => [0, c]);
+ const colors = [];
+ let tracesToColor = [];
+ const dedupedTraceIndexes = [];
+
+ context.traceIndexes.forEach(i => {
+ if (!dedupedTraceIndexes.includes(i)) {
+ dedupedTraceIndexes.push(i);
+ }
+ });
+
+ dedupedTraceIndexes.forEach(traceIndex => {
+ const traces = context.fullData.filter(
+ trace => trace.index === traceIndex
+ );
+ tracesToColor = tracesToColor.concat(traces);
+
+ traces.forEach(t => {
+ const value = nestedProperty(t, props.attr).get();
+ if (value) {
+ colors.push(value);
+ }
+ });
+ });
+
+ plotProps.tracesToColor = tracesToColor;
+ plotProps.fullValue = colors.map(c => [0, c]);
}
},
});
diff --git a/src/default_panels/GraphSubplotsPanel.js b/src/default_panels/GraphSubplotsPanel.js
index 04ed56e41..3aa101da5 100644
--- a/src/default_panels/GraphSubplotsPanel.js
+++ b/src/default_panels/GraphSubplotsPanel.js
@@ -17,7 +17,7 @@ import {
import {TRACE_TO_AXIS} from '../lib/constants';
const GraphSubplotsPanel = (props, {localize: _}) => (
-
+
diff --git a/src/lib/connectTraceToPlot.js b/src/lib/connectTraceToPlot.js
index 8cff167f4..15bd8952b 100644
--- a/src/lib/connectTraceToPlot.js
+++ b/src/lib/connectTraceToPlot.js
@@ -24,15 +24,13 @@ export default function connectTraceToPlot(WrappedComponent) {
this.setLocals(nextProps, nextContext);
}
- setLocals(props, context) {
- const {traceIndexes} = props;
- const {data, fullData, plotly} = context;
-
- const trace = traceIndexes.length > 0 ? data[traceIndexes[0]] : {};
-
+ getFullTraceFromDataIndex(trace, context) {
let fullTrace = {};
- for (let i = 0; i < fullData.length; i++) {
- if (traceIndexes[0] === fullData[i]._fullInput.index) {
+
+ for (let i = 0; i < context.fullData.length; i++) {
+ if (
+ this.props.traceIndexes[0] === context.fullData[i]._fullInput.index
+ ) {
/*
* Fit transforms are custom transforms in our custom plotly.js bundle,
* they are different from others as they create an extra trace in the
@@ -52,15 +50,26 @@ export default function connectTraceToPlot(WrappedComponent) {
trace.transforms &&
trace.transforms.every(t => t.type === 'fit')
) {
- fullData[i]._fullInput = fullData[i];
+ context.fullData[i]._fullInput = context.fullData[i];
}
- fullTrace = fullData[i]._fullInput;
-
+ fullTrace = context.fullData[i]._fullInput;
break;
}
}
+ return fullTrace;
+ }
+
+ setLocals(props, context) {
+ const {traceIndexes, fullDataArrayPosition} = props;
+ const {data, fullData, plotly} = context;
+
+ const trace = data[traceIndexes[0]];
+ const fullTrace = fullDataArrayPosition
+ ? fullData[fullDataArrayPosition[0]]
+ : this.getFullTraceFromDataIndex(trace, context);
+
this.childContext = {
getValObject: attr =>
!plotly
@@ -110,6 +119,12 @@ export default function connectTraceToPlot(WrappedComponent) {
updateTrace(update) {
if (this.context.onUpdate) {
+ const splitTraceGroup = this.props.fullDataArrayPosition
+ ? this.props.fullDataArrayPosition.map(
+ p => this.context.fullData[p]._group
+ )
+ : null;
+
if (Array.isArray(update)) {
update.forEach((u, i) => {
this.context.onUpdate({
@@ -117,6 +132,18 @@ export default function connectTraceToPlot(WrappedComponent) {
payload: {
update: u,
traceIndexes: [this.props.traceIndexes[i]],
+ splitTraceGroup: splitTraceGroup ? splitTraceGroup[i] : null,
+ },
+ });
+ });
+ } else if (splitTraceGroup) {
+ this.props.traceIndexes.forEach((t, i) => {
+ this.context.onUpdate({
+ type: EDITOR_ACTIONS.UPDATE_TRACES,
+ payload: {
+ update,
+ traceIndexes: [this.props.traceIndexes[i]],
+ splitTraceGroup: splitTraceGroup ? splitTraceGroup[i] : null,
},
});
});
@@ -205,6 +232,7 @@ export default function connectTraceToPlot(WrappedComponent) {
TraceConnectedComponent.propTypes = {
traceIndexes: PropTypes.arrayOf(PropTypes.number).isRequired,
+ fullDataArrayPosition: PropTypes.arrayOf(PropTypes.number),
};
TraceConnectedComponent.contextTypes = {
diff --git a/src/lib/customTraceType.js b/src/lib/customTraceType.js
index def6ceaee..5b1e3017d 100644
--- a/src/lib/customTraceType.js
+++ b/src/lib/customTraceType.js
@@ -1,6 +1,12 @@
import {COLORS} from 'lib/constants';
export function plotlyTraceToCustomTrace(trace) {
+ if (typeof trace !== 'object') {
+ throw new Error(
+ `trace provided to plotlyTraceToCustomTrace function should be an object, received ${typeof trace}`
+ );
+ }
+
const gl = 'gl';
const type = trace.type
? trace.type.endsWith(gl)
diff --git a/src/lib/multiValues.js b/src/lib/multiValues.js
index 2f702d9c4..ab0be3898 100644
--- a/src/lib/multiValues.js
+++ b/src/lib/multiValues.js
@@ -22,7 +22,7 @@ function setMultiValuedContainer(intoObj, fromObj, key, config = {}) {
// don't merge private attrs
if (
- (typeof key === 'string' && key.charAt(0) === '_') ||
+ (typeof key === 'string' && key.charAt(0) === '_' && key !== '_group') ||
typeof intoVal === 'function' ||
key === 'module'
) {
diff --git a/src/shame.js b/src/shame.js
index 1c1c0e397..1608d0db8 100644
--- a/src/shame.js
+++ b/src/shame.js
@@ -112,3 +112,93 @@ export const shamefullyAddTableColumns = (graphDiv, {traceIndexes, update}) => {
update['header.values'] = null;
}
};
+
+export const shamefullyAdjustSplitStyleTargetContainers = (
+ graphDiv,
+ {traceIndexes, update}
+) => {
+ for (const attr in update) {
+ if (attr && attr.startsWith('transforms') && attr.endsWith('groups')) {
+ const transformIndex = parseInt(attr.split('[')[1], 10);
+ const transform =
+ graphDiv.data[traceIndexes[0]].transforms[transformIndex];
+
+ if (transform && transform.type === 'groupby' && transform.styles) {
+ // Create style containers for all groups
+ if (!transform.styles.length && update[attr]) {
+ const dedupedGroups = [];
+ update[attr].forEach(group => {
+ if (!dedupedGroups.includes(group)) {
+ dedupedGroups.push(group);
+ }
+ });
+
+ const styles = dedupedGroups.map(groupEl => ({
+ target: groupEl,
+ value: {},
+ }));
+
+ update[`transforms[${transformIndex}].styles`] = styles;
+ }
+
+ // When clearing the data selector of groupby transforms, we want to clear
+ // all the styles we've added
+ if (transform.styles.length && !update[attr]) {
+ update[`transforms[${transformIndex}].styles`] = [];
+ }
+ }
+ }
+ }
+};
+
+export const shamefullyCreateSplitStyleProps = (
+ graphDiv,
+ attr,
+ traceIndex,
+ splitTraceGroup
+) => {
+ if (!Array.isArray(splitTraceGroup)) {
+ splitTraceGroup = [splitTraceGroup]; // eslint-disable-line
+ }
+
+ let indexOfSplitTransform = null;
+
+ graphDiv.data[traceIndex].transforms.forEach((t, i) => {
+ if (t.type === 'groupby') {
+ indexOfSplitTransform = i;
+ }
+ });
+
+ function getProp(group) {
+ let indexOfStyleObject = null;
+
+ graphDiv.data[traceIndex].transforms[indexOfSplitTransform].styles.forEach(
+ (s, i) => {
+ if (s.target.toString() === group) {
+ indexOfStyleObject = i;
+ }
+ }
+ );
+
+ let path =
+ graphDiv.data[traceIndex].transforms[indexOfSplitTransform].styles[
+ indexOfStyleObject
+ ].value;
+
+ attr.split('.').forEach(p => {
+ if (!path[p]) {
+ path[p] = {};
+ }
+ path = path[p];
+ });
+
+ return nestedProperty(
+ graphDiv.data[traceIndex].transforms[indexOfSplitTransform].styles[
+ indexOfStyleObject
+ ].value,
+ attr
+ );
+ }
+
+ return splitTraceGroup.map(g => getProp(g));
+};