Skip to content

Commit

Permalink
Migrate Map visualization to React (#4278)
Browse files Browse the repository at this point in the history
  • Loading branch information
kravets-levko authored Nov 20, 2019
1 parent 5cd6913 commit c6a2725
Show file tree
Hide file tree
Showing 18 changed files with 968 additions and 587 deletions.
30 changes: 30 additions & 0 deletions client/app/components/ColorPicker/Label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';

import { validateColor, getColorName } from './utils';
import './label.less';

export default function Label({ className, color, presetColors, ...props }) {
const name = useMemo(
() => getColorName(validateColor(color), presetColors),
[color, presetColors],
);

return <span className={cx('color-label', className)} {...props}>{name}</span>;
}

Label.propTypes = {
className: PropTypes.string,
color: PropTypes.string,
presetColors: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
PropTypes.objectOf(PropTypes.string), // color name => color value
]),
};

Label.defaultProps = {
className: null,
color: '#FFFFFF',
presetColors: null,
};
5 changes: 3 additions & 2 deletions client/app/components/ColorPicker/Swatch.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { isString } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Tooltip from 'antd/lib/tooltip';

import './swatch.less';

export default function Swatch({ className, color, title, size, ...props }) {
const result = (
<span
className={`color-swatch ${className}`}
className={cx('color-swatch', className)}
style={{ backgroundColor: color, width: size }}
{...props}
/>
Expand All @@ -30,7 +31,7 @@ Swatch.propTypes = {
};

Swatch.defaultProps = {
className: '',
className: null,
title: null,
color: 'transparent',
size: 12,
Expand Down
34 changes: 16 additions & 18 deletions client/app/components/ColorPicker/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toString } from 'lodash';
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import tinycolor from 'tinycolor2';
Expand All @@ -10,19 +10,16 @@ import Icon from 'antd/lib/icon';

import ColorInput from './Input';
import Swatch from './Swatch';
import Label from './Label';
import { validateColor } from './utils';

import './index.less';

function validateColor(value, fallback = null) {
value = tinycolor(value);
return value.isValid() ? '#' + value.toHex().toUpperCase() : fallback;
}

export default function ColorPicker({
color, placement, presetColors, presetColumns, triggerSize, interactive, children, onChange,
className, ...props
color, placement, presetColors, presetColumns, interactive, children, onChange, triggerProps,
}) {
const [visible, setVisible] = useState(false);
const validatedColor = useMemo(() => validateColor(color), [color]);
const [currentColor, setCurrentColor] = useState('');

function handleApply() {
Expand Down Expand Up @@ -59,12 +56,14 @@ export default function ColorPicker({

useEffect(() => {
if (visible) {
setCurrentColor(validateColor(color));
setCurrentColor(validatedColor);
}
}, [color, visible]);
}, [validatedColor, visible]);

return (
<Popover
arrowPointAtCenter
destroyTooltipOnHide
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
overlayStyle={{ '--color-picker-selected-color': currentColor }}
content={(
Expand Down Expand Up @@ -95,10 +94,10 @@ export default function ColorPicker({
>
{children || (
<Swatch
className={cx('color-picker-trigger', className)}
color={validateColor(color)}
size={triggerSize}
{...props}
color={validatedColor}
size={30}
{...triggerProps}
className={cx('color-picker-trigger', triggerProps.className)}
/>
)}
</Popover>
Expand All @@ -117,24 +116,23 @@ ColorPicker.propTypes = {
PropTypes.objectOf(PropTypes.string), // color name => color value
]),
presetColumns: PropTypes.number,
triggerSize: PropTypes.number,
interactive: PropTypes.bool,
triggerProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
children: PropTypes.node,
onChange: PropTypes.func,
className: PropTypes.string,
};

ColorPicker.defaultProps = {
color: '#FFFFFF',
placement: 'top',
presetColors: null,
presetColumns: 8,
triggerSize: 30,
interactive: false,
triggerProps: {},
children: null,
onChange: () => {},
className: null,
};

ColorPicker.Input = ColorInput;
ColorPicker.Swatch = Swatch;
ColorPicker.Label = Label;
7 changes: 7 additions & 0 deletions client/app/components/ColorPicker/label.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.color-label {
vertical-align: middle;

.color-swatch + & {
margin-left: 7px;
}
}
14 changes: 14 additions & 0 deletions client/app/components/ColorPicker/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { isArray, findKey } from 'lodash';
import tinycolor from 'tinycolor2';

export function validateColor(value, fallback = null) {
value = tinycolor(value);
return value.isValid() ? '#' + value.toHex().toUpperCase() : fallback;
}

export function getColorName(color, presetColors) {
if (isArray(presetColors)) {
return color;
}
return findKey(presetColors, v => validateColor(v) === color) || color;
}
25 changes: 15 additions & 10 deletions client/app/visualizations/choropleth/Editor/ColorsSettings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,84 +52,89 @@ export default function ColorsSettings({ options, onOptionsChange }) {
<Grid.Col span={12}>
<label htmlFor="choropleth-editor-color-min">Min Color</label>
</Grid.Col>
<Grid.Col span={12}>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-min"
data-test="Choropleth.Editor.Colors.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>

<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}>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-max"
data-test="Choropleth.Editor.Colors.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>

<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}>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-no-value"
data-test="Choropleth.Editor.Colors.NoValue"
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>

<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}>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-background"
data-test="Choropleth.Editor.Colors.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>

<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}>
<Grid.Col span={12} className="text-nowrap">
<ColorPicker
id="choropleth-editor-color-borders"
data-test="Choropleth.Editor.Colors.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>
</React.Fragment>
Expand Down
71 changes: 71 additions & 0 deletions client/app/visualizations/map/Editor/GeneralSettings.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { isNil, map, filter, difference } from 'lodash';
import React, { useMemo } from 'react';
import Select from 'antd/lib/select';
import { EditorPropTypes } from '@/visualizations';

function getColumns(column, unusedColumns) {
return filter(
[column, ...unusedColumns],
v => !isNil(v),
);
}

export default function GeneralSettings({ options, data, onOptionsChange }) {
const unusedColumns = useMemo(
() => difference(map(data.columns, c => c.name), [options.latColName, options.lonColName, options.classify]),
[data, options.latColName, options.lonColName, options.classify],
);

return (
<React.Fragment>
<div className="m-b-15">
<label htmlFor="map-editor-latitude-column-name">Latitude Column Name</label>
<Select
data-test="Map.Editor.LatitudeColumnName"
id="map-editor-latitude-column-name"
className="w-100"
value={options.latColName}
onChange={latColName => onOptionsChange({ latColName })}
>
{map(getColumns(options.latColName, unusedColumns), col => (
<Select.Option key={col} data-test={'Map.Editor.LatitudeColumnName.' + col}>{col}</Select.Option>
))}
</Select>
</div>

<div className="m-b-15">
<label htmlFor="map-editor-longitude-column-name">Longitude Column Name</label>
<Select
data-test="Map.Editor.LongitudeColumnName"
id="map-editor-longitude-column-name"
className="w-100"
value={options.lonColName}
onChange={lonColName => onOptionsChange({ lonColName })}
>
{map(getColumns(options.lonColName, unusedColumns), col => (
<Select.Option key={col} data-test={'Map.Editor.LongitudeColumnName.' + col}>{col}</Select.Option>
))}
</Select>
</div>

<div className="m-b-15">
<label className="control-label" htmlFor="map-editor-group-by">Group By</label>
<Select
data-test="Map.Editor.GroupBy"
id="map-editor-group-by"
className="w-100"
allowClear
placeholder="none"
value={options.classify || undefined}
onChange={column => onOptionsChange({ classify: column || null })}
>
{map(getColumns(options.classify, unusedColumns), col => (
<Select.Option key={col} data-test={'Map.Editor.GroupBy.' + col}>{col}</Select.Option>
))}
</Select>
</div>
</React.Fragment>
);
}

GeneralSettings.propTypes = EditorPropTypes;
Loading

0 comments on commit c6a2725

Please sign in to comment.