Skip to content

Commit

Permalink
Migrate Counter visualization to React (#4106)
Browse files Browse the repository at this point in the history
* Migrate Counter to React: Renderer

* Migrate Counter to React: Editor

* Cleanup

* Review and fix rows indexing algorithm

* Counter not properly scaled in editor

* Fix wrong label for/input id pair

* Tests

* Tests

* Fix vendor prefixes

* Remove unnecessary useEffect dependencies

* Update tests

* Fix Percy snapshot names
  • Loading branch information
kravets-levko authored and arikfr committed Sep 9, 2019
1 parent e048a69 commit 424751d
Show file tree
Hide file tree
Showing 11 changed files with 716 additions and 364 deletions.
45 changes: 0 additions & 45 deletions client/app/assets/less/inc/visualizations/counter-render.less

This file was deleted.

1 change: 0 additions & 1 deletion client/app/assets/less/main.less
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
@import 'inc/schema-browser';
@import 'inc/toast';
@import 'inc/visualizations/box';
@import 'inc/visualizations/counter-render';
@import 'inc/visualizations/sankey';
@import 'inc/visualizations/pivot-table';
@import 'inc/visualizations/map';
Expand Down
4 changes: 2 additions & 2 deletions client/app/pages/dashboards/dashboard.less
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
overflow: hidden;
}

counter {
.counter-visualization-content {
position: absolute;
left: 10px;
top: 15px;
Expand Down Expand Up @@ -145,7 +145,7 @@

.dashboard__control {
margin: 8px 0;

.save-status {
vertical-align: middle;
margin-right: 7px;
Expand Down
236 changes: 236 additions & 0 deletions client/app/visualizations/counter/Editor.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { merge, map } from 'lodash';
import React from 'react';
import Tabs from 'antd/lib/tabs';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
import Select from 'antd/lib/select';
import Switch from 'antd/lib/switch';
import * as Grid from 'antd/lib/grid';
import { EditorPropTypes } from '@/visualizations';

import { isValueNumber } from './utils';

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>

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

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

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

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

<label className="d-flex align-items-center" htmlFor="counter-count-rows">
<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>
</React.Fragment>
);
}

GeneralSettings.propTypes = EditorPropTypes;

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>

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

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

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

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

<label className="d-flex align-items-center" htmlFor="counter-format-target-value">
<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>
</React.Fragment>
);
}

FormatSettings.propTypes = EditorPropTypes;

export default function Editor(props) {
const { options, onOptionsChange } = props;

const optionsChanged = (newOptions) => {
onOptionsChange(merge({}, options, newOptions));
};

return (
<Tabs animated={false}>
<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;
73 changes: 73 additions & 0 deletions client/app/visualizations/counter/Renderer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { isFinite } from 'lodash';
import React, { useState, useEffect } from 'react';
import cx from 'classnames';
import resizeObserver from '@/services/resizeObserver';
import { RendererPropTypes } from '@/visualizations';

import { getCounterData } from './utils';

import './render.less';

function getCounterStyles(scale) {
return {
msTransform: `scale(${scale})`,
MozTransform: `scale(${scale})`,
WebkitTransform: `scale(${scale})`,
transform: `scale(${scale})`,
};
}

function getCounterScale(container) {
const inner = container.firstChild;
const scale = Math.min(container.offsetWidth / inner.offsetWidth, container.offsetHeight / inner.offsetHeight);
return Number(isFinite(scale) ? scale : 1).toFixed(2); // keep only two decimal places
}

export default function Renderer({ data, options, visualizationName }) {
const [scale, setScale] = useState('1.00');
const [container, setContainer] = useState(null);

useEffect(() => {
if (container) {
const unwatch = resizeObserver(container, () => {
setScale(getCounterScale(container));
});
return unwatch;
}
}, [container]);

useEffect(() => {
if (container) {
// update scaling when options or data change (new formatting, values, etc.
// may change inner container dimensions which will not be tracked by `resizeObserver`);
setScale(getCounterScale(container));
}
}, [data, options, container]);

const {
showTrend, trendPositive,
counterValue, counterValueTooltip,
targetValue, targetValueTooltip,
counterLabel,
} = getCounterData(data.rows, options, visualizationName);
return (
<div
className={cx(
'counter-visualization-container',
{ 'trend-positive': showTrend && trendPositive, 'trend-negative': showTrend && !trendPositive },
)}
>
<div className="counter-visualization-content" ref={setContainer}>
<div style={getCounterStyles(scale)}>
<div className="counter-visualization-value" title={counterValueTooltip}>{counterValue}</div>
{targetValue && (
<div className="counter-visualization-target" title={targetValueTooltip}>({targetValue})</div>
)}
<div className="counter-visualization-label">{counterLabel}</div>
</div>
</div>
</div>
);
}

Renderer.propTypes = RendererPropTypes;
Loading

0 comments on commit 424751d

Please sign in to comment.