diff --git a/client/app/assets/less/ant.less b/client/app/assets/less/ant.less
index 37c1dd122..864bf8ee4 100644
--- a/client/app/assets/less/ant.less
+++ b/client/app/assets/less/ant.less
@@ -386,4 +386,12 @@
}
}
}
-}
\ No newline at end of file
+}
+
+// overrides for checkbox
+@checkbox-prefix-cls: ~'@{ant-prefix}-checkbox';
+
+.@{checkbox-prefix-cls}-wrapper + span,
+.@{checkbox-prefix-cls} + span {
+ padding-right: 0;
+}
diff --git a/client/app/components/ColorPicker/index.jsx b/client/app/components/ColorPicker/index.jsx
index 32bd7703f..d057bbebb 100644
--- a/client/app/components/ColorPicker/index.jsx
+++ b/client/app/components/ColorPicker/index.jsx
@@ -2,11 +2,11 @@ import { toString } from 'lodash';
import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
-import tinycolor from 'tinycolor2';
import Popover from 'antd/lib/popover';
import Card from 'antd/lib/card';
import Tooltip from 'antd/lib/tooltip';
import Icon from 'antd/lib/icon';
+import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
import ColorInput from './Input';
import Swatch from './Swatch';
@@ -17,6 +17,7 @@ import './index.less';
export default function ColorPicker({
color, placement, presetColors, presetColumns, interactive, children, onChange, triggerProps,
+ addonBefore, addonAfter,
}) {
const [visible, setVisible] = useState(false);
const validatedColor = useMemo(() => validateColor(color), [color]);
@@ -61,45 +62,49 @@ export default function ColorPicker({
}, [validatedColor, visible]);
return (
-
-
+ {addonBefore}
+
+
+
+ )}
+ trigger="click"
+ placement={placement}
+ visible={visible}
+ onVisibleChange={setVisible}
+ >
+ {children || (
+
-
- )}
- trigger="click"
- placement={placement}
- visible={visible}
- onVisibleChange={setVisible}
- >
- {children || (
-
- )}
-
+ )}
+
+ {addonAfter}
+
);
}
@@ -118,6 +123,8 @@ ColorPicker.propTypes = {
interactive: PropTypes.bool,
triggerProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
children: PropTypes.node,
+ addonBefore: PropTypes.node,
+ addonAfter: PropTypes.node,
onChange: PropTypes.func,
};
@@ -129,6 +136,8 @@ ColorPicker.defaultProps = {
interactive: false,
triggerProps: {},
children: null,
+ addonBefore: null,
+ addonAfter: null,
onChange: () => {},
};
diff --git a/client/app/components/HelpTrigger.jsx b/client/app/components/HelpTrigger.jsx
index 68fc112ab..857248cbf 100644
--- a/client/app/components/HelpTrigger.jsx
+++ b/client/app/components/HelpTrigger.jsx
@@ -1,3 +1,4 @@
+import { startsWith } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
@@ -87,29 +88,30 @@ export const TYPES = {
'/user-guide/querying/writing-queries#Managing-Query-Permissions',
'Guide: Managing Query Permissions',
],
+ NUMBER_FORMAT_SPECS: [
+ '/user-guide/visualizations/formatting-numbers',
+ 'Formatting Numbers',
+ ],
};
export default class HelpTrigger extends React.Component {
static propTypes = {
type: PropTypes.oneOf(Object.keys(TYPES)).isRequired,
className: PropTypes.string,
+ showTooltip: PropTypes.bool,
children: PropTypes.node,
};
static defaultProps = {
className: null,
+ showTooltip: true,
children: ,
};
- iframeRef = null;
+ iframeRef = React.createRef();
iframeLoadingTimeout = null;
- constructor(props) {
- super(props);
- this.iframeRef = React.createRef();
- }
-
state = {
visible: false,
loading: false,
@@ -118,7 +120,7 @@ export default class HelpTrigger extends React.Component {
};
componentDidMount() {
- window.addEventListener('message', this.onPostMessageReceived, DOMAIN);
+ window.addEventListener('message', this.onPostMessageReceived, false);
}
componentWillUnmount() {
@@ -142,13 +144,17 @@ export default class HelpTrigger extends React.Component {
};
onPostMessageReceived = (event) => {
+ if (!startsWith(event.origin, DOMAIN)) {
+ return;
+ }
+
const { type, message: currentUrl } = event.data || {};
if (type !== IFRAME_URL_UPDATE_MESSAGE) {
return;
}
this.setState({ currentUrl });
- }
+ };
openDrawer = () => {
this.setState({ visible: true });
@@ -174,7 +180,7 @@ export default class HelpTrigger extends React.Component {
return (
-
+
{this.props.children}
diff --git a/client/app/components/TextAlignmentSelect/index.jsx b/client/app/components/TextAlignmentSelect/index.jsx
new file mode 100644
index 000000000..daa854f2d
--- /dev/null
+++ b/client/app/components/TextAlignmentSelect/index.jsx
@@ -0,0 +1,45 @@
+import { pickBy, startsWith } from 'lodash';
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import Radio from 'antd/lib/radio';
+import Icon from 'antd/lib/icon';
+import Tooltip from 'antd/lib/tooltip';
+
+import './index.less';
+
+export default function TextAlignmentSelect({ className, ...props }) {
+ return (
+ // Antd RadioGroup does not use any custom attributes
+ startsWith(k, 'data-'))}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+TextAlignmentSelect.propTypes = {
+ className: PropTypes.string,
+};
+
+TextAlignmentSelect.defaultProps = {
+ className: null,
+};
diff --git a/client/app/components/TextAlignmentSelect/index.less b/client/app/components/TextAlignmentSelect/index.less
new file mode 100644
index 000000000..6cfcb76ac
--- /dev/null
+++ b/client/app/components/TextAlignmentSelect/index.less
@@ -0,0 +1,13 @@
+.ant-radio-group.text-alignment-select {
+ display: flex;
+ align-items: stretch;
+ justify-content: stretch;
+
+ .ant-radio-button-wrapper {
+ flex-grow: 1;
+ text-align: center;
+ // fit height
+ height: 35px;
+ line-height: 33px;
+ }
+}
diff --git a/client/app/components/visualizations/editor/ContextHelp.jsx b/client/app/components/visualizations/editor/ContextHelp.jsx
new file mode 100644
index 000000000..24531fbbe
--- /dev/null
+++ b/client/app/components/visualizations/editor/ContextHelp.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Popover from 'antd/lib/popover';
+import Tooltip from 'antd/lib/tooltip';
+import Icon from 'antd/lib/icon';
+import HelpTrigger from '@/components/HelpTrigger';
+
+import './context-help.less';
+
+export default function ContextHelp({ icon, children, ...props }) {
+ return (
+ {icon || ContextHelp.defaultIcon}
+ );
+}
+
+ContextHelp.propTypes = {
+ icon: PropTypes.node,
+ children: PropTypes.node,
+};
+
+ContextHelp.defaultProps = {
+ icon: null,
+ children: null,
+};
+
+ContextHelp.defaultIcon = (
+
+);
+
+function NumberFormatSpecs() {
+ return (
+
+ {ContextHelp.defaultIcon}
+
+ );
+}
+
+function DateTimeFormatSpecs() {
+ return (
+ Formatting Dates and Times)}>
+
+ {ContextHelp.defaultIcon}
+
+
+ );
+}
+
+ContextHelp.NumberFormatSpecs = NumberFormatSpecs;
+ContextHelp.DateTimeFormatSpecs = DateTimeFormatSpecs;
diff --git a/client/app/components/visualizations/editor/Section.jsx b/client/app/components/visualizations/editor/Section.jsx
new file mode 100644
index 000000000..948bb34ed
--- /dev/null
+++ b/client/app/components/visualizations/editor/Section.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+function SectionTitle({ className, children, ...props }) {
+ if (!children) {
+ return null;
+ }
+
+ return
{children}
;
+}
+
+SectionTitle.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node,
+};
+
+SectionTitle.defaultProps = {
+ className: null,
+ children: null,
+};
+
+export default function Section({ className, children, ...props }) {
+ return (
+
+ {children}
+
+ );
+}
+
+Section.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node,
+};
+
+Section.defaultProps = {
+ className: null,
+ children: null,
+};
+
+Section.Title = SectionTitle;
diff --git a/client/app/components/visualizations/editor/Switch.jsx b/client/app/components/visualizations/editor/Switch.jsx
new file mode 100644
index 000000000..90ac84940
--- /dev/null
+++ b/client/app/components/visualizations/editor/Switch.jsx
@@ -0,0 +1,34 @@
+import React, { useMemo } from 'react';
+import PropTypes from 'prop-types';
+import AntSwitch from 'antd/lib/switch';
+import Typography from 'antd/lib/typography';
+
+export default function Switch({ id, children, disabled, ...props }) {
+ const fallbackId = useMemo(() => `visualization-editor-control-${Math.random().toString(36).substr(2, 10)}`, []);
+ id = id || fallbackId;
+
+ if (children) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+Switch.propTypes = {
+ id: PropTypes.string,
+ disabled: PropTypes.bool,
+ children: PropTypes.node,
+};
+
+Switch.defaultProps = {
+ id: null,
+ disabled: false,
+ children: null,
+};
diff --git a/client/app/components/visualizations/editor/context-help.less b/client/app/components/visualizations/editor/context-help.less
new file mode 100644
index 000000000..287267fbb
--- /dev/null
+++ b/client/app/components/visualizations/editor/context-help.less
@@ -0,0 +1,12 @@
+@import (reference, less) '~@/assets/less/main.less';
+
+a.visualization-editor-context-help {
+ &, .ant-typography & {
+ font: inherit;
+ color: inherit;
+
+ &:hover, &:active {
+ color: @link-hover-color;
+ }
+ }
+}
diff --git a/client/app/components/visualizations/editor/control-label.less b/client/app/components/visualizations/editor/control-label.less
new file mode 100644
index 000000000..acb2f7975
--- /dev/null
+++ b/client/app/components/visualizations/editor/control-label.less
@@ -0,0 +1,7 @@
+.visualization-editor-control-label {
+ &.visualization-editor-control-label-horizontal {
+ label {
+ margin-bottom: 0;
+ }
+ }
+}
diff --git a/client/app/components/visualizations/editor/createTabbedEditor.jsx b/client/app/components/visualizations/editor/createTabbedEditor.jsx
new file mode 100644
index 000000000..52f222a9e
--- /dev/null
+++ b/client/app/components/visualizations/editor/createTabbedEditor.jsx
@@ -0,0 +1,51 @@
+import { isFunction, map, filter, extend, merge } from 'lodash';
+import React from 'react';
+import PropTypes from 'prop-types';
+import Tabs from 'antd/lib/tabs';
+import { EditorPropTypes } from '@/visualizations';
+
+export const UpdateOptionsStrategy = {
+ replace: (existingOptions, newOptions) => merge({}, newOptions),
+ shallowMerge: (existingOptions, newOptions) => extend({}, existingOptions, newOptions),
+ deepMerge: (existingOptions, newOptions) => merge({}, existingOptions, newOptions),
+};
+
+export function TabbedEditor({ tabs, options, data, onOptionsChange, ...restProps }) {
+ const optionsChanged = (newOptions, updateStrategy = UpdateOptionsStrategy.deepMerge) => {
+ onOptionsChange(updateStrategy(options, newOptions));
+ };
+
+ tabs = filter(tabs, tab => (isFunction(tab.isAvailable) ? tab.isAvailable(options, data) : true));
+
+ return (
+
+ {map(tabs, ({ key, title, component: Component }) => (
+ {title}}>
+
+
+ ))}
+
+ );
+}
+
+TabbedEditor.propTypes = {
+ ...EditorPropTypes,
+ tabs: PropTypes.arrayOf(PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ isAvailable: PropTypes.func, // (options) => boolean
+ component: PropTypes.func.isRequired,
+ })),
+};
+
+TabbedEditor.defaultProps = {
+ tabs: [],
+};
+
+export default function createTabbedEditor(tabs) {
+ return function TabbedEditorWrapper(props) {
+ return (
+
+ );
+ };
+}
diff --git a/client/app/components/visualizations/editor/index.js b/client/app/components/visualizations/editor/index.js
new file mode 100644
index 000000000..4760d4c9c
--- /dev/null
+++ b/client/app/components/visualizations/editor/index.js
@@ -0,0 +1,30 @@
+import AntSelect from 'antd/lib/select';
+import AntInput from 'antd/lib/input';
+import AntInputNumber from 'antd/lib/input-number';
+import Checkbox from 'antd/lib/checkbox';
+
+import RedashColorPicker from '@/components/ColorPicker';
+import RedashTextAlignmentSelect from '@/components/TextAlignmentSelect';
+
+import withControlLabel, { ControlLabel } from './withControlLabel';
+import createTabbedEditor from './createTabbedEditor';
+import Section from './Section';
+import Switch from './Switch';
+import ContextHelp from './ContextHelp';
+
+export {
+ Section,
+ ControlLabel,
+ Checkbox,
+ Switch,
+ ContextHelp,
+
+ withControlLabel,
+ createTabbedEditor,
+};
+export const Select = withControlLabel(AntSelect);
+export const Input = withControlLabel(AntInput);
+export const TextArea = withControlLabel(AntInput.TextArea);
+export const InputNumber = withControlLabel(AntInputNumber);
+export const ColorPicker = withControlLabel(RedashColorPicker);
+export const TextAlignmentSelect = withControlLabel(RedashTextAlignmentSelect);
diff --git a/client/app/components/visualizations/editor/withControlLabel.jsx b/client/app/components/visualizations/editor/withControlLabel.jsx
new file mode 100644
index 000000000..0e2b9f83c
--- /dev/null
+++ b/client/app/components/visualizations/editor/withControlLabel.jsx
@@ -0,0 +1,79 @@
+import React, { useMemo } from 'react';
+import PropTypes from 'prop-types';
+import hoistNonReactStatics from 'hoist-non-react-statics';
+import * as Grid from 'antd/lib/grid';
+import Typography from 'antd/lib/typography';
+
+import './control-label.less';
+
+export function ControlLabel({ layout, label, labelProps, disabled, children }) {
+ if ((layout === 'vertical') && label) {
+ return (
+
+
+ {children}
+
+ );
+ }
+
+ if ((layout === 'horizontal') && label) {
+ return (
+
+
+
+
+
+ {children}
+
+
+ );
+ }
+
+ return children;
+}
+
+ControlLabel.propTypes = {
+ layout: PropTypes.oneOf(['vertical', 'horizontal']),
+ label: PropTypes.node,
+ labelProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+ disabled: PropTypes.bool,
+ children: PropTypes.node,
+};
+
+ControlLabel.defaultProps = {
+ layout: 'vertical',
+ label: null,
+ disabled: false,
+ children: null,
+};
+
+export default function withControlLabel(WrappedControl) {
+ // eslint-disable-next-line react/prop-types
+ function ControlWrapper({ id, layout, label, labelProps, disabled, ...props }) {
+ const fallbackId = useMemo(() => `visualization-editor-control-${Math.random().toString(36).substr(2, 10)}`, []);
+ labelProps = {
+ ...labelProps,
+ htmlFor: id || fallbackId,
+ };
+
+ return (
+
+
+
+ );
+ }
+
+ // Copy static methods from `WrappedComponent`
+ hoistNonReactStatics(ControlWrapper, WrappedControl);
+
+ return ControlWrapper;
+}
diff --git a/client/app/lib/chooseTextColorForBackground.js b/client/app/lib/chooseTextColorForBackground.js
new file mode 100644
index 000000000..e9731a2db
--- /dev/null
+++ b/client/app/lib/chooseTextColorForBackground.js
@@ -0,0 +1,11 @@
+import { maxBy } from 'lodash';
+import chroma from 'chroma-js';
+
+export default function chooseTextColorForBackground(backgroundColor, textColors = ['#ffffff', '#333333']) {
+ try {
+ backgroundColor = chroma(backgroundColor);
+ return maxBy(textColors, color => chroma.contrast(backgroundColor, color));
+ } catch (e) {
+ return null;
+ }
+}
diff --git a/client/app/visualizations/ColorPalette.js b/client/app/visualizations/ColorPalette.js
index a33937922..7eaa45e07 100644
--- a/client/app/visualizations/ColorPalette.js
+++ b/client/app/visualizations/ColorPalette.js
@@ -23,7 +23,7 @@ export const AdditionalColors = {
'Indian Red': '#981717',
'Green 2': '#17BF51',
'Green 3': '#049235',
- DarkTurquoise: '#00B6EB',
+ 'Dark Turquoise': '#00B6EB',
'Dark Violet': '#A58AFF',
'Pink 2': '#C63FA9',
};
diff --git a/client/app/visualizations/box-plot/Editor.jsx b/client/app/visualizations/box-plot/Editor.jsx
index 15231f47d..d3ff73c89 100644
--- a/client/app/visualizations/box-plot/Editor.jsx
+++ b/client/app/visualizations/box-plot/Editor.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import Input from 'antd/lib/input';
+import { Section, Input } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
export default function Editor({ options, onOptionsChange }) {
@@ -14,27 +14,25 @@ export default function Editor({ options, onOptionsChange }) {
};
return (
-
-
-
+
+
+
-
-
+
-
+
+
);
}
diff --git a/client/app/visualizations/chart/Editor/AxisSettings.jsx b/client/app/visualizations/chart/Editor/AxisSettings.jsx
index 918b41a27..7996b299d 100644
--- a/client/app/visualizations/chart/Editor/AxisSettings.jsx
+++ b/client/app/visualizations/chart/Editor/AxisSettings.jsx
@@ -2,10 +2,8 @@ import { isString, isObject, isFinite, isNumber, merge } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { useDebouncedCallback } from 'use-debounce';
-import Select from 'antd/lib/select';
-import Input from 'antd/lib/input';
-import InputNumber from 'antd/lib/input-number';
import * as Grid from 'antd/lib/grid';
+import { Section, Select, Input, InputNumber } from '@/components/visualizations/editor';
function toNumber(value) {
value = isNumber(value) ? value : parseFloat(value);
@@ -26,10 +24,9 @@ export default function AxisSettings({ id, options, features, onChange }) {
return (
-
-
+
+
-
-
+
+
{features.range && (
-
-
-
- handleMinMaxChange({ rangeMin: toNumber(value) })}
- />
-
-
-
- handleMinMaxChange({ rangeMax: toNumber(value) })}
- />
-
-
+
+
+
+ handleMinMaxChange({ rangeMin: toNumber(value) })}
+ />
+
+
+ handleMinMaxChange({ rangeMax: toNumber(value) })}
+ />
+
+
+
)}
);
diff --git a/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx b/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx
index 9931130d1..a1a2980de 100644
--- a/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx
+++ b/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx
@@ -1,6 +1,6 @@
import { map } from 'lodash';
import React, { useMemo } from 'react';
-import Select from 'antd/lib/select';
+import { Select } from '@/components/visualizations/editor';
import { clientConfig } from '@/services/auth';
export default function ChartTypeSelect(props) {
diff --git a/client/app/visualizations/chart/Editor/ColorsSettings.test.js b/client/app/visualizations/chart/Editor/ColorsSettings.test.js
index 505f20d48..fa90ec39d 100644
--- a/client/app/visualizations/chart/Editor/ColorsSettings.test.js
+++ b/client/app/visualizations/chart/Editor/ColorsSettings.test.js
@@ -35,8 +35,10 @@ describe('Visualizations -> Chart -> Editor -> Colors Settings', () => {
columnMapping: { a: 'x', b: 'y' },
}, done);
- findByTestID(el, 'Chart.Series.v.Color').first().simulate('click');
- findByTestID(el, 'ColorPicker').first().find('input')
+ findByTestID(el, 'Chart.Series.v.Color')
+ .find('.color-picker-trigger').last()
+ .simulate('click');
+ findByTestID(el, 'ColorPicker').last().find('input')
.simulate('change', { target: { value: 'red' } });
});
});
@@ -48,8 +50,8 @@ describe('Visualizations -> Chart -> Editor -> Colors Settings', () => {
columnMapping: { a: 'x', b: 'y' },
}, done);
- findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme').first().simulate('click');
- findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme.RdBu').first().simulate('click');
+ findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme').last().simulate('click');
+ findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme.RdBu').last().simulate('click');
});
test('Sets custom color scheme', async (done) => {
@@ -59,12 +61,16 @@ describe('Visualizations -> Chart -> Editor -> Colors Settings', () => {
colorScheme: 'Custom...',
}, after(2, done)); // we will perform 2 actions, so call `done` after all of them completed
- findByTestID(el, 'Chart.Colors.Heatmap.MinColor').first().simulate('click');
- findByTestID(el, 'ColorPicker').first().find('input')
+ findByTestID(el, 'Chart.Colors.Heatmap.MinColor')
+ .find('.color-picker-trigger').last()
+ .simulate('click');
+ findByTestID(el, 'ColorPicker').last().find('input')
.simulate('change', { target: { value: 'yellow' } });
- findByTestID(el, 'Chart.Colors.Heatmap.MaxColor').first().simulate('click');
- findByTestID(el, 'ColorPicker').first().find('input')
+ findByTestID(el, 'Chart.Colors.Heatmap.MaxColor')
+ .find('.color-picker-trigger').last()
+ .simulate('click');
+ findByTestID(el, 'ColorPicker').last().find('input')
.simulate('change', { target: { value: 'red' } });
});
});
@@ -76,8 +82,11 @@ describe('Visualizations -> Chart -> Editor -> Colors Settings', () => {
columnMapping: { a: 'x', b: 'y' },
}, done);
- findByTestID(el, 'Chart.Series.b.Color').first().simulate('click');
- findByTestID(el, 'ColorPicker').first().find('input')
+ findByTestID(el, 'Chart.Series.b.Color')
+ .find('.color-picker-trigger').last()
+ .simulate('click');
+
+ findByTestID(el, 'ColorPicker').last().find('input')
.simulate('change', { target: { value: 'red' } });
});
});
diff --git a/client/app/visualizations/chart/Editor/ColumnMappingSelect.jsx b/client/app/visualizations/chart/Editor/ColumnMappingSelect.jsx
index 336c611b4..7117767e7 100644
--- a/client/app/visualizations/chart/Editor/ColumnMappingSelect.jsx
+++ b/client/app/visualizations/chart/Editor/ColumnMappingSelect.jsx
@@ -1,7 +1,7 @@
import { isString, map, uniq, flatten, filter, sortBy, keys } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
-import Select from 'antd/lib/select';
+import { Section, Select } from '@/components/visualizations/editor';
const MappingTypes = {
x: { label: 'X Column' },
@@ -20,10 +20,9 @@ export default function ColumnMappingSelect({ value, availableColumns, type, onC
const { label, multiple } = MappingTypes[type];
return (
-
-
+
+
);
}
diff --git a/client/app/visualizations/chart/Editor/CustomChartSettings.jsx b/client/app/visualizations/chart/Editor/CustomChartSettings.jsx
index 9560f7558..51a27c5c6 100644
--- a/client/app/visualizations/chart/Editor/CustomChartSettings.jsx
+++ b/client/app/visualizations/chart/Editor/CustomChartSettings.jsx
@@ -1,11 +1,8 @@
import { isNil, trimStart } from 'lodash';
import React from 'react';
-import Switch from 'antd/lib/switch';
-import Input from 'antd/lib/input';
+import { Section, Switch, TextArea } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
-const { TextArea } = Input;
-
const defaultCustomCode = trimStart(`
// Available variables are x, ys, element, and Plotly
// Type console.log(x, ys); for more info about x and ys
@@ -16,41 +13,37 @@ const defaultCustomCode = trimStart(`
export default function CustomChartSettings({ options, onOptionsChange }) {
return (
-
-
+
+
-
-
-
+
+ onOptionsChange({ enableConsoleLogs })}
+ >
+ Show errors in the console
+
+
-
-
-
+
+ onOptionsChange({ autoRedraw })}
+ >
+ Auto update graph
+
+
);
}
diff --git a/client/app/visualizations/chart/Editor/DataLabelsSettings.jsx b/client/app/visualizations/chart/Editor/DataLabelsSettings.jsx
index bb32ccaf4..8b06fe713 100644
--- a/client/app/visualizations/chart/Editor/DataLabelsSettings.jsx
+++ b/client/app/visualizations/chart/Editor/DataLabelsSettings.jsx
@@ -1,10 +1,7 @@
import { includes } from 'lodash';
import React from 'react';
import { useDebouncedCallback } from 'use-debounce';
-import Checkbox from 'antd/lib/checkbox';
-import Input from 'antd/lib/input';
-import Popover from 'antd/lib/popover';
-import Icon from 'antd/lib/icon';
+import { Section, Input, Checkbox, ContextHelp } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
export default function DataLabelsSettings({ options, onOptionsChange }) {
@@ -18,93 +15,50 @@ export default function DataLabelsSettings({ options, onOptionsChange }) {
return (
{ isShowDataLabelsAvailable && (
-
-
-
+
+ onOptionsChange({ showDataLabels: event.target.checked })}
+ >
+ Show Data Labels
+
+
)}
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
- )}
- >
-
-
-
-
+
+ )}
data-test="Chart.DataLabels.TextFormat"
placeholder="(auto)"
defaultValue={options.textFormat}
onChange={e => debouncedOnOptionsChange({ textFormat: e.target.value })}
/>
-
+
);
}
diff --git a/client/app/visualizations/chart/Editor/DataLabelsSettings.test.js b/client/app/visualizations/chart/Editor/DataLabelsSettings.test.js
index fba5b365c..675fd9820 100644
--- a/client/app/visualizations/chart/Editor/DataLabelsSettings.test.js
+++ b/client/app/visualizations/chart/Editor/DataLabelsSettings.test.js
@@ -30,7 +30,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
showDataLabels: false,
}, done);
- findByTestID(el, 'Chart.DataLabels.ShowDataLabels').first().find('input')
+ findByTestID(el, 'Chart.DataLabels.ShowDataLabels').last().find('input')
.simulate('change', { target: { checked: true } });
});
@@ -40,7 +40,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
numberFormat: '0[.]0000',
}, done);
- findByTestID(el, 'Chart.DataLabels.NumberFormat').first()
+ findByTestID(el, 'Chart.DataLabels.NumberFormat').last()
.simulate('change', { target: { value: '0.00' } });
});
@@ -50,7 +50,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
percentFormat: '0[.]00%',
}, done);
- findByTestID(el, 'Chart.DataLabels.PercentFormat').first()
+ findByTestID(el, 'Chart.DataLabels.PercentFormat').last()
.simulate('change', { target: { value: '0.0%' } });
});
@@ -60,7 +60,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
dateTimeFormat: 'YYYY-MM-DD HH:mm:ss',
}, done);
- findByTestID(el, 'Chart.DataLabels.DateTimeFormat').first()
+ findByTestID(el, 'Chart.DataLabels.DateTimeFormat').last()
.simulate('change', { target: { value: 'YYYY MMM DD' } });
});
@@ -70,7 +70,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
textFormat: null,
}, done);
- findByTestID(el, 'Chart.DataLabels.TextFormat').first()
+ findByTestID(el, 'Chart.DataLabels.TextFormat').last()
.simulate('change', { target: { value: '{{ @@x }} :: {{ @@y }} / {{ @@yPercent }}' } });
});
});
diff --git a/client/app/visualizations/chart/Editor/DefaultColorsSettings.jsx b/client/app/visualizations/chart/Editor/DefaultColorsSettings.jsx
index 866ca8513..68ed8920c 100644
--- a/client/app/visualizations/chart/Editor/DefaultColorsSettings.jsx
+++ b/client/app/visualizations/chart/Editor/DefaultColorsSettings.jsx
@@ -36,18 +36,17 @@ export default function DefaultColorsSettings({ options, data, onOptionsChange }
title: 'Color',
dataIndex: 'color',
width: '1%',
+ className: 'text-nowrap',
render: (unused, item) => (
-
- updateSeriesOption(item.key, 'color', value)}
- />
-
-
+ updateSeriesOption(item.key, 'color', value)}
+ addonAfter={}
+ />
),
},
];
diff --git a/client/app/visualizations/chart/Editor/GeneralSettings.jsx b/client/app/visualizations/chart/Editor/GeneralSettings.jsx
index 185e1d215..6c0a630e5 100644
--- a/client/app/visualizations/chart/Editor/GeneralSettings.jsx
+++ b/client/app/visualizations/chart/Editor/GeneralSettings.jsx
@@ -1,7 +1,7 @@
import { isArray, map, mapValues, includes, some, each, difference } from 'lodash';
import React, { useMemo } from 'react';
-import Select from 'antd/lib/select';
-import Checkbox from 'antd/lib/checkbox';
+import { Section, Select, Checkbox } from '@/components/visualizations/editor';
+import { UpdateOptionsStrategy } from '@/components/visualizations/editor/createTabbedEditor';
import { EditorPropTypes } from '@/visualizations';
import ChartTypeSelect from './ChartTypeSelect';
@@ -95,21 +95,20 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
...mappedColumns,
[type]: column,
});
- onOptionsChange({ columnMapping }, false);
+ onOptionsChange({ columnMapping }, UpdateOptionsStrategy.shallowMerge);
}
return (
-
-
+
+
{map(mappedColumns, (value, type) => (
-
+
-
+
)}
{!includes(['custom', 'heatmap'], options.globalSeriesType) && (
-
-
-
+
+ onOptionsChange({ legend: { enabled: event.target.checked } })}
+ >
+ Show Legend
+
+
)}
{includes(['box'], options.globalSeriesType) && (
-
-
-
+
+ onOptionsChange({ showpoints: event.target.checked })}
+ >
+ Show All Points
+
+
)}
{!includes(['custom', 'heatmap'], options.globalSeriesType) && (
-
-
-
+
+
)}
{includes(['line', 'area', 'column'], options.globalSeriesType) && (
-
-
-
+
+ onOptionsChange({ series: { percentValues: event.target.checked } })}
+ >
+ Normalize values to percentage
+
+
)}
{!includes(['custom', 'heatmap', 'bubble', 'scatter'], options.globalSeriesType) && (
-
-
+
-
+
)}
);
diff --git a/client/app/visualizations/chart/Editor/GeneralSettings.test.js b/client/app/visualizations/chart/Editor/GeneralSettings.test.js
index 586617649..9d334e957 100644
--- a/client/app/visualizations/chart/Editor/GeneralSettings.test.js
+++ b/client/app/visualizations/chart/Editor/GeneralSettings.test.js
@@ -38,8 +38,8 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
},
}, done);
- findByTestID(el, 'Chart.GlobalSeriesType').first().simulate('click');
- findByTestID(el, 'Chart.ChartType.pie').first().simulate('click');
+ findByTestID(el, 'Chart.GlobalSeriesType').last().simulate('click');
+ findByTestID(el, 'Chart.ChartType.pie').last().simulate('click');
});
test('Pie: changes direction', (done) => {
@@ -48,8 +48,8 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
direction: { type: 'counterclockwise' },
}, done);
- findByTestID(el, 'Chart.PieDirection').first().simulate('click');
- findByTestID(el, 'Chart.PieDirection.Clockwise').first().simulate('click');
+ findByTestID(el, 'Chart.PieDirection').last().simulate('click');
+ findByTestID(el, 'Chart.PieDirection.Clockwise').last().simulate('click');
});
test('Toggles legend', (done) => {
@@ -58,7 +58,7 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
legend: { enabled: true },
}, done);
- findByTestID(el, 'Chart.ShowLegend').first().find('input')
+ findByTestID(el, 'Chart.ShowLegend').last().find('input')
.simulate('change', { target: { checked: false } });
});
@@ -68,7 +68,7 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
showpoints: false,
}, done);
- findByTestID(el, 'Chart.ShowPoints').first().find('input')
+ findByTestID(el, 'Chart.ShowPoints').last().find('input')
.simulate('change', { target: { checked: true } });
});
@@ -78,8 +78,8 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
series: {},
}, done);
- findByTestID(el, 'Chart.Stacking').first().simulate('click');
- findByTestID(el, 'Chart.Stacking.Stack').first().simulate('click');
+ findByTestID(el, 'Chart.Stacking').last().simulate('click');
+ findByTestID(el, 'Chart.Stacking.Stack').last().simulate('click');
});
test('Toggles normalize values to percentage', (done) => {
@@ -88,7 +88,7 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
series: {},
}, done);
- findByTestID(el, 'Chart.NormalizeValues').first().find('input')
+ findByTestID(el, 'Chart.NormalizeValues').last().find('input')
.simulate('change', { target: { checked: true } });
});
@@ -98,8 +98,8 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
missingValuesAsZero: true,
}, done);
- findByTestID(el, 'Chart.MissingValues').first().simulate('click');
- findByTestID(el, 'Chart.MissingValues.Keep').first().simulate('click');
+ findByTestID(el, 'Chart.MissingValues').last().simulate('click');
+ findByTestID(el, 'Chart.MissingValues.Keep').last().simulate('click');
});
describe('Column mappings should be available', () => {
diff --git a/client/app/visualizations/chart/Editor/HeatmapColorsSettings.jsx b/client/app/visualizations/chart/Editor/HeatmapColorsSettings.jsx
index 923a55731..b28632ed8 100644
--- a/client/app/visualizations/chart/Editor/HeatmapColorsSettings.jsx
+++ b/client/app/visualizations/chart/Editor/HeatmapColorsSettings.jsx
@@ -1,8 +1,6 @@
import { map } from 'lodash';
import React from 'react';
-import Select from 'antd/lib/select';
-import * as Grid from 'antd/lib/grid';
-import ColorPicker from '@/components/ColorPicker';
+import { Section, Select, ColorPicker } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
import ColorPalette from '@/visualizations/ColorPalette';
@@ -16,10 +14,9 @@ const ColorSchemes = [
export default function HeatmapColorsSettings({ options, onOptionsChange }) {
return (
-
-
+
+
{(options.colorScheme === 'Custom...') && (
-
-
-
+
+
onOptionsChange({ heatMinColor })}
+ addonAfter={}
/>
-
-
-
+
+
onOptionsChange({ heatMaxColor })}
+ addonAfter={}
/>
-
-
+
+
)}
);
diff --git a/client/app/visualizations/chart/Editor/PieColorsSettings.jsx b/client/app/visualizations/chart/Editor/PieColorsSettings.jsx
index 0db3145f5..e398cea08 100644
--- a/client/app/visualizations/chart/Editor/PieColorsSettings.jsx
+++ b/client/app/visualizations/chart/Editor/PieColorsSettings.jsx
@@ -46,18 +46,17 @@ export default function PieColorsSettings({ options, data, onOptionsChange }) {
title: 'Color',
dataIndex: 'color',
width: '1%',
+ className: 'text-nowrap',
render: (unused, item) => (
-
- updateValuesOption(item.key, 'color', value)}
- />
-
-
+ updateValuesOption(item.key, 'color', value)}
+ addonAfter={}
+ />
),
},
];
diff --git a/client/app/visualizations/chart/Editor/SeriesSettings.test.js b/client/app/visualizations/chart/Editor/SeriesSettings.test.js
index 0075795cd..90ae8d0fc 100644
--- a/client/app/visualizations/chart/Editor/SeriesSettings.test.js
+++ b/client/app/visualizations/chart/Editor/SeriesSettings.test.js
@@ -33,8 +33,8 @@ describe('Visualizations -> Chart -> Editor -> Series Settings', () => {
},
}, done);
- findByTestID(el, 'Chart.Series.a.Type').first().simulate('click');
- findByTestID(el, 'Chart.ChartType.area').first().simulate('click');
+ findByTestID(el, 'Chart.Series.a.Type').last().simulate('click');
+ findByTestID(el, 'Chart.ChartType.area').last().simulate('click');
});
test('Changes series label', (done) => {
@@ -46,7 +46,7 @@ describe('Visualizations -> Chart -> Editor -> Series Settings', () => {
},
}, done);
- findByTestID(el, 'Chart.Series.a.Label').first().simulate('change', { target: { value: 'test' } });
+ findByTestID(el, 'Chart.Series.a.Label').last().simulate('change', { target: { value: 'test' } });
});
test('Changes series axis', (done) => {
@@ -58,7 +58,7 @@ describe('Visualizations -> Chart -> Editor -> Series Settings', () => {
},
}, done);
- findByTestID(el, 'Chart.Series.a.UseRightAxis').first().find('input')
+ findByTestID(el, 'Chart.Series.a.UseRightAxis').last().find('input')
.simulate('change', { target: { checked: true } });
});
});
diff --git a/client/app/visualizations/chart/Editor/XAxisSettings.jsx b/client/app/visualizations/chart/Editor/XAxisSettings.jsx
index 6ad207070..03b82364b 100644
--- a/client/app/visualizations/chart/Editor/XAxisSettings.jsx
+++ b/client/app/visualizations/chart/Editor/XAxisSettings.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import Switch from 'antd/lib/switch';
+import { Section, Switch } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
import AxisSettings from './AxisSettings';
@@ -14,41 +14,35 @@ export default function XAxisSettings({ options, onOptionsChange }) {
onChange={xAxis => onOptionsChange({ xAxis })}
/>
-
-
-
+
+ onOptionsChange({ sortX })}
+ >
+ Sort Values
+
+
-
-
-
+
+ onOptionsChange({ reverseX })}
+ >
+ Reverse Order
+
+
-
-
-
+
+ onOptionsChange({ xAxis: { labels: { enabled } } })}
+ >
+ Show Labels
+
+
);
}
diff --git a/client/app/visualizations/chart/Editor/XAxisSettings.test.js b/client/app/visualizations/chart/Editor/XAxisSettings.test.js
index 57fc4e436..8b6d86571 100644
--- a/client/app/visualizations/chart/Editor/XAxisSettings.test.js
+++ b/client/app/visualizations/chart/Editor/XAxisSettings.test.js
@@ -30,8 +30,8 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
xAxis: { type: '-', labels: { enabled: true } },
}, done);
- findByTestID(el, 'Chart.XAxis.Type').first().simulate('click');
- findByTestID(el, 'Chart.XAxis.Type.Linear').first().simulate('click');
+ findByTestID(el, 'Chart.XAxis.Type').last().simulate('click');
+ findByTestID(el, 'Chart.XAxis.Type.Linear').last().simulate('click');
});
test('Changes axis name', (done) => {
@@ -40,7 +40,7 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
xAxis: { type: '-', labels: { enabled: true } },
}, done);
- findByTestID(el, 'Chart.XAxis.Name').first().simulate('change', { target: { value: 'test' } });
+ findByTestID(el, 'Chart.XAxis.Name').last().simulate('change', { target: { value: 'test' } });
});
test('Sets Show Labels option', (done) => {
@@ -49,7 +49,7 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
xAxis: { type: '-', labels: { enabled: false } },
}, done);
- findByTestID(el, 'Chart.XAxis.ShowLabels').first().simulate('click');
+ findByTestID(el, 'Chart.XAxis.ShowLabels').last().simulate('click');
});
test('Sets Sort X Values option', (done) => {
@@ -58,7 +58,7 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
sortX: false,
}, done);
- findByTestID(el, 'Chart.XAxis.Sort').first().simulate('click');
+ findByTestID(el, 'Chart.XAxis.Sort').last().simulate('click');
});
test('Sets Reverse X Values option', (done) => {
@@ -67,6 +67,6 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
reverseX: false,
}, done);
- findByTestID(el, 'Chart.XAxis.Reverse').first().simulate('click');
+ findByTestID(el, 'Chart.XAxis.Reverse').last().simulate('click');
});
});
diff --git a/client/app/visualizations/chart/Editor/YAxisSettings.jsx b/client/app/visualizations/chart/Editor/YAxisSettings.jsx
index a6889a5bb..077f11479 100644
--- a/client/app/visualizations/chart/Editor/YAxisSettings.jsx
+++ b/client/app/visualizations/chart/Editor/YAxisSettings.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import Switch from 'antd/lib/switch';
+import { Section, Switch } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
import AxisSettings from './AxisSettings';
@@ -9,53 +9,55 @@ export default function YAxisSettings({ options, onOptionsChange }) {
return (
-
-
Left Y Axis
+
Left Y Axis
+
+
onOptionsChange({ yAxis: [axis, rightYAxis] })}
/>
-
+
{(options.globalSeriesType !== 'heatmap') && (
-
-
Right Y Axis
-
onOptionsChange({ yAxis: [leftYAxis, axis] })}
- />
-
+
+ Right Y Axis
+
+
+ onOptionsChange({ yAxis: [leftYAxis, axis] })}
+ />
+
+
)}
{(options.globalSeriesType === 'heatmap') && (
-
-
-
+
+ onOptionsChange({ sortY })}
+ >
+ Sort Values
+
+
-
-
-
+
+ onOptionsChange({ reverseY })}
+ >
+ Reverse Order
+
+
)}
diff --git a/client/app/visualizations/chart/Editor/YAxisSettings.test.js b/client/app/visualizations/chart/Editor/YAxisSettings.test.js
index c609c1c4e..3015f2769 100644
--- a/client/app/visualizations/chart/Editor/YAxisSettings.test.js
+++ b/client/app/visualizations/chart/Editor/YAxisSettings.test.js
@@ -34,8 +34,8 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
}, done);
- findByTestID(el, 'Chart.LeftYAxis.Type').first().simulate('click');
- findByTestID(el, 'Chart.LeftYAxis.Type.Category').first().simulate('click');
+ findByTestID(el, 'Chart.LeftYAxis.Type').last().simulate('click');
+ findByTestID(el, 'Chart.LeftYAxis.Type.Category').last().simulate('click');
});
test('Changes axis name', (done) => {
@@ -44,7 +44,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
}, done);
- findByTestID(el, 'Chart.LeftYAxis.Name').first().simulate('change', { target: { value: 'test' } });
+ findByTestID(el, 'Chart.LeftYAxis.Name').last().simulate('change', { target: { value: 'test' } });
});
test('Changes axis min value', (done) => {
@@ -53,7 +53,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
}, done);
- findByTestID(el, 'Chart.LeftYAxis.RangeMin').find('input').first().simulate('change', { target: { value: '50' } });
+ findByTestID(el, 'Chart.LeftYAxis.RangeMin').find('input').last().simulate('change', { target: { value: '50' } });
});
test('Changes axis max value', (done) => {
@@ -62,7 +62,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
}, done);
- findByTestID(el, 'Chart.LeftYAxis.RangeMax').find('input').first().simulate('change', { target: { value: '200' } });
+ findByTestID(el, 'Chart.LeftYAxis.RangeMax').find('input').last().simulate('change', { target: { value: '200' } });
});
describe('for non-heatmap', () => {
@@ -92,7 +92,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
sortY: false,
}, done);
- findByTestID(el, 'Chart.LeftYAxis.Sort').first().simulate('click');
+ findByTestID(el, 'Chart.LeftYAxis.Sort').last().simulate('click');
});
test('Sets Reverse Y Values option', (done) => {
@@ -101,7 +101,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
reverseY: false,
}, done);
- findByTestID(el, 'Chart.LeftYAxis.Reverse').first().simulate('click');
+ findByTestID(el, 'Chart.LeftYAxis.Reverse').last().simulate('click');
});
});
});
diff --git a/client/app/visualizations/chart/Editor/__snapshots__/ColorsSettings.test.js.snap b/client/app/visualizations/chart/Editor/__snapshots__/ColorsSettings.test.js.snap
index dd8111f6d..b622f085f 100644
--- a/client/app/visualizations/chart/Editor/__snapshots__/ColorsSettings.test.js.snap
+++ b/client/app/visualizations/chart/Editor/__snapshots__/ColorsSettings.test.js.snap
@@ -24,7 +24,7 @@ Object {
exports[`Visualizations -> Chart -> Editor -> Colors Settings for heatmap Sets custom color scheme 2`] = `
Object {
- "heatMinColor": "#FF0000",
+ "heatMaxColor": "#FF0000",
}
`;
diff --git a/client/app/visualizations/chart/Editor/index.jsx b/client/app/visualizations/chart/Editor/index.jsx
index 1ae2e8a67..d797c3aeb 100644
--- a/client/app/visualizations/chart/Editor/index.jsx
+++ b/client/app/visualizations/chart/Editor/index.jsx
@@ -1,7 +1,6 @@
-import { merge, extend } from 'lodash';
+/* eslint-disable react/prop-types */
import React from 'react';
-import Tabs from 'antd/lib/tabs';
-import { EditorPropTypes } from '@/visualizations';
+import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
import GeneralSettings from './GeneralSettings';
import XAxisSettings from './XAxisSettings';
@@ -13,53 +12,48 @@ import CustomChartSettings from './CustomChartSettings';
import './editor.less';
-export default function Editor(props) {
- const { options, onOptionsChange } = props;
+const isCustomChart = options => options.globalSeriesType === 'custom';
+const isPieChart = options => options.globalSeriesType === 'pie';
- const optionsChanged = (newOptions, deepUpdate = true) => {
- if (deepUpdate) {
- onOptionsChange(merge({}, options, newOptions));
- } else {
- onOptionsChange(extend({}, options, newOptions));
- }
- };
-
- const isCustomChart = options.globalSeriesType === 'custom';
- const isPieChart = options.globalSeriesType === 'pie';
-
- return (
-
- General}>
-
- {isCustomChart && }
-
- {!isCustomChart && !isPieChart && (
- X Axis}>
-
-
- )}
- {!isCustomChart && !isPieChart && (
- Y Axis}>
-
-
- )}
- {!isCustomChart && (
- Series}>
-
-
- )}
- {!isCustomChart && (
- Colors}>
-
-
- )}
- {!isCustomChart && (
- Data Labels}>
-
-
- )}
-
- );
-}
-
-Editor.propTypes = EditorPropTypes;
+export default createTabbedEditor([
+ {
+ key: 'General',
+ title: 'General',
+ component: props => (
+
+
+ {isCustomChart(props.options) && }
+
+ ),
+ },
+ {
+ key: 'XAxis',
+ title: 'X Axis',
+ component: XAxisSettings,
+ isAvailable: options => !isCustomChart(options) && !isPieChart(options),
+ },
+ {
+ key: 'YAxis',
+ title: 'Y Axis',
+ component: YAxisSettings,
+ isAvailable: options => !isCustomChart(options) && !isPieChart(options),
+ },
+ {
+ key: 'Series',
+ title: 'Series',
+ component: SeriesSettings,
+ isAvailable: options => !isCustomChart(options),
+ },
+ {
+ key: 'Colors',
+ title: 'Colors',
+ component: ColorsSettings,
+ isAvailable: options => !isCustomChart(options),
+ },
+ {
+ key: 'DataLabels',
+ title: 'Data Labels',
+ component: DataLabelsSettings,
+ isAvailable: options => !isCustomChart(options),
+ },
+]);
diff --git a/client/app/visualizations/chart/Editor/index.test.js b/client/app/visualizations/chart/Editor/index.test.js
index 16165ac0f..eab5787a4 100644
--- a/client/app/visualizations/chart/Editor/index.test.js
+++ b/client/app/visualizations/chart/Editor/index.test.js
@@ -28,12 +28,12 @@ describe('Visualizations -> Chart -> Editor (wrapper)', () => {
test('Renders generic wrapper', () => {
const el = mount({ globalSeriesType: 'column' }, { columns: [], rows: [] });
- expect(elementExists(el, 'Chart.EditorTabs.General')).toBeTruthy();
- expect(elementExists(el, 'Chart.EditorTabs.XAxis')).toBeTruthy();
- expect(elementExists(el, 'Chart.EditorTabs.YAxis')).toBeTruthy();
- expect(elementExists(el, 'Chart.EditorTabs.Series')).toBeTruthy();
- expect(elementExists(el, 'Chart.EditorTabs.Colors')).toBeTruthy();
- expect(elementExists(el, 'Chart.EditorTabs.DataLabels')).toBeTruthy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.General')).toBeTruthy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.XAxis')).toBeTruthy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.YAxis')).toBeTruthy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.Series')).toBeTruthy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.Colors')).toBeTruthy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.DataLabels')).toBeTruthy();
expect(elementExists(el, 'Chart.GlobalSeriesType')).toBeTruthy(); // general settings block exists
expect(elementExists(el, 'Chart.Custom.Code')).toBeFalsy(); // custom settings block does not exist
@@ -42,12 +42,12 @@ describe('Visualizations -> Chart -> Editor (wrapper)', () => {
test('Renders wrapper for custom charts', () => {
const el = mount({ globalSeriesType: 'custom' }, { columns: [], rows: [] });
- expect(elementExists(el, 'Chart.EditorTabs.General')).toBeTruthy();
- expect(elementExists(el, 'Chart.EditorTabs.XAxis')).toBeFalsy();
- expect(elementExists(el, 'Chart.EditorTabs.YAxis')).toBeFalsy();
- expect(elementExists(el, 'Chart.EditorTabs.Series')).toBeFalsy();
- expect(elementExists(el, 'Chart.EditorTabs.Colors')).toBeFalsy();
- expect(elementExists(el, 'Chart.EditorTabs.DataLabels')).toBeFalsy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.General')).toBeTruthy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.XAxis')).toBeFalsy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.YAxis')).toBeFalsy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.Series')).toBeFalsy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.Colors')).toBeFalsy();
+ expect(elementExists(el, 'VisualizationEditor.Tabs.DataLabels')).toBeFalsy();
expect(elementExists(el, 'Chart.GlobalSeriesType')).toBeTruthy(); // general settings block exists
expect(elementExists(el, 'Chart.Custom.Code')).toBeTruthy(); // custom settings block exists
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/default.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/default.json
index 8a31fe2c8..466b4d0a9 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/default.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/default.json
@@ -48,7 +48,7 @@
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"textposition": "inside",
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/normalized.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/normalized.json
index 3a29cdf37..647719734 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/normalized.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/normalized.json
@@ -58,7 +58,7 @@
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
"textposition": "inside",
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
},
{
@@ -73,7 +73,7 @@
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
"textposition": "inside",
"marker": { "color": "blue" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/stacked.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/stacked.json
index cb54f9240..62df3e1a7 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/stacked.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/bar/stacked.json
@@ -58,7 +58,7 @@
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"textposition": "inside",
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
},
{
@@ -73,7 +73,7 @@
"text": ["1 ± 0", "2 ± 0", "3 ± 0", "4 ± 0"],
"textposition": "inside",
"marker": { "color": "blue" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/box/default.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/box/default.json
index 5a5ba12db..40096b747 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/box/default.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/box/default.json
@@ -49,7 +49,7 @@
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/box/with-points.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/box/with-points.json
index 710cf6bd1..5318e48c4 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/box/with-points.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/box/with-points.json
@@ -52,7 +52,7 @@
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/bubble/default.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/bubble/default.json
index 10e9b4550..a287594af 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/bubble/default.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/bubble/default.json
@@ -47,7 +47,7 @@
"hoverinfo": "text+x+name",
"hover": [],
"text": ["10 ± 0: 51", "20 ± 0: 52", "30 ± 0: 53", "40 ± 0: 54"],
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/default.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/default.json
index ca3f54097..8e12fe101 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/default.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/default.json
@@ -47,7 +47,7 @@
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/keep-missing-values.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/keep-missing-values.json
index 108be880c..be49bec3d 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/keep-missing-values.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/keep-missing-values.json
@@ -55,7 +55,7 @@
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
},
{
@@ -69,7 +69,7 @@
"hover": [],
"text": ["", "2 ± 0", "", "4 ± 0"],
"marker": { "color": "blue" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/missing-values-0.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/missing-values-0.json
index 23e6a15df..e7d4a449a 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/missing-values-0.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/missing-values-0.json
@@ -55,7 +55,7 @@
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
},
{
@@ -69,7 +69,7 @@
"hover": [],
"text": ["0", "2 ± 0", "0", "4 ± 0"],
"marker": { "color": "blue" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/normalized-stacked.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/normalized-stacked.json
index a5b25b6e7..17776da78 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/normalized-stacked.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/normalized-stacked.json
@@ -57,7 +57,7 @@
"hover": [],
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
},
{
@@ -71,7 +71,7 @@
"hover": [],
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
"marker": { "color": "blue" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/normalized.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/normalized.json
index c016e392d..40e8b03c2 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/normalized.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/normalized.json
@@ -57,7 +57,7 @@
"hover": [],
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
},
{
@@ -71,7 +71,7 @@
"hover": [],
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
"marker": { "color": "blue" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/stacked.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/stacked.json
index bcb7a5157..d6c2027e6 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/stacked.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/line-area/stacked.json
@@ -57,7 +57,7 @@
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
},
{
@@ -71,7 +71,7 @@
"hover": [],
"text": ["1 ± 0", "2 ± 0", "3 ± 0", "4 ± 0"],
"marker": { "color": "blue" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/scatter/default.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/scatter/default.json
index 5daed9494..3feb04d82 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/scatter/default.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/scatter/default.json
@@ -48,7 +48,7 @@
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/fixtures/prepareData/scatter/without-labels.json b/client/app/visualizations/chart/plotly/fixtures/prepareData/scatter/without-labels.json
index 926734619..def53bbd8 100644
--- a/client/app/visualizations/chart/plotly/fixtures/prepareData/scatter/without-labels.json
+++ b/client/app/visualizations/chart/plotly/fixtures/prepareData/scatter/without-labels.json
@@ -48,7 +48,7 @@
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
- "insidetextfont": { "color": "#333333" },
+ "insidetextfont": { "color": "#ffffff" },
"yaxis": "y"
}
]
diff --git a/client/app/visualizations/chart/plotly/prepareDefaultData.js b/client/app/visualizations/chart/plotly/prepareDefaultData.js
index d42a935b6..36ef83606 100644
--- a/client/app/visualizations/chart/plotly/prepareDefaultData.js
+++ b/client/app/visualizations/chart/plotly/prepareDefaultData.js
@@ -1,42 +1,12 @@
-import { isNil, isString, extend, each, includes, map, sortBy } from 'lodash';
-import { cleanNumber, normalizeValue, getSeriesAxis } from './utils';
+import { isNil, extend, each, includes, map, sortBy } from 'lodash';
+import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
import { ColorPaletteArray } from '@/visualizations/ColorPalette';
+import { cleanNumber, normalizeValue, getSeriesAxis } from './utils';
function getSeriesColor(seriesOptions, seriesIndex) {
return seriesOptions.color || ColorPaletteArray[seriesIndex % ColorPaletteArray.length];
}
-function getFontColor(backgroundColor) {
- let result = '#333333';
- if (isString(backgroundColor)) {
- let matches = /#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i.exec(backgroundColor);
- let r;
- let g;
- let b;
- if (matches) {
- r = parseInt(matches[1], 16);
- g = parseInt(matches[2], 16);
- b = parseInt(matches[3], 16);
- } else {
- matches = /#?([0-9a-f])([0-9a-f])([0-9a-f])/i.exec(backgroundColor);
- if (matches) {
- r = parseInt(matches[1] + matches[1], 16);
- g = parseInt(matches[2] + matches[2], 16);
- b = parseInt(matches[3] + matches[3], 16);
- } else {
- return result;
- }
- }
-
- const lightness = r * 0.299 + g * 0.587 + b * 0.114;
- if (lightness < 170) {
- result = '#ffffff';
- }
- }
-
- return result;
-}
-
function getHoverInfoPattern(options) {
const hasX = /{{\s*@@x\s*}}/.test(options.textFormat);
const hasName = /{{\s*@@name\s*}}/.test(options.textFormat);
@@ -152,7 +122,7 @@ function prepareSeries(series, options, additionalOptions) {
name: seriesOptions.name || series.name,
marker: { color: seriesColor },
insidetextfont: {
- color: getFontColor(seriesColor),
+ color: chooseTextColorForBackground(seriesColor),
},
yaxis: seriesYAxis,
sourceData,
diff --git a/client/app/visualizations/chart/plotly/preparePieData.js b/client/app/visualizations/chart/plotly/preparePieData.js
index b8ac69699..a34e460df 100644
--- a/client/app/visualizations/chart/plotly/preparePieData.js
+++ b/client/app/visualizations/chart/plotly/preparePieData.js
@@ -64,7 +64,11 @@ function prepareSeries(series, options, additionalOptions) {
text: [],
textinfo: options.showDataLabels ? 'percent' : 'none',
textposition: 'inside',
- textfont: { color: '#ffffff' },
+ textfont: {
+ // In Plotly@1.42.0 and upper this options can be set to array of colors (similar to `marker.colors`):
+ // `colors: map(markerColors, c => chooseTextColorForBackground(c))`
+ color: '#ffffff',
+ },
name: series.name,
direction: options.direction.type,
domain: {
diff --git a/client/app/visualizations/choropleth/Editor/BoundsSettings.jsx b/client/app/visualizations/choropleth/Editor/BoundsSettings.jsx
index 29b5f0c0a..79add2b3a 100644
--- a/client/app/visualizations/choropleth/Editor/BoundsSettings.jsx
+++ b/client/app/visualizations/choropleth/Editor/BoundsSettings.jsx
@@ -1,8 +1,8 @@
import { isFinite, cloneDeep } from 'lodash';
import React, { useState, useEffect, useCallback } from 'react';
import { useDebouncedCallback } from 'use-debounce';
-import InputNumber from 'antd/lib/input-number';
import * as Grid from 'antd/lib/grid';
+import { Section, InputNumber, ControlLabel } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
export default function BoundsSettings({ options, onOptionsChange }) {
@@ -32,47 +32,47 @@ export default function BoundsSettings({ options, onOptionsChange }) {
return (
-
-
-
-
- updateBounds(1, 0, value)}
- />
-
-
- updateBounds(1, 1, value)}
- />
-
-
-
+
+
+
+
+ updateBounds(1, 0, value)}
+ />
+
+
+ updateBounds(1, 1, value)}
+ />
+
+
+
+
-
-
-
-
- updateBounds(0, 0, value)}
- />
-
-
- updateBounds(0, 1, value)}
- />
-
-
-
+
+
+
+
+ updateBounds(0, 0, value)}
+ />
+
+
+ updateBounds(0, 1, value)}
+ />
+
+
+
+
);
}
diff --git a/client/app/visualizations/choropleth/Editor/ColorsSettings.jsx b/client/app/visualizations/choropleth/Editor/ColorsSettings.jsx
index fa637cf35..8431d7262 100644
--- a/client/app/visualizations/choropleth/Editor/ColorsSettings.jsx
+++ b/client/app/visualizations/choropleth/Editor/ColorsSettings.jsx
@@ -1,9 +1,6 @@
import React from 'react';
import { useDebouncedCallback } from 'use-debounce';
-import Select from 'antd/lib/select';
-import InputNumber from 'antd/lib/input-number';
-import * as Grid from 'antd/lib/grid';
-import ColorPicker from '@/components/ColorPicker';
+import { Section, Select, InputNumber, ColorPicker } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
import ColorPalette from '../ColorPalette';
@@ -12,131 +9,103 @@ export default function ColorsSettings({ options, onOptionsChange }) {
return (
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
- onOptionsChangeDebounced({ steps })}
- />
-
-
+
+ onOptionsChangeDebounced({ steps })}
+ />
+
-
-
-
-
-
- onOptionsChange({ colors: { min } })}
- />
-
-
-
+
+ onOptionsChange({ colors: { min } })}
+ addonAfter={}
+ />
+
-
-
-
-
-
- onOptionsChange({ colors: { max } })}
- />
-
-
-
+
+ onOptionsChange({ colors: { max } })}
+ addonAfter={}
+ />
+
-
-
-
-
-
- onOptionsChange({ colors: { noValue } })}
- />
-
-
-
+
+ onOptionsChange({ colors: { noValue } })}
+ addonAfter={}
+ />
+
-
-
-
-
-
- onOptionsChange({ colors: { background } })}
- />
-
-
-
+
+ onOptionsChange({ colors: { background } })}
+ addonAfter={}
+ />
+
-
-
-
-
-
- onOptionsChange({ colors: { borders } })}
- />
-
-
-
+
+ onOptionsChange({ colors: { borders } })}
+ addonAfter={}
+ />
+
);
}
diff --git a/client/app/visualizations/choropleth/Editor/FormatSettings.jsx b/client/app/visualizations/choropleth/Editor/FormatSettings.jsx
index 49c5934b2..95eff6f59 100644
--- a/client/app/visualizations/choropleth/Editor/FormatSettings.jsx
+++ b/client/app/visualizations/choropleth/Editor/FormatSettings.jsx
@@ -1,47 +1,33 @@
import React from 'react';
import { useDebouncedCallback } from 'use-debounce';
-import Input from 'antd/lib/input';
-import Checkbox from 'antd/lib/checkbox';
-import Select from 'antd/lib/select';
-import Radio from 'antd/lib/radio';
-import Tooltip from 'antd/lib/tooltip';
-import Popover from 'antd/lib/popover';
-import Icon from 'antd/lib/icon';
import * as Grid from 'antd/lib/grid';
+import { Section, Select, Input, Checkbox, TextArea, TextAlignmentSelect, ContextHelp } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
function TemplateFormatHint({ mapType }) { // eslint-disable-line react/prop-types
return (
-
+ All query result columns can be referenced using {'{{ column_name }}'} syntax.
+ Use special names to access additional properties:
+ {'{{ @@value }}'} formatted value;
+ {mapType === 'countries' && (
- All query result columns can be referenced using {'{{ column_name }}'} syntax.
- Use special names to access additional properties:
- {'{{ @@value }}'} formatted value;
- {mapType === 'countries' && (
-
- {'{{ @@name }}'} short country name;
- {'{{ @@name_long }}'} full country name;
- {'{{ @@abbrev }}'} abbreviated country name;
- {'{{ @@iso_a2 }}'} two-letter ISO country code;
- {'{{ @@iso_a3 }}'} three-letter ISO country code;
- {'{{ @@iso_n3 }}'} three-digit ISO country code.
-
- )}
- {mapType === 'subdiv_japan' && (
-
- {'{{ @@name }}'} Prefecture name in English;
- {'{{ @@name_local }}'} Prefecture name in Kanji;
- {'{{ @@iso_3166_2 }}'} five-letter ISO subdivision code (JP-xx);
-
- )}
+ {'{{ @@name }}'} short country name;
+ {'{{ @@name_long }}'} full country name;
+ {'{{ @@abbrev }}'} abbreviated country name;
+ {'{{ @@iso_a2 }}'} two-letter ISO country code;
+ {'{{ @@iso_a3 }}'} three-letter ISO country code;
+ {'{{ @@iso_n3 }}'} three-digit ISO country code.
)}
- >
-
-
+ {mapType === 'subdiv_japan' && (
+
+ {'{{ @@name }}'} Prefecture name in English;
+ {'{{ @@name_local }}'} Prefecture name in Kanji;
+ {'{{ @@iso_3166_2 }}'} five-letter ISO subdivision code (JP-xx);
+
+ )}
+
);
}
@@ -52,139 +38,102 @@ export default function GeneralSettings({ options, onOptionsChange }) {
return (
-
-
-
- onOptionsChangeDebounced({ valueFormat: event.target.value })}
- />
-
-
-
- onOptionsChangeDebounced({ noValuePlaceholder: event.target.value })}
- />
-
-
+
top / left
+
top / right
+
bottom / left
+
bottom / right
+
+
+
+ onOptionsChange({ legend: { alignText: event.target.value } })}
+ />
+
+
+
-
-
-
+
+ onOptionsChange({ tooltip: { enabled: event.target.checked } })}
+ >
+ Show tooltip
+
+
-
-
-
-
-
-
-
- onOptionsChange({ legend: { alignText: event.target.value } })}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
+
+ onOptionsChange({ popup: { enabled: event.target.checked } })}
+ >
+ Show popup
+
+
-
-
-
+
);
}
diff --git a/client/app/visualizations/choropleth/Editor/GeneralSettings.jsx b/client/app/visualizations/choropleth/Editor/GeneralSettings.jsx
index 2ab7e3019..931f70984 100644
--- a/client/app/visualizations/choropleth/Editor/GeneralSettings.jsx
+++ b/client/app/visualizations/choropleth/Editor/GeneralSettings.jsx
@@ -1,7 +1,7 @@
import { map } from 'lodash';
import React, { useMemo } from 'react';
-import Select from 'antd/lib/select';
import { EditorPropTypes } from '@/visualizations';
+import { Section, Select } from '@/components/visualizations/editor';
import { inferCountryCodeType } from './utils';
export default function GeneralSettings({ options, data, onOptionsChange }) {
@@ -38,10 +38,9 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
return (
-
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
);
}
diff --git a/client/app/visualizations/choropleth/Editor/editor.less b/client/app/visualizations/choropleth/Editor/editor.less
deleted file mode 100644
index 27c315e84..000000000
--- a/client/app/visualizations/choropleth/Editor/editor.less
+++ /dev/null
@@ -1,15 +0,0 @@
-.choropleth-visualization-editor-format-settings {
- .choropleth-visualization-editor-legend-align-text {
- display: flex;
- align-items: stretch;
- justify-content: stretch;
-
- .ant-radio-button-wrapper {
- flex-grow: 1;
- text-align: center;
- // fit height
- height: 35px;
- line-height: 33px;
- }
- }
-}
diff --git a/client/app/visualizations/choropleth/Editor/index.js b/client/app/visualizations/choropleth/Editor/index.js
new file mode 100644
index 000000000..ee06e255d
--- /dev/null
+++ b/client/app/visualizations/choropleth/Editor/index.js
@@ -0,0 +1,13 @@
+import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
+
+import GeneralSettings from './GeneralSettings';
+import ColorsSettings from './ColorsSettings';
+import FormatSettings from './FormatSettings';
+import BoundsSettings from './BoundsSettings';
+
+export default createTabbedEditor([
+ { key: 'General', title: 'General', component: GeneralSettings },
+ { key: 'Colors', title: 'Colors', component: ColorsSettings },
+ { key: 'Format', title: 'Format', component: FormatSettings },
+ { key: 'Bounds', title: 'Bounds', component: BoundsSettings },
+]);
diff --git a/client/app/visualizations/choropleth/Editor/index.jsx b/client/app/visualizations/choropleth/Editor/index.jsx
deleted file mode 100644
index ec18d0917..000000000
--- a/client/app/visualizations/choropleth/Editor/index.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { merge } from 'lodash';
-import React from 'react';
-import Tabs from 'antd/lib/tabs';
-import { EditorPropTypes } from '@/visualizations';
-
-import GeneralSettings from './GeneralSettings';
-import ColorsSettings from './ColorsSettings';
-import FormatSettings from './FormatSettings';
-import BoundsSettings from './BoundsSettings';
-
-import './editor.less';
-
-export default function Editor(props) {
- const { options, onOptionsChange } = props;
-
- const optionsChanged = (newOptions) => {
- onOptionsChange(merge({}, options, newOptions));
- };
-
- return (
-
- General}>
-
-
- Colors}>
-
-
- Format}>
-
-
- Bounds}>
-
-
-
- );
-}
-
-Editor.propTypes = EditorPropTypes;
diff --git a/client/app/visualizations/cohort/Cornelius.jsx b/client/app/visualizations/cohort/Cornelius.jsx
index bac74fb44..ec60f3f16 100644
--- a/client/app/visualizations/cohort/Cornelius.jsx
+++ b/client/app/visualizations/cohort/Cornelius.jsx
@@ -10,6 +10,7 @@ import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import Tooltip from 'antd/lib/tooltip';
import { createNumberFormatter, formatSimpleTemplate } from '@/lib/value-format';
+import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
import './cornelius.less';
@@ -68,11 +69,11 @@ function prepareOptions(options) {
});
}
-function isDarkColor(backgroundColor) {
+function isLightColor(backgroundColor) {
backgroundColor = chroma(backgroundColor);
const white = '#ffffff';
const black = '#000000';
- return chroma.contrast(backgroundColor, white) > chroma.contrast(backgroundColor, black);
+ return chroma.contrast(backgroundColor, white) < chroma.contrast(backgroundColor, black);
}
function formatStageTitle(options, index) {
@@ -120,8 +121,11 @@ function CorneliusRow({ options, data, index, maxRowLength }) { // eslint-disabl
options.formatPercent(percentageValue);
const backgroundColor = options.getColorForValue(percentageValue);
- cellProps.style = { backgroundColor };
- if (isDarkColor(backgroundColor)) {
+ cellProps.style = {
+ backgroundColor,
+ color: chooseTextColorForBackground(backgroundColor),
+ };
+ if (isLightColor(cellProps.style.color)) {
cellProps.className += ' cornelius-white-text';
}
diff --git a/client/app/visualizations/cohort/Editor.jsx b/client/app/visualizations/cohort/Editor.jsx
deleted file mode 100644
index 54adffe04..000000000
--- a/client/app/visualizations/cohort/Editor.jsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import { map, merge } from 'lodash';
-import React from 'react';
-import Tabs from 'antd/lib/tabs';
-import Select from 'antd/lib/select';
-import { EditorPropTypes } from '@/visualizations';
-
-const CohortTimeIntervals = {
- daily: 'Daily',
- weekly: 'Weekly',
- monthly: 'Monthly',
-};
-
-const CohortModes = {
- diagonal: 'Fill gaps with zeros',
- simple: 'Show data as is',
-};
-
-export default function Editor({ options, data, onOptionsChange }) {
- const optionsChanged = (newOptions) => {
- onOptionsChange(merge({}, options, newOptions));
- };
-
- return (
-
- Columns}>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Options}>
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-Editor.propTypes = EditorPropTypes;
diff --git a/client/app/visualizations/cohort/Editor/ColumnsSettings.jsx b/client/app/visualizations/cohort/Editor/ColumnsSettings.jsx
new file mode 100644
index 000000000..5f2e9c0d8
--- /dev/null
+++ b/client/app/visualizations/cohort/Editor/ColumnsSettings.jsx
@@ -0,0 +1,68 @@
+import { map } from 'lodash';
+import React from 'react';
+import { Section, Select } from '@/components/visualizations/editor';
+import { EditorPropTypes } from '@/visualizations';
+
+export default function ColumnsSettings({ options, data, onOptionsChange }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+ColumnsSettings.propTypes = EditorPropTypes;
diff --git a/client/app/visualizations/cohort/Editor/OptionsSettings.jsx b/client/app/visualizations/cohort/Editor/OptionsSettings.jsx
new file mode 100644
index 000000000..ed3b83afb
--- /dev/null
+++ b/client/app/visualizations/cohort/Editor/OptionsSettings.jsx
@@ -0,0 +1,51 @@
+import { map } from 'lodash';
+import React from 'react';
+import { Section, Select } from '@/components/visualizations/editor';
+import { EditorPropTypes } from '@/visualizations';
+
+const CohortTimeIntervals = {
+ daily: 'Daily',
+ weekly: 'Weekly',
+ monthly: 'Monthly',
+};
+
+const CohortModes = {
+ diagonal: 'Fill gaps with zeros',
+ simple: 'Show data as is',
+};
+
+export default function OptionsSettings({ options, onOptionsChange }) {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+OptionsSettings.propTypes = EditorPropTypes;
diff --git a/client/app/visualizations/cohort/Editor/index.js b/client/app/visualizations/cohort/Editor/index.js
new file mode 100644
index 000000000..bceb75353
--- /dev/null
+++ b/client/app/visualizations/cohort/Editor/index.js
@@ -0,0 +1,9 @@
+import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
+
+import ColumnsSettings from './ColumnsSettings';
+import OptionsSettings from './OptionsSettings';
+
+export default createTabbedEditor([
+ { key: 'Columns', title: 'Columns', component: ColumnsSettings },
+ { key: 'Options', title: 'Options', component: OptionsSettings },
+]);
diff --git a/client/app/visualizations/cohort/cornelius.less b/client/app/visualizations/cohort/cornelius.less
index 0d4a4bdea..cafc5e134 100644
--- a/client/app/visualizations/cohort/cornelius.less
+++ b/client/app/visualizations/cohort/cornelius.less
@@ -1,10 +1,13 @@
.cornelius-container {
+ // replace with variable from `ant-variables.less` after Bootstrap removed
+ @table-header-color: #333;
+
.cornelius-title {
text-align: center;
padding-bottom: 10px;
font-weight: bold;
font-size: 14pt;
- color: #3A3838;
+ color: @table-header-color;
border-collapse: collapse;
}
@@ -18,7 +21,7 @@
text-align: center;
padding: 10px;
border: 1px solid #E4E4E4;
- color: #3A3838;
+ color: @table-header-color;
font-weight: bold;
}
@@ -37,15 +40,12 @@
.cornelius-people,
.cornelius-stage {
font-weight: bold;
- color: #3A3838;
+ color: @table-header-color;
}
.cornelius-percentage,
.cornelius-absolute {
- color: #000000;
-
&.cornelius-white-text {
- color: #ffffff;
text-shadow: 1px 1px 1px #000000;
}
}
diff --git a/client/app/visualizations/counter/Editor/FormatSettings.jsx b/client/app/visualizations/counter/Editor/FormatSettings.jsx
index 144a1e356..49ebe948c 100644
--- a/client/app/visualizations/counter/Editor/FormatSettings.jsx
+++ b/client/app/visualizations/counter/Editor/FormatSettings.jsx
@@ -1,8 +1,5 @@
import React from 'react';
-import * as Grid from 'antd/lib/grid';
-import Input from 'antd/lib/input';
-import InputNumber from 'antd/lib/input-number';
-import Switch from 'antd/lib/switch';
+import { Section, Input, InputNumber, Switch } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
import { isValueNumber } from '../utils';
@@ -11,95 +8,75 @@ export default function FormatSettings({ options, data, onOptionsChange }) {
const inputsEnabled = isValueNumber(data.rows, options);
return (
-
-
-
-
-
- onOptionsChange({ stringDecimal })}
- />
-
-
+
+ onOptionsChange({ stringDecimal })}
+ />
+
-
-
-
-
-
- onOptionsChange({ stringDecChar: e.target.value })}
- />
-
-
+
-
-
-
-
-
- onOptionsChange({ stringThouSep: e.target.value })}
- />
-
-
+
-
-
-
-
-
- onOptionsChange({ stringPrefix: e.target.value })}
- />
-
-
+
-
-
-
-
-
- onOptionsChange({ stringSuffix: e.target.value })}
- />
-
-
+
-
+ >
+ Format Target Value
+
+
);
}
diff --git a/client/app/visualizations/counter/Editor/GeneralSettings.jsx b/client/app/visualizations/counter/Editor/GeneralSettings.jsx
index 554a588bc..506a8b8c4 100644
--- a/client/app/visualizations/counter/Editor/GeneralSettings.jsx
+++ b/client/app/visualizations/counter/Editor/GeneralSettings.jsx
@@ -1,111 +1,87 @@
import { map } from 'lodash';
import React from 'react';
-import * as Grid from 'antd/lib/grid';
-import Select from 'antd/lib/select';
-import Input from 'antd/lib/input';
-import InputNumber from 'antd/lib/input-number';
-import Switch from 'antd/lib/switch';
+import { Section, Select, Input, InputNumber, Switch } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
export default function GeneralSettings({ options, data, visualizationName, onOptionsChange }) {
return (
-
-
-
-
-
- onOptionsChange({ counterLabel: e.target.value })}
- />
-
-
+
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
- onOptionsChange({ rowNumber })}
- />
-
-
+
+ onOptionsChange({ rowNumber })}
+ />
+
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
- onOptionsChange({ targetRowNumber })}
- />
-
-
+
+ onOptionsChange({ targetRowNumber })}
+ />
+
-
+ >
+ Count Rows
+
+
);
}
diff --git a/client/app/visualizations/counter/Editor/index.js b/client/app/visualizations/counter/Editor/index.js
new file mode 100644
index 000000000..3640d7d22
--- /dev/null
+++ b/client/app/visualizations/counter/Editor/index.js
@@ -0,0 +1,9 @@
+import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
+
+import GeneralSettings from './GeneralSettings';
+import FormatSettings from './FormatSettings';
+
+export default createTabbedEditor([
+ { key: 'General', title: 'General', component: GeneralSettings },
+ { key: 'Format', title: 'Format', component: FormatSettings },
+]);
diff --git a/client/app/visualizations/counter/Editor/index.jsx b/client/app/visualizations/counter/Editor/index.jsx
deleted file mode 100644
index e897a5667..000000000
--- a/client/app/visualizations/counter/Editor/index.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { merge } from 'lodash';
-import React from 'react';
-import Tabs from 'antd/lib/tabs';
-import { EditorPropTypes } from '@/visualizations';
-
-import GeneralSettings from './GeneralSettings';
-import FormatSettings from './FormatSettings';
-
-export default function Editor(props) {
- const { options, onOptionsChange } = props;
-
- const optionsChanged = (newOptions) => {
- onOptionsChange(merge({}, options, newOptions));
- };
-
- return (
-
- General}>
-
-
- Format}>
-
-
-
- );
-}
-
-Editor.propTypes = EditorPropTypes;
diff --git a/client/app/visualizations/details/DetailsEditor.jsx b/client/app/visualizations/details/DetailsEditor.jsx
deleted file mode 100644
index 0e3b67f72..000000000
--- a/client/app/visualizations/details/DetailsEditor.jsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function DetailsEditor() {
- return null;
-}
diff --git a/client/app/visualizations/details/index.js b/client/app/visualizations/details/index.js
index 11a60c78b..eac6747d2 100644
--- a/client/app/visualizations/details/index.js
+++ b/client/app/visualizations/details/index.js
@@ -1,6 +1,6 @@
import { registerVisualization } from '@/visualizations';
+
import DetailsRenderer from './DetailsRenderer';
-import DetailsEditor from './DetailsEditor';
const DEFAULT_OPTIONS = {};
@@ -10,8 +10,6 @@ export default function init() {
name: 'Details View',
getOptions: options => ({ ...DEFAULT_OPTIONS, ...options }),
Renderer: DetailsRenderer,
- Editor: DetailsEditor,
-
defaultColumns: 2,
defaultRows: 2,
});
diff --git a/client/app/visualizations/funnel/Editor/AppearanceSettings.jsx b/client/app/visualizations/funnel/Editor/AppearanceSettings.jsx
index c664ffb2d..3b8416856 100644
--- a/client/app/visualizations/funnel/Editor/AppearanceSettings.jsx
+++ b/client/app/visualizations/funnel/Editor/AppearanceSettings.jsx
@@ -1,10 +1,6 @@
import React from 'react';
import { useDebouncedCallback } from 'use-debounce';
-import Input from 'antd/lib/input';
-import InputNumber from 'antd/lib/input-number';
-import Popover from 'antd/lib/popover';
-import Icon from 'antd/lib/icon';
-import * as Grid from 'antd/lib/grid';
+import { Section, Input, InputNumber, ContextHelp } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
export default function AppearanceSettings({ options, onOptionsChange }) {
@@ -12,107 +8,63 @@ export default function AppearanceSettings({ options, onOptionsChange }) {
return (
-
-
-
- )}
- >
-
-
-
-
-
- onOptionsChangeDebounced({ numberFormat: event.target.value })}
- />
-
-
+
-
-
-
-
-
- onOptionsChangeDebounced({ percentFormat: event.target.value })}
- />
-
-
+
-
-
-
-
-
- onOptionsChangeDebounced({ itemsLimit })}
- />
-
-
+
+ onOptionsChangeDebounced({ itemsLimit })}
+ />
+
-
-
-
-
-
- onOptionsChangeDebounced({ percentValuesRange: { min } })}
- />
-
-
+
+ onOptionsChangeDebounced({ percentValuesRange: { min } })}
+ />
+
-
-
-
-
-
- onOptionsChangeDebounced({ percentValuesRange: { max } })}
- />
-
-
+
+ onOptionsChangeDebounced({ percentValuesRange: { max } })}
+ />
+
);
}
diff --git a/client/app/visualizations/funnel/Editor/GeneralSettings.jsx b/client/app/visualizations/funnel/Editor/GeneralSettings.jsx
index c889f339a..62b4917cf 100644
--- a/client/app/visualizations/funnel/Editor/GeneralSettings.jsx
+++ b/client/app/visualizations/funnel/Editor/GeneralSettings.jsx
@@ -1,10 +1,7 @@
import { map } from 'lodash';
import React, { useMemo } from 'react';
import { useDebouncedCallback } from 'use-debounce';
-import Select from 'antd/lib/select';
-import Input from 'antd/lib/input';
-import Checkbox from 'antd/lib/checkbox';
-import * as Grid from 'antd/lib/grid';
+import { Section, Select, Input, Checkbox } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
export default function GeneralSettings({ options, data, onOptionsChange }) {
@@ -14,130 +11,103 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
return (
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
- onOptionsChangeDebounced({ stepCol: { displayAs: event.target.value } })}
- />
-
-
+
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
- onOptionsChangeDebounced({ valueCol: { displayAs: event.target.value } })}
- />
-
-
+
-
-
-
-
+
+ onOptionsChange({ autoSort: !event.target.checked })}
+ >
+ Custom Sorting
+
+
{!options.autoSort && (
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
)}
diff --git a/client/app/visualizations/funnel/Editor/index.js b/client/app/visualizations/funnel/Editor/index.js
new file mode 100644
index 000000000..2e44b08b8
--- /dev/null
+++ b/client/app/visualizations/funnel/Editor/index.js
@@ -0,0 +1,9 @@
+import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
+
+import GeneralSettings from './GeneralSettings';
+import AppearanceSettings from './AppearanceSettings';
+
+export default createTabbedEditor([
+ { key: 'General', title: 'General', component: GeneralSettings },
+ { key: 'Appearance', title: 'Appearance', component: AppearanceSettings },
+]);
diff --git a/client/app/visualizations/funnel/Editor/index.jsx b/client/app/visualizations/funnel/Editor/index.jsx
deleted file mode 100644
index ed6739434..000000000
--- a/client/app/visualizations/funnel/Editor/index.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { merge } from 'lodash';
-import React from 'react';
-import Tabs from 'antd/lib/tabs';
-import { EditorPropTypes } from '@/visualizations';
-
-import GeneralSettings from './GeneralSettings';
-import AppearanceSettings from './AppearanceSettings';
-
-export default function Editor(props) {
- const { options, onOptionsChange } = props;
-
- const optionsChanged = (newOptions) => {
- onOptionsChange(merge({}, options, newOptions));
- };
-
- return (
-
- General}>
-
-
- Appearance}>
-
-
-
- );
-}
-
-Editor.propTypes = EditorPropTypes;
diff --git a/client/app/visualizations/index.js b/client/app/visualizations/index.js
index 02ddad228..500b2db1f 100644
--- a/client/app/visualizations/index.js
+++ b/client/app/visualizations/index.js
@@ -70,6 +70,7 @@ function validateVisualizationConfig(config) {
export function registerVisualization(config) {
validateVisualizationConfig(config);
config = {
+ Editor: () => null,
...config,
isDefault: config.isDefault && !config.isDeprecated,
};
diff --git a/client/app/visualizations/map/Editor/GeneralSettings.jsx b/client/app/visualizations/map/Editor/GeneralSettings.jsx
index d24a1641d..0033297e9 100644
--- a/client/app/visualizations/map/Editor/GeneralSettings.jsx
+++ b/client/app/visualizations/map/Editor/GeneralSettings.jsx
@@ -1,6 +1,6 @@
import { isNil, map, filter, difference } from 'lodash';
import React, { useMemo } from 'react';
-import Select from 'antd/lib/select';
+import { Section, Select } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
function getColumns(column, unusedColumns) {
@@ -18,11 +18,10 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
return (
-
-
+
-
+
-
-
+
-
+
-
-
+
+
);
}
diff --git a/client/app/visualizations/map/Editor/GroupsSettings.jsx b/client/app/visualizations/map/Editor/GroupsSettings.jsx
index 7e4c03f0f..1692923f6 100644
--- a/client/app/visualizations/map/Editor/GroupsSettings.jsx
+++ b/client/app/visualizations/map/Editor/GroupsSettings.jsx
@@ -37,18 +37,17 @@ export default function GroupsSettings({ options, data, onOptionsChange }) {
title: 'Color',
dataIndex: 'color',
width: '1%',
+ className: 'text-nowrap',
render: (unused, item) => (
-
- updateGroupOption(item.name, 'color', value)}
- />
-
-
+ updateGroupOption(item.name, 'color', value)}
+ addonAfter={}
+ />
),
},
];
diff --git a/client/app/visualizations/map/Editor/StyleSettings.jsx b/client/app/visualizations/map/Editor/StyleSettings.jsx
index 97aa6c797..6ed29418e 100644
--- a/client/app/visualizations/map/Editor/StyleSettings.jsx
+++ b/client/app/visualizations/map/Editor/StyleSettings.jsx
@@ -1,14 +1,7 @@
import { isNil, map } from 'lodash';
import React, { useMemo } from 'react';
import { useDebouncedCallback } from 'use-debounce';
-import Select from 'antd/lib/select';
-import Input from 'antd/lib/input';
-import Checkbox from 'antd/lib/checkbox';
-import Popover from 'antd/lib/popover';
-import Icon from 'antd/lib/icon';
-import Typography from 'antd/lib/typography';
-import * as Grid from 'antd/lib/grid';
-import ColorPicker from '@/components/ColorPicker';
+import { Section, Select, Checkbox, Input, ColorPicker, ContextHelp } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
import ColorPalette from '@/visualizations/ColorPalette';
@@ -92,11 +85,10 @@ export default function StyleSettings({ options, onOptionsChange }) {
return (
-
-
+
-
+
- Markers
+ Markers
-
-
-
+
+ onOptionsChange({ clusterMarkers: event.target.checked })}
+ >
+ Cluster Markers
+
+
-
-
-
+
+ onOptionsChange({ customizeMarkers: event.target.checked })}
+ >
+ Override default style
+
+ {!isCustomMarkersStyleAllowed && (
+
+ Custom marker styles are not available
+ when Group By column selected.
+
+ )}
+
{isCustomMarkersStyleAllowed && options.customizeMarkers && (
-
-
-
-
-
-
-
-
+
+
+
{showIcon && (
-
-
-
- )}
- >
-
-
-
-
-
- debouncedOnOptionsChange({ iconFont: event.target.value })}
- />
-
-
+
+ )}
+ className="w-100"
+ data-test="Map.Editor.MarkerIcon"
+ defaultValue={options.iconFont}
+ onChange={event => debouncedOnOptionsChange({ iconFont: event.target.value })}
+ />
+
)}
{showIcon && (
-
-
-
-
-
- onOptionsChange({ foregroundColor })}
- />
-
-
-
+
+ onOptionsChange({ foregroundColor })}
+ addonAfter={}
+ />
+
)}
{showBackgroundColor && (
-
-
-
-
-
- onOptionsChange({ backgroundColor })}
- />
-
-
-
+
+ onOptionsChange({ backgroundColor })}
+ addonAfter={}
+ />
+
)}
{showBorderColor && (
-
-
-
-
-
- onOptionsChange({ borderColor })}
- />
-
-
-
+
+ onOptionsChange({ borderColor })}
+ addonAfter={}
+ />
+
)}
)}
diff --git a/client/app/visualizations/map/Editor/index.js b/client/app/visualizations/map/Editor/index.js
new file mode 100644
index 000000000..3cfbbf9dd
--- /dev/null
+++ b/client/app/visualizations/map/Editor/index.js
@@ -0,0 +1,11 @@
+import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
+
+import GeneralSettings from './GeneralSettings';
+import GroupsSettings from './GroupsSettings';
+import StyleSettings from './StyleSettings';
+
+export default createTabbedEditor([
+ { key: 'General', title: 'General', component: GeneralSettings },
+ { key: 'Groups', title: 'Groups', component: GroupsSettings },
+ { key: 'Style', title: 'Style', component: StyleSettings },
+]);
diff --git a/client/app/visualizations/map/Editor/index.jsx b/client/app/visualizations/map/Editor/index.jsx
deleted file mode 100644
index d1f97f869..000000000
--- a/client/app/visualizations/map/Editor/index.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { merge } from 'lodash';
-import React from 'react';
-import Tabs from 'antd/lib/tabs';
-import { EditorPropTypes } from '@/visualizations';
-
-import GeneralSettings from './GeneralSettings';
-import GroupsSettings from './GroupsSettings';
-import StyleSettings from './StyleSettings';
-
-export default function Editor(props) {
- const { options, onOptionsChange } = props;
-
- const optionsChanged = (newOptions) => {
- onOptionsChange(merge({}, options, newOptions));
- };
-
- return (
-
- General}>
-
-
- Groups}>
-
-
- Style}>
-
-
-
- );
-}
-
-Editor.propTypes = EditorPropTypes;
diff --git a/client/app/visualizations/map/initMap.js b/client/app/visualizations/map/initMap.js
index 905bd8d5e..0503718d4 100644
--- a/client/app/visualizations/map/initMap.js
+++ b/client/app/visualizations/map/initMap.js
@@ -1,4 +1,4 @@
-import { isFunction, each, map, maxBy, toString } from 'lodash';
+import { isFunction, each, map, toString } from 'lodash';
import chroma from 'chroma-js';
import L from 'leaflet';
import 'leaflet.markercluster';
@@ -13,6 +13,7 @@ import markerShadow from 'leaflet/dist/images/marker-shadow.png';
import 'leaflet-fullscreen';
import 'leaflet-fullscreen/dist/leaflet.fullscreen.css';
import resizeObserver from '@/services/resizeObserver';
+import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
// This is a workaround for an issue with giving Leaflet load the icon on its own.
L.Icon.Default.mergeOptions({
@@ -50,7 +51,7 @@ L.MarkerClusterIcon = L.DivIcon.extend({
},
createIcon(...args) {
const color = chroma(this.options.color);
- const textColor = maxBy(['#ffffff', '#000000'], c => chroma.contrast(color, c));
+ const textColor = chooseTextColorForBackground(color);
const borderColor = color.alpha(0.4).css();
const backgroundColor = color.alpha(0.8).css();
diff --git a/client/app/visualizations/pivot/Editor.jsx b/client/app/visualizations/pivot/Editor.jsx
index 360b1a552..c586f69e8 100644
--- a/client/app/visualizations/pivot/Editor.jsx
+++ b/client/app/visualizations/pivot/Editor.jsx
@@ -1,6 +1,6 @@
import { merge } from 'lodash';
import React from 'react';
-import Switch from 'antd/lib/switch';
+import { Section, Switch } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
export default function Editor({ options, onOptionsChange }) {
@@ -9,33 +9,36 @@ export default function Editor({ options, onOptionsChange }) {
};
return (
-
-
-
-
-
+ >
+ Show Column Totals
+
+
+
);
}
diff --git a/client/app/visualizations/sankey/Editor.jsx b/client/app/visualizations/sankey/Editor.jsx
index 0e7a4822d..4447625a8 100644
--- a/client/app/visualizations/sankey/Editor.jsx
+++ b/client/app/visualizations/sankey/Editor.jsx
@@ -2,19 +2,16 @@ import React from 'react';
export default function Editor() {
return (
-
-
- This visualization expects the query result to have rows in the following format:
-
-
- - stage1 - stage 1 value
- - stage2 - stage 2 value (or null)
- - stage3 - stage 3 value (or null)
- - stage4 - stage 4 value (or null)
- - stage5 - stage 5 value (or null)
- - value - number of times this sequence occurred
-
-
-
+
+ This visualization expects the query result to have rows in the following format:
+
+ - stage1 - stage 1 value
+ - stage2 - stage 2 value (or null)
+ - stage3 - stage 3 value (or null)
+ - stage4 - stage 4 value (or null)
+ - stage5 - stage 5 value (or null)
+ - value - number of times this sequence occurred
+
+
);
}
diff --git a/client/app/visualizations/sunburst/Editor.jsx b/client/app/visualizations/sunburst/Editor.jsx
index 2457b904d..55d400b9d 100644
--- a/client/app/visualizations/sunburst/Editor.jsx
+++ b/client/app/visualizations/sunburst/Editor.jsx
@@ -1,33 +1,30 @@
import React from 'react';
+import { Section } from '@/components/visualizations/editor';
export default function Editor() {
return (
-
-
- This visualization expects the query result to have rows in one of the following formats:
-
-
-
Option 1:
-
- - sequence - sequence id
- - stage - what stage in sequence this is (1, 2, ...)
- - node - stage name
- - value - number of times this sequence occurred
-
-
-
-
-
Option 2:
-
- - stage1 - stage 1 value
- - stage2 - stage 2 value (or null)
- - stage3 - stage 3 value (or null)
- - stage4 - stage 4 value (or null)
- - stage5 - stage 5 value (or null)
- - value - number of times this sequence occurred
-
-
-
-
+
+ This visualization expects the query result to have rows in one of the following formats:
+
+ Option 1:
+
+ - sequence - sequence id
+ - stage - what stage in sequence this is (1, 2, ...)
+ - node - stage name
+ - value - number of times this sequence occurred
+
+
+
+ Option 2:
+
+ - stage1 - stage 1 value
+ - stage2 - stage 2 value (or null)
+ - stage3 - stage 3 value (or null)
+ - stage4 - stage 4 value (or null)
+ - stage5 - stage 5 value (or null)
+ - value - number of times this sequence occurred
+
+
+
);
}
diff --git a/client/app/visualizations/table/Editor/ColumnEditor.jsx b/client/app/visualizations/table/Editor/ColumnEditor.jsx
index a79d1918f..ed009cebb 100644
--- a/client/app/visualizations/table/Editor/ColumnEditor.jsx
+++ b/client/app/visualizations/table/Editor/ColumnEditor.jsx
@@ -3,12 +3,7 @@ import React from 'react';
import { useDebouncedCallback } from 'use-debounce';
import PropTypes from 'prop-types';
import * as Grid from 'antd/lib/grid';
-import Input from 'antd/lib/input';
-import Radio from 'antd/lib/radio';
-import Checkbox from 'antd/lib/checkbox';
-import Select from 'antd/lib/select';
-import Icon from 'antd/lib/icon';
-import Tooltip from 'antd/lib/tooltip';
+import { Section, Select, Input, Checkbox, TextAlignmentSelect } from '@/components/visualizations/editor';
import ColumnTypes from '../columns';
@@ -23,55 +18,38 @@ export default function ColumnEditor({ column, onChange }) {
return (
-
-
- handleChangeDebounced({ title: event.target.value })}
- />
-
-
- handleChange({ alignContent: event.target.value })}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+ handleChange({ allowSearch: event.target.checked })}
+ >
+ Use for search
+
+
-
-
+
+
{AdditionalOptions &&
}
diff --git a/client/app/visualizations/table/Editor/ColumnsSettings.test.js b/client/app/visualizations/table/Editor/ColumnsSettings.test.js
index 2115eb32d..bdc0f9b33 100644
--- a/client/app/visualizations/table/Editor/ColumnsSettings.test.js
+++ b/client/app/visualizations/table/Editor/ColumnsSettings.test.js
@@ -31,37 +31,38 @@ describe('Visualizations -> Table -> Editor -> Columns Settings', () => {
test('Toggles column visibility', (done) => {
const el = mount({}, done);
- findByTestID(el, 'Table.Column.a.Visibility').first().simulate('click');
+ findByTestID(el, 'Table.Column.a.Visibility').last().simulate('click');
});
test('Changes column title', (done) => {
const el = mount({}, done);
- findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings
+ findByTestID(el, 'Table.Column.a.Name').last().simulate('click'); // expand settings
- findByTestID(el, 'Table.Column.a.Title').first().simulate('change', { target: { value: 'test' } });
+ findByTestID(el, 'Table.Column.a.Title').last().simulate('change', { target: { value: 'test' } });
});
test('Changes column alignment', (done) => {
const el = mount({}, done);
- findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings
+ findByTestID(el, 'Table.Column.a.Name').last().simulate('click'); // expand settings
- findByTestID(el, 'Table.Column.a.AlignRight').first().find('input')
+ findByTestID(el, 'Table.Column.a.TextAlignment').last()
+ .find('[data-test="TextAlignmentSelect.Right"] input')
.simulate('change', { target: { checked: true } });
});
test('Enables search by column data', (done) => {
const el = mount({}, done);
- findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings
+ findByTestID(el, 'Table.Column.a.Name').last().simulate('click'); // expand settings
- findByTestID(el, 'Table.Column.a.UseForSearch').first().find('input')
+ findByTestID(el, 'Table.Column.a.UseForSearch').last().find('input')
.simulate('change', { target: { checked: true } });
});
test('Changes column display type', (done) => {
const el = mount({}, done);
- findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings
+ findByTestID(el, 'Table.Column.a.Name').last().simulate('click'); // expand settings
- findByTestID(el, 'Table.Column.a.DisplayAs').first().simulate('click');
- findByTestID(el, 'Table.Column.a.DisplayAs.number').first().simulate('click');
+ findByTestID(el, 'Table.Column.a.DisplayAs').last().simulate('click');
+ findByTestID(el, 'Table.Column.a.DisplayAs.number').last().simulate('click');
});
});
diff --git a/client/app/visualizations/table/Editor/GridSettings.jsx b/client/app/visualizations/table/Editor/GridSettings.jsx
index 30ccd58da..30b2f4097 100644
--- a/client/app/visualizations/table/Editor/GridSettings.jsx
+++ b/client/app/visualizations/table/Editor/GridSettings.jsx
@@ -1,16 +1,15 @@
import { map } from 'lodash';
import React from 'react';
-import Select from 'antd/lib/select';
+import { Section, Select } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
const ALLOWED_ITEM_PER_PAGE = [5, 10, 15, 20, 25, 50, 100, 150, 200, 250];
export default function GridSettings({ options, onOptionsChange }) {
return (
-
-
+
+
);
}
diff --git a/client/app/visualizations/table/Editor/GridSettings.test.js b/client/app/visualizations/table/Editor/GridSettings.test.js
index 1eb6209a0..272ac1df8 100644
--- a/client/app/visualizations/table/Editor/GridSettings.test.js
+++ b/client/app/visualizations/table/Editor/GridSettings.test.js
@@ -30,7 +30,7 @@ describe('Visualizations -> Table -> Editor -> Grid Settings', () => {
itemsPerPage: 25,
}, done);
- findByTestID(el, 'Table.ItemsPerPage').first().simulate('click');
- findByTestID(el, 'Table.ItemsPerPage.100').first().simulate('click');
+ findByTestID(el, 'Table.ItemsPerPage').last().simulate('click');
+ findByTestID(el, 'Table.ItemsPerPage.100').last().simulate('click');
});
});
diff --git a/client/app/visualizations/table/Editor/editor.less b/client/app/visualizations/table/Editor/editor.less
index 2290298c6..43efc2fcd 100644
--- a/client/app/visualizations/table/Editor/editor.less
+++ b/client/app/visualizations/table/Editor/editor.less
@@ -20,18 +20,4 @@
.table-visualization-editor-column {
padding-left: 6px;
-
- .table-visualization-editor-column-align-content {
- display: flex;
- align-items: stretch;
- justify-content: stretch;
-
- .ant-radio-button-wrapper {
- flex-grow: 1;
- text-align: center;
- // fit height
- height: 35px;
- line-height: 33px;
- }
- }
}
diff --git a/client/app/visualizations/table/Editor/index.jsx b/client/app/visualizations/table/Editor/index.jsx
index e390cee96..c8a23a7fd 100644
--- a/client/app/visualizations/table/Editor/index.jsx
+++ b/client/app/visualizations/table/Editor/index.jsx
@@ -1,30 +1,11 @@
-import { merge } from 'lodash';
-import React from 'react';
-import Tabs from 'antd/lib/tabs';
-import { EditorPropTypes } from '@/visualizations';
+import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
import ColumnsSettings from './ColumnsSettings';
import GridSettings from './GridSettings';
import './editor.less';
-export default function index(props) {
- const { options, onOptionsChange } = props;
-
- const optionsChanged = (newOptions) => {
- onOptionsChange(merge({}, options, newOptions));
- };
-
- return (
-
- Columns}>
-
-
- Grid}>
-
-
-
- );
-}
-
-index.propTypes = EditorPropTypes;
+export default createTabbedEditor([
+ { key: 'Columns', title: 'Columns', component: ColumnsSettings },
+ { key: 'Grid', title: 'Grid', component: GridSettings },
+]);
diff --git a/client/app/visualizations/table/columns/boolean.jsx b/client/app/visualizations/table/columns/boolean.jsx
index 04eeadb50..25fc16a3d 100644
--- a/client/app/visualizations/table/columns/boolean.jsx
+++ b/client/app/visualizations/table/columns/boolean.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useDebouncedCallback } from 'use-debounce';
-import Input from 'antd/lib/input';
+import { Section, Input } from '@/components/visualizations/editor';
import { createBooleanFormatter } from '@/lib/value-format';
function Editor({ column, onChange }) {
@@ -15,33 +15,23 @@ function Editor({ column, onChange }) {
return (
-
-
-
- handleChangeDebounced(0, event.target.value)}
- />
-
-
+ )}
+ data-test="Table.ColumnEditor.Boolean.False"
+ defaultValue={column.booleanValues[0]}
+ onChange={event => handleChangeDebounced(0, event.target.value)}
+ />
+
-
-
-
- handleChangeDebounced(1, event.target.value)}
- />
-
-
+
);
}
diff --git a/client/app/visualizations/table/columns/boolean.test.js b/client/app/visualizations/table/columns/boolean.test.js
index 47596970e..0e719dd4b 100644
--- a/client/app/visualizations/table/columns/boolean.test.js
+++ b/client/app/visualizations/table/columns/boolean.test.js
@@ -28,7 +28,7 @@ describe('Visualizations -> Table -> Columns -> Boolean', () => {
booleanValues: ['false', 'true'],
}, done);
- findByTestID(el, 'Table.ColumnEditor.Boolean.False').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Boolean.False').last().find('input')
.simulate('change', { target: { value: 'no' } });
});
@@ -38,7 +38,7 @@ describe('Visualizations -> Table -> Columns -> Boolean', () => {
booleanValues: ['false', 'true'],
}, done);
- findByTestID(el, 'Table.ColumnEditor.Boolean.True').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Boolean.True').last().find('input')
.simulate('change', { target: { value: 'yes' } });
});
});
diff --git a/client/app/visualizations/table/columns/datetime.jsx b/client/app/visualizations/table/columns/datetime.jsx
index b8fe5b6e5..3a550f6de 100644
--- a/client/app/visualizations/table/columns/datetime.jsx
+++ b/client/app/visualizations/table/columns/datetime.jsx
@@ -1,38 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useDebouncedCallback } from 'use-debounce';
-import Input from 'antd/lib/input';
-import Popover from 'antd/lib/popover';
-import Icon from 'antd/lib/icon';
+import { Section, Input, ContextHelp } from '@/components/visualizations/editor';
import { createDateTimeFormatter } from '@/lib/value-format';
function Editor({ column, onChange }) {
const [onChangeDebounced] = useDebouncedCallback(onChange, 200);
return (
-
-
-
-
onChangeDebounced({ dateTimeFormat: event.target.value })}
- />
-
-
+
);
}
diff --git a/client/app/visualizations/table/columns/datetime.test.js b/client/app/visualizations/table/columns/datetime.test.js
index f3e2c903f..cacdde371 100644
--- a/client/app/visualizations/table/columns/datetime.test.js
+++ b/client/app/visualizations/table/columns/datetime.test.js
@@ -28,7 +28,7 @@ describe('Visualizations -> Table -> Columns -> Date/Time', () => {
dateTimeFormat: 'YYYY-MM-DD HH:mm:ss',
}, done);
- findByTestID(el, 'Table.ColumnEditor.DateTime.Format').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.DateTime.Format').last().find('input')
.simulate('change', { target: { value: 'YYYY/MM/DD HH:ss' } });
});
});
diff --git a/client/app/visualizations/table/columns/image.jsx b/client/app/visualizations/table/columns/image.jsx
index 127621ca9..f4e2931ec 100644
--- a/client/app/visualizations/table/columns/image.jsx
+++ b/client/app/visualizations/table/columns/image.jsx
@@ -2,9 +2,7 @@ import { extend, trim } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { useDebouncedCallback } from 'use-debounce';
-import Input from 'antd/lib/input';
-import Popover from 'antd/lib/popover';
-import Icon from 'antd/lib/icon';
+import { Section, Input, ControlLabel, ContextHelp } from '@/components/visualizations/editor';
import { formatSimpleTemplate } from '@/lib/value-format';
function Editor({ column, onChange }) {
@@ -12,73 +10,69 @@ function Editor({ column, onChange }) {
return (
-
-
+
+
-
+
+
+ Size
+
+ Any positive integer value that specifies size in pixels.
+ Leave empty to use default value.
+
+
+ )}
+ >
+
+ onChangeDebounced({ imageWidth: event.target.value })}
+ />
+ ×
+ onChangeDebounced({ imageHeight: event.target.value })}
+ />
+
+
+
-
-
+
+
-
-
- All columns can be referenced using {'{{ column_name }}'} syntax.
- Use {'{{ @ }}'} to reference current (this) column.
- This syntax is applicable to URL, Title and Size options.
-
- )}
+
+
+ Format specs {ContextHelp.defaultIcon}
+
+ )}
>
-
- Format specs
-
-
-
+ All columns can be referenced using {'{{ column_name }}'} syntax.
+ Use {'{{ @ }}'} to reference current (this) column.
+ This syntax is applicable to URL, Title and Size options.
+
+
);
}
diff --git a/client/app/visualizations/table/columns/image.test.js b/client/app/visualizations/table/columns/image.test.js
index e2ba0dc41..8e13e787f 100644
--- a/client/app/visualizations/table/columns/image.test.js
+++ b/client/app/visualizations/table/columns/image.test.js
@@ -28,7 +28,7 @@ describe('Visualizations -> Table -> Columns -> Image', () => {
imageUrlTemplate: '{{ @ }}',
}, done);
- findByTestID(el, 'Table.ColumnEditor.Image.UrlTemplate').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Image.UrlTemplate').last().find('input')
.simulate('change', { target: { value: 'http://{{ @ }}.jpeg' } });
});
@@ -38,7 +38,7 @@ describe('Visualizations -> Table -> Columns -> Image', () => {
imageWidth: null,
}, done);
- findByTestID(el, 'Table.ColumnEditor.Image.Width').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Image.Width').last().find('input')
.simulate('change', { target: { value: '400' } });
});
@@ -48,7 +48,7 @@ describe('Visualizations -> Table -> Columns -> Image', () => {
imageHeight: null,
}, done);
- findByTestID(el, 'Table.ColumnEditor.Image.Height').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Image.Height').last().find('input')
.simulate('change', { target: { value: '300' } });
});
@@ -58,7 +58,7 @@ describe('Visualizations -> Table -> Columns -> Image', () => {
imageUrlTemplate: '{{ @ }}',
}, done);
- findByTestID(el, 'Table.ColumnEditor.Image.TitleTemplate').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Image.TitleTemplate').last().find('input')
.simulate('change', { target: { value: 'Image {{ @ }}' } });
});
});
diff --git a/client/app/visualizations/table/columns/link.jsx b/client/app/visualizations/table/columns/link.jsx
index 22af26813..494422171 100644
--- a/client/app/visualizations/table/columns/link.jsx
+++ b/client/app/visualizations/table/columns/link.jsx
@@ -2,10 +2,7 @@ import { extend, trim } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { useDebouncedCallback } from 'use-debounce';
-import Input from 'antd/lib/input';
-import Checkbox from 'antd/lib/checkbox';
-import Popover from 'antd/lib/popover';
-import Icon from 'antd/lib/icon';
+import { Section, Input, Checkbox, ContextHelp } from '@/components/visualizations/editor';
import { formatSimpleTemplate } from '@/lib/value-format';
function Editor({ column, onChange }) {
@@ -13,65 +10,58 @@ function Editor({ column, onChange }) {
return (
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+ onChange({ linkOpenInNewTab: event.target.checked })}
+ >
+ Open in new tab
+
+
-
-
- All columns can be referenced using {'{{ column_name }}'} syntax.
- Use {'{{ @ }}'} to reference current (this) column.
- This syntax is applicable to URL, Text and Title options.
-
- )}
+
+
+ Format specs {ContextHelp.defaultIcon}
+
+ )}
>
-
- Format specs
-
-
-
+ All columns can be referenced using {'{{ column_name }}'} syntax.
+ Use {'{{ @ }}'} to reference current (this) column.
+ This syntax is applicable to URL, Text and Title options.
+
+
);
}
diff --git a/client/app/visualizations/table/columns/link.test.js b/client/app/visualizations/table/columns/link.test.js
index 55b3e9da2..11da83116 100644
--- a/client/app/visualizations/table/columns/link.test.js
+++ b/client/app/visualizations/table/columns/link.test.js
@@ -28,7 +28,7 @@ describe('Visualizations -> Table -> Columns -> Link', () => {
linkUrlTemplate: '{{ @ }}',
}, done);
- findByTestID(el, 'Table.ColumnEditor.Link.UrlTemplate').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Link.UrlTemplate').last().find('input')
.simulate('change', { target: { value: 'http://{{ @ }}/index.html' } });
});
@@ -38,7 +38,7 @@ describe('Visualizations -> Table -> Columns -> Link', () => {
linkTextTemplate: '{{ @ }}',
}, done);
- findByTestID(el, 'Table.ColumnEditor.Link.TextTemplate').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Link.TextTemplate').last().find('input')
.simulate('change', { target: { value: 'Text of {{ @ }}' } });
});
@@ -48,7 +48,7 @@ describe('Visualizations -> Table -> Columns -> Link', () => {
linkTitleTemplate: '{{ @ }}',
}, done);
- findByTestID(el, 'Table.ColumnEditor.Link.TitleTemplate').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Link.TitleTemplate').last().find('input')
.simulate('change', { target: { value: 'Title of {{ @ }}' } });
});
@@ -58,7 +58,7 @@ describe('Visualizations -> Table -> Columns -> Link', () => {
linkOpenInNewTab: false,
}, done);
- findByTestID(el, 'Table.ColumnEditor.Link.OpenInNewTab').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Link.OpenInNewTab').last().find('input')
.simulate('change', { target: { checked: true } });
});
});
diff --git a/client/app/visualizations/table/columns/number.jsx b/client/app/visualizations/table/columns/number.jsx
index 4f3f83e94..742d9915a 100644
--- a/client/app/visualizations/table/columns/number.jsx
+++ b/client/app/visualizations/table/columns/number.jsx
@@ -1,38 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useDebouncedCallback } from 'use-debounce';
-import Input from 'antd/lib/input';
-import Popover from 'antd/lib/popover';
-import Icon from 'antd/lib/icon';
+import { Section, Input, ContextHelp } from '@/components/visualizations/editor';
import { createNumberFormatter } from '@/lib/value-format';
function Editor({ column, onChange }) {
const [onChangeDebounced] = useDebouncedCallback(onChange, 200);
return (
-
-
-
-
onChangeDebounced({ numberFormat: event.target.value })}
- />
-
-
+
);
}
diff --git a/client/app/visualizations/table/columns/number.test.js b/client/app/visualizations/table/columns/number.test.js
index d7c5e4d85..91c5c7f89 100644
--- a/client/app/visualizations/table/columns/number.test.js
+++ b/client/app/visualizations/table/columns/number.test.js
@@ -28,7 +28,7 @@ describe('Visualizations -> Table -> Columns -> Number', () => {
numberFormat: '0[.]0000',
}, done);
- findByTestID(el, 'Table.ColumnEditor.Number.Format').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Number.Format').last().find('input')
.simulate('change', { target: { value: '0.00%' } });
});
});
diff --git a/client/app/visualizations/table/columns/text.jsx b/client/app/visualizations/table/columns/text.jsx
index 0e47f1ab8..58d39208b 100644
--- a/client/app/visualizations/table/columns/text.jsx
+++ b/client/app/visualizations/table/columns/text.jsx
@@ -1,36 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
-import Checkbox from 'antd/lib/checkbox';
import HtmlContent from '@/components/HtmlContent';
+import { Section, Checkbox } from '@/components/visualizations/editor';
import { createTextFormatter } from '@/lib/value-format';
function Editor({ column, onChange }) {
return (
-
-
-
+
+ onChange({ allowHTML: event.target.checked })}
+ >
+ Allow HTML content
+
+
{column.allowHTML && (
-
-
-
+
+ onChange({ highlightLinks: event.target.checked })}
+ >
+ Highlight links
+
+
)}
);
diff --git a/client/app/visualizations/table/columns/text.test.js b/client/app/visualizations/table/columns/text.test.js
index 47b690a48..d1153152a 100644
--- a/client/app/visualizations/table/columns/text.test.js
+++ b/client/app/visualizations/table/columns/text.test.js
@@ -29,7 +29,7 @@ describe('Visualizations -> Table -> Columns -> Text', () => {
highlightLinks: false,
}, done);
- findByTestID(el, 'Table.ColumnEditor.Text.AllowHTML').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Text.AllowHTML').last().find('input')
.simulate('change', { target: { checked: true } });
});
@@ -40,7 +40,7 @@ describe('Visualizations -> Table -> Columns -> Text', () => {
highlightLinks: false,
}, done);
- findByTestID(el, 'Table.ColumnEditor.Text.HighlightLinks').first().find('input')
+ findByTestID(el, 'Table.ColumnEditor.Text.HighlightLinks').last().find('input')
.simulate('change', { target: { checked: true } });
});
});
diff --git a/client/app/visualizations/word-cloud/Editor.jsx b/client/app/visualizations/word-cloud/Editor.jsx
index 37cee6ab8..db5918363 100644
--- a/client/app/visualizations/word-cloud/Editor.jsx
+++ b/client/app/visualizations/word-cloud/Editor.jsx
@@ -1,8 +1,7 @@
import { map, merge } from 'lodash';
import React from 'react';
-import Select from 'antd/lib/select';
-import InputNumber from 'antd/lib/input-number';
import * as Grid from 'antd/lib/grid';
+import { Section, Select, InputNumber, ControlLabel } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';
export default function Editor({ options, data, onOptionsChange }) {
@@ -12,11 +11,10 @@ export default function Editor({ options, data, onOptionsChange }) {
return (
-
-
+
-
-
-
+
+
-
-
-
-
-
- optionsChanged({ wordLengthLimit: { min: value > 0 ? value : null } })}
- />
-
-
- optionsChanged({ wordLengthLimit: { max: value > 0 ? value : null } })}
- />
-
-
-
-
-
-
-
- optionsChanged({ wordCountLimit: { min: value > 0 ? value : null } })}
- />
-
-
- optionsChanged({ wordCountLimit: { max: value > 0 ? value : null } })}
- />
-
-
-
+
+
+
+
+
+ optionsChanged({ wordLengthLimit: { min: value > 0 ? value : null } })}
+ />
+
+
+ optionsChanged({ wordLengthLimit: { max: value > 0 ? value : null } })}
+ />
+
+
+
+
+
+
+
+
+ optionsChanged({ wordCountLimit: { min: value > 0 ? value : null } })}
+ />
+
+
+ optionsChanged({ wordCountLimit: { max: value > 0 ? value : null } })}
+ />
+
+
+
+
);
}
diff --git a/client/cypress/integration/visualizations/choropleth_spec.js b/client/cypress/integration/visualizations/choropleth_spec.js
index 4e3b56ace..78809706c 100644
--- a/client/cypress/integration/visualizations/choropleth_spec.js
+++ b/client/cypress/integration/visualizations/choropleth_spec.js
@@ -48,7 +48,7 @@ describe('Choropleth', () => {
`);
cy.clickThrough(`
- Choropleth.EditorTabs.General
+ VisualizationEditor.Tabs.General
Choropleth.Editor.MapType
Choropleth.Editor.MapType.Countries
Choropleth.Editor.KeyColumn
@@ -59,7 +59,7 @@ describe('Choropleth', () => {
Choropleth.Editor.ValueColumn.value
`);
- cy.clickThrough('Choropleth.EditorTabs.Colors');
+ cy.clickThrough('VisualizationEditor.Tabs.Colors');
cy.clickThrough('Choropleth.Editor.Colors.Min');
cy.fillInputs({ 'ColorPicker.CustomColor': 'yellow{enter}' });
cy.getByTestId('ColorPicker.CustomColor').should('not.be.visible');
@@ -71,12 +71,14 @@ describe('Choropleth', () => {
cy.getByTestId('ColorPicker.CustomColor').should('not.be.visible');
cy.clickThrough(`
- Choropleth.EditorTabs.Format
+ VisualizationEditor.Tabs.Format
Choropleth.Editor.LegendPosition
Choropleth.Editor.LegendPosition.TopRight
`);
- cy.getByTestId('Choropleth.Editor.LegendTextAlignment.Left').check({ force: true });
+ cy.getByTestId('Choropleth.Editor.LegendTextAlignment')
+ .find('[data-test="TextAlignmentSelect.Left"]')
+ .check({ force: true });
// Wait for proper initialization of visualization
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
diff --git a/client/cypress/integration/visualizations/cohort_spec.js b/client/cypress/integration/visualizations/cohort_spec.js
index 6d344ea31..259852ff3 100644
--- a/client/cypress/integration/visualizations/cohort_spec.js
+++ b/client/cypress/integration/visualizations/cohort_spec.js
@@ -38,13 +38,13 @@ describe('Cohort', () => {
`);
cy.clickThrough(`
- Cohort.EditorTabs.Options
+ VisualizationEditor.Tabs.Options
Cohort.TimeInterval
Cohort.TimeInterval.monthly
Cohort.Mode
Cohort.Mode.simple
- Cohort.EditorTabs.Columns
+ VisualizationEditor.Tabs.Columns
Cohort.DateColumn
Cohort.DateColumn.date
Cohort.StageColumn
@@ -61,7 +61,7 @@ describe('Cohort', () => {
cy.percySnapshot('Visualizations - Cohort (simple)', { widths: [viewportWidth] });
cy.clickThrough(`
- Cohort.EditorTabs.Options
+ VisualizationEditor.Tabs.Options
Cohort.Mode
Cohort.Mode.diagonal
`);
diff --git a/client/cypress/integration/visualizations/counter_spec.js b/client/cypress/integration/visualizations/counter_spec.js
index dadfa68f4..8d9306c48 100644
--- a/client/cypress/integration/visualizations/counter_spec.js
+++ b/client/cypress/integration/visualizations/counter_spec.js
@@ -156,7 +156,7 @@ describe('Counter', () => {
Counter.General.TargetValueColumn
Counter.General.TargetValueColumn.b
- Counter.EditorTabs.Formatting
+ VisualizationEditor.Tabs.Format
`);
cy.fillInputs({
@@ -186,7 +186,7 @@ describe('Counter', () => {
Counter.General.TargetValueColumn
Counter.General.TargetValueColumn.b
- Counter.EditorTabs.Formatting
+ VisualizationEditor.Tabs.Format
Counter.Formatting.FormatTargetValue
`);
diff --git a/client/cypress/integration/visualizations/funnel_spec.js b/client/cypress/integration/visualizations/funnel_spec.js
index 5e20fee64..efee345d3 100644
--- a/client/cypress/integration/visualizations/funnel_spec.js
+++ b/client/cypress/integration/visualizations/funnel_spec.js
@@ -39,7 +39,7 @@ describe('Funnel', () => {
`);
cy.clickThrough(`
- Funnel.EditorTabs.General
+ VisualizationEditor.Tabs.General
Funnel.StepColumn
Funnel.StepColumn.a
@@ -64,7 +64,7 @@ describe('Funnel', () => {
cy.percySnapshot('Visualizations - Funnel (basic)', { widths: [viewportWidth] });
cy.clickThrough(`
- Funnel.EditorTabs.Appearance
+ VisualizationEditor.Tabs.Appearance
`);
cy.fillInputs({
diff --git a/client/cypress/integration/visualizations/map_spec.js b/client/cypress/integration/visualizations/map_spec.js
index 5e8e41d0d..b0e2c905b 100644
--- a/client/cypress/integration/visualizations/map_spec.js
+++ b/client/cypress/integration/visualizations/map_spec.js
@@ -33,7 +33,7 @@ describe('Map (Markers)', () => {
`);
cy.clickThrough(`
- Map.EditorTabs.General
+ VisualizationEditor.Tabs.General
Map.Editor.LatitudeColumnName
Map.Editor.LatitudeColumnName.lat
Map.Editor.LongitudeColumnName
@@ -42,7 +42,7 @@ describe('Map (Markers)', () => {
Map.Editor.GroupBy.country
`);
- cy.clickThrough('Map.EditorTabs.Groups');
+ cy.clickThrough('VisualizationEditor.Tabs.Groups');
cy.clickThrough('Map.Editor.Groups.Israel.Color');
cy.fillInputs({ 'ColorPicker.CustomColor': 'red{enter}' });
cy.getByTestId('ColorPicker.CustomColor').should('not.be.visible');
@@ -68,7 +68,7 @@ describe('Map (Markers)', () => {
`);
cy.clickThrough(`
- Map.EditorTabs.General
+ VisualizationEditor.Tabs.General
Map.Editor.LatitudeColumnName
Map.Editor.LatitudeColumnName.lat
Map.Editor.LongitudeColumnName
@@ -76,7 +76,7 @@ describe('Map (Markers)', () => {
`);
cy.clickThrough(`
- Map.EditorTabs.Style
+ VisualizationEditor.Tabs.Style
Map.Editor.ClusterMarkers
Map.Editor.CustomizeMarkers
`);