Set of improvements and refinements to visualizations after React migration (#4382)

This commit is contained in:
Levko Kravets
2019-12-04 16:23:29 +02:00
committed by GitHub
parent 041d05d18b
commit 94bd03dc42
109 changed files with 2007 additions and 2190 deletions

View File

@@ -386,4 +386,12 @@
}
}
}
}
}
// overrides for checkbox
@checkbox-prefix-cls: ~'@{ant-prefix}-checkbox';
.@{checkbox-prefix-cls}-wrapper + span,
.@{checkbox-prefix-cls} + span {
padding-right: 0;
}

View File

@@ -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 (
<Popover
arrowPointAtCenter
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
overlayStyle={{ '--color-picker-selected-color': currentColor }}
content={(
<Card
data-test="ColorPicker"
className="color-picker-panel"
bordered={false}
title={toString(currentColor).toUpperCase()}
headStyle={{
backgroundColor: currentColor,
color: tinycolor(currentColor).isLight() ? '#000000' : '#ffffff',
}}
actions={actions}
>
<ColorInput
color={currentColor}
presetColors={presetColors}
presetColumns={presetColumns}
onChange={handleInputChange}
onPressEnter={handleApply}
<React.Fragment>
{addonBefore}
<Popover
arrowPointAtCenter
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
overlayStyle={{ '--color-picker-selected-color': currentColor }}
content={(
<Card
data-test="ColorPicker"
className="color-picker-panel"
bordered={false}
title={toString(currentColor).toUpperCase()}
headStyle={{
backgroundColor: currentColor,
color: chooseTextColorForBackground(currentColor),
}}
actions={actions}
>
<ColorInput
color={currentColor}
presetColors={presetColors}
presetColumns={presetColumns}
onChange={handleInputChange}
onPressEnter={handleApply}
/>
</Card>
)}
trigger="click"
placement={placement}
visible={visible}
onVisibleChange={setVisible}
>
{children || (
<Swatch
color={validatedColor}
size={30}
{...triggerProps}
className={cx('color-picker-trigger', triggerProps.className)}
/>
</Card>
)}
trigger="click"
placement={placement}
visible={visible}
onVisibleChange={setVisible}
>
{children || (
<Swatch
color={validatedColor}
size={30}
{...triggerProps}
className={cx('color-picker-trigger', triggerProps.className)}
/>
)}
</Popover>
)}
</Popover>
{addonAfter}
</React.Fragment>
);
}
@@ -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: () => {},
};

View File

@@ -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: <i className="fa fa-question-circle" />,
};
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 (
<React.Fragment>
<Tooltip title={tooltip}>
<Tooltip title={this.props.showTooltip ? tooltip : null}>
<a onClick={this.openDrawer} className={className}>
{this.props.children}
</a>

View File

@@ -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
<div {...pickBy(props, (v, k) => startsWith(k, 'data-'))}>
<Radio.Group
className={cx('text-alignment-select', className)}
{...props}
>
<Tooltip title="Align left" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="left" data-test="TextAlignmentSelect.Left">
<Icon type="align-left" />
</Radio.Button>
</Tooltip>
<Tooltip title="Align center" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="center" data-test="TextAlignmentSelect.Center">
<Icon type="align-center" />
</Radio.Button>
</Tooltip>
<Tooltip title="Align right" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="right" data-test="TextAlignmentSelect.Right">
<Icon type="align-right" />
</Radio.Button>
</Tooltip>
</Radio.Group>
</div>
);
}
TextAlignmentSelect.propTypes = {
className: PropTypes.string,
};
TextAlignmentSelect.defaultProps = {
className: null,
};

View File

@@ -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 <Input> height
height: 35px;
line-height: 33px;
}
}

View File

@@ -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 (
<Popover {...props} content={children}>{icon || ContextHelp.defaultIcon}</Popover>
);
}
ContextHelp.propTypes = {
icon: PropTypes.node,
children: PropTypes.node,
};
ContextHelp.defaultProps = {
icon: null,
children: null,
};
ContextHelp.defaultIcon = (
<Icon className="m-l-5 m-r-5" type="question-circle" theme="filled" />
);
function NumberFormatSpecs() {
return (
<HelpTrigger type="NUMBER_FORMAT_SPECS" className="visualization-editor-context-help">
{ContextHelp.defaultIcon}
</HelpTrigger>
);
}
function DateTimeFormatSpecs() {
return (
<Tooltip title={(<React.Fragment>Formatting Dates and Times<i className="fa fa-external-link m-l-5" /></React.Fragment>)}>
<a
className="visualization-editor-context-help"
href="https://momentjs.com/docs/#/displaying/format/"
target="_blank"
rel="noopener noreferrer"
>
{ContextHelp.defaultIcon}
</a>
</Tooltip>
);
}
ContextHelp.NumberFormatSpecs = NumberFormatSpecs;
ContextHelp.DateTimeFormatSpecs = DateTimeFormatSpecs;

View File

@@ -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 <h4 className={cx('m-t-0', 'm-b-15', className)} {...props}>{children}</h4>;
}
SectionTitle.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
};
SectionTitle.defaultProps = {
className: null,
children: null,
};
export default function Section({ className, children, ...props }) {
return (
<div className={cx('m-b-15', className)} {...props}>
{children}
</div>
);
}
Section.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
};
Section.defaultProps = {
className: null,
children: null,
};
Section.Title = SectionTitle;

View File

@@ -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 (
<label htmlFor={id} className="d-flex align-items-center">
<AntSwitch id={id} disabled={disabled} {...props} />
<Typography.Text className="m-l-10" disabled={disabled}>{children}</Typography.Text>
</label>
);
}
return (
<AntSwitch {...props} />
);
}
Switch.propTypes = {
id: PropTypes.string,
disabled: PropTypes.bool,
children: PropTypes.node,
};
Switch.defaultProps = {
id: null,
disabled: false,
children: null,
};

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,7 @@
.visualization-editor-control-label {
&.visualization-editor-control-label-horizontal {
label {
margin-bottom: 0;
}
}
}

View File

@@ -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 (
<Tabs animated={false} tabBarGutter={0}>
{map(tabs, ({ key, title, component: Component }) => (
<Tabs.TabPane key={key} tab={<span data-test={`VisualizationEditor.Tabs.${key}`}>{title}</span>}>
<Component options={options} data={data} onOptionsChange={optionsChanged} {...restProps} />
</Tabs.TabPane>
))}
</Tabs>
);
}
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 (
<TabbedEditor {...props} tabs={tabs} />
);
};
}

View File

@@ -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);

View File

@@ -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 (
<div className="visualization-editor-control-label visualization-editor-control-label-vertical">
<label {...labelProps}>
<Typography.Text disabled={disabled}>{label}</Typography.Text>
</label>
{children}
</div>
);
}
if ((layout === 'horizontal') && label) {
return (
<Grid.Row
className="visualization-editor-control-label visualization-editor-control-label-horizontal"
type="flex"
align="middle"
gutter={15}
>
<Grid.Col span={12}>
<label {...labelProps}>
<Typography.Text disabled={disabled}>{label}</Typography.Text>
</label>
</Grid.Col>
<Grid.Col span={12}>
{children}
</Grid.Col>
</Grid.Row>
);
}
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 (
<ControlLabel layout={layout} label={label} labelProps={labelProps} disabled={disabled}>
<WrappedControl id={labelProps.htmlFor} disabled={disabled} {...props} />
</ControlLabel>
);
}
// Copy static methods from `WrappedComponent`
hoistNonReactStatics(ControlWrapper, WrappedControl);
return ControlWrapper;
}

View File

@@ -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;
}
}

View File

@@ -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',
};

View File

@@ -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 (
<div>
<div className="form-group">
<label className="control-label" htmlFor="box-plot-x-axis-label">X Axis Label</label>
<React.Fragment>
<Section>
<Input
label="X Axis Label"
data-test="BoxPlot.XAxisLabel"
id="box-plot-x-axis-label"
value={options.xAxisLabel}
onChange={event => onXAxisLabelChanged(event.target.value)}
/>
</div>
</Section>
<div className="form-group">
<label className="control-label" htmlFor="box-plot-y-axis-label">Y Axis Label</label>
<Section>
<Input
label="Y Axis Label"
data-test="BoxPlot.YAxisLabel"
id="box-plot-y-axis-label"
value={options.yAxisLabel}
onChange={event => onYAxisLabelChanged(event.target.value)}
/>
</div>
</div>
</Section>
</React.Fragment>
);
}

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor={`chart-editor-${id}-type`}>Scale</label>
<Section>
<Select
id={`chart-editor-${id}-type`}
label="Scale"
className="w-100"
data-test={`Chart.${id}.Type`}
defaultValue={options.type}
@@ -41,43 +38,42 @@ export default function AxisSettings({ id, options, features, onChange }) {
<Select.Option value="logarithmic" data-test={`Chart.${id}.Type.Logarithmic`}>Logarithmic</Select.Option>
<Select.Option value="category" data-test={`Chart.${id}.Type.Category`}>Category</Select.Option>
</Select>
</div>
</Section>
<div className="m-b-15">
<label htmlFor={`chart-editor-${id}-name`}>Name</label>
<Section>
<Input
id={`chart-editor-${id}-name`}
label="Name"
data-test={`Chart.${id}.Name`}
defaultValue={isObject(options.title) ? options.title.text : null}
onChange={event => handleNameChange(event.target.value)}
/>
</div>
</Section>
{features.range && (
<Grid.Row gutter={15} type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor={`chart-editor-${id}-range-min`}>Min Value</label>
<InputNumber
id={`chart-editor-${id}-range-min`}
className="w-100"
placeholder="Auto"
data-test={`Chart.${id}.RangeMin`}
defaultValue={toNumber(options.rangeMin)}
onChange={value => handleMinMaxChange({ rangeMin: toNumber(value) })}
/>
</Grid.Col>
<Grid.Col span={12}>
<label htmlFor={`chart-editor-${id}-range-max`}>Max Value</label>
<InputNumber
id={`chart-editor-${id}-range-max`}
className="w-100"
placeholder="Auto"
data-test={`Chart.${id}.RangeMax`}
defaultValue={toNumber(options.rangeMax)}
onChange={value => handleMinMaxChange({ rangeMax: toNumber(value) })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Grid.Row gutter={15} type="flex" align="middle">
<Grid.Col span={12}>
<InputNumber
label="Min Value"
className="w-100"
placeholder="Auto"
data-test={`Chart.${id}.RangeMin`}
defaultValue={toNumber(options.rangeMin)}
onChange={value => handleMinMaxChange({ rangeMin: toNumber(value) })}
/>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
label="Max Value"
className="w-100"
placeholder="Auto"
data-test={`Chart.${id}.RangeMax`}
defaultValue={toNumber(options.rangeMax)}
onChange={value => handleMinMaxChange({ rangeMax: toNumber(value) })}
/>
</Grid.Col>
</Grid.Row>
</Section>
)}
</React.Fragment>
);

View File

@@ -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) {

View File

@@ -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' } });
});
});

View File

@@ -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 (
<div className="m-b-15">
<label htmlFor={`chart-editor-column-mapping-${type}`}>{label}</label>
<Section>
<Select
id={`chart-editor-column-mapping-${type}`}
label={label}
className="w-100"
data-test={`Chart.ColumnMapping.${type}`}
mode={multiple ? 'multiple' : 'default'}
@@ -36,7 +35,7 @@ export default function ColumnMappingSelect({ value, availableColumns, type, onC
<Select.Option key={c} value={c} data-test={`Chart.ColumnMapping.${type}.${c}`}>{c}</Select.Option>
))}
</Select>
</div>
</Section>
);
}

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor="chart-editor-custom-code">Custom code</label>
<Section>
<TextArea
id="chart-editor-custom-code"
label="Custom code"
data-test="Chart.Custom.Code"
className="form-control v-resizable"
rows="10"
defaultValue={isNil(options.customCode) ? defaultCustomCode : options.customCode}
onChange={event => onOptionsChange({ customCode: event.target.value })}
/>
</div>
</Section>
<div className="m-b-15">
<label className="d-flex align-items-center" htmlFor="chart-editor-enable-console-logs">
<Switch
id="chart-editor-enable-console-logs"
data-test="Chart.Custom.EnableConsoleLogs"
defaultChecked={options.enableConsoleLogs}
onChange={enableConsoleLogs => onOptionsChange({ enableConsoleLogs })}
/>
<span className="m-l-10">Show errors in the console</span>
</label>
</div>
<Section>
<Switch
data-test="Chart.Custom.EnableConsoleLogs"
defaultChecked={options.enableConsoleLogs}
onChange={enableConsoleLogs => onOptionsChange({ enableConsoleLogs })}
>
Show errors in the console
</Switch>
</Section>
<div className="m-b-15">
<label className="d-flex align-items-center" htmlFor="chart-editor-auto-update-custom-chart">
<Switch
id="chart-editor-auto-update-custom-chart"
data-test="Chart.Custom.AutoUpdate"
defaultChecked={options.autoRedraw}
onChange={autoRedraw => onOptionsChange({ autoRedraw })}
/>
<span className="m-l-10">Auto update graph</span>
</label>
</div>
<Section>
<Switch
id="chart-editor-auto-update-custom-chart"
data-test="Chart.Custom.AutoUpdate"
defaultChecked={options.autoRedraw}
onChange={autoRedraw => onOptionsChange({ autoRedraw })}
>
Auto update graph
</Switch>
</Section>
</React.Fragment>
);
}

View File

@@ -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 (
<React.Fragment>
{ isShowDataLabelsAvailable && (
<div className="m-b-15">
<label htmlFor="chart-editor-show-data-labels">
<Checkbox
id="chart-editor-show-data-labels"
data-test="Chart.DataLabels.ShowDataLabels"
defaultChecked={options.showDataLabels}
onChange={event => onOptionsChange({ showDataLabels: event.target.checked })}
/>
<span>Show Data Labels</span>
</label>
</div>
<Section>
<Checkbox
data-test="Chart.DataLabels.ShowDataLabels"
defaultChecked={options.showDataLabels}
onChange={event => onOptionsChange({ showDataLabels: event.target.checked })}
>
Show Data Labels
</Checkbox>
</Section>
)}
<div className="m-b-15">
<label htmlFor="chart-editor-number-format">
Number Values Format
<Popover
content={(
<React.Fragment>
Format&nbsp;
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
<Section>
<Input
id="chart-editor-number-format"
label={<React.Fragment>Number Values Format<ContextHelp.NumberFormatSpecs /></React.Fragment>}
data-test="Chart.DataLabels.NumberFormat"
defaultValue={options.numberFormat}
onChange={e => debouncedOnOptionsChange({ numberFormat: e.target.value })}
/>
</div>
</Section>
<div className="m-b-15">
<label htmlFor="chart-editor-percent-format">
Percent Values Format
<Popover
content={(
<React.Fragment>
Format&nbsp;
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
<Section>
<Input
id="chart-editor-percent-format"
label={<React.Fragment>Percent Values Format<ContextHelp.NumberFormatSpecs /></React.Fragment>}
data-test="Chart.DataLabels.PercentFormat"
defaultValue={options.percentFormat}
onChange={e => debouncedOnOptionsChange({ percentFormat: e.target.value })}
/>
</div>
</Section>
<div className="m-b-15">
<label htmlFor="chart-editor-datetime-format">
Date/Time Values Format
<Popover
content={(
<React.Fragment>
Format&nbsp;
<a href="https://momentjs.com/docs/#/displaying/format/" target="_blank" rel="noopener noreferrer">specs.</a>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
<Section>
<Input
id="chart-editor-datetime-format"
label={<React.Fragment>Date/Time Values Format<ContextHelp.DateTimeFormatSpecs /></React.Fragment>}
data-test="Chart.DataLabels.DateTimeFormat"
defaultValue={options.dateTimeFormat}
onChange={e => debouncedOnOptionsChange({ dateTimeFormat: e.target.value })}
/>
</div>
</Section>
<div className="m-b-15">
<label htmlFor="chart-editor-text-format">
Data Labels
<Popover
placement="topRight"
arrowPointAtCenter
content={(
<React.Fragment>
<Section>
<Input
label={(
<React.Fragment>
Data Labels
<ContextHelp placement="topRight" arrowPointAtCenter>
<div className="p-b-5">Use special names to access additional properties:</div>
<div><code>{'{{ @@name }}'}</code> series name;</div>
<div><code>{'{{ @@x }}'}</code> x-value;</div>
@@ -116,20 +70,15 @@ export default function DataLabelsSettings({ options, onOptionsChange }) {
Also, all query result columns can be referenced<br />using
<code className="text-nowrap">{'{{ column_name }}'}</code> syntax.
</div>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
<Input
id="chart-editor-text-format"
</ContextHelp>
</React.Fragment>
)}
data-test="Chart.DataLabels.TextFormat"
placeholder="(auto)"
defaultValue={options.textFormat}
onChange={e => debouncedOnOptionsChange({ textFormat: e.target.value })}
/>
</div>
</Section>
</React.Fragment>
);
}

View File

@@ -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 }}' } });
});
});

View File

@@ -36,18 +36,17 @@ export default function DefaultColorsSettings({ options, data, onOptionsChange }
title: 'Color',
dataIndex: 'color',
width: '1%',
className: 'text-nowrap',
render: (unused, item) => (
<div className="text-nowrap">
<ColorPicker
data-test={`Chart.Series.${item.key}.Color`}
interactive
presetColors={colors}
placement="topRight"
color={item.color}
onChange={value => updateSeriesOption(item.key, 'color', value)}
/>
<ColorPicker.Label color={item.color} presetColors={colors} />
</div>
<ColorPicker
data-test={`Chart.Series.${item.key}.Color`}
interactive
presetColors={colors}
placement="topRight"
color={item.color}
onChange={value => updateSeriesOption(item.key, 'color', value)}
addonAfter={<ColorPicker.Label color={item.color} presetColors={colors} />}
/>
),
},
];

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor="chart-editor-global-series-type">Chart Type</label>
<Section>
<ChartTypeSelect
id="chart-editor-global-series-type"
label="Chart Type"
className="w-100"
data-test="Chart.GlobalSeriesType"
defaultValue={options.globalSeriesType}
onChange={handleGlobalSeriesTypeChange}
/>
</div>
</Section>
{map(mappedColumns, (value, type) => (
<ColumnMappingSelect
@@ -122,10 +121,9 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
))}
{includes(['pie'], options.globalSeriesType) && (
<div className="m-b-15">
<label htmlFor="chart-editor-pie-direction">Direction</label>
<Section>
<Select
id="chart-editor-pie-direction"
label="Direction"
className="w-100"
data-test="Chart.PieDirection"
defaultValue={options.direction.type}
@@ -134,43 +132,37 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option value="counterclockwise" data-test="Chart.PieDirection.Counterclockwise">Counterclockwise</Select.Option>
<Select.Option value="clockwise" data-test="Chart.PieDirection.Clockwise">Clockwise</Select.Option>
</Select>
</div>
</Section>
)}
{!includes(['custom', 'heatmap'], options.globalSeriesType) && (
<div className="m-b-15">
<label htmlFor="chart-editor-show-legend">
<Checkbox
id="chart-editor-show-legend"
data-test="Chart.ShowLegend"
defaultChecked={options.legend.enabled}
onChange={event => onOptionsChange({ legend: { enabled: event.target.checked } })}
/>
<span>Show Legend</span>
</label>
</div>
<Section>
<Checkbox
data-test="Chart.ShowLegend"
defaultChecked={options.legend.enabled}
onChange={event => onOptionsChange({ legend: { enabled: event.target.checked } })}
>
Show Legend
</Checkbox>
</Section>
)}
{includes(['box'], options.globalSeriesType) && (
<div className="m-b-15">
<label htmlFor="chart-editor-show-points">
<Checkbox
id="chart-editor-show-points"
data-test="Chart.ShowPoints"
defaultChecked={options.showpoints}
onChange={event => onOptionsChange({ showpoints: event.target.checked })}
/>
<span>Show All Points</span>
</label>
</div>
<Section>
<Checkbox
data-test="Chart.ShowPoints"
defaultChecked={options.showpoints}
onChange={event => onOptionsChange({ showpoints: event.target.checked })}
>
Show All Points
</Checkbox>
</Section>
)}
{!includes(['custom', 'heatmap'], options.globalSeriesType) && (
<div className="m-b-15">
<label htmlFor="chart-editor-stacking">Stacking</label>
<Section>
<Select
id="chart-editor-stacking"
label="Stacking"
className="w-100"
data-test="Chart.Stacking"
defaultValue={options.series.stacking}
@@ -180,28 +172,25 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option value={null} data-test="Chart.Stacking.Disabled">Disabled</Select.Option>
<Select.Option value="stack" data-test="Chart.Stacking.Stack">Stack</Select.Option>
</Select>
</div>
</Section>
)}
{includes(['line', 'area', 'column'], options.globalSeriesType) && (
<div className="m-b-15">
<label htmlFor="chart-editor-normalize-values">
<Checkbox
id="chart-editor-normalize-values"
data-test="Chart.NormalizeValues"
defaultChecked={options.series.percentValues}
onChange={event => onOptionsChange({ series: { percentValues: event.target.checked } })}
/>
<span>Normalize values to percentage</span>
</label>
</div>
<Section>
<Checkbox
data-test="Chart.NormalizeValues"
defaultChecked={options.series.percentValues}
onChange={event => onOptionsChange({ series: { percentValues: event.target.checked } })}
>
Normalize values to percentage
</Checkbox>
</Section>
)}
{!includes(['custom', 'heatmap', 'bubble', 'scatter'], options.globalSeriesType) && (
<div className="m-b-15">
<label className="d-flex align-items-center" htmlFor="chart-editor-missing-values">Missing and NULL values</label>
<Section>
<Select
id="chart-editor-missing-values"
label="Missing and NULL values"
className="w-100"
data-test="Chart.MissingValues"
defaultValue={options.missingValuesAsZero ? 1 : 0}
@@ -210,7 +199,7 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option value={0} data-test="Chart.MissingValues.Keep">Do not display in chart</Select.Option>
<Select.Option value={1} data-test="Chart.MissingValues.Zero">Convert to 0 and display in chart</Select.Option>
</Select>
</div>
</Section>
)}
</React.Fragment>
);

View File

@@ -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', () => {

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor="chart-editor-colors-heatmap-scheme">Color Scheme</label>
<Section>
<Select
id="chart-editor-colors-heatmap-scheme"
label="Color Scheme"
className="w-100"
data-test="Chart.Colors.Heatmap.ColorScheme"
placeholder="Choose Color Scheme..."
@@ -33,35 +30,37 @@ export default function HeatmapColorsSettings({ options, onOptionsChange }) {
</Select.Option>
))}
</Select>
</div>
</Section>
{(options.colorScheme === 'Custom...') && (
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label className="m-r-10" htmlFor="chart-editor-colors-heatmap-min-color">Min Color:</label>
<React.Fragment>
<Section>
<ColorPicker
layout="horizontal"
label="Min Color:"
data-test="Chart.Colors.Heatmap.MinColor"
id="chart-editor-colors-heatmap-min-color"
interactive
placement="topLeft"
presetColors={ColorPalette}
color={options.heatMinColor}
onChange={heatMinColor => onOptionsChange({ heatMinColor })}
addonAfter={<ColorPicker.Label color={options.heatMinColor} presetColors={ColorPalette} />}
/>
</Grid.Col>
<Grid.Col span={12}>
<label className="m-r-10" htmlFor="chart-editor-colors-heatmap-max-color">Max Color:</label>
</Section>
<Section>
<ColorPicker
layout="horizontal"
label="Max Color:"
data-test="Chart.Colors.Heatmap.MaxColor"
id="chart-editor-colors-heatmap-max-color"
interactive
placement="topRight"
presetColors={ColorPalette}
color={options.heatMaxColor}
onChange={heatMaxColor => onOptionsChange({ heatMaxColor })}
addonAfter={<ColorPicker.Label color={options.heatMaxColor} presetColors={ColorPalette} />}
/>
</Grid.Col>
</Grid.Row>
</Section>
</React.Fragment>
)}
</React.Fragment>
);

View File

@@ -46,18 +46,17 @@ export default function PieColorsSettings({ options, data, onOptionsChange }) {
title: 'Color',
dataIndex: 'color',
width: '1%',
className: 'text-nowrap',
render: (unused, item) => (
<div className="text-nowrap">
<ColorPicker
data-test={`Chart.Series.${item.key}.Color`}
interactive
presetColors={colors}
placement="topRight"
color={item.color}
onChange={value => updateValuesOption(item.key, 'color', value)}
/>
<ColorPicker.Label color={item.color} presetColors={colors} />
</div>
<ColorPicker
data-test={`Chart.Series.${item.key}.Color`}
interactive
presetColors={colors}
placement="topRight"
color={item.color}
onChange={value => updateValuesOption(item.key, 'color', value)}
addonAfter={<ColorPicker.Label color={item.color} presetColors={colors} />}
/>
),
},
];

View File

@@ -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 } });
});
});

View File

@@ -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 })}
/>
<div className="m-b-15">
<label className="d-flex align-items-center" htmlFor="chart-editor-x-axis-sort">
<Switch
id="chart-editor-x-axis-sort"
data-test="Chart.XAxis.Sort"
defaultChecked={options.sortX}
onChange={sortX => onOptionsChange({ sortX })}
/>
<span className="m-l-10">Sort Values</span>
</label>
</div>
<Section>
<Switch
data-test="Chart.XAxis.Sort"
defaultChecked={options.sortX}
onChange={sortX => onOptionsChange({ sortX })}
>
Sort Values
</Switch>
</Section>
<div className="m-b-15">
<label className="d-flex align-items-center" htmlFor="chart-editor-x-axis-reverse">
<Switch
id="chart-editor-x-axis-reverse"
data-test="Chart.XAxis.Reverse"
defaultChecked={options.reverseX}
onChange={reverseX => onOptionsChange({ reverseX })}
/>
<span className="m-l-10">Reverse Order</span>
</label>
</div>
<Section>
<Switch
data-test="Chart.XAxis.Reverse"
defaultChecked={options.reverseX}
onChange={reverseX => onOptionsChange({ reverseX })}
>
Reverse Order
</Switch>
</Section>
<div className="m-b-15">
<label className="d-flex align-items-center" htmlFor="chart-editor-x-axis-show-labels">
<Switch
id="chart-editor-x-axis-show-labels"
data-test="Chart.XAxis.ShowLabels"
defaultChecked={options.xAxis.labels.enabled}
onChange={enabled => onOptionsChange({ xAxis: { labels: { enabled } } })}
/>
<span className="m-l-10">Show Labels</span>
</label>
</div>
<Section>
<Switch
data-test="Chart.XAxis.ShowLabels"
defaultChecked={options.xAxis.labels.enabled}
onChange={enabled => onOptionsChange({ xAxis: { labels: { enabled } } })}
>
Show Labels
</Switch>
</Section>
</React.Fragment>
);
}

View File

@@ -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');
});
});

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<h4>Left Y Axis</h4>
<Section.Title>Left Y Axis</Section.Title>
<Section>
<AxisSettings
id="LeftYAxis"
features={{ range: true }}
options={leftYAxis}
onChange={axis => onOptionsChange({ yAxis: [axis, rightYAxis] })}
/>
</div>
</Section>
{(options.globalSeriesType !== 'heatmap') && (
<div className="m-b-15">
<h4>Right Y Axis</h4>
<AxisSettings
id="RightYAxis"
features={{ range: true }}
options={rightYAxis}
onChange={axis => onOptionsChange({ yAxis: [leftYAxis, axis] })}
/>
</div>
<React.Fragment>
<Section.Title>Right Y Axis</Section.Title>
<Section>
<AxisSettings
id="RightYAxis"
features={{ range: true }}
options={rightYAxis}
onChange={axis => onOptionsChange({ yAxis: [leftYAxis, axis] })}
/>
</Section>
</React.Fragment>
)}
{(options.globalSeriesType === 'heatmap') && (
<React.Fragment>
<div className="m-b-15">
<label className="d-flex align-items-center" htmlFor="chart-editor-y-axis-sort">
<Switch
id="chart-editor-y-axis-sort"
data-test="Chart.LeftYAxis.Sort"
defaultChecked={options.sortY}
onChange={sortY => onOptionsChange({ sortY })}
/>
<span className="m-l-10">Sort Values</span>
</label>
</div>
<Section>
<Switch
id="chart-editor-y-axis-sort"
data-test="Chart.LeftYAxis.Sort"
defaultChecked={options.sortY}
onChange={sortY => onOptionsChange({ sortY })}
>
Sort Values
</Switch>
</Section>
<div className="m-b-15">
<label className="d-flex align-items-center" htmlFor="chart-editor-y-axis-reverse">
<Switch
id="chart-editor-y-axis-reverse"
data-test="Chart.LeftYAxis.Reverse"
defaultChecked={options.reverseY}
onChange={reverseY => onOptionsChange({ reverseY })}
/>
<span className="m-l-10">Reverse Order</span>
</label>
</div>
<Section>
<Switch
id="chart-editor-y-axis-reverse"
data-test="Chart.LeftYAxis.Reverse"
defaultChecked={options.reverseY}
onChange={reverseY => onOptionsChange({ reverseY })}
>
Reverse Order
</Switch>
</Section>
</React.Fragment>
)}
</React.Fragment>

View File

@@ -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');
});
});
});

View File

@@ -24,7 +24,7 @@ Object {
exports[`Visualizations -> Chart -> Editor -> Colors Settings for heatmap Sets custom color scheme 2`] = `
Object {
"heatMinColor": "#FF0000",
"heatMaxColor": "#FF0000",
}
`;

View File

@@ -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 (
<Tabs animated={false} tabBarGutter={0}>
<Tabs.TabPane key="general" tab={<span data-test="Chart.EditorTabs.General">General</span>}>
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
{isCustomChart && <CustomChartSettings {...props} onOptionsChange={optionsChanged} />}
</Tabs.TabPane>
{!isCustomChart && !isPieChart && (
<Tabs.TabPane key="x-axis" tab={<span data-test="Chart.EditorTabs.XAxis">X Axis</span>}>
<XAxisSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
)}
{!isCustomChart && !isPieChart && (
<Tabs.TabPane key="y-axis" tab={<span data-test="Chart.EditorTabs.YAxis">Y Axis</span>}>
<YAxisSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
)}
{!isCustomChart && (
<Tabs.TabPane key="series" tab={<span data-test="Chart.EditorTabs.Series">Series</span>}>
<SeriesSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
)}
{!isCustomChart && (
<Tabs.TabPane key="colors" tab={<span data-test="Chart.EditorTabs.Colors">Colors</span>}>
<ColorsSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
)}
{!isCustomChart && (
<Tabs.TabPane key="data labels" tab={<span data-test="Chart.EditorTabs.DataLabels">Data Labels</span>}>
<DataLabelsSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
)}
</Tabs>
);
}
Editor.propTypes = EditorPropTypes;
export default createTabbedEditor([
{
key: 'General',
title: 'General',
component: props => (
<React.Fragment>
<GeneralSettings {...props} />
{isCustomChart(props.options) && <CustomChartSettings {...props} />}
</React.Fragment>
),
},
{
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),
},
]);

View File

@@ -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

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor="choropleth-editor-bounds-ne">North-East latitude and longitude</label>
<Grid.Row gutter={15}>
<Grid.Col span={12}>
<InputNumber
id="choropleth-editor-bounds-ne"
className="w-100"
value={bounds[1][0]}
onChange={value => updateBounds(1, 0, value)}
/>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
className="w-100"
value={bounds[1][1]}
onChange={value => updateBounds(1, 1, value)}
/>
</Grid.Col>
</Grid.Row>
</div>
<Section>
<ControlLabel label="North-East latitude and longitude">
<Grid.Row gutter={15}>
<Grid.Col span={12}>
<InputNumber
className="w-100"
value={bounds[1][0]}
onChange={value => updateBounds(1, 0, value)}
/>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
className="w-100"
value={bounds[1][1]}
onChange={value => updateBounds(1, 1, value)}
/>
</Grid.Col>
</Grid.Row>
</ControlLabel>
</Section>
<div className="m-b-15">
<label htmlFor="choropleth-editor-bounds-sw">South-West latitude and longitude</label>
<Grid.Row gutter={15}>
<Grid.Col span={12}>
<InputNumber
id="choropleth-editor-bounds-sw"
className="w-100"
value={bounds[0][0]}
onChange={value => updateBounds(0, 0, value)}
/>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
className="w-100"
value={bounds[0][1]}
onChange={value => updateBounds(0, 1, value)}
/>
</Grid.Col>
</Grid.Row>
</div>
<Section>
<ControlLabel label="South-West latitude and longitude">
<Grid.Row gutter={15}>
<Grid.Col span={12}>
<InputNumber
className="w-100"
value={bounds[0][0]}
onChange={value => updateBounds(0, 0, value)}
/>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
className="w-100"
value={bounds[0][1]}
onChange={value => updateBounds(0, 1, value)}
/>
</Grid.Col>
</Grid.Row>
</ControlLabel>
</Section>
</React.Fragment>
);
}

View File

@@ -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 (
<React.Fragment>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-clustering-mode">Clustering mode</label>
</Grid.Col>
<Grid.Col span={12}>
<Select
id="choropleth-editor-clustering-mode"
className="w-100"
data-test="Choropleth.Editor.ClusteringMode"
defaultValue={options.clusteringMode}
onChange={clusteringMode => onOptionsChange({ clusteringMode })}
>
<Select.Option value="q" data-test="Choropleth.Editor.ClusteringMode.q">quantile</Select.Option>
<Select.Option value="e" data-test="Choropleth.Editor.ClusteringMode.e">equidistant</Select.Option>
<Select.Option value="k" data-test="Choropleth.Editor.ClusteringMode.k">k-means</Select.Option>
</Select>
</Grid.Col>
</Grid.Row>
<Section>
<Select
layout="horizontal"
label="Clustering mode"
className="w-100"
data-test="Choropleth.Editor.ClusteringMode"
defaultValue={options.clusteringMode}
onChange={clusteringMode => onOptionsChange({ clusteringMode })}
>
<Select.Option value="q" data-test="Choropleth.Editor.ClusteringMode.q">quantile</Select.Option>
<Select.Option value="e" data-test="Choropleth.Editor.ClusteringMode.e">equidistant</Select.Option>
<Select.Option value="k" data-test="Choropleth.Editor.ClusteringMode.k">k-means</Select.Option>
</Select>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-color-steps">Steps</label>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
id="choropleth-editor-color-steps"
className="w-100"
data-test="Choropleth.Editor.ColorSteps"
min={3}
max={11}
defaultValue={options.steps}
onChange={steps => onOptionsChangeDebounced({ steps })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<InputNumber
layout="horizontal"
label="Steps"
className="w-100"
data-test="Choropleth.Editor.ColorSteps"
min={3}
max={11}
defaultValue={options.steps}
onChange={steps => onOptionsChangeDebounced({ steps })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-color-min">Min Color</label>
</Grid.Col>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-min"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.min}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Min' }}
onChange={min => onOptionsChange({ colors: { min } })}
/>
<ColorPicker.Label color={options.colors.min} presetColors={ColorPalette} />
</Grid.Col>
</Grid.Row>
<Section>
<ColorPicker
layout="horizontal"
label="Min Color"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.min}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Min' }}
onChange={min => onOptionsChange({ colors: { min } })}
addonAfter={<ColorPicker.Label color={options.colors.min} presetColors={ColorPalette} />}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-color-max">Max Color</label>
</Grid.Col>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-max"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.max}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Max' }}
onChange={max => onOptionsChange({ colors: { max } })}
/>
<ColorPicker.Label color={options.colors.max} presetColors={ColorPalette} />
</Grid.Col>
</Grid.Row>
<Section>
<ColorPicker
layout="horizontal"
label="Max Color"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.max}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Max' }}
onChange={max => onOptionsChange({ colors: { max } })}
addonAfter={<ColorPicker.Label color={options.colors.max} presetColors={ColorPalette} />}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-color-no-value">No value color</label>
</Grid.Col>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-no-value"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.noValue}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.NoValue' }}
onChange={noValue => onOptionsChange({ colors: { noValue } })}
/>
<ColorPicker.Label color={options.colors.noValue} presetColors={ColorPalette} />
</Grid.Col>
</Grid.Row>
<Section>
<ColorPicker
layout="horizontal"
label="No value color"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.noValue}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.NoValue' }}
onChange={noValue => onOptionsChange({ colors: { noValue } })}
addonAfter={<ColorPicker.Label color={options.colors.noValue} presetColors={ColorPalette} />}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-color-background">Background color</label>
</Grid.Col>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-background"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.background}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Background' }}
onChange={background => onOptionsChange({ colors: { background } })}
/>
<ColorPicker.Label color={options.colors.background} presetColors={ColorPalette} />
</Grid.Col>
</Grid.Row>
<Section>
<ColorPicker
layout="horizontal"
label="Background color"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.background}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Background' }}
onChange={background => onOptionsChange({ colors: { background } })}
addonAfter={<ColorPicker.Label color={options.colors.background} presetColors={ColorPalette} />}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-color-borders">Borders color</label>
</Grid.Col>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-borders"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.borders}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Borders' }}
onChange={borders => onOptionsChange({ colors: { borders } })}
/>
<ColorPicker.Label color={options.colors.borders} presetColors={ColorPalette} />
</Grid.Col>
</Grid.Row>
<Section>
<ColorPicker
layout="horizontal"
label="Borders color"
interactive
presetColors={ColorPalette}
placement="topRight"
color={options.colors.borders}
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Borders' }}
onChange={borders => onOptionsChange({ colors: { borders } })}
addonAfter={<ColorPicker.Label color={options.colors.borders} presetColors={ColorPalette} />}
/>
</Section>
</React.Fragment>
);
}

View File

@@ -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 (
<Popover
placement="topLeft"
arrowPointAtCenter
content={(
<ContextHelp placement="topLeft" arrowPointAtCenter>
<div className="p-b-5">All query result columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
<div className="p-b-5">Use special names to access additional properties:</div>
<div><code>{'{{ @@value }}'}</code> formatted value;</div>
{mapType === 'countries' && (
<React.Fragment>
<div className="p-b-5">All query result columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
<div className="p-b-5">Use special names to access additional properties:</div>
<div><code>{'{{ @@value }}'}</code> formatted value;</div>
{mapType === 'countries' && (
<React.Fragment>
<div><code>{'{{ @@name }}'}</code> short country name;</div>
<div><code>{'{{ @@name_long }}'}</code> full country name;</div>
<div><code>{'{{ @@abbrev }}'}</code> abbreviated country name;</div>
<div><code>{'{{ @@iso_a2 }}'}</code> two-letter ISO country code;</div>
<div><code>{'{{ @@iso_a3 }}'}</code> three-letter ISO country code;</div>
<div><code>{'{{ @@iso_n3 }}'}</code> three-digit ISO country code.</div>
</React.Fragment>
)}
{mapType === 'subdiv_japan' && (
<React.Fragment>
<div><code>{'{{ @@name }}'}</code> Prefecture name in English;</div>
<div><code>{'{{ @@name_local }}'}</code> Prefecture name in Kanji;</div>
<div><code>{'{{ @@iso_3166_2 }}'}</code> five-letter ISO subdivision code (JP-xx);</div>
</React.Fragment>
)}
<div><code>{'{{ @@name }}'}</code> short country name;</div>
<div><code>{'{{ @@name_long }}'}</code> full country name;</div>
<div><code>{'{{ @@abbrev }}'}</code> abbreviated country name;</div>
<div><code>{'{{ @@iso_a2 }}'}</code> two-letter ISO country code;</div>
<div><code>{'{{ @@iso_a3 }}'}</code> three-letter ISO country code;</div>
<div><code>{'{{ @@iso_n3 }}'}</code> three-digit ISO country code.</div>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
{mapType === 'subdiv_japan' && (
<React.Fragment>
<div><code>{'{{ @@name }}'}</code> Prefecture name in English;</div>
<div><code>{'{{ @@name_local }}'}</code> Prefecture name in Kanji;</div>
<div><code>{'{{ @@iso_3166_2 }}'}</code> five-letter ISO subdivision code (JP-xx);</div>
</React.Fragment>
)}
</ContextHelp>
);
}
@@ -52,139 +38,102 @@ export default function GeneralSettings({ options, onOptionsChange }) {
return (
<div className="choropleth-visualization-editor-format-settings">
<Grid.Row gutter={15} className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-value-format">
Value format
<Popover
content={(
<React.Fragment>
Format&nbsp;
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
</React.Fragment>
)}
<Section>
<Grid.Row gutter={15}>
<Grid.Col span={12}>
<Input
label={(<React.Fragment>Value format<ContextHelp.NumberFormatSpecs /></React.Fragment>)}
className="w-100"
data-test="Choropleth.Editor.ValueFormat"
defaultValue={options.valueFormat}
onChange={event => onOptionsChangeDebounced({ valueFormat: event.target.value })}
/>
</Grid.Col>
<Grid.Col span={12}>
<Input
label="Value placeholder"
className="w-100"
data-test="Choropleth.Editor.ValuePlaceholder"
defaultValue={options.noValuePlaceholder}
onChange={event => onOptionsChangeDebounced({ noValuePlaceholder: event.target.value })}
/>
</Grid.Col>
</Grid.Row>
</Section>
<Section>
<Checkbox
data-test="Choropleth.Editor.LegendVisibility"
checked={options.legend.visible}
onChange={event => onOptionsChange({ legend: { visible: event.target.checked } })}
>
Show legend
</Checkbox>
</Section>
<Section>
<Grid.Row gutter={15}>
<Grid.Col span={12}>
<Select
label="Legend position"
className="w-100"
data-test="Choropleth.Editor.LegendPosition"
disabled={!options.legend.visible}
defaultValue={options.legend.position}
onChange={position => onOptionsChange({ legend: { position } })}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
<Input
id="choropleth-editor-value-format"
className="w-100"
data-test="Choropleth.Editor.ValueFormat"
defaultValue={options.valueFormat}
onChange={event => onOptionsChangeDebounced({ valueFormat: event.target.value })}
/>
</Grid.Col>
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-value-placeholder">Value placeholder</label>
<Input
id="choropleth-editor-value-placeholder"
className="w-100"
data-test="Choropleth.Editor.ValuePlaceholder"
defaultValue={options.noValuePlaceholder}
onChange={event => onOptionsChangeDebounced({ noValuePlaceholder: event.target.value })}
/>
</Grid.Col>
</Grid.Row>
<Select.Option value="top-left" data-test="Choropleth.Editor.LegendPosition.TopLeft">top / left</Select.Option>
<Select.Option value="top-right" data-test="Choropleth.Editor.LegendPosition.TopRight">top / right</Select.Option>
<Select.Option value="bottom-left" data-test="Choropleth.Editor.LegendPosition.BottomLeft">bottom / left</Select.Option>
<Select.Option value="bottom-right" data-test="Choropleth.Editor.LegendPosition.BottomRight">bottom / right</Select.Option>
</Select>
</Grid.Col>
<Grid.Col span={12}>
<TextAlignmentSelect
data-test="Choropleth.Editor.LegendTextAlignment"
label="Legend text alignment"
disabled={!options.legend.visible}
defaultValue={options.legend.alignText}
onChange={event => onOptionsChange({ legend: { alignText: event.target.value } })}
/>
</Grid.Col>
</Grid.Row>
</Section>
<div className="m-b-5">
<label htmlFor="choropleth-editor-show-legend">
<Checkbox
id="choropleth-editor-show-legend"
data-test="Choropleth.Editor.LegendVisibility"
checked={options.legend.visible}
onChange={event => onOptionsChange({ legend: { visible: event.target.checked } })}
/>
<span>Show legend</span>
</label>
</div>
<Section>
<Checkbox
data-test="Choropleth.Editor.TooltipEnabled"
checked={options.tooltip.enabled}
onChange={event => onOptionsChange({ tooltip: { enabled: event.target.checked } })}
>
Show tooltip
</Checkbox>
</Section>
<Grid.Row gutter={15} className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-legend-position">Legend position</label>
<Select
id="choropleth-editor-legend-position"
className="w-100"
data-test="Choropleth.Editor.LegendPosition"
disabled={!options.legend.visible}
defaultValue={options.legend.position}
onChange={position => onOptionsChange({ legend: { position } })}
>
<Select.Option value="top-left" data-test="Choropleth.Editor.LegendPosition.TopLeft">top / left</Select.Option>
<Select.Option value="top-right" data-test="Choropleth.Editor.LegendPosition.TopRight">top / right</Select.Option>
<Select.Option value="bottom-left" data-test="Choropleth.Editor.LegendPosition.BottomLeft">bottom / left</Select.Option>
<Select.Option value="bottom-right" data-test="Choropleth.Editor.LegendPosition.BottomRight">bottom / right</Select.Option>
</Select>
</Grid.Col>
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-legend-text-alignment">Legend text alignment</label>
<Radio.Group
id="choropleth-editor-legend-text-alignment"
className="choropleth-visualization-editor-legend-align-text"
data-test="Choropleth.Editor.LegendTextAlignment"
disabled={!options.legend.visible}
defaultValue={options.legend.alignText}
onChange={event => onOptionsChange({ legend: { alignText: event.target.value } })}
>
<Tooltip title="Align left" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="left" data-test="Choropleth.Editor.LegendTextAlignment.Left">
<Icon type="align-left" />
</Radio.Button>
</Tooltip>
<Tooltip title="Align center" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="center" data-test="Choropleth.Editor.LegendTextAlignment.Center">
<Icon type="align-center" />
</Radio.Button>
</Tooltip>
<Tooltip title="Align right" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="right" data-test="Choropleth.Editor.LegendTextAlignment.Right">
<Icon type="align-right" />
</Radio.Button>
</Tooltip>
</Radio.Group>
</Grid.Col>
</Grid.Row>
<div className="m-b-5">
<label htmlFor="choropleth-editor-show-tooltip">
<Checkbox
id="choropleth-editor-show-tooltip"
data-test="Choropleth.Editor.TooltipEnabled"
checked={options.tooltip.enabled}
onChange={event => onOptionsChange({ tooltip: { enabled: event.target.checked } })}
/>
<span>Show tooltip</span>
</label>
</div>
<div className="m-b-15">
<label htmlFor="choropleth-editor-tooltip-template">Tooltip template {templateFormatHint}</label>
<Section>
<Input
id="choropleth-editor-tooltip-template"
label={<React.Fragment>Tooltip template {templateFormatHint}</React.Fragment>}
className="w-100"
data-test="Choropleth.Editor.TooltipTemplate"
disabled={!options.tooltip.enabled}
defaultValue={options.tooltip.template}
onChange={event => onOptionsChangeDebounced({ tooltip: { template: event.target.value } })}
/>
</div>
</Section>
<div className="m-b-5">
<label htmlFor="choropleth-editor-show-popup">
<Checkbox
id="choropleth-editor-show-popup"
data-test="Choropleth.Editor.PopupEnabled"
checked={options.popup.enabled}
onChange={event => onOptionsChange({ popup: { enabled: event.target.checked } })}
/>
<span>Show popup</span>
</label>
</div>
<Section>
<Checkbox
data-test="Choropleth.Editor.PopupEnabled"
checked={options.popup.enabled}
onChange={event => onOptionsChange({ popup: { enabled: event.target.checked } })}
>
Show popup
</Checkbox>
</Section>
<div className="m-b-15">
<label htmlFor="choropleth-editor-popup-template">Popup template {templateFormatHint}</label>
<Input.TextArea
id="choropleth-editor-popup-template"
<Section>
<TextArea
label={<React.Fragment>Popup template {templateFormatHint}</React.Fragment>}
className="w-100"
data-test="Choropleth.Editor.PopupTemplate"
disabled={!options.popup.enabled}
@@ -192,7 +141,7 @@ export default function GeneralSettings({ options, onOptionsChange }) {
defaultValue={options.popup.template}
onChange={event => onOptionsChangeDebounced({ popup: { template: event.target.value } })}
/>
</div>
</Section>
</div>
);
}

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor="choropleth-editor-map-type">Map type</label>
<Section>
<Select
id="choropleth-editor-map-type"
label="Map type"
className="w-100"
data-test="Choropleth.Editor.MapType"
defaultValue={options.mapType}
@@ -50,12 +49,11 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option key="countries" data-test="Choropleth.Editor.MapType.Countries">Countries</Select.Option>
<Select.Option key="subdiv_japan" data-test="Choropleth.Editor.MapType.Japan">Japan/Prefectures</Select.Option>
</Select>
</div>
</Section>
<div className="m-b-15">
<label htmlFor="choropleth-editor-key-column">Key column</label>
<Section>
<Select
id="choropleth-editor-key-column"
label="Key column"
className="w-100"
data-test="Choropleth.Editor.KeyColumn"
defaultValue={options.countryCodeColumn}
@@ -65,12 +63,11 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option key={name} data-test={`Choropleth.Editor.KeyColumn.${name}`}>{name}</Select.Option>
))}
</Select>
</div>
</Section>
<div className="m-b-15">
<label htmlFor="choropleth-editor-key-type">Key type</label>
<Section>
<Select
id="choropleth-editor-key-type"
label="Key type"
className="w-100"
data-test="Choropleth.Editor.KeyType"
value={options.countryCodeType}
@@ -80,12 +77,11 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option key={type} data-test={`Choropleth.Editor.KeyType.${type}`}>{name}</Select.Option>
))}
</Select>
</div>
</Section>
<div className="m-b-15">
<label htmlFor="choropleth-editor-value-column">Value column</label>
<Section>
<Select
id="choropleth-editor-value-column"
label="Value column"
className="w-100"
data-test="Choropleth.Editor.ValueColumn"
defaultValue={options.valueColumn}
@@ -95,7 +91,7 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option key={name} data-test={`Choropleth.Editor.ValueColumn.${name}`}>{name}</Select.Option>
))}
</Select>
</div>
</Section>
</React.Fragment>
);
}

View File

@@ -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 <Input> height
height: 35px;
line-height: 33px;
}
}
}

View File

@@ -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 },
]);

View File

@@ -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 (
<Tabs animated={false} tabBarGutter={0}>
<Tabs.TabPane key="general" tab={<span data-test="Choropleth.EditorTabs.General">General</span>}>
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
<Tabs.TabPane key="colors" tab={<span data-test="Choropleth.EditorTabs.Colors">Colors</span>}>
<ColorsSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
<Tabs.TabPane key="format" tab={<span data-test="Choropleth.EditorTabs.Format">Format</span>}>
<FormatSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
<Tabs.TabPane key="bounds" tab={<span data-test="Choropleth.EditorTabs.Bounds">Bounds</span>}>
<BoundsSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
</Tabs>
);
}
Editor.propTypes = EditorPropTypes;

View File

@@ -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';
}

View File

@@ -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 (
<Tabs animated={false} tabBarGutter={0}>
<Tabs.TabPane key="columns" tab={<span data-test="Cohort.EditorTabs.Columns">Columns</span>}>
<div className="m-b-15">
<label htmlFor="cohort-date-column">Date (Bucket)</label>
<Select
data-test="Cohort.DateColumn"
id="cohort-date-column"
className="w-100"
value={options.dateColumn}
onChange={dateColumn => optionsChanged({ dateColumn })}
>
{map(data.columns, ({ name }) => (
<Select.Option key={name} data-test={'Cohort.DateColumn.' + name}>{name}</Select.Option>
))}
</Select>
</div>
<div className="m-b-15">
<label htmlFor="cohort-stage-column">Stage</label>
<Select
data-test="Cohort.StageColumn"
id="cohort-stage-column"
className="w-100"
value={options.stageColumn}
onChange={stageColumn => optionsChanged({ stageColumn })}
>
{map(data.columns, ({ name }) => (
<Select.Option key={name} data-test={'Cohort.StageColumn.' + name}>{name}</Select.Option>
))}
</Select>
</div>
<div className="m-b-15">
<label htmlFor="cohort-total-column">Bucket Population Size</label>
<Select
data-test="Cohort.TotalColumn"
id="cohort-total-column"
className="w-100"
value={options.totalColumn}
onChange={totalColumn => optionsChanged({ totalColumn })}
>
{map(data.columns, ({ name }) => (
<Select.Option key={name} data-test={'Cohort.TotalColumn.' + name}>{name}</Select.Option>
))}
</Select>
</div>
<div className="m-b-15">
<label htmlFor="cohort-value-column">Stage Value</label>
<Select
data-test="Cohort.ValueColumn"
id="cohort-value-column"
className="w-100"
value={options.valueColumn}
onChange={valueColumn => optionsChanged({ valueColumn })}
>
{map(data.columns, ({ name }) => (
<Select.Option key={name} data-test={'Cohort.ValueColumn.' + name}>{name}</Select.Option>
))}
</Select>
</div>
</Tabs.TabPane>
<Tabs.TabPane key="options" tab={<span data-test="Cohort.EditorTabs.Options">Options</span>}>
<div className="m-b-15">
<label htmlFor="cohort-time-interval">Time Interval</label>
<Select
data-test="Cohort.TimeInterval"
id="cohort-time-interval"
className="w-100"
value={options.timeInterval}
onChange={timeInterval => optionsChanged({ timeInterval })}
>
{map(CohortTimeIntervals, (name, value) => (
<Select.Option key={value} data-test={'Cohort.TimeInterval.' + value}>{name}</Select.Option>
))}
</Select>
</div>
<div className="m-b-15">
<label htmlFor="cohort-time-mode">Mode</label>
<Select
data-test="Cohort.Mode"
id="cohort-mode"
className="w-100"
value={options.mode}
onChange={mode => optionsChanged({ mode })}
>
{map(CohortModes, (name, value) => (
<Select.Option key={value} data-test={'Cohort.Mode.' + value}>{name}</Select.Option>
))}
</Select>
</div>
</Tabs.TabPane>
</Tabs>
);
}
Editor.propTypes = EditorPropTypes;

View File

@@ -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 (
<React.Fragment>
<Section>
<Select
label="Date (Bucket)"
data-test="Cohort.DateColumn"
className="w-100"
value={options.dateColumn}
onChange={dateColumn => onOptionsChange({ dateColumn })}
>
{map(data.columns, ({ name }) => (
<Select.Option key={name} data-test={'Cohort.DateColumn.' + name}>{name}</Select.Option>
))}
</Select>
</Section>
<Section>
<Select
label="Stage"
data-test="Cohort.StageColumn"
className="w-100"
value={options.stageColumn}
onChange={stageColumn => onOptionsChange({ stageColumn })}
>
{map(data.columns, ({ name }) => (
<Select.Option key={name} data-test={'Cohort.StageColumn.' + name}>{name}</Select.Option>
))}
</Select>
</Section>
<Section>
<Select
label="Bucket Population Size"
data-test="Cohort.TotalColumn"
className="w-100"
value={options.totalColumn}
onChange={totalColumn => onOptionsChange({ totalColumn })}
>
{map(data.columns, ({ name }) => (
<Select.Option key={name} data-test={'Cohort.TotalColumn.' + name}>{name}</Select.Option>
))}
</Select>
</Section>
<Section>
<Select
label="Stage Value"
data-test="Cohort.ValueColumn"
className="w-100"
value={options.valueColumn}
onChange={valueColumn => onOptionsChange({ valueColumn })}
>
{map(data.columns, ({ name }) => (
<Select.Option key={name} data-test={'Cohort.ValueColumn.' + name}>{name}</Select.Option>
))}
</Select>
</Section>
</React.Fragment>
);
}
ColumnsSettings.propTypes = EditorPropTypes;

View File

@@ -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 (
<React.Fragment>
<Section>
<Select
label="Time Interval"
data-test="Cohort.TimeInterval"
className="w-100"
value={options.timeInterval}
onChange={timeInterval => onOptionsChange({ timeInterval })}
>
{map(CohortTimeIntervals, (name, value) => (
<Select.Option key={value} data-test={'Cohort.TimeInterval.' + value}>{name}</Select.Option>
))}
</Select>
</Section>
<Section>
<Select
label="Mode"
data-test="Cohort.Mode"
className="w-100"
value={options.mode}
onChange={mode => onOptionsChange({ mode })}
>
{map(CohortModes, (name, value) => (
<Select.Option key={value} data-test={'Cohort.Mode.' + value}>{name}</Select.Option>
))}
</Select>
</Section>
</React.Fragment>
);
}
OptionsSettings.propTypes = EditorPropTypes;

View File

@@ -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 },
]);

View File

@@ -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;
}
}

View File

@@ -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 (
<React.Fragment>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-formatting-decimal-place">Formatting Decimal Place</label>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
id="counter-formatting-decimal-place"
className="w-100"
data-test="Counter.Formatting.DecimalPlace"
defaultValue={options.stringDecimal}
disabled={!inputsEnabled}
onChange={stringDecimal => onOptionsChange({ stringDecimal })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<InputNumber
layout="horizontal"
label="Formatting Decimal Place"
className="w-100"
data-test="Counter.Formatting.DecimalPlace"
defaultValue={options.stringDecimal}
disabled={!inputsEnabled}
onChange={stringDecimal => onOptionsChange({ stringDecimal })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-formatting-decimal-character">Formatting Decimal Character</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="counter-formatting-decimal-character"
className="w-100"
data-test="Counter.Formatting.DecimalCharacter"
defaultValue={options.stringDecChar}
disabled={!inputsEnabled}
onChange={e => onOptionsChange({ stringDecChar: e.target.value })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label="Formatting Decimal Character"
className="w-100"
data-test="Counter.Formatting.DecimalCharacter"
defaultValue={options.stringDecChar}
disabled={!inputsEnabled}
onChange={e => onOptionsChange({ stringDecChar: e.target.value })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-formatting-thousands-separator">Formatting Thousands Separator</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="counter-formatting-thousands-separator"
className="w-100"
data-test="Counter.Formatting.ThousandsSeparator"
defaultValue={options.stringThouSep}
disabled={!inputsEnabled}
onChange={e => onOptionsChange({ stringThouSep: e.target.value })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label="Formatting Thousands Separator"
className="w-100"
data-test="Counter.Formatting.ThousandsSeparator"
defaultValue={options.stringThouSep}
disabled={!inputsEnabled}
onChange={e => onOptionsChange({ stringThouSep: e.target.value })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-formatting-string-prefix">Formatting String Prefix</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="counter-formatting-string-prefix"
className="w-100"
data-test="Counter.Formatting.StringPrefix"
defaultValue={options.stringPrefix}
disabled={!inputsEnabled}
onChange={e => onOptionsChange({ stringPrefix: e.target.value })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label="Formatting String Prefix"
className="w-100"
data-test="Counter.Formatting.StringPrefix"
defaultValue={options.stringPrefix}
disabled={!inputsEnabled}
onChange={e => onOptionsChange({ stringPrefix: e.target.value })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-formatting-string-suffix">Formatting String Suffix</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="counter-formatting-string-suffix"
className="w-100"
data-test="Counter.Formatting.StringSuffix"
defaultValue={options.stringSuffix}
disabled={!inputsEnabled}
onChange={e => onOptionsChange({ stringSuffix: e.target.value })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label="Formatting String Suffix"
className="w-100"
data-test="Counter.Formatting.StringSuffix"
defaultValue={options.stringSuffix}
disabled={!inputsEnabled}
onChange={e => onOptionsChange({ stringSuffix: e.target.value })}
/>
</Section>
<label className="d-flex align-items-center" htmlFor="counter-format-target-value">
<Section>
<Switch
id="counter-format-target-value"
data-test="Counter.Formatting.FormatTargetValue"
defaultChecked={options.formatTargetValue}
onChange={formatTargetValue => onOptionsChange({ formatTargetValue })}
/>
<span className="m-l-10">Format Target Value</span>
</label>
>
Format Target Value
</Switch>
</Section>
</React.Fragment>
);
}

View File

@@ -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 (
<React.Fragment>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-label">Counter Label</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="counter-label"
className="w-100"
data-test="Counter.General.Label"
defaultValue={options.counterLabel}
placeholder={visualizationName}
onChange={e => onOptionsChange({ counterLabel: e.target.value })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label="Counter Label"
className="w-100"
data-test="Counter.General.Label"
defaultValue={options.counterLabel}
placeholder={visualizationName}
onChange={e => onOptionsChange({ counterLabel: e.target.value })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-value-column">Counter Value Column Name</label>
</Grid.Col>
<Grid.Col span={12}>
<Select
id="counter-value-column"
className="w-100"
data-test="Counter.General.ValueColumn"
defaultValue={options.counterColName}
disabled={options.countRow}
onChange={counterColName => onOptionsChange({ counterColName })}
>
{map(data.columns, col => (
<Select.Option key={col.name} data-test={'Counter.General.ValueColumn.' + col.name}>{col.name}</Select.Option>
))}
</Select>
</Grid.Col>
</Grid.Row>
<Section>
<Select
layout="horizontal"
label="Counter Value Column Name"
className="w-100"
data-test="Counter.General.ValueColumn"
defaultValue={options.counterColName}
disabled={options.countRow}
onChange={counterColName => onOptionsChange({ counterColName })}
>
{map(data.columns, col => (
<Select.Option key={col.name} data-test={'Counter.General.ValueColumn.' + col.name}>{col.name}</Select.Option>
))}
</Select>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-value-row-number">Counter Value Row Number</label>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
id="counter-value-row-number"
className="w-100"
data-test="Counter.General.ValueRowNumber"
defaultValue={options.rowNumber}
disabled={options.countRow}
onChange={rowNumber => onOptionsChange({ rowNumber })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<InputNumber
layout="horizontal"
label="Counter Value Row Number"
className="w-100"
data-test="Counter.General.ValueRowNumber"
defaultValue={options.rowNumber}
disabled={options.countRow}
onChange={rowNumber => onOptionsChange({ rowNumber })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-target-value-column">Target Value Column Name</label>
</Grid.Col>
<Grid.Col span={12}>
<Select
id="counter-target-value-column"
className="w-100"
data-test="Counter.General.TargetValueColumn"
defaultValue={options.targetColName}
onChange={targetColName => onOptionsChange({ targetColName })}
>
<Select.Option value="">No target value</Select.Option>
{map(data.columns, col => (
<Select.Option key={col.name} data-test={'Counter.General.TargetValueColumn.' + col.name}>{col.name}</Select.Option>
))}
</Select>
</Grid.Col>
</Grid.Row>
<Section>
<Select
layout="horizontal"
label="Target Value Column Name"
className="w-100"
data-test="Counter.General.TargetValueColumn"
defaultValue={options.targetColName}
onChange={targetColName => onOptionsChange({ targetColName })}
>
<Select.Option value="">No target value</Select.Option>
{map(data.columns, col => (
<Select.Option key={col.name} data-test={'Counter.General.TargetValueColumn.' + col.name}>{col.name}</Select.Option>
))}
</Select>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="counter-target-row-number">Target Value Row Number</label>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
id="counter-target-row-number"
className="w-100"
data-test="Counter.General.TargetValueRowNumber"
defaultValue={options.targetRowNumber}
onChange={targetRowNumber => onOptionsChange({ targetRowNumber })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<InputNumber
layout="horizontal"
label="Target Value Row Number"
className="w-100"
data-test="Counter.General.TargetValueRowNumber"
defaultValue={options.targetRowNumber}
onChange={targetRowNumber => onOptionsChange({ targetRowNumber })}
/>
</Section>
<label className="d-flex align-items-center" htmlFor="counter-count-rows">
<Section>
<Switch
id="counter-count-rows"
data-test="Counter.General.CountRows"
defaultChecked={options.countRow}
onChange={countRow => onOptionsChange({ countRow })}
/>
<span className="m-l-10">Count Rows</span>
</label>
>
Count Rows
</Switch>
</Section>
</React.Fragment>
);
}

View File

@@ -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 },
]);

View File

@@ -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 (
<Tabs animated={false} tabBarGutter={0}>
<Tabs.TabPane key="general" tab={<span data-test="Counter.EditorTabs.General">General</span>}>
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
<Tabs.TabPane key="format" tab={<span data-test="Counter.EditorTabs.Formatting">Format</span>}>
<FormatSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
</Tabs>
);
}
Editor.propTypes = EditorPropTypes;

View File

@@ -1,3 +0,0 @@
export default function DetailsEditor() {
return null;
}

View File

@@ -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,
});

View File

@@ -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 (
<React.Fragment>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-number-format">
Number Values Format
<Popover
content={(
<React.Fragment>
Format&nbsp;
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="funnel-editor-step-column-title"
className="w-100"
data-test="Funnel.NumberFormat"
defaultValue={options.numberFormat}
onChange={event => onOptionsChangeDebounced({ numberFormat: event.target.value })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label={(<React.Fragment>Number Values Format<ContextHelp.NumberFormatSpecs /></React.Fragment>)}
className="w-100"
data-test="Funnel.NumberFormat"
defaultValue={options.numberFormat}
onChange={event => onOptionsChangeDebounced({ numberFormat: event.target.value })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-number-format">
Percent Values Format
<Popover
content={(
<React.Fragment>
Format&nbsp;
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="funnel-editor-step-column-title"
className="w-100"
data-test="Funnel.PercentFormat"
defaultValue={options.percentFormat}
onChange={event => onOptionsChangeDebounced({ percentFormat: event.target.value })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label={(<React.Fragment>Percent Values Format<ContextHelp.NumberFormatSpecs /></React.Fragment>)}
className="w-100"
data-test="Funnel.PercentFormat"
defaultValue={options.percentFormat}
onChange={event => onOptionsChangeDebounced({ percentFormat: event.target.value })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-items-limit">Items Count Limit</label>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
id="funnel-editor-items-limit"
className="w-100"
data-test="Funnel.ItemsLimit"
min={2}
defaultValue={options.itemsLimit}
onChange={itemsLimit => onOptionsChangeDebounced({ itemsLimit })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<InputNumber
layout="horizontal"
label="Items Count Limit"
className="w-100"
data-test="Funnel.ItemsLimit"
min={2}
defaultValue={options.itemsLimit}
onChange={itemsLimit => onOptionsChangeDebounced({ itemsLimit })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-percent-values-range-min">Min Percent Value</label>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
id="funnel-editor-percent-values-range-min"
className="w-100"
data-test="Funnel.PercentRangeMin"
min={0}
defaultValue={options.percentValuesRange.min}
onChange={min => onOptionsChangeDebounced({ percentValuesRange: { min } })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<InputNumber
layout="horizontal"
label="Min Percent Value"
className="w-100"
data-test="Funnel.PercentRangeMin"
min={0}
defaultValue={options.percentValuesRange.min}
onChange={min => onOptionsChangeDebounced({ percentValuesRange: { min } })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-percent-values-range-max">Max Percent Value</label>
</Grid.Col>
<Grid.Col span={12}>
<InputNumber
id="funnel-editor-percent-values-range-max"
className="w-100"
data-test="Funnel.PercentRangeMax"
min={0}
defaultValue={options.percentValuesRange.max}
onChange={max => onOptionsChangeDebounced({ percentValuesRange: { max } })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<InputNumber
layout="horizontal"
label="Max Percent Value"
className="w-100"
data-test="Funnel.PercentRangeMax"
min={0}
defaultValue={options.percentValuesRange.max}
onChange={max => onOptionsChangeDebounced({ percentValuesRange: { max } })}
/>
</Section>
</React.Fragment>
);
}

View File

@@ -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 (
<React.Fragment>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-step-column-name">Step Column</label>
</Grid.Col>
<Grid.Col span={12}>
<Select
id="funnel-editor-step-column-name"
className="w-100"
data-test="Funnel.StepColumn"
placeholder="Choose column..."
defaultValue={options.stepCol.colName || undefined}
onChange={colName => onOptionsChange({ stepCol: { colName: colName || null } })}
>
{map(columnNames, col => (
<Select.Option key={col} data-test={`Funnel.StepColumn.${col}`}>{col}</Select.Option>
))}
</Select>
</Grid.Col>
</Grid.Row>
<Section>
<Select
layout="horizontal"
label="Step Column"
className="w-100"
data-test="Funnel.StepColumn"
placeholder="Choose column..."
defaultValue={options.stepCol.colName || undefined}
onChange={colName => onOptionsChange({ stepCol: { colName: colName || null } })}
>
{map(columnNames, col => (
<Select.Option key={col} data-test={`Funnel.StepColumn.${col}`}>{col}</Select.Option>
))}
</Select>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-step-column-title">Step Column Title</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="funnel-editor-step-column-title"
className="w-100"
data-test="Funnel.StepColumnTitle"
defaultValue={options.stepCol.displayAs}
onChange={event => onOptionsChangeDebounced({ stepCol: { displayAs: event.target.value } })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label="Step Column Title"
className="w-100"
data-test="Funnel.StepColumnTitle"
defaultValue={options.stepCol.displayAs}
onChange={event => onOptionsChangeDebounced({ stepCol: { displayAs: event.target.value } })}
/>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-value-column-name">Value Column</label>
</Grid.Col>
<Grid.Col span={12}>
<Select
id="funnel-editor-value-column-name"
className="w-100"
data-test="Funnel.ValueColumn"
placeholder="Choose column..."
defaultValue={options.valueCol.colName || undefined}
onChange={colName => onOptionsChange({ valueCol: { colName: colName || null } })}
>
{map(columnNames, col => (
<Select.Option key={col} data-test={`Funnel.ValueColumn.${col}`}>{col}</Select.Option>
))}
</Select>
</Grid.Col>
</Grid.Row>
<Section>
<Select
layout="horizontal"
label="Value Column"
className="w-100"
data-test="Funnel.ValueColumn"
placeholder="Choose column..."
defaultValue={options.valueCol.colName || undefined}
onChange={colName => onOptionsChange({ valueCol: { colName: colName || null } })}
>
{map(columnNames, col => (
<Select.Option key={col} data-test={`Funnel.ValueColumn.${col}`}>{col}</Select.Option>
))}
</Select>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-value-column-title">Value Column Title</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="funnel-editor-value-column-title"
className="w-100"
data-test="Funnel.ValueColumnTitle"
defaultValue={options.valueCol.displayAs}
onChange={event => onOptionsChangeDebounced({ valueCol: { displayAs: event.target.value } })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label="Value Column Title"
className="w-100"
data-test="Funnel.ValueColumnTitle"
defaultValue={options.valueCol.displayAs}
onChange={event => onOptionsChangeDebounced({ valueCol: { displayAs: event.target.value } })}
/>
</Section>
<div className="m-b-15">
<label htmlFor="funnel-editor-custom-sort">
<Checkbox
id="funnel-editor-custom-sort"
data-test="Funnel.CustomSort"
checked={!options.autoSort}
onChange={event => onOptionsChange({ autoSort: !event.target.checked })}
/>
<span>Custom Sorting</span>
</label>
</div>
<Section>
<Checkbox
data-test="Funnel.CustomSort"
checked={!options.autoSort}
onChange={event => onOptionsChange({ autoSort: !event.target.checked })}
>
Custom Sorting
</Checkbox>
</Section>
{!options.autoSort && (
<React.Fragment>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-sort-column-name">Sort Column</label>
</Grid.Col>
<Grid.Col span={12}>
<Select
id="funnel-editor-sort-column-name"
className="w-100"
data-test="Funnel.SortColumn"
allowClear
placeholder="Choose column..."
defaultValue={options.sortKeyCol.colName || undefined}
onChange={colName => onOptionsChange({ sortKeyCol: { colName: colName || null } })}
>
{map(columnNames, col => (
<Select.Option key={col} data-test={`Funnel.SortColumn.${col}`}>{col}</Select.Option>
))}
</Select>
</Grid.Col>
</Grid.Row>
<Section>
<Select
layout="horizontal"
label="Sort Column"
className="w-100"
data-test="Funnel.SortColumn"
allowClear
placeholder="Choose column..."
defaultValue={options.sortKeyCol.colName || undefined}
onChange={colName => onOptionsChange({ sortKeyCol: { colName: colName || null } })}
>
{map(columnNames, col => (
<Select.Option key={col} data-test={`Funnel.SortColumn.${col}`}>{col}</Select.Option>
))}
</Select>
</Section>
<Grid.Row type="flex" align="middle" className="m-b-15">
<Grid.Col span={12}>
<label htmlFor="funnel-editor-sort-reverse">Sort Order</label>
</Grid.Col>
<Grid.Col span={12}>
<Select
id="funnel-editor-sort-reverse"
className="w-100"
data-test="Funnel.SortDirection"
disabled={!options.sortKeyCol.colName}
defaultValue={options.sortKeyCol.reverse ? 'desc' : 'asc'}
onChange={order => onOptionsChange({ sortKeyCol: { reverse: order === 'desc' } })}
>
<Select.Option value="asc" data-test="Funnel.SortDirection.Ascending">ascending</Select.Option>
<Select.Option value="desc" data-test="Funnel.SortDirection.Descending">descending</Select.Option>
</Select>
</Grid.Col>
</Grid.Row>
<Section>
<Select
layout="horizontal"
label="Sort Order"
className="w-100"
data-test="Funnel.SortDirection"
disabled={!options.sortKeyCol.colName}
defaultValue={options.sortKeyCol.reverse ? 'desc' : 'asc'}
onChange={order => onOptionsChange({ sortKeyCol: { reverse: order === 'desc' } })}
>
<Select.Option value="asc" data-test="Funnel.SortDirection.Ascending">ascending</Select.Option>
<Select.Option value="desc" data-test="Funnel.SortDirection.Descending">descending</Select.Option>
</Select>
</Section>
</React.Fragment>
)}
</React.Fragment>

View File

@@ -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 },
]);

View File

@@ -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 (
<Tabs animated={false} tabBarGutter={0}>
<Tabs.TabPane key="general" tab={<span data-test="Funnel.EditorTabs.General">General</span>}>
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
<Tabs.TabPane key="appearance" tab={<span data-test="Funnel.EditorTabs.Appearance">Appearance</span>}>
<AppearanceSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
</Tabs>
);
}
Editor.propTypes = EditorPropTypes;

View File

@@ -70,6 +70,7 @@ function validateVisualizationConfig(config) {
export function registerVisualization(config) {
validateVisualizationConfig(config);
config = {
Editor: () => null,
...config,
isDefault: config.isDefault && !config.isDeprecated,
};

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor="map-editor-latitude-column-name">Latitude Column Name</label>
<Section>
<Select
label="Latitude Column Name"
data-test="Map.Editor.LatitudeColumnName"
id="map-editor-latitude-column-name"
className="w-100"
value={options.latColName}
onChange={latColName => onOptionsChange({ latColName })}
@@ -31,13 +30,12 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option key={col} data-test={'Map.Editor.LatitudeColumnName.' + col}>{col}</Select.Option>
))}
</Select>
</div>
</Section>
<div className="m-b-15">
<label htmlFor="map-editor-longitude-column-name">Longitude Column Name</label>
<Section>
<Select
label="Longitude Column Name"
data-test="Map.Editor.LongitudeColumnName"
id="map-editor-longitude-column-name"
className="w-100"
value={options.lonColName}
onChange={lonColName => onOptionsChange({ lonColName })}
@@ -46,13 +44,12 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option key={col} data-test={'Map.Editor.LongitudeColumnName.' + col}>{col}</Select.Option>
))}
</Select>
</div>
</Section>
<div className="m-b-15">
<label className="control-label" htmlFor="map-editor-group-by">Group By</label>
<Section>
<Select
label="Group By"
data-test="Map.Editor.GroupBy"
id="map-editor-group-by"
className="w-100"
allowClear
placeholder="none"
@@ -63,7 +60,7 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
<Select.Option key={col} data-test={'Map.Editor.GroupBy.' + col}>{col}</Select.Option>
))}
</Select>
</div>
</Section>
</React.Fragment>
);
}

View File

@@ -37,18 +37,17 @@ export default function GroupsSettings({ options, data, onOptionsChange }) {
title: 'Color',
dataIndex: 'color',
width: '1%',
className: 'text-nowrap',
render: (unused, item) => (
<div className="text-nowrap">
<ColorPicker
interactive
presetColors={colors}
placement="topRight"
color={item.color}
triggerProps={{ 'data-test': `Map.Editor.Groups.${item.name}.Color` }}
onChange={value => updateGroupOption(item.name, 'color', value)}
/>
<ColorPicker.Label color={item.color} presetColors={colors} />
</div>
<ColorPicker
interactive
presetColors={colors}
placement="topRight"
color={item.color}
triggerProps={{ 'data-test': `Map.Editor.Groups.${item.name}.Color` }}
onChange={value => updateGroupOption(item.name, 'color', value)}
addonAfter={<ColorPicker.Label color={item.color} presetColors={colors} />}
/>
),
},
];

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor="map-editor-tiles">Map Tiles</label>
<Section>
<Select
label="Map Tiles"
data-test="Map.Editor.Tiles"
id="map-editor-tiles"
className="w-100"
value={options.mapTileUrl}
onChange={mapTileUrl => onOptionsChange({ mapTileUrl })}
@@ -105,168 +97,130 @@ export default function StyleSettings({ options, onOptionsChange }) {
<Select.Option key={url} data-test={'Map.Editor.Tiles.' + name}>{name}</Select.Option>
))}
</Select>
</div>
</Section>
<h4 className="m-t-15 m-b-15">Markers</h4>
<Section.Title>Markers</Section.Title>
<div className="m-b-15">
<label htmlFor="map-editor-cluster-markers">
<Checkbox
id="map-editor-cluster-markers"
data-test="Map.Editor.ClusterMarkers"
defaultChecked={options.clusterMarkers}
onChange={event => onOptionsChange({ clusterMarkers: event.target.checked })}
/>
<span>Cluster Markers</span>
</label>
</div>
<Section>
<Checkbox
data-test="Map.Editor.ClusterMarkers"
defaultChecked={options.clusterMarkers}
onChange={event => onOptionsChange({ clusterMarkers: event.target.checked })}
>
Cluster Markers
</Checkbox>
</Section>
<div className="m-b-15">
<label htmlFor="map-editor-customize-markers">
<Checkbox
id="map-editor-customize-markers"
data-test="Map.Editor.CustomizeMarkers"
disabled={!isCustomMarkersStyleAllowed}
defaultChecked={options.customizeMarkers}
onChange={event => onOptionsChange({ customizeMarkers: event.target.checked })}
/>
<Typography.Text disabled={!isCustomMarkersStyleAllowed}>Override default style</Typography.Text>
{!isCustomMarkersStyleAllowed && (
<Popover
placement="topLeft"
arrowPointAtCenter
content={(
<span>
Custom marker styles are not available<br />
when <b>Group By</b> column selected.
</span>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
)}
</label>
</div>
<Section>
<Checkbox
data-test="Map.Editor.CustomizeMarkers"
disabled={!isCustomMarkersStyleAllowed}
defaultChecked={options.customizeMarkers}
onChange={event => onOptionsChange({ customizeMarkers: event.target.checked })}
>
Override default style
</Checkbox>
{!isCustomMarkersStyleAllowed && (
<ContextHelp placement="topLeft" arrowPointAtCenter>
Custom marker styles are not available<br />
when <b>Group By</b> column selected.
</ContextHelp>
)}
</Section>
{isCustomMarkersStyleAllowed && options.customizeMarkers && (
<React.Fragment>
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="map-editor-marker-shape">Shape</label>
</Grid.Col>
<Grid.Col span={12}>
<Select
id="map-editor-marker-shape"
className="w-100"
data-test="Map.Editor.MarkerShape"
value={options.iconShape}
onChange={iconShape => onOptionsChange({ iconShape })}
>
<Select.Option key="marker" data-test="Map.Editor.MarkerShape.marker">Marker + Icon</Select.Option>
<Select.Option key="doughnut" data-test="Map.Editor.MarkerShape.doughnut">Circle</Select.Option>
<Select.Option key="circle-dot" data-test="Map.Editor.MarkerShape.circle-dot">Circle Dot</Select.Option>
<Select.Option key="circle" data-test="Map.Editor.MarkerShape.circle">Circle + Icon</Select.Option>
<Select.Option key="rectangle-dot" data-test="Map.Editor.MarkerShape.rectangle-dot">Square Dot</Select.Option>
<Select.Option key="rectangle" data-test="Map.Editor.MarkerShape.rectangle">Square + Icon</Select.Option>
</Select>
</Grid.Col>
</Grid.Row>
<Section>
<Select
layout="horizontal"
label="Shape"
className="w-100"
data-test="Map.Editor.MarkerShape"
value={options.iconShape}
onChange={iconShape => onOptionsChange({ iconShape })}
>
<Select.Option key="marker" data-test="Map.Editor.MarkerShape.marker">Marker + Icon</Select.Option>
<Select.Option key="doughnut" data-test="Map.Editor.MarkerShape.doughnut">Circle</Select.Option>
<Select.Option key="circle-dot" data-test="Map.Editor.MarkerShape.circle-dot">Circle Dot</Select.Option>
<Select.Option key="circle" data-test="Map.Editor.MarkerShape.circle">Circle + Icon</Select.Option>
<Select.Option key="rectangle-dot" data-test="Map.Editor.MarkerShape.rectangle-dot">Square Dot</Select.Option>
<Select.Option key="rectangle" data-test="Map.Editor.MarkerShape.rectangle">Square + Icon</Select.Option>
</Select>
</Section>
{showIcon && (
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="map-editor-marker-icon">
Icon
<Popover
placement="topLeft"
arrowPointAtCenter
content={(
<React.Fragment>
<div className="m-b-5">
Enter an icon name from{' '}
<a href="https://fontawesome.com/v4.7.0/icons/" target="_blank" rel="noopener noreferrer">Font-Awesome 4.7</a>
</div>
<div className="m-b-5">
Examples: <code>check</code>, <code>times-circle</code>, <code>flag</code>
</div>
<div>Leave blank to remove.</div>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
</Grid.Col>
<Grid.Col span={12}>
<Input
id="map-editor-marker-icon"
className="w-100"
data-test="Map.Editor.MarkerIcon"
defaultValue={options.iconFont}
onChange={event => debouncedOnOptionsChange({ iconFont: event.target.value })}
/>
</Grid.Col>
</Grid.Row>
<Section>
<Input
layout="horizontal"
label={(
<React.Fragment>
Icon
<ContextHelp placement="topLeft" arrowPointAtCenter>
<div className="m-b-5">
Enter an icon name from{' '}
<a href="https://fontawesome.com/v4.7.0/icons/" target="_blank" rel="noopener noreferrer">Font-Awesome 4.7</a>
</div>
<div className="m-b-5">
Examples: <code>check</code>, <code>times-circle</code>, <code>flag</code>
</div>
<div>Leave blank to remove.</div>
</ContextHelp>
</React.Fragment>
)}
className="w-100"
data-test="Map.Editor.MarkerIcon"
defaultValue={options.iconFont}
onChange={event => debouncedOnOptionsChange({ iconFont: event.target.value })}
/>
</Section>
)}
{showIcon && (
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="map-editor-marker-icon-color">Icon Color</label>
</Grid.Col>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="map-editor-marker-icon-color"
interactive
presetColors={CustomColorPalette}
placement="topRight"
color={options.foregroundColor}
triggerProps={{ 'data-test': 'Map.Editor.MarkerIconColor' }}
onChange={foregroundColor => onOptionsChange({ foregroundColor })}
/>
<ColorPicker.Label color={options.foregroundColor} presetColors={CustomColorPalette} />
</Grid.Col>
</Grid.Row>
<Section>
<ColorPicker
layout="horizontal"
label="Icon Color"
interactive
presetColors={CustomColorPalette}
placement="topRight"
color={options.foregroundColor}
triggerProps={{ 'data-test': 'Map.Editor.MarkerIconColor' }}
onChange={foregroundColor => onOptionsChange({ foregroundColor })}
addonAfter={<ColorPicker.Label color={options.foregroundColor} presetColors={CustomColorPalette} />}
/>
</Section>
)}
{showBackgroundColor && (
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="map-editor-marker-background-color">Background Color</label>
</Grid.Col>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="map-editor-marker-background-color"
interactive
presetColors={CustomColorPalette}
placement="topRight"
color={options.backgroundColor}
triggerProps={{ 'data-test': 'Map.Editor.MarkerBackgroundColor' }}
onChange={backgroundColor => onOptionsChange({ backgroundColor })}
/>
<ColorPicker.Label color={options.backgroundColor} presetColors={CustomColorPalette} />
</Grid.Col>
</Grid.Row>
<Section>
<ColorPicker
layout="horizontal"
label="Background Color"
interactive
presetColors={CustomColorPalette}
placement="topRight"
color={options.backgroundColor}
triggerProps={{ 'data-test': 'Map.Editor.MarkerBackgroundColor' }}
onChange={backgroundColor => onOptionsChange({ backgroundColor })}
addonAfter={<ColorPicker.Label color={options.backgroundColor} presetColors={CustomColorPalette} />}
/>
</Section>
)}
{showBorderColor && (
<Grid.Row type="flex" align="middle" className="m-b-10">
<Grid.Col span={12}>
<label htmlFor="map-editor-marker-border-color">Border Color</label>
</Grid.Col>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="map-editor-marker-border-color"
interactive
presetColors={CustomColorPalette}
placement="topRight"
color={options.borderColor}
triggerProps={{ 'data-test': 'Map.Editor.MarkerBorderColor' }}
onChange={borderColor => onOptionsChange({ borderColor })}
/>
<ColorPicker.Label color={options.borderColor} presetColors={CustomColorPalette} />
</Grid.Col>
</Grid.Row>
<Section>
<ColorPicker
layout="horizontal"
label="Border Color"
interactive
presetColors={CustomColorPalette}
placement="topRight"
color={options.borderColor}
triggerProps={{ 'data-test': 'Map.Editor.MarkerBorderColor' }}
onChange={borderColor => onOptionsChange({ borderColor })}
addonAfter={<ColorPicker.Label color={options.borderColor} presetColors={CustomColorPalette} />}
/>
</Section>
)}
</React.Fragment>
)}

View File

@@ -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 },
]);

View File

@@ -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 (
<Tabs animated={false} tabBarGutter={0}>
<Tabs.TabPane key="general" tab={<span data-test="Map.EditorTabs.General">General</span>}>
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
<Tabs.TabPane key="groups" tab={<span data-test="Map.EditorTabs.Groups">Groups</span>}>
<GroupsSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
<Tabs.TabPane key="style" tab={<span data-test="Map.EditorTabs.Style">Style</span>}>
<StyleSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
</Tabs>
);
}
Editor.propTypes = EditorPropTypes;

View File

@@ -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();

View File

@@ -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 (
<div className="form-group m-t-30">
<label className="d-flex align-items-center" htmlFor="pivot-show-controls">
<React.Fragment>
<Section>
<Switch
data-test="PivotEditor.HideControls"
id="pivot-show-controls"
checked={!options.controls.enabled}
defaultChecked={!options.controls.enabled}
onChange={enabled => updateOptions({ controls: { enabled: !enabled } })}
/>
<span className="m-l-10">Show Pivot Controls</span>
</label>
<label className="d-flex align-items-center" htmlFor="pivot-show-row-totals">
>
Show Pivot Controls
</Switch>
</Section>
<Section>
<Switch
id="pivot-show-row-totals"
checked={options.rendererOptions.table.rowTotals}
defaultChecked={options.rendererOptions.table.rowTotals}
onChange={rowTotals => updateOptions({ rendererOptions: { table: { rowTotals } } })}
/>
<span className="m-l-10">Show Row Totals</span>
</label>
<label className="d-flex align-items-center" htmlFor="pivot-show-col-totals">
>
Show Row Totals
</Switch>
</Section>
<Section>
<Switch
id="pivot-show-row-totals"
checked={options.rendererOptions.table.colTotals}
id="pivot-show-column-totals"
defaultChecked={options.rendererOptions.table.colTotals}
onChange={colTotals => updateOptions({ rendererOptions: { table: { colTotals } } })}
/>
<span className="m-l-10">Show Column Totals</span>
</label>
</div>
>
Show Column Totals
</Switch>
</Section>
</React.Fragment>
);
}

View File

@@ -2,19 +2,16 @@ import React from 'react';
export default function Editor() {
return (
<div className="form-horizontal">
<div>
This visualization expects the query result to have rows in the following format:
<ul>
<li><strong>stage1</strong> - stage 1 value</li>
<li><strong>stage2</strong> - stage 2 value (or null)</li>
<li><strong>stage3</strong> - stage 3 value (or null)</li>
<li><strong>stage4</strong> - stage 4 value (or null)</li>
<li><strong>stage5</strong> - stage 5 value (or null)</li>
<li><strong>value</strong> - number of times this sequence occurred</li>
</ul>
</div>
</div>
<React.Fragment>
<p>This visualization expects the query result to have rows in the following format:</p>
<ul>
<li><strong>stage1</strong> - stage 1 value</li>
<li><strong>stage2</strong> - stage 2 value (or null)</li>
<li><strong>stage3</strong> - stage 3 value (or null)</li>
<li><strong>stage4</strong> - stage 4 value (or null)</li>
<li><strong>stage5</strong> - stage 5 value (or null)</li>
<li><strong>value</strong> - number of times this sequence occurred</li>
</ul>
</React.Fragment>
);
}

View File

@@ -1,33 +1,30 @@
import React from 'react';
import { Section } from '@/components/visualizations/editor';
export default function Editor() {
return (
<div className="form-horizontal">
<div>
This visualization expects the query result to have rows in one of the following formats:
<div className="m-t-10">
<strong>Option 1:</strong>
<ul>
<li><strong>sequence</strong> - sequence id</li>
<li><strong>stage</strong> - what stage in sequence this is (1, 2, ...)</li>
<li><strong>node</strong> - stage name</li>
<li><strong>value</strong> - number of times this sequence occurred</li>
</ul>
</div>
<div className="m-t-10">
<strong>Option 2:</strong>
<ul>
<li><strong>stage1</strong> - stage 1 value</li>
<li><strong>stage2</strong> - stage 2 value (or null)</li>
<li><strong>stage3</strong> - stage 3 value (or null)</li>
<li><strong>stage4</strong> - stage 4 value (or null)</li>
<li><strong>stage5</strong> - stage 5 value (or null)</li>
<li><strong>value</strong> - number of times this sequence occurred</li>
</ul>
</div>
</div>
</div>
<React.Fragment>
<p>This visualization expects the query result to have rows in one of the following formats:</p>
<Section>
<p><strong>Option 1:</strong></p>
<ul>
<li><strong>sequence</strong> - sequence id</li>
<li><strong>stage</strong> - what stage in sequence this is (1, 2, ...)</li>
<li><strong>node</strong> - stage name</li>
<li><strong>value</strong> - number of times this sequence occurred</li>
</ul>
</Section>
<Section>
<p><strong>Option 2:</strong></p>
<ul>
<li><strong>stage1</strong> - stage 1 value</li>
<li><strong>stage2</strong> - stage 2 value (or null)</li>
<li><strong>stage3</strong> - stage 3 value (or null)</li>
<li><strong>stage4</strong> - stage 4 value (or null)</li>
<li><strong>stage5</strong> - stage 5 value (or null)</li>
<li><strong>value</strong> - number of times this sequence occurred</li>
</ul>
</Section>
</React.Fragment>
);
}

View File

@@ -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 (
<div className="table-visualization-editor-column">
<Grid.Row gutter={15} type="flex" align="middle" className="m-b-15">
<Grid.Col span={16}>
<Input
data-test={`Table.Column.${column.name}.Title`}
defaultValue={column.title}
onChange={event => handleChangeDebounced({ title: event.target.value })}
/>
</Grid.Col>
<Grid.Col span={8}>
<Radio.Group
className="table-visualization-editor-column-align-content"
defaultValue={column.alignContent}
onChange={event => handleChange({ alignContent: event.target.value })}
>
<Tooltip title="Align left" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="left" data-test={`Table.Column.${column.name}.AlignLeft`}>
<Icon type="align-left" />
</Radio.Button>
</Tooltip>
<Tooltip title="Align center" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="center" data-test={`Table.Column.${column.name}.AlignCenter`}>
<Icon type="align-center" />
</Radio.Button>
</Tooltip>
<Tooltip title="Align right" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="right" data-test={`Table.Column.${column.name}.AlignRight`}>
<Icon type="align-right" />
</Radio.Button>
</Tooltip>
</Radio.Group>
</Grid.Col>
</Grid.Row>
<Section>
<Grid.Row gutter={15} type="flex" align="middle">
<Grid.Col span={16}>
<Input
data-test={`Table.Column.${column.name}.Title`}
defaultValue={column.title}
onChange={event => handleChangeDebounced({ title: event.target.value })}
/>
</Grid.Col>
<Grid.Col span={8}>
<TextAlignmentSelect
data-test={`Table.Column.${column.name}.TextAlignment`}
defaultValue={column.alignContent}
onChange={event => handleChange({ alignContent: event.target.value })}
/>
</Grid.Col>
</Grid.Row>
</Section>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-allow-search`}>
<Checkbox
id={`table-column-editor-${column.name}-allow-search`}
data-test={`Table.Column.${column.name}.UseForSearch`}
defaultChecked={column.allowSearch}
onChange={event => handleChange({ allowSearch: event.target.checked })}
/>
<span>Use for search</span>
</label>
</div>
<Section>
<Checkbox
data-test={`Table.Column.${column.name}.UseForSearch`}
defaultChecked={column.allowSearch}
onChange={event => handleChange({ allowSearch: event.target.checked })}
>
Use for search
</Checkbox>
</Section>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-display-as`}>Display as:</label>
<Section>
<Select
id={`table-column-editor-${column.name}-display-as`}
label="Display as:"
data-test={`Table.Column.${column.name}.DisplayAs`}
className="w-100"
defaultValue={column.displayAs}
@@ -81,7 +59,7 @@ export default function ColumnEditor({ column, onChange }) {
<Select.Option key={key} data-test={`Table.Column.${column.name}.DisplayAs.${key}`}>{friendlyName}</Select.Option>
))}
</Select>
</div>
</Section>
{AdditionalOptions && <AdditionalOptions column={column} onChange={handleChange} />}
</div>

View File

@@ -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');
});
});

View File

@@ -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 (
<div className="m-b-15">
<label htmlFor="table-editor-items-per-page">Items per page</label>
<Section>
<Select
id="table-editor-items-per-page"
label="Items per page"
data-test="Table.ItemsPerPage"
className="w-100"
defaultValue={options.itemsPerPage}
@@ -20,7 +19,7 @@ export default function GridSettings({ options, onOptionsChange }) {
<Select.Option key={`ipp${value}`} value={value} data-test={`Table.ItemsPerPage.${value}`}>{value}</Select.Option>
))}
</Select>
</div>
</Section>
);
}

View File

@@ -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');
});
});

View File

@@ -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 <Input> height
height: 35px;
line-height: 33px;
}
}
}

View File

@@ -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 (
<Tabs className="table-editor-container" animated={false} tabBarGutter={0}>
<Tabs.TabPane key="columns" tab={<span data-test="Counter.EditorTabs.General">Columns</span>}>
<ColumnsSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
<Tabs.TabPane key="grid" tab={<span data-test="Counter.EditorTabs.Formatting">Grid</span>}>
<GridSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
</Tabs>
);
}
index.propTypes = EditorPropTypes;
export default createTabbedEditor([
{ key: 'Columns', title: 'Columns', component: ColumnsSettings },
{ key: 'Grid', title: 'Grid', component: GridSettings },
]);

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-boolean-false`}>
Value for <code>false</code>
</label>
<Input
id={`table-column-editor-${column.name}-boolean-false`}
data-test="Table.ColumnEditor.Boolean.False"
defaultValue={column.booleanValues[0]}
onChange={event => handleChangeDebounced(0, event.target.value)}
/>
</div>
</div>
<Section>
<Input
label={(<React.Fragment>Value for <code>false</code></React.Fragment>)}
data-test="Table.ColumnEditor.Boolean.False"
defaultValue={column.booleanValues[0]}
onChange={event => handleChangeDebounced(0, event.target.value)}
/>
</Section>
<div className="m-b-15">
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-boolean-true`}>
Value for <code>true</code>
</label>
<Input
id={`table-column-editor-${column.name}-boolean-true`}
data-test="Table.ColumnEditor.Boolean.True"
defaultValue={column.booleanValues[1]}
onChange={event => handleChangeDebounced(1, event.target.value)}
/>
</div>
</div>
<Section>
<Input
label={(<React.Fragment>Value for <code>true</code></React.Fragment>)}
data-test="Table.ColumnEditor.Boolean.True"
defaultValue={column.booleanValues[1]}
onChange={event => handleChangeDebounced(1, event.target.value)}
/>
</Section>
</React.Fragment>
);
}

View File

@@ -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' } });
});
});

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-datetime-format`}>
Date/Time format
<Popover
content={(
<React.Fragment>
Format&nbsp;
<a href="https://momentjs.com/docs/#/displaying/format/" target="_blank" rel="noopener noreferrer">specs.</a>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
<Input
id={`table-column-editor-${column.name}-datetime-format`}
data-test="Table.ColumnEditor.DateTime.Format"
defaultValue={column.dateTimeFormat}
onChange={event => onChangeDebounced({ dateTimeFormat: event.target.value })}
/>
</div>
</React.Fragment>
<Section>
<Input
label={(<React.Fragment>Date/Time format<ContextHelp.DateTimeFormatSpecs /></React.Fragment>)}
data-test="Table.ColumnEditor.DateTime.Format"
defaultValue={column.dateTimeFormat}
onChange={event => onChangeDebounced({ dateTimeFormat: event.target.value })}
/>
</Section>
);
}

View File

@@ -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' } });
});
});

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-image-url`}>URL template</label>
<Section>
<Input
id={`table-column-editor-${column.name}-image-url`}
label="URL template"
data-test="Table.ColumnEditor.Image.UrlTemplate"
defaultValue={column.imageUrlTemplate}
onChange={event => onChangeDebounced({ imageUrlTemplate: event.target.value })}
/>
</div>
</Section>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-image-width`}>
Size
<Popover
content="Any positive integer value that specifies size in pixels. Leave empty to use default value."
placement="topLeft"
arrowPointAtCenter
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
<div className="d-flex align-items-center">
<Input
id={`table-column-editor-${column.name}-image-width`}
data-test="Table.ColumnEditor.Image.Width"
placeholder="Width"
defaultValue={column.imageWidth}
onChange={event => onChangeDebounced({ imageWidth: event.target.value })}
/>
<span className="p-l-5 p-r-5">&times;</span>
<Input
id={`table-column-editor-${column.name}-image-height`}
data-test="Table.ColumnEditor.Image.Height"
placeholder="Height"
defaultValue={column.imageHeight}
onChange={event => onChangeDebounced({ imageHeight: event.target.value })}
/>
</div>
</div>
<Section>
<ControlLabel
label={(
<React.Fragment>
Size
<ContextHelp placement="topLeft" arrowPointAtCenter>
<div className="m-b-5">Any positive integer value that specifies size in pixels.</div>
<div>Leave empty to use default value.</div>
</ContextHelp>
</React.Fragment>
)}
>
<div className="d-flex align-items-center">
<Input
data-test="Table.ColumnEditor.Image.Width"
placeholder="Width"
defaultValue={column.imageWidth}
onChange={event => onChangeDebounced({ imageWidth: event.target.value })}
/>
<span className="p-l-5 p-r-5">&times;</span>
<Input
data-test="Table.ColumnEditor.Image.Height"
placeholder="Height"
defaultValue={column.imageHeight}
onChange={event => onChangeDebounced({ imageHeight: event.target.value })}
/>
</div>
</ControlLabel>
</Section>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-image-title`}>Title template</label>
<Section>
<Input
id={`table-column-editor-${column.name}-image-title`}
label="Title template"
data-test="Table.ColumnEditor.Image.TitleTemplate"
defaultValue={column.imageTitleTemplate}
onChange={event => onChangeDebounced({ imageTitleTemplate: event.target.value })}
/>
</div>
</Section>
<div className="m-b-15">
<Popover
content={(
<React.Fragment>
<div>All columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
<div>Use <code>{'{{ @ }}'}</code> to reference current (this) column.</div>
<div>This syntax is applicable to URL, Title and Size options.</div>
</React.Fragment>
)}
<Section>
<ContextHelp
placement="topLeft"
arrowPointAtCenter
icon={(
<span style={{ cursor: 'default' }}>
Format specs {ContextHelp.defaultIcon}
</span>
)}
>
<span style={{ cursor: 'default' }}>
Format specs <Icon className="m-l-5" type="question-circle" theme="filled" />
</span>
</Popover>
</div>
<div>All columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
<div>Use <code>{'{{ @ }}'}</code> to reference current (this) column.</div>
<div>This syntax is applicable to URL, Title and Size options.</div>
</ContextHelp>
</Section>
</React.Fragment>
);
}

View File

@@ -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 {{ @ }}' } });
});
});

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-link-url`}>URL template</label>
<Section>
<Input
id={`table-column-editor-${column.name}-link-url`}
label="URL template"
data-test="Table.ColumnEditor.Link.UrlTemplate"
defaultValue={column.linkUrlTemplate}
onChange={event => onChangeDebounced({ linkUrlTemplate: event.target.value })}
/>
</div>
</Section>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-link-text`}>Text template</label>
<Section>
<Input
id={`table-column-editor-${column.name}-link-text`}
label="Text template"
data-test="Table.ColumnEditor.Link.TextTemplate"
defaultValue={column.linkTextTemplate}
onChange={event => onChangeDebounced({ linkTextTemplate: event.target.value })}
/>
</div>
</Section>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-link-title`}>Title template</label>
<Section>
<Input
id={`table-column-editor-${column.name}-link-title`}
label="Title template"
data-test="Table.ColumnEditor.Link.TitleTemplate"
defaultValue={column.linkTitleTemplate}
onChange={event => onChangeDebounced({ linkTitleTemplate: event.target.value })}
/>
</div>
</Section>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-link-open-in-new-tab`}>
<Checkbox
id={`table-column-editor-${column.name}-link-open-in-new-tab`}
data-test="Table.ColumnEditor.Link.OpenInNewTab"
checked={column.linkOpenInNewTab}
onChange={event => onChange({ linkOpenInNewTab: event.target.checked })}
/>
<span>Open in new tab</span>
</label>
</div>
<Section>
<Checkbox
data-test="Table.ColumnEditor.Link.OpenInNewTab"
checked={column.linkOpenInNewTab}
onChange={event => onChange({ linkOpenInNewTab: event.target.checked })}
>
Open in new tab
</Checkbox>
</Section>
<div className="m-b-15">
<Popover
content={(
<React.Fragment>
<div>All columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
<div>Use <code>{'{{ @ }}'}</code> to reference current (this) column.</div>
<div>This syntax is applicable to URL, Text and Title options.</div>
</React.Fragment>
)}
<Section>
<ContextHelp
placement="topLeft"
arrowPointAtCenter
icon={(
<span style={{ cursor: 'default' }}>
Format specs {ContextHelp.defaultIcon}
</span>
)}
>
<span style={{ cursor: 'default' }}>
Format specs <Icon className="m-l-5" type="question-circle" theme="filled" />
</span>
</Popover>
</div>
<div>All columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
<div>Use <code>{'{{ @ }}'}</code> to reference current (this) column.</div>
<div>This syntax is applicable to URL, Text and Title options.</div>
</ContextHelp>
</Section>
</React.Fragment>
);
}

View File

@@ -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 } });
});
});

View File

@@ -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 (
<React.Fragment>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-number-format`}>
Number format
<Popover
content={(
<React.Fragment>
Format&nbsp;
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
<Input
id={`table-column-editor-${column.name}-number-format`}
data-test="Table.ColumnEditor.Number.Format"
defaultValue={column.numberFormat}
onChange={event => onChangeDebounced({ numberFormat: event.target.value })}
/>
</div>
</React.Fragment>
<Section>
<Input
label={(<React.Fragment>Number format<ContextHelp.NumberFormatSpecs /></React.Fragment>)}
data-test="Table.ColumnEditor.Number.Format"
defaultValue={column.numberFormat}
onChange={event => onChangeDebounced({ numberFormat: event.target.value })}
/>
</Section>
);
}

Some files were not shown because too many files have changed in this diff Show More