From 274a2dec68bcb343fa6a6cec42bcce1ab658eaf1 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Mon, 10 Feb 2020 08:47:34 -0600 Subject: [PATCH 01/18] feat: add color picker render prop --- src/components/legend/_legend_item.scss | 10 ++- src/components/legend/legend.tsx | 3 +- src/components/legend/legend_item.tsx | 105 ++++++++++++++++++------ src/specs/settings.tsx | 2 + 4 files changed, 89 insertions(+), 31 deletions(-) diff --git a/src/components/legend/_legend_item.scss b/src/components/legend/_legend_item.scss index e312e19eb3..9b98486862 100644 --- a/src/components/legend/_legend_item.scss +++ b/src/components/legend/_legend_item.scss @@ -9,12 +9,16 @@ $legendItemVerticalPadding: $echLegendRowGap / 2; align-items: center; width: 100%; - &:hover { - .echLegendItem__title { - text-decoration: underline; + &:not(&--hidden) { + .echLegendItem__color--changable { + cursor: pointer; } } + &__title:hover { + text-decoration: underline; + } + &__color { margin-right: $euiSizeXS; } diff --git a/src/components/legend/legend.tsx b/src/components/legend/legend.tsx index 27b7c910d1..7632637b04 100644 --- a/src/components/legend/legend.tsx +++ b/src/components/legend/legend.tsx @@ -139,7 +139,7 @@ class LegendComponent extends React.Component { } const { key, displayValue, banded } = item; const { legendItemTooltipValues, settings } = this.props; - const { showLegendDisplayValue, legendPosition } = settings; + const { showLegendDisplayValue, legendPosition, renderColorPicker } = settings; const legendValues = this.getLegendValues(legendItemTooltipValues, key, banded); return legendValues.map((value, index) => { const yAccessor: BandedAccessorType = index === 0 ? BandedAccessorType.Y1 : BandedAccessorType.Y0; @@ -149,6 +149,7 @@ class LegendComponent extends React.Component { label={getItemLabel(item, yAccessor)} key={`${key}-${yAccessor}`} legendItem={item} + renderColorPicker={renderColorPicker} displayValue={value !== '' ? value : displayValue.formatted[yAccessor]} showLegendDisplayValue={showLegendDisplayValue} legendPosition={legendPosition} diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index c5a3f305fd..7af9e92455 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import React from 'react'; +import React, { PureComponent, ReactNode } from 'react'; import deepEqual from 'fast-deep-equal/es6/react'; import { Icon } from '../icons/icon'; import { LegendItemListener, BasicListener } from '../../specs/settings'; @@ -8,6 +8,13 @@ import { onLegendItemOutAction, onLegendItemOverAction } from '../../state/actio import { Position } from '../../chart_types/xy_chart/utils/specs'; import { SeriesIdentifier } from '../../chart_types/xy_chart/utils/series'; +export type RenderColorPicker = ( + onChange: (color: string) => void, + onClose: () => void, + isOpen: boolean, + button: NonNullable, +) => ReactNode; + interface LegendItemProps { selectedLegendItem?: LegendItem | null; legendItem: LegendItem; @@ -15,6 +22,7 @@ interface LegendItemProps { label?: string; legendPosition: Position; showLegendDisplayValue: boolean; + renderColorPicker?: RenderColorPicker; onLegendItemClickListener?: LegendItemListener; onLegendItemOutListener?: BasicListener; onLegendItemOverListener?: LegendItemListener; @@ -67,36 +75,61 @@ function renderTitle( ); } -/** - * Create a div for the color/eye icon - * @param color - * @param isSeriesVisible - */ -function renderColor(color: string, isSeriesVisible = true) { - // TODO add color picker - if (isSeriesVisible) { - return ( -
- -
- ); - } - // changing the default viewBox for the eyeClosed icon to keep the same dimensions - return ( -
- -
- ); +interface LegendItemState { + isOpen: boolean; } -export class LegendListItem extends React.Component { +export class LegendListItem extends PureComponent { static displayName = 'LegendItem'; - shouldComponentUpdate(nextProps: LegendItemProps) { - return !deepEqual(this.props, nextProps); + state: LegendItemState = { + isOpen: false, + }; + + shouldComponentUpdate(nextProps: LegendItemProps, nextState: LegendItemState) { + return !deepEqual(this.props, nextProps) || !deepEqual(this.state, nextState); } - render() { + /** + * Create a div for the color/eye icon + * @param color + * @param isSeriesVisible + */ + renderColor = (color: string, isSeriesVisible = true) => { + if (isSeriesVisible) { + const changable = Boolean(this.props.renderColorPicker); + const colorClasses = classNames('echLegendItem__color', { + 'echLegendItem__color--changable': changable, + }); + + return ( +
{ + e.nativeEvent.stopPropagation(); + this.setToggleIsOpen(); + } + : undefined + } + className={colorClasses} + aria-label="series color" + title="series color" + > + +
+ ); + } + + // changing the default viewBox for the eyeClosed icon to keep the same dimensions + return ( +
+ +
+ ); + }; + + renderItem = () => { const { displayValue, legendItem, legendPosition, label } = this.props; const { color, isSeriesVisible, seriesIdentifier, isLegendItemVisible } = legendItem; const onTitleClick = this.onVisibilityClick(seriesIdentifier); @@ -106,19 +139,37 @@ export class LegendListItem extends React.Component { selectedLegendItem == null ? false : selectedLegendItem.seriesIdentifier.key === seriesIdentifier.key; const hasTitleClickListener = Boolean(onLegendItemClickListener); const itemClasses = classNames('echLegendItem', `echLegendItem--${legendPosition}`, { - 'echLegendItem-isHidden': !isSeriesVisible, + 'echLegendItem--hidden': !isSeriesVisible, 'echLegendItem__displayValue--hidden': !isLegendItemVisible, }); return (
- {color && renderColor(color, isSeriesVisible)} + {color && this.renderColor(color, isSeriesVisible)} {label && renderTitle(label, onTitleClick, hasTitleClickListener, isSelected, showLegendDisplayValue)} {showLegendDisplayValue && renderDisplayValue(displayValue, isSeriesVisible)}
); + }; + + render() { + const { renderColorPicker } = this.props; + if (renderColorPicker && this.state.isOpen) { + return renderColorPicker(this.onColorChange, this.setToggleIsOpen, this.state.isOpen, this.renderItem()); + } + + return this.renderItem(); } + setToggleIsOpen = () => { + this.setState({ isOpen: !this.state.isOpen }); + }; + + onColorChange = (color: string) => { + this.setToggleIsOpen(); + console.log(color); + }; + onLegendItemMouseOver = () => { const { onLegendItemOverListener, legendItemOverAction, legendItem } = this.props; // call the settings listener directly if available diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 72d2c615a7..24c2ebd92b 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -10,6 +10,7 @@ import { LIGHT_THEME } from '../utils/themes/light_theme'; import { ChartTypes } from '../chart_types'; import { GeometryValue } from '../utils/geometry'; import { SeriesIdentifier } from '../chart_types/xy_chart/utils/series'; +import { RenderColorPicker } from '../components/legend/legend_item'; export type ElementClickListener = (elements: Array<[GeometryValue, SeriesIdentifier]>) => void; export type ElementOverListener = (elements: Array<[GeometryValue, SeriesIdentifier]>) => void; @@ -109,6 +110,7 @@ export interface SettingsSpec extends Spec { onRenderChange?: RenderChangeListener; xDomain?: Domain | DomainRange; resizeDebounce?: number; + renderColorPicker?: RenderColorPicker; } export type DefaultSettingsProps = From ab436395e807af4cb1c70cd3ff8744e03fe4c5e9 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Tue, 11 Feb 2020 16:47:29 -0600 Subject: [PATCH 02/18] chore: remove color override global logic in favor of local state --- src/chart_types/xy_chart/state/utils.test.ts | 32 +++----------------- src/chart_types/xy_chart/state/utils.ts | 7 +---- src/components/legend/legend_item.tsx | 9 ++++-- 3 files changed, 11 insertions(+), 37 deletions(-) diff --git a/src/chart_types/xy_chart/state/utils.test.ts b/src/chart_types/xy_chart/state/utils.test.ts index aa7641cb1a..0bf0275642 100644 --- a/src/chart_types/xy_chart/state/utils.test.ts +++ b/src/chart_types/xy_chart/state/utils.test.ts @@ -328,11 +328,10 @@ describe('Chart State utils', () => { // 4 groups generated const data = dg.generateGroupedSeries(50, 4); const targetKey = 'spec{bar1}yAccessor{y}splitAccessors{g-b}'; - const seriesColorOverrides = new Map([[targetKey, 'blue']]); describe('empty series collection and specs', () => { it('it should return an empty map', () => { - const actual = getCustomSeriesColors(MockSeriesSpecs.empty(), MockSeriesCollection.empty(), new Map()); + const actual = getCustomSeriesColors(MockSeriesSpecs.empty(), MockSeriesCollection.empty()); expect(actual.size).toBe(0); }); @@ -344,7 +343,7 @@ describe('Chart State utils', () => { const barSpec2 = MockSeriesSpec.bar({ id: specId2, data }); const barSeriesSpecs = MockSeriesSpecs.fromSpecs([barSpec1, barSpec2]); const barSeriesCollection = MockSeriesCollection.fromSpecs(barSeriesSpecs); - const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection, new Map()); + const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection); expect(actual.size).toBe(0); }); @@ -357,7 +356,7 @@ describe('Chart State utils', () => { const barSeriesCollection = MockSeriesCollection.fromSpecs(barSeriesSpecs); it('it should return color from customSeriesColors array', () => { - const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection, new Map()); + const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection); expect(actual.size).toBe(4); barSeriesCollection.forEach(({ seriesIdentifier: { specId, key } }) => { @@ -369,22 +368,6 @@ describe('Chart State utils', () => { } }); }); - - it('it should return color from seriesColorOverrides', () => { - const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection, seriesColorOverrides); - - expect(actual.size).toBe(4); - barSeriesCollection.forEach(({ seriesIdentifier: { specId, key } }) => { - const color = actual.get(key); - if (key === targetKey) { - expect(color).toBe('blue'); - } else if (specId === specId1) { - expect(customSeriesColors).toContainEqual(color); - } else { - expect(color).toBeUndefined(); - } - }); - }); }); describe('with customSeriesColors function', () => { @@ -401,18 +384,11 @@ describe('Chart State utils', () => { const barSeriesCollection = MockSeriesCollection.fromSpecs(barSeriesSpecs); it('it should return color from customSeriesColors function', () => { - const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection, new Map()); + const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection); expect(actual.size).toBe(1); expect(actual.get(targetKey)).toBe('aquamarine'); }); - - it('it should return color from seriesColorOverrides', () => { - const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection, seriesColorOverrides); - - expect(actual.size).toBe(1); - expect(actual.get(targetKey)).toBe('blue'); - }); }); }); }); diff --git a/src/chart_types/xy_chart/state/utils.ts b/src/chart_types/xy_chart/state/utils.ts index 34b1bd9364..1ab92191ed 100644 --- a/src/chart_types/xy_chart/state/utils.ts +++ b/src/chart_types/xy_chart/state/utils.ts @@ -121,7 +121,6 @@ export function updateDeselectedDataSeries(series: SeriesIdentifier[], target: S export function getCustomSeriesColors( seriesSpecs: BasicSeriesSpec[], seriesCollection: Map, - seriesColorOverrides: Map = new Map(), ): Map { const updatedCustomSeriesColors = new Map(); const counters = new Map(); @@ -129,16 +128,12 @@ export function getCustomSeriesColors( seriesCollection.forEach(({ seriesIdentifier }, seriesKey) => { const spec = getSpecsById(seriesSpecs, seriesIdentifier.specId); - if (!spec || !(spec.customSeriesColors || seriesColorOverrides.size > 0)) { + if (!spec || !spec.customSeriesColors) { return; } let color: string | undefined | null; - if (seriesColorOverrides.has(seriesKey)) { - color = seriesColorOverrides.get(seriesKey); - } - if (!color && spec.customSeriesColors) { const counter = counters.get(seriesIdentifier.specId) || 0; color = Array.isArray(spec.customSeriesColors) diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index 7af9e92455..e88087bc3e 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -77,6 +77,7 @@ function renderTitle( interface LegendItemState { isOpen: boolean; + color?: string; } export class LegendListItem extends PureComponent { @@ -145,7 +146,7 @@ export class LegendListItem extends PureComponent - {color && this.renderColor(color, isSeriesVisible)} + {color && this.renderColor(this.state.color ?? color, isSeriesVisible)} {label && renderTitle(label, onTitleClick, hasTitleClickListener, isSelected, showLegendDisplayValue)} {showLegendDisplayValue && renderDisplayValue(displayValue, isSeriesVisible)} @@ -166,8 +167,10 @@ export class LegendListItem extends PureComponent { - this.setToggleIsOpen(); - console.log(color); + this.setState({ + color, + isOpen: !this.state.isOpen, + }); }; onLegendItemMouseOver = () => { From 8f0ae0e4dc90712091ab5118e0ccc052383bb254 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Tue, 11 Feb 2020 17:49:24 -0600 Subject: [PATCH 03/18] tests: add tests for render legend --- .playground/playground.tsx | 145 +++++------------- .../legend/__snapshots__/legend.test.tsx.snap | 9 ++ src/components/legend/legend.test.tsx | 125 ++++++++++++++- src/components/legend/legend_item.tsx | 6 +- 4 files changed, 168 insertions(+), 117 deletions(-) create mode 100644 src/components/legend/__snapshots__/legend.test.tsx.snap diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 402d76f4bc..3201f53cca 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -1,117 +1,42 @@ import React from 'react'; -import { Chart, LineSeries, ScaleType, Settings, Position, Axis, BarSeries, HistogramBarSeries } from '../src'; -export class Playground extends React.Component<{}, { isSunburstShown: boolean }> { - chartRef: React.RefObject = React.createRef(); - state = { - isSunburstShown: true, - }; - onBrushEnd = (min: number, max: number) => { - // eslint-disable-next-line no-console - console.log({ min, max }); - }; +import { Chart, ScaleType, Settings, BarSeries, DataGenerator } from '../src'; +import { RenderColorPicker } from '../src/components/legend/legend_item'; +export class Playground extends React.Component<{}, { isSunburstShown: boolean }> { render() { - return ( - <> -
- - - - - - -
-
- - - - - - + const dg = new DataGenerator(); + const data = dg.generateGroupedSeries(10, 4, 'split'); + const renderColorPicker: RenderColorPicker = (onChange, onClose, isOpen, button) => + isOpen ? ( +
+ Custom Color Picker + + + {button}
-
- - - - - - - -
-
- - - - - - -
- + ) : ( + { button } + ); + + return ( +
+ + + + +
); } } diff --git a/src/components/legend/__snapshots__/legend.test.tsx.snap b/src/components/legend/__snapshots__/legend.test.tsx.snap new file mode 100644 index 0000000000..c4177de3e6 --- /dev/null +++ b/src/components/legend/__snapshots__/legend.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Legend #renderColorPicker should match snapshot after onChange is called 1`] = `"
splita
3.8709519805215598
splitb
6.64868931925065
splitc
4.095359849556172
splitd
6.252951939116635
"`; + +exports[`Legend #renderColorPicker should match snapshot after onClose is called 1`] = `"
splita
3.8709519805215598
splitb
6.64868931925065
splitc
4.095359849556172
splitd
6.252951939116635
"`; + +exports[`Legend #renderColorPicker should render colorPicker when color is clicked 1`] = `"
Custom Color Picker
splita
3.8709519805215598
"`; + +exports[`Legend #renderColorPicker should render colorPicker when color is clicked 2`] = `"
Custom Color Picker
splita
3.8709519805215598
splitb
6.64868931925065
splitc
4.095359849556172
splitd
6.252951939116635
"`; diff --git a/src/components/legend/legend.test.tsx b/src/components/legend/legend.test.tsx index fee9a175cf..b3a79a25c8 100644 --- a/src/components/legend/legend.test.tsx +++ b/src/components/legend/legend.test.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import { Chart } from '../chart'; import { Settings, BarSeries } from '../../specs'; import { ScaleType } from '../../utils/scales/scales'; import { DataGenerator } from '../../utils/data_generators/data_generator'; import { Legend } from './legend'; -import { LegendListItem } from './legend_item'; +import { LegendListItem, RenderColorPicker } from './legend_item'; + +const dg = new DataGenerator(); describe('Legend', () => { it('shall render the all the series names', () => { @@ -73,7 +75,6 @@ describe('Legend', () => { it('shall call the over and out listeners for every list item', () => { const onLegendItemOver = jest.fn(); const onLegendItemOut = jest.fn(); - const dg = new DataGenerator(); const numberOfSeries = 4; const data = dg.generateGroupedSeries(10, numberOfSeries, 'split'); const wrapper = mount( @@ -103,7 +104,6 @@ describe('Legend', () => { }); it('shall call click listener for every list item', () => { const onLegendItemClick = jest.fn(); - const dg = new DataGenerator(); const numberOfSeries = 4; const data = dg.generateGroupedSeries(10, numberOfSeries, 'split'); const wrapper = mount( @@ -131,4 +131,121 @@ describe('Legend', () => { expect(onLegendItemClick).toBeCalledTimes(i + 1); }); }); + + describe('#renderColorPicker', () => { + let wrapper: ReactWrapper; + const customColor = '#0c7b93'; + const onLegendItemClick = jest.fn(); + const data = dg.generateGroupedSeries(10, 4, 'split'); + const renderColorPicker: RenderColorPicker = (onChange, onClose, isOpen, button) => + isOpen ? ( +
+ Custom Color Picker + + + {button} +
+ ) : ( + { button } + ); + + beforeEach(() => { + wrapper = mount( + + + + , + ); + }); + + const clickFirstColor = () => { + const legendWrapper = wrapper.find(Legend); + expect(legendWrapper.exists).toBeTruthy(); + const legendItems = legendWrapper.find(LegendListItem); + expect(legendItems.exists).toBeTruthy(); + expect(legendItems).toHaveLength(4); + legendItems + .first() + .find('.echLegendItem__color') + .simulate('click'); + }; + + it('should render colorPicker when color is clicked', () => { + clickFirstColor(); + expect(wrapper.find('#colorPicker').html()).toMatchSnapshot(); + expect(wrapper.find(Legend).html()).toMatchSnapshot(); + }); + + it('should match snapshot after onChange is called', () => { + clickFirstColor(); + wrapper + .find('#change') + .simulate('click') + .first(); + + expect(wrapper.find(Legend).html()).toMatchSnapshot(); + }); + + it('should set isOpen to false after onChange is called', () => { + clickFirstColor(); + wrapper + .find('#change') + .simulate('click') + .first(); + expect(wrapper.find('#colorPicker').exists()).toBe(false); + }); + + it('should set color after onChange is called', () => { + clickFirstColor(); + wrapper + .find('#change') + .simulate('click') + .first(); + const dot = wrapper.find('.echLegendItem__color svg'); + expect(dot.exists(`[color="${customColor}"]`)).toBe(true); + }); + + it('should match snapshot after onClose is called', () => { + clickFirstColor(); + wrapper + .find('#close') + .simulate('click') + .first(); + expect(wrapper.find(Legend).html()).toMatchSnapshot(); + }); + + it('should set isOpen to false after onClose is called', () => { + clickFirstColor(); + wrapper + .find('#close') + .simulate('click') + .first(); + expect(wrapper.find('#colorPicker').exists()).toBe(false); + }); + + it('should call click listener for every list item', () => { + const legendWrapper = wrapper.find(Legend); + expect(legendWrapper.exists).toBeTruthy(); + const legendItems = legendWrapper.find(LegendListItem); + expect(legendItems.exists).toBeTruthy(); + expect(legendItems).toHaveLength(4); + legendItems.forEach((legendItem, i) => { + // toggle click is only enabled on the title + legendItem.find('.echLegendItem__title').simulate('click'); + expect(onLegendItemClick).toBeCalledTimes(i + 1); + }); + }); + }); }); diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index e88087bc3e..f4baf3e317 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import React, { PureComponent, ReactNode } from 'react'; +import React, { Component, ReactNode } from 'react'; import deepEqual from 'fast-deep-equal/es6/react'; import { Icon } from '../icons/icon'; import { LegendItemListener, BasicListener } from '../../specs/settings'; @@ -80,7 +80,7 @@ interface LegendItemState { color?: string; } -export class LegendListItem extends PureComponent { +export class LegendListItem extends Component { static displayName = 'LegendItem'; state: LegendItemState = { @@ -108,7 +108,7 @@ export class LegendListItem extends PureComponent { - e.nativeEvent.stopPropagation(); + e.stopPropagation(); this.setToggleIsOpen(); } : undefined From f056b77a4a6efa63aa2dcd1c8a76753f40acaebc Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 13 Feb 2020 13:01:36 +0100 Subject: [PATCH 04/18] chore: add parsing action and split tooltipValues (#516) --- .../xy_chart/renderer/dom/tooltips.tsx | 25 +++--- .../state/chart_state.interactions.test.ts | 40 ++++----- .../xy_chart/state/chart_state.specs.test.ts | 83 +++++++++++++++++++ .../state/chart_state.timescales.test.ts | 72 ++++++++-------- .../state/chart_state.tooltip.test.ts | 15 ++-- .../selectors/get_annotation_tooltip_state.ts | 7 +- .../selectors/get_legend_tooltip_values.ts | 4 +- .../get_tooltip_values_highlighted_geoms.ts | 27 ++++-- .../state/selectors/is_tooltip_visible.ts | 8 +- src/chart_types/xy_chart/tooltip/tooltip.ts | 7 +- src/specs/specs_parser.tsx | 5 +- src/state/actions/specs.ts | 16 +++- src/state/chart_state.ts | 17 ++-- 13 files changed, 218 insertions(+), 108 deletions(-) create mode 100644 src/chart_types/xy_chart/state/chart_state.specs.test.ts diff --git a/src/chart_types/xy_chart/renderer/dom/tooltips.tsx b/src/chart_types/xy_chart/renderer/dom/tooltips.tsx index ba1538e6c3..8d3d14b200 100644 --- a/src/chart_types/xy_chart/renderer/dom/tooltips.tsx +++ b/src/chart_types/xy_chart/renderer/dom/tooltips.tsx @@ -6,7 +6,7 @@ import { GlobalChartState, BackwardRef } from '../../../../state/chart_state'; import { isTooltipVisibleSelector } from '../../state/selectors/is_tooltip_visible'; import { getTooltipHeaderFormatterSelector } from '../../state/selectors/get_tooltip_header_formatter'; import { getTooltipPositionSelector } from '../../state/selectors/get_tooltip_position'; -import { getTooltipValuesSelector } from '../../state/selectors/get_tooltip_values_highlighted_geoms'; +import { getTooltipValuesSelector, TooltipData } from '../../state/selectors/get_tooltip_values_highlighted_geoms'; import { isInitialized } from '../../../../state/selectors/is_initialized'; import { createPortal } from 'react-dom'; import { getFinalTooltipPosition, TooltipPosition } from '../../crosshair/crosshair_utils'; @@ -15,7 +15,7 @@ import { isAnnotationTooltipVisibleSelector } from '../../state/selectors/is_ann interface TooltipStateProps { isTooltipVisible: boolean; isAnnotationTooltipVisible: boolean; - tooltipValues: TooltipValue[]; + tooltip: TooltipData; tooltipPosition: TooltipPosition | null; tooltipHeaderFormatter?: TooltipValueFormatter; } @@ -74,7 +74,7 @@ class TooltipsComponent extends React.Component { } } - renderHeader(headerData?: TooltipValue, formatter?: TooltipValueFormatter) { + renderHeader(headerData: TooltipValue | null, formatter?: TooltipValueFormatter) { if (!headerData) { return null; } @@ -83,21 +83,21 @@ class TooltipsComponent extends React.Component { } render() { - const { isTooltipVisible, tooltipValues, tooltipHeaderFormatter, isAnnotationTooltipVisible } = this.props; + const { isTooltipVisible, tooltip, tooltipHeaderFormatter, isAnnotationTooltipVisible } = this.props; if (!this.portalNode) { return null; } const { getChartContainerRef } = this.props; const chartContainerRef = getChartContainerRef(); - let tooltip; + let tooltipComponent; if (chartContainerRef.current === null || !isTooltipVisible || isAnnotationTooltipVisible) { return null; } else { - tooltip = ( + tooltipComponent = (
-
{this.renderHeader(tooltipValues[0], tooltipHeaderFormatter)}
+
{this.renderHeader(tooltip.header, tooltipHeaderFormatter)}
- {tooltipValues.slice(1).map(({ name, value, color, isHighlighted, seriesKey, yAccessor, isVisible }) => { + {tooltip.values.map(({ name, value, color, isHighlighted, seriesKey, yAccessor, isVisible }) => { if (!isVisible) { return null; } @@ -122,7 +122,7 @@ class TooltipsComponent extends React.Component {
); } - return createPortal(tooltip, this.portalNode); + return createPortal(tooltipComponent, this.portalNode); } } @@ -131,7 +131,10 @@ const mapStateToProps = (state: GlobalChartState): TooltipStateProps => { return { isTooltipVisible: false, isAnnotationTooltipVisible: false, - tooltipValues: [], + tooltip: { + header: null, + values: [], + }, tooltipPosition: null, tooltipHeaderFormatter: undefined, }; @@ -139,7 +142,7 @@ const mapStateToProps = (state: GlobalChartState): TooltipStateProps => { return { isTooltipVisible: isTooltipVisibleSelector(state), isAnnotationTooltipVisible: isAnnotationTooltipVisibleSelector(state), - tooltipValues: getTooltipValuesSelector(state), + tooltip: getTooltipValuesSelector(state), tooltipPosition: getTooltipPositionSelector(state), tooltipHeaderFormatter: getTooltipHeaderFormatterSelector(state), }; diff --git a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts index 0a1c80b5de..15adffbed6 100644 --- a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts @@ -222,7 +222,7 @@ describe('Chart state pointer interactions', () => { store.dispatch(onPointerMove({ x: 10, y: 10 + 70 }, 0)); const tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); // no tooltip values exist if we have a TooltipType === None - expect(tooltipData.tooltipValues.length).toBe(0); + expect(tooltipData.tooltip.values.length).toBe(0); let isTooltipVisible = isTooltipVisibleSelector(store.getState()); expect(isTooltipVisible).toBe(false); @@ -284,7 +284,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { onPointerMoveCaller(state); }); const tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData.tooltipValues).toEqual([]); + expect(tooltipData.tooltip.values).toEqual([]); }); test('store is correctly configured', () => { @@ -299,13 +299,13 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(onPointerUpdateListener).toBeCalledTimes(1); const tooltipData1 = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData1.tooltipValues.length).toBe(2); + expect(tooltipData1.tooltip.values.length).toBe(1); // avoid calls store.dispatch(onPointerMove({ x: chartLeft + 12, y: chartTop + 12 }, 1)); expect(onPointerUpdateListener).toBeCalledTimes(1); const tooltipData2 = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData2.tooltipValues.length).toBe(2); + expect(tooltipData2.tooltip.values.length).toBe(1); expect(tooltipData1).toEqual(tooltipData2); }); @@ -385,7 +385,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { test('can hover top-left corner of the first bar', () => { let tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData.tooltipValues).toEqual([]); + expect(tooltipData.tooltip.values).toEqual([]); store.dispatch(onPointerMove({ x: chartLeft + 0, y: chartTop + 0 }, 0)); let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); expect(projectedPointerPosition).toEqual({ x: 0, y: 0 }); @@ -396,7 +396,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { let isTooltipVisible = isTooltipVisibleSelector(store.getState()); expect(isTooltipVisible).toBe(true); tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData.tooltipValues.length).toBe(2); // x value + 1 y value + expect(tooltipData.tooltip.values.length).toBe(1); expect(tooltipData.highlightedGeometries.length).toBe(1); expect(onOverListener).toBeCalledTimes(1); expect(onOutListener).toBeCalledTimes(0); @@ -423,7 +423,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { isTooltipVisible = isTooltipVisibleSelector(store.getState()); expect(isTooltipVisible).toBe(false); tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData.tooltipValues.length).toBe(0); + expect(tooltipData.tooltip.values.length).toBe(0); expect(tooltipData.highlightedGeometries.length).toBe(0); expect(onOverListener).toBeCalledTimes(1); expect(onOutListener).toBeCalledTimes(1); @@ -441,7 +441,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(isTooltipVisible).toBe(true); let tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); expect(tooltipData.highlightedGeometries.length).toBe(1); - expect(tooltipData.tooltipValues.length).toBe(2); // x value + 1 y value + expect(tooltipData.tooltip.values.length).toBe(1); expect(onOverListener).toBeCalledTimes(1); expect(onOutListener).toBeCalledTimes(0); expect(onOverListener.mock.calls[0][0]).toEqual([ @@ -466,7 +466,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { isTooltipVisible = isTooltipVisibleSelector(store.getState()); expect(isTooltipVisible).toBe(false); tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData.tooltipValues.length).toBe(0); + expect(tooltipData.tooltip.values.length).toBe(0); expect(tooltipData.highlightedGeometries.length).toBe(0); expect(onOverListener).toBeCalledTimes(1); expect(onOutListener).toBeCalledTimes(1); @@ -488,7 +488,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(isTooltipVisible).toBe(true); let tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); expect(tooltipData.highlightedGeometries.length).toBe(1); - expect(tooltipData.tooltipValues.length).toBe(2); + expect(tooltipData.tooltip.values.length).toBe(1); expect(onOverListener).toBeCalledTimes(1); expect(onOutListener).toBeCalledTimes(0); expect(onOverListener.mock.calls[0][0]).toEqual([ @@ -518,7 +518,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { isTooltipVisible = isTooltipVisibleSelector(store.getState()); expect(isTooltipVisible).toBe(true); tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData.tooltipValues.length).toBe(2); + expect(tooltipData.tooltip.values.length).toBe(1); expect(tooltipData.highlightedGeometries.length).toBe(0); expect(onOverListener).toBeCalledTimes(1); expect(onOutListener).toBeCalledTimes(1); @@ -540,7 +540,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(isTooltipVisible).toBe(true); let tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); expect(tooltipData.highlightedGeometries.length).toBe(1); - expect(tooltipData.tooltipValues.length).toBe(2); + expect(tooltipData.tooltip.values.length).toBe(1); expect(onOverListener).toBeCalledTimes(1); expect(onOutListener).toBeCalledTimes(0); expect(onOverListener.mock.calls[0][0]).toEqual([ @@ -570,7 +570,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { isTooltipVisible = isTooltipVisibleSelector(store.getState()); expect(isTooltipVisible).toBe(true); tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData.tooltipValues.length).toBe(2); + expect(tooltipData.tooltip.values.length).toBe(1); // we are over the second bar here expect(tooltipData.highlightedGeometries.length).toBe(1); expect(onOverListener).toBeCalledTimes(2); @@ -601,7 +601,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(onOutListener).toBeCalledTimes(0); let tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); expect(tooltipData.highlightedGeometries.length).toBe(0); - expect(tooltipData.tooltipValues.length).toBe(0); + expect(tooltipData.tooltip.values.length).toBe(0); store.dispatch(onPointerMove({ x: chartLeft + 89, y: chartTop + 0 }, 0)); const projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); @@ -615,7 +615,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(isTooltipVisible).toBe(true); tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); expect(tooltipData.highlightedGeometries.length).toBe(0); - expect(tooltipData.tooltipValues.length).toBe(2); + expect(tooltipData.tooltip.values.length).toBe(1); expect(onOverListener).toBeCalledTimes(0); expect(onOutListener).toBeCalledTimes(0); }); @@ -667,7 +667,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(isTooltipVisible).toBe(true); const tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); expect(tooltipData.highlightedGeometries.length).toBe(1); - expect(tooltipData.tooltipValues.length).toBe(2); + expect(tooltipData.tooltip.values.length).toBe(1); expect(onOverListener).toBeCalledTimes(1); expect(onOverListener.mock.calls[0][0]).toEqual([ [ @@ -744,8 +744,8 @@ function mouseOverTestSuite(scaleType: ScaleType) { test('chart 0 rotation', () => { store.dispatch(onPointerMove({ x: chartLeft + 0, y: chartTop + 89 }, 0)); const tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData.tooltipValues[0].value).toBe('bottom 0'); - expect(tooltipData.tooltipValues[1].value).toBe('left 10'); + expect(tooltipData.tooltip.header?.value).toBe('bottom 0'); + expect(tooltipData.tooltip.values[0].value).toBe('left 10'); }); test('chart 90 deg rotated', () => { @@ -758,8 +758,8 @@ function mouseOverTestSuite(scaleType: ScaleType) { store.dispatch(specParsed()); store.dispatch(onPointerMove({ x: chartLeft + 0, y: chartTop + 89 }, 0)); const tooltipData = getTooltipValuesAndGeometriesSelector(store.getState()); - expect(tooltipData.tooltipValues[0].value).toBe('left 1'); - expect(tooltipData.tooltipValues[1].value).toBe('bottom 5'); + expect(tooltipData.tooltip.header?.value).toBe('left 1'); + expect(tooltipData.tooltip.values[0].value).toBe('bottom 5'); }); }); describe('brush', () => { diff --git a/src/chart_types/xy_chart/state/chart_state.specs.test.ts b/src/chart_types/xy_chart/state/chart_state.specs.test.ts new file mode 100644 index 0000000000..be8dd55cf6 --- /dev/null +++ b/src/chart_types/xy_chart/state/chart_state.specs.test.ts @@ -0,0 +1,83 @@ +import { GlobalChartState, chartStoreReducer } from '../../../state/chart_state'; +import { createStore, Store } from 'redux'; +import { upsertSpec, specParsed, specParsing } from '../../../state/actions/specs'; +import { MockSeriesSpec } from '../../../mocks/specs'; +import { getLegendItemsSelector } from '../../../state/selectors/get_legend_items'; + +const data = [ + { x: 0, y: 10 }, + { x: 1, y: 10 }, +]; + +describe('XYChart - specs ordering', () => { + let store: Store; + beforeEach(() => { + const storeReducer = chartStoreReducer('chartId'); + store = createStore(storeReducer); + store.dispatch(specParsing()); + }); + + it('the legend respect the insert [A, B, C] order', () => { + store.dispatch(specParsing()); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'A', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'B', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'C', data }))); + store.dispatch(specParsed()); + + const legendItems = getLegendItemsSelector(store.getState()); + const labels = [...legendItems.values()].map((item) => item.label); + expect(labels).toEqual(['A', 'B', 'C']); + }); + it('the legend respect the insert order [B, A, C]', () => { + store.dispatch(specParsing()); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'B', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'A', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'C', data }))); + store.dispatch(specParsed()); + const legendItems = getLegendItemsSelector(store.getState()); + const labels = [...legendItems.values()].map((item) => item.label); + expect(labels).toEqual(['B', 'A', 'C']); + }); + it('the legend respect the order when changing properties of existing specs', () => { + store.dispatch(specParsing()); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'A', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'B', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'C', data }))); + store.dispatch(specParsed()); + + let legendItems = getLegendItemsSelector(store.getState()); + let labels = [...legendItems.values()].map((item) => item.label); + expect(labels).toEqual(['A', 'B', 'C']); + + store.dispatch(specParsing()); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'A', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'B', name: 'B updated', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'C', data }))); + store.dispatch(specParsed()); + + legendItems = getLegendItemsSelector(store.getState()); + labels = [...legendItems.values()].map((item) => item.label); + expect(labels).toEqual(['A', 'B updated', 'C']); + }); + it('the legend respect the order when changing the order of the specs', () => { + store.dispatch(specParsing()); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'A', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'B', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'C', data }))); + store.dispatch(specParsed()); + + let legendItems = getLegendItemsSelector(store.getState()); + let labels = [...legendItems.values()].map((item) => item.label); + expect(labels).toEqual(['A', 'B', 'C']); + + store.dispatch(specParsing()); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'B', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'A', data }))); + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'C', data }))); + store.dispatch(specParsed()); + + legendItems = getLegendItemsSelector(store.getState()); + labels = [...legendItems.values()].map((item) => item.label); + expect(labels).toEqual(['B', 'A', 'C']); + }); +}); diff --git a/src/chart_types/xy_chart/state/chart_state.timescales.test.ts b/src/chart_types/xy_chart/state/chart_state.timescales.test.ts index 83978d1b1f..677ec838f0 100644 --- a/src/chart_types/xy_chart/state/chart_state.timescales.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.timescales.test.ts @@ -69,20 +69,20 @@ describe('Render chart', () => { }); test('check mouse position correctly return inverted value', () => { store.dispatch(onPointerMove({ x: 15, y: 10 }, 0)); // check first valid tooltip - let tooltipData = getTooltipValuesSelector(store.getState()); - expect(tooltipData.length).toBe(2); // x value + y value - expect(tooltipData[0].value).toBe(day1); // x value - expect(tooltipData[1].value).toBe(10); // y value + let tooltip = getTooltipValuesSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(day1); + expect(tooltip.values[0].value).toBe(10); store.dispatch(onPointerMove({ x: 35, y: 10 }, 1)); // check second valid tooltip - tooltipData = getTooltipValuesSelector(store.getState()); - expect(tooltipData.length).toBe(2); // x value + y value - expect(tooltipData[0].value).toBe(day2); // x value - expect(tooltipData[1].value).toBe(22); // y value + tooltip = getTooltipValuesSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(day2); + expect(tooltip.values[0].value).toBe(22); store.dispatch(onPointerMove({ x: 76, y: 10 }, 2)); // check third valid tooltip - tooltipData = getTooltipValuesSelector(store.getState()); - expect(tooltipData.length).toBe(2); // x value + y value - expect(tooltipData[0].value).toBe(day3); // x value - expect(tooltipData[1].value).toBe(6); // y value + tooltip = getTooltipValuesSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(day3); + expect(tooltip.values[0].value).toBe(6); }); }); describe('line, utc-time, 5m interval', () => { @@ -138,20 +138,20 @@ describe('Render chart', () => { }); test('check mouse position correctly return inverted value', () => { store.dispatch(onPointerMove({ x: 15, y: 10 }, 0)); // check first valid tooltip - let tooltipData = getTooltipValuesSelector(store.getState()); - expect(tooltipData.length).toBe(2); // x value + y value - expect(tooltipData[0].value).toBe(date1); // x value - expect(tooltipData[1].value).toBe(10); // y value + let tooltip = getTooltipValuesSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date1); + expect(tooltip.values[0].value).toBe(10); store.dispatch(onPointerMove({ x: 35, y: 10 }, 1)); // check second valid tooltip - tooltipData = getTooltipValuesSelector(store.getState()); - expect(tooltipData.length).toBe(2); // x value + y value - expect(tooltipData[0].value).toBe(date2); // x value - expect(tooltipData[1].value).toBe(22); // y value + tooltip = getTooltipValuesSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date2); + expect(tooltip.values[0].value).toBe(22); store.dispatch(onPointerMove({ x: 76, y: 10 }, 2)); // check third valid tooltip - tooltipData = getTooltipValuesSelector(store.getState()); - expect(tooltipData.length).toBe(2); // x value + y value - expect(tooltipData[0].value).toBe(date3); // x value - expect(tooltipData[1].value).toBe(6); // y value + tooltip = getTooltipValuesSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date3); + expect(tooltip.values[0].value).toBe(6); }); }); describe('line, non utc-time, 5m + 1s interval', () => { @@ -225,20 +225,20 @@ describe('Render chart', () => { }); test('check mouse position correctly return inverted value', () => { store.dispatch(onPointerMove({ x: 15, y: 10 }, 0)); // check first valid tooltip - let tooltipData = getTooltipValuesSelector(store.getState()); - expect(tooltipData.length).toBe(2); // x value + y value - expect(tooltipData[0].value).toBe(date1); // x value - expect(tooltipData[1].value).toBe(10); // y value + let tooltip = getTooltipValuesSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date1); + expect(tooltip.values[0].value).toBe(10); store.dispatch(onPointerMove({ x: 35, y: 10 }, 1)); // check second valid tooltip - tooltipData = getTooltipValuesSelector(store.getState()); - expect(tooltipData.length).toBe(2); // x value + y value - expect(tooltipData[0].value).toBe(date2); // x value - expect(tooltipData[1].value).toBe(22); // y value + tooltip = getTooltipValuesSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date2); + expect(tooltip.values[0].value).toBe(22); store.dispatch(onPointerMove({ x: 76, y: 10 }, 2)); // check third valid tooltip - tooltipData = getTooltipValuesSelector(store.getState()); - expect(tooltipData.length).toBe(2); // x value + y value - expect(tooltipData[0].value).toBe(date3); // x value - expect(tooltipData[1].value).toBe(6); // y value + tooltip = getTooltipValuesSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date3); + expect(tooltip.values[0].value).toBe(6); }); }); }); diff --git a/src/chart_types/xy_chart/state/chart_state.tooltip.test.ts b/src/chart_types/xy_chart/state/chart_state.tooltip.test.ts index b03d06915e..c4c703f103 100644 --- a/src/chart_types/xy_chart/state/chart_state.tooltip.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.tooltip.test.ts @@ -28,12 +28,12 @@ describe('XYChart - State tooltips', () => { }); describe('should compute tooltip values depending on tooltip type', () => { - it.each<[TooltipType, number, number]>([ - [TooltipType.None, 0, 0], - [TooltipType.Follow, 1, 2], - [TooltipType.VerticalCursor, 1, 2], - [TooltipType.Crosshairs, 1, 2], - ])('tooltip type %s', (tooltipType, expectedHgeomsLength, expectedTooltipValuesLength) => { + it.each<[TooltipType, number, boolean, number]>([ + [TooltipType.None, 0, true, 0], + [TooltipType.Follow, 1, false, 1], + [TooltipType.VerticalCursor, 1, false, 1], + [TooltipType.Crosshairs, 1, false, 1], + ])('tooltip type %s', (tooltipType, expectedHgeomsLength, expectHeader, expectedTooltipValuesLength) => { store.dispatch(onPointerMove({ x: 25, y: 50 }, 0)); store.dispatch( upsertSpec( @@ -47,7 +47,8 @@ describe('XYChart - State tooltips', () => { store.dispatch(specParsed()); const state = store.getState(); const tooltipValues = getTooltipValuesAndGeometriesSelector(state); - expect(tooltipValues.tooltipValues).toHaveLength(expectedTooltipValuesLength); + expect(tooltipValues.tooltip.values).toHaveLength(expectedTooltipValuesLength); + expect(tooltipValues.tooltip.header === null).toBe(expectHeader); expect(tooltipValues.highlightedGeometries).toHaveLength(expectedHgeomsLength); }); }); diff --git a/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts b/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts index 5dbb58c6f5..d776fe18c8 100644 --- a/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts +++ b/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts @@ -1,7 +1,6 @@ import createCachedSelector from 're-reselect'; import { Dimensions } from '../../../../utils/dimensions'; import { Point } from '../../../../utils/point'; -import { TooltipValue } from '../../utils/interactions'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; import { AxisSpec, AnnotationSpec, Rotation, AnnotationTypes } from '../../utils/specs'; @@ -15,7 +14,7 @@ import { getChartRotationSelector } from '../../../../state/selectors/get_chart_ import { AnnotationId } from '../../../../utils/ids'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; import { ComputedGeometries } from '../utils'; -import { getTooltipValuesSelector } from './get_tooltip_values_highlighted_geoms'; +import { getTooltipValuesSelector, TooltipData } from './get_tooltip_values_highlighted_geoms'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { GlobalChartState } from '../../../../state/chart_state'; @@ -47,7 +46,7 @@ function getAnnotationTooltipState( annotationSpecs: AnnotationSpec[], axesSpecs: AxisSpec[], annotationDimensions: Map, - tooltipValues: TooltipValue[], + tooltip: TooltipData, ): AnnotationTooltipState | null { // get positions relative to chart if (x < 0 || y < 0) { @@ -70,7 +69,7 @@ function getAnnotationTooltipState( ); // If there's a highlighted chart element tooltip value, don't show annotation tooltip - const isChartTooltipDisplayed = tooltipValues.some(({ isHighlighted }) => isHighlighted); + const isChartTooltipDisplayed = tooltip.values.some(({ isHighlighted }) => isHighlighted); if ( tooltipState && tooltipState.isVisible && diff --git a/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts b/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts index 6c4e692ad4..4f580c3d05 100644 --- a/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts +++ b/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts @@ -5,7 +5,7 @@ import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; export const getLegendTooltipValuesSelector = createCachedSelector( [getTooltipValuesSelector], - (tooltipData): Map => { - return getSeriesTooltipValues(tooltipData); + ({ values }): Map => { + return getSeriesTooltipValues(values); }, )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts index 1b529e08bc..fc90708cc2 100644 --- a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts +++ b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts @@ -21,12 +21,19 @@ import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { hasSingleSeriesSelector } from './has_single_series'; const EMPTY_VALUES = Object.freeze({ - tooltipValues: [], + tooltip: { + header: null, + values: [], + }, highlightedGeometries: [], }); +export interface TooltipData { + header: TooltipValue | null; + values: TooltipValue[]; +} export interface TooltipAndHighlightedGeoms { - tooltipValues: TooltipValue[]; + tooltip: TooltipData; highlightedGeometries: IndexedGeometry[]; } @@ -84,7 +91,7 @@ function getTooltipAndHighlightFromXValue( } // build the tooltip value list - let xValueInfo: TooltipValue | null = null; + let tooltipHeader: TooltipValue | null = null; const highlightedGeometries: IndexedGeometry[] = []; const tooltipValues = xMatchingGeoms .filter(({ value: { y } }) => y !== null) @@ -134,27 +141,29 @@ function getTooltipAndHighlightFromXValue( ); // format only one time the x value - if (!xValueInfo) { + if (!tooltipHeader) { // if we have a tooltipHeaderFormatter, then don't pass in the xAxis as the user will define a formatter const xAxisFormatSpec = [0, 180].includes(chartRotation) ? xAxis : yAxis; const formatterAxis = tooltipHeaderFormatter ? undefined : xAxisFormatSpec; - xValueInfo = formatTooltip(indexedGeometry, spec, true, false, hasSingleSeries, formatterAxis); - return [xValueInfo, ...acc, formattedTooltip]; + tooltipHeader = formatTooltip(indexedGeometry, spec, true, false, hasSingleSeries, formatterAxis); } return [...acc, formattedTooltip]; }, []); return { - tooltipValues, + tooltip: { + header: tooltipHeader, + values: tooltipValues, + }, highlightedGeometries, }; } export const getTooltipValuesSelector = createCachedSelector( [getTooltipValuesAndGeometriesSelector], - (values): TooltipValue[] => { - return values.tooltipValues; + ({ tooltip }): TooltipData => { + return tooltip; }, )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts b/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts index 931919ac5a..d9a659b796 100644 --- a/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts +++ b/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts @@ -1,10 +1,10 @@ import createCachedSelector from 're-reselect'; -import { TooltipType, TooltipValue, isTooltipType, isTooltipProps } from '../../utils/interactions'; +import { TooltipType, isTooltipType, isTooltipProps } from '../../utils/interactions'; import { Point } from '../../../../utils/point'; import { GlobalChartState, PointerStates } from '../../../../state/chart_state'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; -import { getTooltipValuesSelector } from './get_tooltip_values_highlighted_geoms'; +import { getTooltipValuesSelector, TooltipData } from './get_tooltip_values_highlighted_geoms'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; const getTooltipType = (state: GlobalChartState): TooltipType | undefined => { @@ -30,13 +30,13 @@ function isTooltipVisible( tooltipType: TooltipType | undefined, pointer: PointerStates, projectedPointerPosition: Point, - tooltipValues: TooltipValue[], + tooltip: TooltipData, ) { return ( tooltipType !== TooltipType.None && pointer.down === null && projectedPointerPosition.x > -1 && projectedPointerPosition.y > -1 && - tooltipValues.length > 0 + tooltip.values.length > 0 ); } diff --git a/src/chart_types/xy_chart/tooltip/tooltip.ts b/src/chart_types/xy_chart/tooltip/tooltip.ts index b8b1d3f285..77ecaf985b 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -26,12 +26,7 @@ export function getSeriesTooltipValues( // map from seriesKey to TooltipLegendValue const seriesTooltipValues = new Map(); - // First TooltipLegendValue is the header - if (tooltipValues.length <= 1) { - return seriesTooltipValues; - } - - tooltipValues.slice(1).forEach(({ seriesKey, value, yAccessor }) => { + tooltipValues.forEach(({ seriesKey, value, yAccessor }) => { const seriesValue = defaultValue ? defaultValue : value; const current = seriesTooltipValues.get(seriesKey) || {}; diff --git a/src/specs/specs_parser.tsx b/src/specs/specs_parser.tsx index 8c4ae957e0..50ff4a21db 100644 --- a/src/specs/specs_parser.tsx +++ b/src/specs/specs_parser.tsx @@ -1,10 +1,11 @@ import React, { useEffect } from 'react'; import { bindActionCreators, Dispatch } from 'redux'; import { connect } from 'react-redux'; -import { specParsed, specUnmounted } from '../state/actions/specs'; +import { specParsing, specParsed, specUnmounted } from '../state/actions/specs'; export const SpecsParserComponent: React.FunctionComponent<{}> = (props) => { const injected = props as DispatchProps; + injected.specParsing(); useEffect(() => { injected.specParsed(); }); @@ -18,6 +19,7 @@ export const SpecsParserComponent: React.FunctionComponent<{}> = (props) => { }; interface DispatchProps { + specParsing: () => void; specParsed: () => void; specUnmounted: () => void; } @@ -25,6 +27,7 @@ interface DispatchProps { const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => bindActionCreators( { + specParsing, specParsed, specUnmounted, }, diff --git a/src/state/actions/specs.ts b/src/state/actions/specs.ts index 0bf622a9ea..23d8bc0a45 100644 --- a/src/state/actions/specs.ts +++ b/src/state/actions/specs.ts @@ -3,8 +3,13 @@ import { Spec } from '../../specs'; export const UPSERT_SPEC = 'UPSERT_SPEC'; export const REMOVE_SPEC = 'REMOVE_SPEC'; export const SPEC_PARSED = 'SPEC_PARSED'; +export const SPEC_PARSING = 'SPEC_PARSING'; export const SPEC_UNMOUNTED = 'SPEC_UNMOUNTED'; +interface SpecParsingAction { + type: typeof SPEC_PARSING; +} + interface SpecParsedAction { type: typeof SPEC_PARSED; } @@ -35,8 +40,17 @@ export function specParsed(): SpecParsedAction { return { type: SPEC_PARSED }; } +export function specParsing(): SpecParsingAction { + return { type: SPEC_PARSING }; +} + export function specUnmounted(): SpecUnmountedAction { return { type: SPEC_UNMOUNTED }; } -export type SpecActions = SpecParsedAction | SpecUnmountedAction | UpsertSpecAction | RemoveSpecAction; +export type SpecActions = + | SpecParsingAction + | SpecParsedAction + | SpecUnmountedAction + | UpsertSpecAction + | RemoveSpecAction; diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index ce5f3769aa..09cb75e537 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -1,4 +1,4 @@ -import { SPEC_PARSED, SPEC_UNMOUNTED, UPSERT_SPEC, REMOVE_SPEC } from './actions/specs'; +import { SPEC_PARSED, SPEC_UNMOUNTED, UPSERT_SPEC, REMOVE_SPEC, SPEC_PARSING } from './actions/specs'; import { interactionsReducer } from './reducers/interactions'; import { ChartTypes } from '../chart_types'; import { XYAxisChartState } from '../chart_types/xy_chart/state/chart_state'; @@ -142,6 +142,15 @@ export const chartStoreReducer = (chartId: string) => { const initialState = getInitialState(chartId); return (state = initialState, action: StateActions): GlobalChartState => { switch (action.type) { + case SPEC_PARSING: + return { + ...state, + specsInitialized: false, + chartRendered: false, + specs: { + [DEFAULT_SETTINGS_SPEC.id]: DEFAULT_SETTINGS_SPEC, + }, + }; case SPEC_PARSED: const chartType = findMainChartType(state.specs); @@ -150,7 +159,6 @@ export const chartStoreReducer = (chartId: string) => { return { ...state, specsInitialized: true, - chartRendered: false, chartType, internalChartState, }; @@ -158,7 +166,6 @@ export const chartStoreReducer = (chartId: string) => { return { ...state, specsInitialized: true, - chartRendered: false, chartType, }; } @@ -171,8 +178,6 @@ export const chartStoreReducer = (chartId: string) => { case UPSERT_SPEC: return { ...state, - specsInitialized: false, - chartRendered: false, specs: { ...state.specs, [action.spec.id]: action.spec, @@ -182,8 +187,6 @@ export const chartStoreReducer = (chartId: string) => { const { [action.id]: specToRemove, ...rest } = state.specs; return { ...state, - specsInitialized: false, - chartRendered: false, specs: { ...rest, }, From 2242fca6800995eaa8b7c4402cc8f82cad383c1c Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Fri, 14 Feb 2020 12:44:33 +0100 Subject: [PATCH 05/18] refactor: decouple legend and tooltip phase 1 (#491) Move `Datum`, `Rotation`, `Position` and `Color` to `utils/commons`. Decouple legend from axis position method and move the `scales` to `utils/scales`. --- .../layout/types/config_types.ts | 3 +- .../partition_chart/layout/types/types.ts | 2 - .../layout/types/viewmodel_types.ts | 3 +- .../partition_chart/specs/index.ts | 3 +- .../annotations/annotation_marker.test.tsx | 7 ++-- .../annotations/annotation_utils.test.ts | 7 +--- .../xy_chart/annotations/annotation_utils.ts | 5 +-- .../annotations/line_annotation_tooltip.ts | 5 +-- .../annotations/rect_annotation_tooltip.ts | 5 ++- .../crosshair_utils.linear_snap.test.ts | 2 +- .../crosshair_utils.ordinal_snap.test.ts | 2 +- .../xy_chart/crosshair/crosshair_utils.ts | 4 +- src/chart_types/xy_chart/domains/domain.ts | 2 +- .../xy_chart/domains/x_domain.test.ts | 2 +- src/chart_types/xy_chart/domains/x_domain.ts | 2 +- .../xy_chart/domains/y_domain.test.ts | 2 +- src/chart_types/xy_chart/domains/y_domain.ts | 2 +- .../xy_chart/legend/legend.test.ts | 5 ++- .../xy_chart/renderer/canvas/axes/line.ts | 2 +- .../xy_chart/renderer/canvas/axes/tick.ts | 2 +- .../xy_chart/renderer/canvas/axes/title.ts | 2 +- .../xy_chart/renderer/canvas/values/bar.ts | 2 +- .../xy_chart/renderer/canvas/xy_chart.tsx | 3 +- .../xy_chart/renderer/dom/crosshair.tsx | 2 +- .../xy_chart/renderer/dom/highlighter.tsx | 2 +- .../rendering/rendering.areas.test.ts | 2 +- .../rendering/rendering.bands.test.ts | 2 +- .../xy_chart/rendering/rendering.bars.test.ts | 2 +- .../rendering/rendering.lines.test.ts | 2 +- .../xy_chart/rendering/rendering.ts | 3 +- .../xy_chart/specs/area_series.tsx | 2 +- src/chart_types/xy_chart/specs/axis.tsx | 3 +- src/chart_types/xy_chart/specs/bar_series.tsx | 2 +- .../xy_chart/specs/histogram_bar_series.tsx | 2 +- .../xy_chart/specs/line_series.tsx | 2 +- .../state/chart_state.interactions.test.ts | 5 ++- .../xy_chart/state/chart_state.test.ts | 7 +--- .../state/chart_state.timescales.test.ts | 2 +- .../selectors/get_annotation_tooltip_state.ts | 3 +- .../state/selectors/get_cursor_band.ts | 2 +- .../get_tooltip_values_highlighted_geoms.ts | 3 +- .../state/selectors/is_brush_available.ts | 2 +- .../selectors/is_tooltip_snap_enabled.ts | 2 +- .../state/selectors/merge_y_custom_domains.ts | 3 +- .../state/selectors/on_pointer_move_caller.ts | 2 +- src/chart_types/xy_chart/state/utils.test.ts | 3 +- src/chart_types/xy_chart/state/utils.ts | 5 +-- .../xy_chart/tooltip/tooltip.test.ts | 5 ++- .../xy_chart/utils/axis_utils.test.ts | 5 ++- src/chart_types/xy_chart/utils/axis_utils.ts | 5 +-- .../xy_chart/utils/dimensions.test.ts | 3 +- src/chart_types/xy_chart/utils/dimensions.ts | 3 +- .../xy_chart/utils/fit_function.test.ts | 2 +- .../xy_chart/utils/fit_function.ts | 2 +- .../xy_chart/utils/interactions.ts | 3 +- .../utils/nonstacked_series_utils.test.ts | 2 +- .../xy_chart/utils/nonstacked_series_utils.ts | 2 +- src/chart_types/xy_chart/utils/scales.test.ts | 2 +- src/chart_types/xy_chart/utils/scales.ts | 4 +- src/chart_types/xy_chart/utils/series.test.ts | 2 +- src/chart_types/xy_chart/utils/series.ts | 4 +- src/chart_types/xy_chart/utils/specs.ts | 22 +--------- .../stacked_percent_series_utils.test.ts | 2 +- .../utils/stacked_series_utils.test.ts | 2 +- .../xy_chart/utils/stacked_series_utils.ts | 2 +- src/components/chart.tsx | 3 +- src/components/legend/legend.test.tsx | 2 +- src/components/legend/legend.tsx | 41 ++++++++----------- src/components/legend/legend_item.tsx | 2 +- src/index.ts | 8 ++-- src/mocks/scale/scale.ts | 2 +- src/mocks/specs/specs.ts | 5 +-- .../scales/scales.ts => scales/index.ts} | 27 ++++++------ src/{utils => }/scales/scale_band.test.ts | 2 +- src/{utils => }/scales/scale_band.ts | 4 +- .../scales/scale_continuous.test.ts | 10 ++--- src/{utils => }/scales/scale_continuous.ts | 10 ++--- src/{utils => }/scales/scale_time.test.ts | 3 +- src/{utils => }/scales/scales.test.ts | 3 +- src/specs/settings.test.tsx | 2 +- src/specs/settings.tsx | 7 ++-- src/state/selectors/get_chart_rotation.ts | 2 +- src/utils/accessor.ts | 2 +- src/utils/commons.ts | 21 ++++++++-- src/utils/domain.ts | 4 +- src/utils/events.ts | 2 +- stories/annotations.tsx | 2 +- stories/utils/utils.ts | 2 +- 88 files changed, 175 insertions(+), 202 deletions(-) rename src/{utils/scales/scales.ts => scales/index.ts} (68%) rename src/{utils => }/scales/scale_band.test.ts (99%) rename src/{utils => }/scales/scale_band.ts (96%) rename src/{utils => }/scales/scale_continuous.test.ts (98%) rename src/{utils => }/scales/scale_continuous.ts (97%) rename src/{utils => }/scales/scale_time.test.ts (99%) rename src/{utils => }/scales/scales.test.ts (99%) diff --git a/src/chart_types/partition_chart/layout/types/config_types.ts b/src/chart_types/partition_chart/layout/types/config_types.ts index 52d0f6bad7..b855f8dd8c 100644 --- a/src/chart_types/partition_chart/layout/types/config_types.ts +++ b/src/chart_types/partition_chart/layout/types/config_types.ts @@ -1,6 +1,7 @@ import { Distance, Pixels, Radian, Radius, Ratio, SizeRatio, TimeMs } from './geometry_types'; -import { Color, Font, FontFamily, PartialFont } from './types'; +import { Font, FontFamily, PartialFont } from './types'; import { $Values as Values } from 'utility-types'; +import { Color } from '../../../../utils/commons'; export const PartitionLayout = Object.freeze({ sunburst: 'sunburst', diff --git a/src/chart_types/partition_chart/layout/types/types.ts b/src/chart_types/partition_chart/layout/types/types.ts index daa713563e..c1003a412d 100644 --- a/src/chart_types/partition_chart/layout/types/types.ts +++ b/src/chart_types/partition_chart/layout/types/types.ts @@ -1,7 +1,5 @@ import { ArrayEntry } from '../utils/group_by_rollup'; -export type Color = string; // todo refine later (union type) - export const FONT_VARIANTS = Object.freeze(['normal', 'small-caps'] as const); export type FontVariant = typeof FONT_VARIANTS[number]; diff --git a/src/chart_types/partition_chart/layout/types/viewmodel_types.ts b/src/chart_types/partition_chart/layout/types/viewmodel_types.ts index 31f64dc984..03e2061630 100644 --- a/src/chart_types/partition_chart/layout/types/viewmodel_types.ts +++ b/src/chart_types/partition_chart/layout/types/viewmodel_types.ts @@ -1,8 +1,9 @@ import { Config } from './config_types'; import { Coordinate, Distance, PointObject, PointTuple, Radian } from './geometry_types'; -import { Color, Font } from './types'; +import { Font } from './types'; import { config } from '../config/config'; import { ArrayNode } from '../utils/group_by_rollup'; +import { Color } from '../../../../utils/commons'; export type LinkLabelVM = { link: [PointTuple, ...PointTuple[]]; // at least one point diff --git a/src/chart_types/partition_chart/specs/index.ts b/src/chart_types/partition_chart/specs/index.ts index 5a2bf655ad..36c35a48a1 100644 --- a/src/chart_types/partition_chart/specs/index.ts +++ b/src/chart_types/partition_chart/specs/index.ts @@ -5,8 +5,7 @@ import { getConnect, specComponentFactory } from '../../../state/spec_factory'; import { AccessorFn, IndexedAccessorFn } from '../../../utils/accessor'; import { Spec, SpecTypes } from '../../../specs/index'; import { Config, FillLabelConfig } from '../layout/types/config_types'; -import { RecursivePartial } from '../../../utils/commons'; -import { Datum } from '../../../utils/domain'; +import { RecursivePartial, Datum } from '../../../utils/commons'; type ColorAccessor = (d: Datum, index: number, array: Datum[]) => string; diff --git a/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx b/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx index c0da56e403..eb9f2dd9ce 100644 --- a/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx +++ b/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; - -import { AnnotationDomainTypes, AnnotationSpec, Position, Rotation, AnnotationTypes } from '../utils/specs'; +import { AnnotationDomainTypes, AnnotationSpec, AnnotationTypes } from '../utils/specs'; +import { Position, Rotation } from '../../../utils/commons'; import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../utils/themes/theme'; import { Dimensions } from '../../../utils/dimensions'; import { GroupId } from '../../../utils/ids'; -import { ScaleContinuous } from '../../../utils/scales/scale_continuous'; -import { Scale, ScaleType } from '../../../utils/scales/scales'; +import { Scale, ScaleType, ScaleContinuous } from '../../../scales'; import { computeLineAnnotationDimensions, AnnotationLineProps } from './line_annotation_tooltip'; import { ChartTypes } from '../..'; import { SpecTypes } from '../../../specs/settings'; diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.test.ts b/src/chart_types/xy_chart/annotations/annotation_utils.test.ts index 98541fcf8f..c34257e562 100644 --- a/src/chart_types/xy_chart/annotations/annotation_utils.test.ts +++ b/src/chart_types/xy_chart/annotations/annotation_utils.test.ts @@ -4,17 +4,14 @@ import { AnnotationSpec, AxisSpec, LineAnnotationSpec, - Position, RectAnnotationSpec, - Rotation, AnnotationTypes, } from '../utils/specs'; +import { Position, Rotation } from '../../../utils/commons'; import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../utils/themes/theme'; import { Dimensions } from '../../../utils/dimensions'; import { getAxisId, getGroupId, GroupId, AnnotationId } from '../../../utils/ids'; -import { ScaleBand } from '../../../utils/scales/scale_band'; -import { ScaleContinuous } from '../../../utils/scales/scale_continuous'; -import { Scale, ScaleType } from '../../../utils/scales/scales'; +import { Scale, ScaleType, ScaleBand, ScaleContinuous } from '../../../scales'; import { computeAnnotationDimensions, computeAnnotationTooltipState, diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.ts b/src/chart_types/xy_chart/annotations/annotation_utils.ts index 71b6f7bdd2..fd7463e263 100644 --- a/src/chart_types/xy_chart/annotations/annotation_utils.ts +++ b/src/chart_types/xy_chart/annotations/annotation_utils.ts @@ -7,12 +7,10 @@ import { HistogramModeAlignments, isLineAnnotation, isRectAnnotation, - Position, - Rotation, } from '../utils/specs'; import { Dimensions } from '../../../utils/dimensions'; import { AnnotationId, GroupId } from '../../../utils/ids'; -import { Scale, ScaleType } from '../../../utils/scales/scales'; +import { Scale, ScaleType } from '../../../scales'; import { computeXScaleOffset, getAxesSpecForSpecId, isHorizontalRotation, getSpecsById } from '../state/utils'; import { Point } from '../../../utils/point'; import { @@ -25,6 +23,7 @@ import { AnnotationRectProps, computeRectAnnotationDimensions, } from './rect_annotation_tooltip'; +import { Rotation, Position } from '../../../utils/commons'; export type AnnotationTooltipFormatter = (details?: string) => JSX.Element | null; diff --git a/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts b/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts index 6b681b98cc..82ace9d8f8 100644 --- a/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts +++ b/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts @@ -2,12 +2,11 @@ import { AnnotationDomainType, AnnotationDomainTypes, AnnotationTypes, - Position, - Rotation, LineAnnotationSpec, LineAnnotationDatum, AxisSpec, } from '../utils/specs'; +import { Position, Rotation } from '../../../utils/commons'; import { AnnotationTooltipState, AnnotationDetails, @@ -19,7 +18,7 @@ import { import { isHorizontalRotation, getAxesSpecForSpecId } from '../state/utils'; import { isHorizontalAxis } from '../utils/axis_utils'; import { Dimensions } from '../../../utils/dimensions'; -import { Scale } from '../../../utils/scales/scales'; +import { Scale } from '../../../scales'; import { GroupId } from '../../../utils/ids'; import { LineAnnotationStyle } from '../../../utils/themes/theme'; import { Point } from '../../../utils/point'; diff --git a/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts b/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts index 1794af0fa9..958aa10a68 100644 --- a/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts +++ b/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts @@ -1,7 +1,8 @@ -import { AnnotationTypes, RectAnnotationDatum, RectAnnotationSpec, Rotation } from '../utils/specs'; +import { AnnotationTypes, RectAnnotationDatum, RectAnnotationSpec } from '../utils/specs'; +import { Rotation } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; import { GroupId } from '../../../utils/ids'; -import { Scale } from '../../../utils/scales/scales'; +import { Scale } from '../../../scales'; import { Point } from '../../../utils/point'; import { AnnotationTooltipFormatter, diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts index 938926965a..d0c4dc4020 100644 --- a/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts @@ -2,7 +2,7 @@ import { computeXScale } from '../utils/scales'; import { BasicSeriesSpec, SeriesTypes } from '../utils/specs'; import { Dimensions } from '../../../utils/dimensions'; import { getGroupId, getSpecId } from '../../../utils/ids'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { getCursorBandPosition, getSnapPosition } from './crosshair_utils'; import { computeSeriesDomains } from '../state/utils'; import { ChartTypes } from '../..'; diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts index e9527a8ffa..e53d6f5e29 100644 --- a/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts @@ -1,7 +1,7 @@ import { computeXScale } from '../utils/scales'; import { BasicSeriesSpec, SeriesTypes } from '../utils/specs'; import { getGroupId, getSpecId } from '../../../utils/ids'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { getSnapPosition } from './crosshair_utils'; import { computeSeriesDomains } from '../state/utils'; import { ChartTypes } from '../..'; diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.ts index f1ccff6fd9..3852d8416b 100644 --- a/src/chart_types/xy_chart/crosshair/crosshair_utils.ts +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.ts @@ -1,6 +1,6 @@ -import { Rotation } from '../utils/specs'; +import { Rotation } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; -import { Scale } from '../../../utils/scales/scales'; +import { Scale } from '../../../scales'; import { isHorizontalRotation } from '../state/utils'; import { Point } from '../../../utils/point'; diff --git a/src/chart_types/xy_chart/domains/domain.ts b/src/chart_types/xy_chart/domains/domain.ts index 2278b352bf..c60c93017e 100644 --- a/src/chart_types/xy_chart/domains/domain.ts +++ b/src/chart_types/xy_chart/domains/domain.ts @@ -1,5 +1,5 @@ import { Domain } from '../../../utils/domain'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; export interface BaseDomain { scaleType: ScaleType; diff --git a/src/chart_types/xy_chart/domains/x_domain.test.ts b/src/chart_types/xy_chart/domains/x_domain.test.ts index ede1a8bcea..e32192bb84 100644 --- a/src/chart_types/xy_chart/domains/x_domain.test.ts +++ b/src/chart_types/xy_chart/domains/x_domain.test.ts @@ -1,4 +1,4 @@ -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { getSplittedSeries } from '../utils/series'; import { BasicSeriesSpec, SeriesTypes } from '../utils/specs'; import { convertXScaleTypes, findMinInterval, mergeXDomain } from './x_domain'; diff --git a/src/chart_types/xy_chart/domains/x_domain.ts b/src/chart_types/xy_chart/domains/x_domain.ts index f2aef2bd2f..5f8785994b 100644 --- a/src/chart_types/xy_chart/domains/x_domain.ts +++ b/src/chart_types/xy_chart/domains/x_domain.ts @@ -1,7 +1,7 @@ import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_utils'; import { compareByValueAsc, identity, isNumberArray } from '../../../utils/commons'; import { computeContinuousDataDomain, computeOrdinalDataDomain, Domain } from '../../../utils/domain'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { BasicSeriesSpec, DomainRange, SeriesTypes } from '../utils/specs'; import { BaseDomain } from './domain'; diff --git a/src/chart_types/xy_chart/domains/y_domain.test.ts b/src/chart_types/xy_chart/domains/y_domain.test.ts index ed0831b80a..3527b31ee2 100644 --- a/src/chart_types/xy_chart/domains/y_domain.test.ts +++ b/src/chart_types/xy_chart/domains/y_domain.test.ts @@ -1,4 +1,4 @@ -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { RawDataSeries } from '../utils/series'; import { BasicSeriesSpec, DomainRange, SeriesTypes } from '../utils/specs'; import { BARCHART_1Y0G } from '../../../utils/data_samples/test_dataset'; diff --git a/src/chart_types/xy_chart/domains/y_domain.ts b/src/chart_types/xy_chart/domains/y_domain.ts index cd630c1ed8..9f01c0f135 100644 --- a/src/chart_types/xy_chart/domains/y_domain.ts +++ b/src/chart_types/xy_chart/domains/y_domain.ts @@ -1,6 +1,6 @@ import { BasicSeriesSpec, DomainRange, DEFAULT_GLOBAL_ID, SeriesTypes } from '../utils/specs'; import { GroupId, SpecId, getGroupId } from '../../../utils/ids'; -import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales'; +import { ScaleContinuousType, ScaleType } from '../../../scales'; import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_utils'; import { BaseDomain } from './domain'; import { RawDataSeries } from '../utils/series'; diff --git a/src/chart_types/xy_chart/legend/legend.test.ts b/src/chart_types/xy_chart/legend/legend.test.ts index f05b317bd0..ea36a3497e 100644 --- a/src/chart_types/xy_chart/legend/legend.test.ts +++ b/src/chart_types/xy_chart/legend/legend.test.ts @@ -1,8 +1,9 @@ import { getAxisId, getGroupId, getSpecId } from '../../../utils/ids'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { computeLegend } from './legend'; import { SeriesCollectionValue, getSeriesLabel } from '../utils/series'; -import { AxisSpec, BasicSeriesSpec, Position, SeriesTypes } from '../utils/specs'; +import { AxisSpec, BasicSeriesSpec, SeriesTypes } from '../utils/specs'; +import { Position } from '../../../utils/commons'; import { ChartTypes } from '../..'; import { SpecTypes } from '../../../specs/settings'; diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/line.ts b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts index 0e285a572e..20112ed41c 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/line.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts @@ -1,6 +1,6 @@ import { isVerticalAxis } from '../../../utils/axis_utils'; import { AxisProps } from '.'; -import { Position } from '../../../utils/specs'; +import { Position } from '../../../../../utils/commons'; export function renderLine(ctx: CanvasRenderingContext2D, props: AxisProps) { const { diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts index 6feeb4a133..c70f2f03f2 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts @@ -1,6 +1,6 @@ import { AxisTick, isVerticalAxis } from '../../../utils/axis_utils'; import { AxisProps } from '.'; -import { Position } from '../../../utils/specs'; +import { Position } from '../../../../../utils/commons'; import { TickStyle } from '../../../../../utils/themes/theme'; import { renderLine, MIN_STROKE_WIDTH } from '../primitives/line'; import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts index 5787cdac48..a9ad66f5ac 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts @@ -2,7 +2,7 @@ import { AxisProps } from '.'; import { isHorizontalAxis } from '../../../utils/axis_utils'; import { renderDebugRect } from '../utils/debug'; import { renderText } from '../primitives/text'; -import { Position } from '../../../utils/specs'; +import { Position } from '../../../../../utils/commons'; import { Font, FontStyle } from '../../../../partition_chart/layout/types/types'; export function renderTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { diff --git a/src/chart_types/xy_chart/renderer/canvas/values/bar.ts b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts index 9f48cd3bb1..d43ae48403 100644 --- a/src/chart_types/xy_chart/renderer/canvas/values/bar.ts +++ b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts @@ -1,4 +1,4 @@ -import { Rotation } from '../../../utils/specs'; +import { Rotation } from '../../../../../utils/commons'; import { Dimensions } from '../../../../../utils/dimensions'; import { Theme } from '../../../../../utils/themes/theme'; import { BarGeometry } from '../../../../../utils/geometry'; diff --git a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index 65d4a71f9c..49646a7b9a 100644 --- a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -24,10 +24,11 @@ import { getHighlightedSeriesSelector } from '../../state/selectors/get_highligh import { getAnnotationSpecsSelector, getAxisSpecsSelector } from '../../state/selectors/get_specs'; import { Geometries, Transform } from '../../state/utils'; import { AxisLinePosition, AxisTicksDimensions } from '../../utils/axis_utils'; -import { AxisSpec, Rotation, AnnotationSpec } from '../../utils/specs'; +import { AxisSpec, AnnotationSpec } from '../../utils/specs'; import { renderXYChartCanvas2d } from './renderers'; import { isChartEmptySelector } from '../../state/selectors/is_chart_empty'; import { deepEqual } from '../../../../utils/fast_deep_equal'; +import { Rotation } from '../../../../utils/commons'; export interface ReactiveChartStateProps { initialized: boolean; diff --git a/src/chart_types/xy_chart/renderer/dom/crosshair.tsx b/src/chart_types/xy_chart/renderer/dom/crosshair.tsx index fac0424e9e..eef4518d06 100644 --- a/src/chart_types/xy_chart/renderer/dom/crosshair.tsx +++ b/src/chart_types/xy_chart/renderer/dom/crosshair.tsx @@ -4,7 +4,7 @@ import { TooltipType } from '../../utils/interactions'; import { isHorizontalRotation } from '../../state/utils'; import { Dimensions } from '../../../../utils/dimensions'; import { Theme } from '../../../../utils/themes/theme'; -import { Rotation } from '../../../../chart_types/xy_chart/utils/specs'; +import { Rotation } from '../../../../utils/commons'; import { GlobalChartState } from '../../../../state/chart_state'; import { isInitialized } from '../../../../state/selectors/is_initialized'; import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; diff --git a/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/src/chart_types/xy_chart/renderer/dom/highlighter.tsx index 21e4c3aeb2..4ffb245549 100644 --- a/src/chart_types/xy_chart/renderer/dom/highlighter.tsx +++ b/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -6,7 +6,7 @@ import { isInitialized } from '../../../../state/selectors/is_initialized'; import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; import { getHighlightedGeomsSelector } from '../../state/selectors/get_tooltip_values_highlighted_geoms'; import { Dimensions } from '../../../../utils/dimensions'; -import { Rotation } from '../../../../chart_types/xy_chart/utils/specs'; +import { Rotation } from '../../../../utils/commons'; import { Transform } from '../../state/utils'; import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; diff --git a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts index d720d447ef..c660ad60e6 100644 --- a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts @@ -1,4 +1,4 @@ -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { CurveType } from '../../../utils/curves'; import { IndexedGeometry, PointGeometry, AreaGeometry } from '../../../utils/geometry'; import { LIGHT_THEME } from '../../../utils/themes/light_theme'; diff --git a/src/chart_types/xy_chart/rendering/rendering.bands.test.ts b/src/chart_types/xy_chart/rendering/rendering.bands.test.ts index 1f9fa55501..3cff91dff8 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bands.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bands.test.ts @@ -1,5 +1,5 @@ import { computeSeriesDomains } from '../state/utils'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { CurveType } from '../../../utils/curves'; import { renderArea, renderBars } from './rendering'; import { computeXScale, computeYScales } from '../utils/scales'; diff --git a/src/chart_types/xy_chart/rendering/rendering.bars.test.ts b/src/chart_types/xy_chart/rendering/rendering.bars.test.ts index d9aa07feea..ad23ce4625 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bars.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bars.test.ts @@ -1,6 +1,6 @@ import { computeSeriesDomains } from '../state/utils'; import { identity } from '../../../utils/commons'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { renderBars } from './rendering'; import { computeXScale, computeYScales } from '../utils/scales'; import { BarSeriesSpec, DomainRange, SeriesTypes } from '../utils/specs'; diff --git a/src/chart_types/xy_chart/rendering/rendering.lines.test.ts b/src/chart_types/xy_chart/rendering/rendering.lines.test.ts index f89a96084c..697b2a5ee1 100644 --- a/src/chart_types/xy_chart/rendering/rendering.lines.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.lines.test.ts @@ -1,5 +1,5 @@ import { computeSeriesDomains } from '../state/utils'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { CurveType } from '../../../utils/curves'; import { renderLine } from './rendering'; import { computeXScale, computeYScales } from '../utils/scales'; diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index a30aae9bef..3d006d0937 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -8,8 +8,7 @@ import { BarSeriesStyle, GeometryStateStyle, } from '../../../utils/themes/theme'; -import { isLogarithmicScale } from '../../../utils/scales/scale_continuous'; -import { Scale, ScaleType } from '../../../utils/scales/scales'; +import { Scale, ScaleType, isLogarithmicScale } from '../../../scales'; import { CurveType, getCurveFactory } from '../../../utils/curves'; import { DataSeriesDatum, SeriesIdentifier, DataSeries } from '../utils/series'; import { DisplayValueSpec, PointStyleAccessor, BarStyleAccessor } from '../utils/specs'; diff --git a/src/chart_types/xy_chart/specs/area_series.tsx b/src/chart_types/xy_chart/specs/area_series.tsx index c4540ecf4a..1edadc49a5 100644 --- a/src/chart_types/xy_chart/specs/area_series.tsx +++ b/src/chart_types/xy_chart/specs/area_series.tsx @@ -1,5 +1,5 @@ import { AreaSeriesSpec, HistogramModeAlignments, DEFAULT_GLOBAL_ID, SeriesTypes } from '../utils/specs'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { specComponentFactory, getConnect } from '../../../state/spec_factory'; import { ChartTypes } from '../../../chart_types'; import { SpecTypes } from '../../../specs/settings'; diff --git a/src/chart_types/xy_chart/specs/axis.tsx b/src/chart_types/xy_chart/specs/axis.tsx index cda9cf5bdc..66d892751c 100644 --- a/src/chart_types/xy_chart/specs/axis.tsx +++ b/src/chart_types/xy_chart/specs/axis.tsx @@ -1,4 +1,5 @@ -import { AxisSpec, Position, DEFAULT_GLOBAL_ID } from '../utils/specs'; +import { AxisSpec, DEFAULT_GLOBAL_ID } from '../utils/specs'; +import { Position } from '../../../utils/commons'; import { ChartTypes } from '../../../chart_types'; import { specComponentFactory, getConnect } from '../../../state/spec_factory'; import { SpecTypes } from '../../../specs/settings'; diff --git a/src/chart_types/xy_chart/specs/bar_series.tsx b/src/chart_types/xy_chart/specs/bar_series.tsx index 51863fde66..a1a887e38d 100644 --- a/src/chart_types/xy_chart/specs/bar_series.tsx +++ b/src/chart_types/xy_chart/specs/bar_series.tsx @@ -1,5 +1,5 @@ import { BarSeriesSpec, DEFAULT_GLOBAL_ID, SeriesTypes } from '../utils/specs'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { ChartTypes } from '../../../chart_types'; import { specComponentFactory, getConnect } from '../../../state/spec_factory'; import { SpecTypes } from '../../../specs/settings'; diff --git a/src/chart_types/xy_chart/specs/histogram_bar_series.tsx b/src/chart_types/xy_chart/specs/histogram_bar_series.tsx index 00c6a8cd0f..c217566cf2 100644 --- a/src/chart_types/xy_chart/specs/histogram_bar_series.tsx +++ b/src/chart_types/xy_chart/specs/histogram_bar_series.tsx @@ -1,5 +1,5 @@ import { HistogramBarSeriesSpec, DEFAULT_GLOBAL_ID, SeriesTypes } from '../utils/specs'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { specComponentFactory, getConnect } from '../../../state/spec_factory'; import { ChartTypes } from '../../../chart_types'; import { SpecTypes } from '../../../specs/settings'; diff --git a/src/chart_types/xy_chart/specs/line_series.tsx b/src/chart_types/xy_chart/specs/line_series.tsx index 2d654b44d5..8bd029a074 100644 --- a/src/chart_types/xy_chart/specs/line_series.tsx +++ b/src/chart_types/xy_chart/specs/line_series.tsx @@ -1,5 +1,5 @@ import { LineSeriesSpec, DEFAULT_GLOBAL_ID, HistogramModeAlignments, SeriesTypes } from '../utils/specs'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { ChartTypes } from '../../../chart_types'; import { specComponentFactory, getConnect } from '../../../state/spec_factory'; import { SpecTypes } from '../../../specs/settings'; diff --git a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts index 15adffbed6..888c334603 100644 --- a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts @@ -1,7 +1,8 @@ import { createStore, Store } from 'redux'; -import { BarSeriesSpec, BasicSeriesSpec, AxisSpec, Position, SeriesTypes } from '../utils/specs'; +import { BarSeriesSpec, BasicSeriesSpec, AxisSpec, SeriesTypes } from '../utils/specs'; +import { Position } from '../../../utils/commons'; import { TooltipType } from '../utils/interactions'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { chartStoreReducer, GlobalChartState } from '../../../state/chart_state'; import { SettingsSpec, DEFAULT_SETTINGS_SPEC, SpecTypes } from '../../../specs'; import { computeSeriesGeometriesSelector } from './selectors/compute_series_geometries'; diff --git a/src/chart_types/xy_chart/state/chart_state.test.ts b/src/chart_types/xy_chart/state/chart_state.test.ts index 6a603c306c..cff92ad127 100644 --- a/src/chart_types/xy_chart/state/chart_state.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.test.ts @@ -4,15 +4,12 @@ import { AnnotationTypes, AxisSpec, BarSeriesSpec, - Position, RectAnnotationSpec, SeriesTypes, } from '../utils/specs'; +import { Position } from '../../../utils/commons'; import { TooltipType, TooltipValue } from '../utils/interactions'; -import { ScaleBand } from '../../../utils/scales/scale_band'; -import { ScaleContinuous } from '../../../utils/scales/scale_continuous'; -import { ScaleType } from '../../../utils/scales/scales'; -// import { ChartStore } from './chart_state'; +import { ScaleType, ScaleContinuous, ScaleBand } from '../../../scales'; import { IndexedGeometry, GeometryValue, BandedAccessorType } from '../../../utils/geometry'; import { AxisTicksDimensions, isDuplicateAxis } from '../utils/axis_utils'; import { AxisId } from '../../../utils/ids'; diff --git a/src/chart_types/xy_chart/state/chart_state.timescales.test.ts b/src/chart_types/xy_chart/state/chart_state.timescales.test.ts index 677ec838f0..d6c0540ed5 100644 --- a/src/chart_types/xy_chart/state/chart_state.timescales.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.timescales.test.ts @@ -1,5 +1,5 @@ import { LineSeriesSpec, SeriesTypes } from '../utils/specs'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { createStore, Store } from 'redux'; import { chartStoreReducer, GlobalChartState } from '../../../state/chart_state'; import { upsertSpec, specParsed } from '../../../state/actions/specs'; diff --git a/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts b/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts index d776fe18c8..d3dde59ab8 100644 --- a/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts +++ b/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts @@ -3,7 +3,8 @@ import { Dimensions } from '../../../../utils/dimensions'; import { Point } from '../../../../utils/point'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; -import { AxisSpec, AnnotationSpec, Rotation, AnnotationTypes } from '../../utils/specs'; +import { AxisSpec, AnnotationSpec, AnnotationTypes } from '../../utils/specs'; +import { Rotation } from '../../../../utils/commons'; import { computeAnnotationTooltipState, AnnotationTooltipState, diff --git a/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts b/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts index d708da0ab6..72eda830de 100644 --- a/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts +++ b/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts @@ -1,7 +1,7 @@ import { Dimensions } from '../../../../utils/dimensions'; import createCachedSelector from 're-reselect'; import { Point } from '../../../../utils/point'; -import { Scale } from '../../../../utils/scales/scales'; +import { Scale } from '../../../../scales'; import { isLineAreaOnlyChart } from '../utils'; import { getCursorBandPosition } from '../../crosshair/crosshair_utils'; import { SettingsSpec, PointerEvent } from '../../../../specs/settings'; diff --git a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts index fc90708cc2..99d50da730 100644 --- a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts +++ b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts @@ -8,7 +8,8 @@ import { getComputedScalesSelector } from './get_computed_scales'; import { getElementAtCursorPositionSelector } from './get_elements_at_cursor_pos'; import { IndexedGeometry } from '../../../../utils/geometry'; import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; -import { BasicSeriesSpec, AxisSpec, Rotation } from '../../utils/specs'; +import { BasicSeriesSpec, AxisSpec } from '../../utils/specs'; +import { Rotation } from '../../../../utils/commons'; import { getTooltipTypeSelector } from './get_tooltip_type'; import { formatTooltip } from '../../tooltip/tooltip'; import { getTooltipHeaderFormatterSelector } from './get_tooltip_header_formatter'; diff --git a/src/chart_types/xy_chart/state/selectors/is_brush_available.ts b/src/chart_types/xy_chart/state/selectors/is_brush_available.ts index 4c36e69418..1d6669e562 100644 --- a/src/chart_types/xy_chart/state/selectors/is_brush_available.ts +++ b/src/chart_types/xy_chart/state/selectors/is_brush_available.ts @@ -1,7 +1,7 @@ import createCachedSelector from 're-reselect'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { getComputedScalesSelector } from './get_computed_scales'; -import { ScaleType } from '../../../../utils/scales/scales'; +import { ScaleType } from '../../../../scales'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; /** diff --git a/src/chart_types/xy_chart/state/selectors/is_tooltip_snap_enabled.ts b/src/chart_types/xy_chart/state/selectors/is_tooltip_snap_enabled.ts index b1ef7e2ea2..89d55e65c2 100644 --- a/src/chart_types/xy_chart/state/selectors/is_tooltip_snap_enabled.ts +++ b/src/chart_types/xy_chart/state/selectors/is_tooltip_snap_enabled.ts @@ -1,6 +1,6 @@ import createCachedSelector from 're-reselect'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; -import { Scale } from '../../../../utils/scales/scales'; +import { Scale } from '../../../../scales'; import { getTooltipSnapSelector } from './get_tooltip_snap'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; diff --git a/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts b/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts index 40ff022f8e..c37c9b47fa 100644 --- a/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts +++ b/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts @@ -1,7 +1,8 @@ import createCachedSelector from 're-reselect'; import { getAxisSpecsSelector } from './get_specs'; import { isYDomain, isCompleteBound, isLowerBound, isUpperBound, isBounded } from '../../utils/axis_utils'; -import { AxisSpec, DomainRange, Rotation } from '../../utils/specs'; +import { AxisSpec, DomainRange } from '../../utils/specs'; +import { Rotation } from '../../../../utils/commons'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; diff --git a/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts b/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts index 3a0afaa8a2..42c08ee9aa 100644 --- a/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts +++ b/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts @@ -4,7 +4,7 @@ import { GlobalChartState } from '../../../../state/chart_state'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { SettingsSpec, PointerEvent, PointerEventType } from '../../../../specs'; import { ChartTypes } from '../../../index'; -import { Scale } from '../../../../utils/scales/scales'; +import { Scale } from '../../../../scales'; import { Point } from '../../../../utils/point'; import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; diff --git a/src/chart_types/xy_chart/state/utils.test.ts b/src/chart_types/xy_chart/state/utils.test.ts index aa7641cb1a..723049fff1 100644 --- a/src/chart_types/xy_chart/state/utils.test.ts +++ b/src/chart_types/xy_chart/state/utils.test.ts @@ -11,8 +11,7 @@ import { import { BARCHART_1Y0G, BARCHART_1Y1G } from '../../../utils/data_samples/test_dataset'; import { LIGHT_THEME } from '../../../utils/themes/light_theme'; import { SpecId } from '../../../utils/ids'; -import { ScaleContinuous } from '../../../utils/scales/scale_continuous'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType, ScaleContinuous } from '../../../scales'; import { computeSeriesDomains, computeSeriesGeometries, diff --git a/src/chart_types/xy_chart/state/utils.ts b/src/chart_types/xy_chart/state/utils.ts index 3073bcc9a0..2bc7ab826e 100644 --- a/src/chart_types/xy_chart/state/utils.ts +++ b/src/chart_types/xy_chart/state/utils.ts @@ -26,18 +26,17 @@ import { isBarSeriesSpec, isLineSeriesSpec, LineSeriesSpec, - Rotation, isBandedSpec, Fit, FitConfig, SeriesTypes, } from '../utils/specs'; import { ColorConfig, Theme } from '../../../utils/themes/theme'; -import { identity, mergePartial } from '../../../utils/commons'; +import { identity, mergePartial, Rotation } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; import { Domain } from '../../../utils/domain'; import { GroupId, SpecId } from '../../../utils/ids'; -import { Scale } from '../../../utils/scales/scales'; +import { Scale } from '../../../scales'; import { PointGeometry, BarGeometry, AreaGeometry, LineGeometry, IndexedGeometry } from '../../../utils/geometry'; import { LegendItem } from '../legend/legend'; import { Spec } from '../../../specs'; diff --git a/src/chart_types/xy_chart/tooltip/tooltip.test.ts b/src/chart_types/xy_chart/tooltip/tooltip.test.ts index d825b091b0..a43bc62da8 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.test.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.test.ts @@ -1,5 +1,6 @@ -import { ScaleType } from '../../../utils/scales/scales'; -import { AxisSpec, BarSeriesSpec, Position, SeriesTypes } from '../utils/specs'; +import { ScaleType } from '../../../scales'; +import { AxisSpec, BarSeriesSpec, SeriesTypes } from '../utils/specs'; +import { Position } from '../../../utils/commons'; import { formatTooltip } from './tooltip'; import { BarGeometry } from '../../../utils/geometry'; import { ChartTypes } from '../..'; diff --git a/src/chart_types/xy_chart/utils/axis_utils.test.ts b/src/chart_types/xy_chart/utils/axis_utils.test.ts index f90041dd1b..ea09f8e0ee 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -1,9 +1,10 @@ import { XDomain } from '../domains/x_domain'; import { YDomain } from '../domains/y_domain'; -import { AxisSpec, DomainRange, Position, AxisStyle } from './specs'; +import { AxisSpec, DomainRange, AxisStyle } from './specs'; +import { Position } from '../../../utils/commons'; import { LIGHT_THEME } from '../../../utils/themes/light_theme'; import { AxisId, GroupId } from '../../../utils/ids'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { AxisTick, AxisTicksDimensions, diff --git a/src/chart_types/xy_chart/utils/axis_utils.ts b/src/chart_types/xy_chart/utils/axis_utils.ts index 6a378a64d4..f607dd7e93 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.ts @@ -6,17 +6,16 @@ import { CompleteBoundedDomain, DomainRange, LowerBoundedDomain, - Position, - Rotation, TickFormatter, UpperBoundedDomain, AxisStyle, TickFormatterOptions, } from './specs'; +import { Position, Rotation } from '../../../utils/commons'; import { AxisConfig, Theme } from '../../../utils/themes/theme'; import { Dimensions, Margins } from '../../../utils/dimensions'; import { AxisId } from '../../../utils/ids'; -import { Scale } from '../../../utils/scales/scales'; +import { Scale } from '../../../scales'; import { BBox, BBoxCalculator } from '../../../utils/bbox/bbox_calculator'; import { getSpecsById } from '../state/utils'; diff --git a/src/chart_types/xy_chart/utils/dimensions.test.ts b/src/chart_types/xy_chart/utils/dimensions.test.ts index f180effc2a..4ad366cb70 100644 --- a/src/chart_types/xy_chart/utils/dimensions.test.ts +++ b/src/chart_types/xy_chart/utils/dimensions.test.ts @@ -1,5 +1,6 @@ import { AxisTicksDimensions } from './axis_utils'; -import { AxisSpec, Position } from './specs'; +import { AxisSpec } from './specs'; +import { Position } from '../../../utils/commons'; import { LIGHT_THEME } from '../../../utils/themes/light_theme'; import { LegendStyle } from '../../../utils/themes/theme'; import { computeChartDimensions } from './dimensions'; diff --git a/src/chart_types/xy_chart/utils/dimensions.ts b/src/chart_types/xy_chart/utils/dimensions.ts index 5a868a5171..04b2d0b583 100644 --- a/src/chart_types/xy_chart/utils/dimensions.ts +++ b/src/chart_types/xy_chart/utils/dimensions.ts @@ -1,5 +1,6 @@ import { AxisTicksDimensions } from './axis_utils'; -import { AxisSpec, Position } from './specs'; +import { AxisSpec } from './specs'; +import { Position } from '../../../utils/commons'; import { Theme } from '../../../utils/themes/theme'; import { AxisId } from '../../../utils/ids'; import { Dimensions } from '../../../utils/dimensions'; diff --git a/src/chart_types/xy_chart/utils/fit_function.test.ts b/src/chart_types/xy_chart/utils/fit_function.test.ts index 7701d5950b..9e500b345b 100644 --- a/src/chart_types/xy_chart/utils/fit_function.test.ts +++ b/src/chart_types/xy_chart/utils/fit_function.test.ts @@ -6,7 +6,7 @@ import { MockDataSeriesDatum, } from '../../../mocks'; import { Fit } from './specs'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { DataSeries } from './series'; import * as seriesUtils from './stacked_series_utils'; diff --git a/src/chart_types/xy_chart/utils/fit_function.ts b/src/chart_types/xy_chart/utils/fit_function.ts index 6e3f1bd818..b53e8f10e1 100644 --- a/src/chart_types/xy_chart/utils/fit_function.ts +++ b/src/chart_types/xy_chart/utils/fit_function.ts @@ -3,7 +3,7 @@ import { DeepNonNullable } from 'utility-types'; import { Fit, FitConfig } from './specs'; import { DataSeries, DataSeriesDatum } from './series'; import { datumXSortPredicate } from './stacked_series_utils'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; /** * Fit type that requires previous and/or next `non-nullable` values diff --git a/src/chart_types/xy_chart/utils/interactions.ts b/src/chart_types/xy_chart/utils/interactions.ts index 158d07da4e..73dff95d9a 100644 --- a/src/chart_types/xy_chart/utils/interactions.ts +++ b/src/chart_types/xy_chart/utils/interactions.ts @@ -1,9 +1,8 @@ import { $Values } from 'utility-types'; -import { Rotation } from './specs'; +import { Datum, Rotation } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; import { Accessor } from '../../../utils/accessor'; import { BarGeometry, PointGeometry, IndexedGeometry, isPointGeometry, isBarGeometry } from '../../../utils/geometry'; -import { Datum } from '../../../utils/domain'; /** The type of tooltip to use */ export const TooltipType = Object.freeze({ diff --git a/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts b/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts index 8ede0992d1..efb48b558e 100644 --- a/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts @@ -1,5 +1,5 @@ import { RawDataSeries } from './series'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { MockRawDataSeries, MockDataSeries } from '../../../mocks'; import { MockSeriesSpecs, MockSeriesSpec } from '../../../mocks/specs'; diff --git a/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts b/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts index a9e57dd946..7b28b25f69 100644 --- a/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts +++ b/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts @@ -1,7 +1,7 @@ import { DataSeries, DataSeriesDatum, RawDataSeries } from './series'; import { fitFunction } from './fit_function'; import { isAreaSeriesSpec, isLineSeriesSpec, SeriesSpecs, BasicSeriesSpec } from './specs'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { getSpecsById } from '../state/utils'; export const formatNonStackedDataSeriesValues = ( diff --git a/src/chart_types/xy_chart/utils/scales.test.ts b/src/chart_types/xy_chart/utils/scales.test.ts index 576b7ac596..05a42252ab 100644 --- a/src/chart_types/xy_chart/utils/scales.test.ts +++ b/src/chart_types/xy_chart/utils/scales.test.ts @@ -1,4 +1,4 @@ -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { XDomain } from '../domains/x_domain'; import { computeXScale, countBarsInCluster } from './scales'; import { FormattedDataSeries } from './series'; diff --git a/src/chart_types/xy_chart/utils/scales.ts b/src/chart_types/xy_chart/utils/scales.ts index bb7d22e167..78ff50fd3f 100644 --- a/src/chart_types/xy_chart/utils/scales.ts +++ b/src/chart_types/xy_chart/utils/scales.ts @@ -1,7 +1,5 @@ import { GroupId } from '../../../utils/ids'; -import { ScaleBand } from '../../../utils/scales/scale_band'; -import { ScaleContinuous } from '../../../utils/scales/scale_continuous'; -import { Scale, ScaleType } from '../../../utils/scales/scales'; +import { Scale, ScaleType, ScaleBand, ScaleContinuous } from '../../../scales'; import { XDomain } from '../domains/x_domain'; import { YDomain } from '../domains/y_domain'; import { FormattedDataSeries } from './series'; diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index 93d3f26098..b00f3aa8fc 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -1,5 +1,5 @@ import { ColorConfig } from '../../../utils/themes/theme'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { SeriesCollectionValue, getFormattedDataseries, diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index fb14f10fe3..40d9503d94 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -5,9 +5,9 @@ import { splitSpecsByGroupId, YBasicSeriesSpec } from '../domains/y_domain'; import { formatNonStackedDataSeriesValues } from './nonstacked_series_utils'; import { BasicSeriesSpec, SubSeriesStringPredicate, SeriesTypes, SeriesSpecs } from './specs'; import { formatStackedDataSeriesValues } from './stacked_series_utils'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; import { LastValues } from '../state/utils'; -import { Datum } from '../../../utils/domain'; +import { Datum } from '../../../utils/commons'; export interface FilledValues { /** the x value */ diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index aa8a77b0f4..026ee0fbc9 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -9,19 +9,15 @@ import { RectAnnotationStyle, } from '../../../utils/themes/theme'; import { Accessor, AccessorFormat } from '../../../utils/accessor'; -import { RecursivePartial } from '../../../utils/commons'; +import { RecursivePartial, Color, Position, Datum } from '../../../utils/commons'; import { AxisId, GroupId } from '../../../utils/ids'; -import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales'; +import { ScaleContinuousType, ScaleType } from '../../../scales'; import { CurveType } from '../../../utils/curves'; import { RawDataSeriesDatum, SeriesIdentifier } from './series'; import { AnnotationTooltipFormatter } from '../annotations/annotation_utils'; import { Spec, SpecTypes } from '../../..'; import { ChartTypes } from '../..'; -import { Datum } from '../../../utils/domain'; -export type Rotation = 0 | 90 | -90 | 180; -export type Rendering = 'canvas' | 'svg'; -export type Color = string; export type BarStyleOverride = RecursivePartial | Color | null; export type PointStyleOverride = RecursivePartial | Color | null; @@ -484,20 +480,6 @@ export interface AxisStyle { tickLabelPadding?: number; } -/** - * The position of the axis relative to the chart. - * A left or right positioned axis is a vertical axis. - * A top or bottom positioned axis is an horizontal axis. - */ -export const Position = Object.freeze({ - Top: 'top' as 'top', - Bottom: 'bottom' as 'bottom', - Left: 'left' as 'left', - Right: 'right' as 'right', -}); - -export type Position = $Values; - export const AnnotationTypes = Object.freeze({ Line: 'line' as 'line', Rectangle: 'rectangle' as 'rectangle', diff --git a/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts b/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts index b288a4730b..927acc1a77 100644 --- a/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts @@ -1,6 +1,6 @@ import { RawDataSeries } from './series'; import { formatStackedDataSeriesValues } from './stacked_series_utils'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; describe('Stacked Series Utils', () => { const STANDARD_DATA_SET: RawDataSeries[] = [ diff --git a/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts b/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts index 85fd02ed96..39a7bd309b 100644 --- a/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts @@ -1,6 +1,6 @@ import { RawDataSeries } from './series'; import { computeYStackedMapValues, formatStackedDataSeriesValues, getYValueStackMap } from './stacked_series_utils'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; describe('Stacked Series Utils', () => { const EMPTY_DATA_SET: RawDataSeries[] = [ diff --git a/src/chart_types/xy_chart/utils/stacked_series_utils.ts b/src/chart_types/xy_chart/utils/stacked_series_utils.ts index 0a3f02f125..f608de8b94 100644 --- a/src/chart_types/xy_chart/utils/stacked_series_utils.ts +++ b/src/chart_types/xy_chart/utils/stacked_series_utils.ts @@ -1,5 +1,5 @@ import { DataSeries, DataSeriesDatum, RawDataSeries, RawDataSeriesDatum, FilledValues } from './series'; -import { ScaleType } from '../../../utils/scales/scales'; +import { ScaleType } from '../../../scales'; interface StackedValues { values: number[]; diff --git a/src/components/chart.tsx b/src/components/chart.tsx index af2edba66d..d5a86145ef 100644 --- a/src/components/chart.tsx +++ b/src/components/chart.tsx @@ -3,13 +3,12 @@ import classNames from 'classnames'; import { Provider } from 'react-redux'; import { createStore, Store } from 'redux'; import uuid from 'uuid'; - import { SpecsParser } from '../specs/specs_parser'; import { ChartResizer } from './chart_resizer'; import { Legend } from './legend/legend'; import { ChartContainer } from './chart_container'; import { isHorizontalAxis } from '../chart_types/xy_chart/utils/axis_utils'; -import { Position } from '../chart_types/xy_chart/utils/specs'; +import { Position } from '../utils/commons'; import { ChartSize, getChartSize } from '../utils/chart_size'; import { ChartStatus } from './chart_status'; import { chartStoreReducer, GlobalChartState } from '../state/chart_state'; diff --git a/src/components/legend/legend.test.tsx b/src/components/legend/legend.test.tsx index fee9a175cf..68dd197d4f 100644 --- a/src/components/legend/legend.test.tsx +++ b/src/components/legend/legend.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { Chart } from '../chart'; import { Settings, BarSeries } from '../../specs'; -import { ScaleType } from '../../utils/scales/scales'; +import { ScaleType } from '../../scales'; import { DataGenerator } from '../../utils/data_generators/data_generator'; import { Legend } from './legend'; import { LegendListItem } from './legend_item'; diff --git a/src/components/legend/legend.tsx b/src/components/legend/legend.tsx index 27b7c910d1..7ea0c0bc06 100644 --- a/src/components/legend/legend.tsx +++ b/src/components/legend/legend.tsx @@ -1,8 +1,8 @@ import React, { createRef } from 'react'; import classNames from 'classnames'; -import { isVerticalAxis, isHorizontalAxis } from '../../chart_types/xy_chart/utils/axis_utils'; +import { Dispatch, bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { Position } from '../../chart_types/xy_chart/utils/specs'; +import { Position } from '../../utils/commons'; import { GlobalChartState } from '../../state/chart_state'; import { getLegendItemsSelector } from '../../state/selectors/get_legend_items'; import { getSettingsSpecSelector } from '../../state/selectors/get_settings_specs'; @@ -10,7 +10,6 @@ import { getChartThemeSelector } from '../../state/selectors/get_chart_theme'; import { getLegendItemsValuesSelector } from '../../state/selectors/get_legend_items_values'; import { getLegendSizeSelector } from '../../state/selectors/get_legend_size'; import { onToggleLegend } from '../../state/actions/legend'; -import { Dispatch, bindActionCreators } from 'redux'; import { LIGHT_THEME } from '../../utils/themes/light_theme'; import { LegendListItem } from './legend_item'; import { Theme } from '../../utils/themes/theme'; @@ -90,7 +89,7 @@ class LegendComponent extends React.Component { getLegendListStyle = (position: Position, { chartMargins, legend }: Theme): LegendListStyle => { const { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight } = chartMargins; - if (isHorizontalAxis(position)) { + if (position === Position.Bottom || position === Position.Top) { return { paddingLeft, paddingRight, @@ -105,7 +104,7 @@ class LegendComponent extends React.Component { }; getLegendStyle = (position: Position, size: BBox): LegendStyle => { - if (isVerticalAxis(position)) { + if (position === Position.Left || position === Position.Right) { const width = `${size.width}px`; return { width, @@ -175,31 +174,23 @@ const mapDispatchToProps = (dispatch: Dispatch): LegendDispatchProps => dispatch, ); +const EMPTY_DEFAULT_STATE = { + legendItems: new Map(), + legendPosition: Position.Right, + showLegend: false, + legendCollapsed: false, + legendItemTooltipValues: new Map(), + debug: false, + chartTheme: LIGHT_THEME, + legendSize: { width: 0, height: 0 }, +}; const mapStateToProps = (state: GlobalChartState): LegendStateProps => { if (!state.specsInitialized) { - return { - legendItems: new Map(), - legendPosition: Position.Right, - showLegend: false, - legendCollapsed: false, - legendItemTooltipValues: new Map(), - debug: false, - chartTheme: LIGHT_THEME, - legendSize: { width: 0, height: 0 }, - }; + return EMPTY_DEFAULT_STATE; } const { legendPosition, showLegend, debug } = getSettingsSpecSelector(state); if (!showLegend) { - return { - legendItems: new Map(), - legendPosition: Position.Right, - showLegend: false, - legendCollapsed: false, - legendItemTooltipValues: new Map(), - debug: false, - chartTheme: LIGHT_THEME, - legendSize: { width: 0, height: 0 }, - }; + return EMPTY_DEFAULT_STATE; } const legendItems = getLegendItemsSelector(state); return { diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index d982dcfeba..c9decc5e99 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -5,7 +5,7 @@ import { Icon } from '../icons/icon'; import { LegendItemListener, BasicListener } from '../../specs/settings'; import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; import { onLegendItemOutAction, onLegendItemOverAction } from '../../state/actions/legend'; -import { Position } from '../../chart_types/xy_chart/utils/specs'; +import { Position } from '../../utils/commons'; import { SeriesIdentifier } from '../../chart_types/xy_chart/utils/series'; interface LegendItemProps { diff --git a/src/index.ts b/src/index.ts index e7d04cf272..2e6d548c6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,9 @@ +import 'path2d-polyfill'; export * from './specs'; export { Chart } from './components/chart'; export { ChartSize, ChartSizeArray, ChartSizeObject } from './utils/chart_size'; export { SpecId, GroupId, AxisId, AnnotationId, getAxisId, getGroupId, getSpecId, getAnnotationId } from './utils/ids'; -export { ScaleType } from './utils/scales/scales'; +export { ScaleType } from './scales'; export * from './utils/themes/theme'; export { LIGHT_THEME } from './utils/themes/light_theme'; export { DARK_THEME } from './utils/themes/dark_theme'; @@ -10,11 +11,11 @@ export * from './utils/themes/theme_commons'; export { RecursivePartial } from './utils/commons'; export { CurveType } from './utils/curves'; export { timeFormatter, niceTimeFormatter, niceTimeFormatByDay } from './utils/data/formatters'; -import 'path2d-polyfill'; export { DataGenerator } from './utils/data_generators/data_generator'; export { SeriesCollectionValue } from './chart_types/xy_chart/utils/series'; export { ChartTypes } from './chart_types'; -export { Position, Rendering, Rotation, TickFormatter } from './chart_types/xy_chart/utils/specs'; +export { Datum, Position, Rendering, Rotation } from './utils/commons'; +export { TickFormatter } from './chart_types/xy_chart/utils/specs'; export { TooltipType, TooltipValue, TooltipValueFormatter } from './chart_types/xy_chart/utils/interactions'; export { SeriesIdentifier } from './chart_types/xy_chart/utils/series'; export { @@ -40,5 +41,4 @@ export { } from './chart_types/partition_chart/layout/types/config_types'; export { Layer as PartitionLayer } from './chart_types/partition_chart/specs/index'; export { AccessorFn, IndexedAccessorFn } from './utils/accessor'; -export { Datum } from './utils/domain'; export { SpecTypes } from './specs/settings'; diff --git a/src/mocks/scale/scale.ts b/src/mocks/scale/scale.ts index 386988db6a..afd36ef5b4 100644 --- a/src/mocks/scale/scale.ts +++ b/src/mocks/scale/scale.ts @@ -1,5 +1,5 @@ import { mergePartial } from '../../utils/commons'; -import { Scale, ScaleType } from '../../utils/scales/scales'; +import { Scale, ScaleType } from '../../scales'; export class MockScale { private static readonly base: Scale = { diff --git a/src/mocks/specs/specs.ts b/src/mocks/specs/specs.ts index 6f324422a9..572a8d7915 100644 --- a/src/mocks/specs/specs.ts +++ b/src/mocks/specs/specs.ts @@ -1,4 +1,4 @@ -import { mergePartial } from '../../utils/commons'; +import { mergePartial, Position } from '../../utils/commons'; import { SeriesSpecs, DEFAULT_GLOBAL_ID, @@ -9,10 +9,9 @@ import { LineSeriesSpec, BasicSeriesSpec, SeriesTypes, - Position, } from '../../chart_types/xy_chart/utils/specs'; import { getSpecId, getGroupId } from '../../utils/ids'; -import { ScaleType } from '../../utils/scales/scales'; +import { ScaleType } from '../../scales'; import { ChartTypes } from '../../chart_types'; import { SettingsSpec, SpecTypes } from '../../specs'; import { TooltipType } from '../../chart_types/xy_chart/utils/interactions'; diff --git a/src/utils/scales/scales.ts b/src/scales/index.ts similarity index 68% rename from src/utils/scales/scales.ts rename to src/scales/index.ts index f78c4cd8f7..814854fdf2 100644 --- a/src/utils/scales/scales.ts +++ b/src/scales/index.ts @@ -1,5 +1,10 @@ import { $Values } from 'utility-types'; +/** + * A `Scale` interface. A scale can map an input value within a specified domain + * to an output value from a specified range. + * The the value is mapped depending on the `type` (linear, log, sqrt, time, ordinal) + */ export interface Scale { domain: any[]; range: number[]; @@ -29,7 +34,6 @@ export interface Scale { isInverted: boolean; barsPadding: number; } -export type ScaleFunction = (value: any) => number; /** * The scale type @@ -44,17 +48,14 @@ export const ScaleType = Object.freeze({ export type ScaleType = $Values; -export interface ScaleConfig { - accessor: (value: any) => any; - domain: any[]; - type: ScaleType; - clamp?: boolean; -} +export type ScaleContinuousType = Exclude; -export type ScaleContinuousType = - | typeof ScaleType.Linear - | typeof ScaleType.Sqrt - | typeof ScaleType.Log - | typeof ScaleType.Time; export type ScaleOrdinalType = typeof ScaleType.Ordinal; -export type ScaleTypes = ScaleContinuousType | ScaleOrdinalType; + +export { ScaleBand } from './scale_band'; + +export { ScaleContinuous } from './scale_continuous'; + +export function isLogarithmicScale(scale: Scale) { + return scale.type === ScaleType.Log; +} diff --git a/src/utils/scales/scale_band.test.ts b/src/scales/scale_band.test.ts similarity index 99% rename from src/utils/scales/scale_band.test.ts rename to src/scales/scale_band.test.ts index 70b06f23e3..13cd045a89 100644 --- a/src/utils/scales/scale_band.test.ts +++ b/src/scales/scale_band.test.ts @@ -1,4 +1,4 @@ -import { ScaleBand } from './scale_band'; +import { ScaleBand } from '.'; describe('Scale Band', () => { it('shall clone domain and range arrays', () => { diff --git a/src/utils/scales/scale_band.ts b/src/scales/scale_band.ts similarity index 96% rename from src/utils/scales/scale_band.ts rename to src/scales/scale_band.ts index e8cf04f696..af620d8d9a 100644 --- a/src/utils/scales/scale_band.ts +++ b/src/scales/scale_band.ts @@ -1,6 +1,6 @@ import { scaleBand, scaleQuantize, ScaleQuantize } from 'd3-scale'; -import { clamp } from '../commons'; -import { ScaleType, Scale } from './scales'; +import { clamp } from '../utils/commons'; +import { ScaleType, Scale } from '.'; export class ScaleBand implements Scale { readonly bandwidth: number; diff --git a/src/utils/scales/scale_continuous.test.ts b/src/scales/scale_continuous.test.ts similarity index 98% rename from src/utils/scales/scale_continuous.test.ts rename to src/scales/scale_continuous.test.ts index 3443386891..e0aa0f953b 100644 --- a/src/utils/scales/scale_continuous.test.ts +++ b/src/scales/scale_continuous.test.ts @@ -1,10 +1,8 @@ -import { XDomain } from '../../chart_types/xy_chart/domains/x_domain'; -import { computeXScale } from '../../chart_types/xy_chart/utils/scales'; -import { Domain } from '../domain'; +import { XDomain } from '../chart_types/xy_chart/domains/x_domain'; +import { computeXScale } from '../chart_types/xy_chart/utils/scales'; +import { Domain } from '../utils/domain'; import { DateTime, Settings } from 'luxon'; -import { ScaleBand } from './scale_band'; -import { isLogarithmicScale, ScaleContinuous } from './scale_continuous'; -import { ScaleType } from './scales'; +import { ScaleContinuous, ScaleType, ScaleBand, isLogarithmicScale } from '.'; describe('Scale Continuous', () => { test('shall invert on continuous scale linear', () => { diff --git a/src/utils/scales/scale_continuous.ts b/src/scales/scale_continuous.ts similarity index 97% rename from src/utils/scales/scale_continuous.ts rename to src/scales/scale_continuous.ts index 0508a6d15e..9e7108c466 100644 --- a/src/utils/scales/scale_continuous.ts +++ b/src/scales/scale_continuous.ts @@ -10,9 +10,9 @@ import { ScaleTime, } from 'd3-scale'; -import { clamp, mergePartial } from '../commons'; -import { ScaleContinuousType, ScaleType, Scale } from './scales'; -import { getMomentWithTz } from '../data/date_time'; +import { clamp, mergePartial } from '../utils/commons'; +import { ScaleContinuousType, ScaleType, Scale } from '.'; +import { getMomentWithTz } from '../utils/data/date_time'; /** * d3 scales excluding time scale @@ -304,7 +304,3 @@ export class ScaleContinuous implements Scale { return value >= this.domain[0] && value <= this.domain[1]; } } - -export function isLogarithmicScale(scale: Scale) { - return scale.type === ScaleType.Log; -} diff --git a/src/utils/scales/scale_time.test.ts b/src/scales/scale_time.test.ts similarity index 99% rename from src/utils/scales/scale_time.test.ts rename to src/scales/scale_time.test.ts index 870f711a7c..2d45a6b7f5 100644 --- a/src/utils/scales/scale_time.test.ts +++ b/src/scales/scale_time.test.ts @@ -1,6 +1,5 @@ import { DateTime } from 'luxon'; -import { ScaleContinuous } from './scale_continuous'; -import { ScaleType } from './scales'; +import { ScaleContinuous, ScaleType } from '.'; describe('[Scale Time] - timezones', () => { describe('timezone checks', () => { diff --git a/src/utils/scales/scales.test.ts b/src/scales/scales.test.ts similarity index 99% rename from src/utils/scales/scales.test.ts rename to src/scales/scales.test.ts index 36fc568fbe..fc04d7f8ed 100644 --- a/src/utils/scales/scales.test.ts +++ b/src/scales/scales.test.ts @@ -1,7 +1,6 @@ import { DateTime } from 'luxon'; -import { ScaleBand } from './scale_band'; +import { ScaleType, ScaleBand } from '.'; import { limitLogScaleDomain, ScaleContinuous } from './scale_continuous'; -import { ScaleType } from './scales'; describe('Scale Test', () => { test('Create an ordinal scale', () => { diff --git a/src/specs/settings.test.tsx b/src/specs/settings.test.tsx index 6896479197..7c1180a0b7 100644 --- a/src/specs/settings.test.tsx +++ b/src/specs/settings.test.tsx @@ -1,6 +1,6 @@ import { mount } from 'enzyme'; import * as React from 'react'; -import { Position, Rendering, Rotation } from '../chart_types/xy_chart/utils/specs'; +import { Position, Rendering, Rotation } from '../utils/commons'; import { DARK_THEME } from '../utils/themes/dark_theme'; import { TooltipType } from '../chart_types/xy_chart/utils/interactions'; import { Settings, SettingsSpec } from './settings'; diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 72d2c615a7..388f4ed166 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -1,15 +1,16 @@ import { $Values } from 'utility-types'; -import { DomainRange, Position, Rendering, Rotation } from '../chart_types/xy_chart/utils/specs'; +import { DomainRange } from '../chart_types/xy_chart/utils/specs'; import { PartialTheme, Theme } from '../utils/themes/theme'; import { Domain } from '../utils/domain'; import { TooltipType, TooltipValueFormatter } from '../chart_types/xy_chart/utils/interactions'; -import { ScaleTypes } from '../utils/scales/scales'; import { getConnect, specComponentFactory } from '../state/spec_factory'; import { Spec } from '.'; import { LIGHT_THEME } from '../utils/themes/light_theme'; import { ChartTypes } from '../chart_types'; import { GeometryValue } from '../utils/geometry'; import { SeriesIdentifier } from '../chart_types/xy_chart/utils/series'; +import { Position, Rendering, Rotation } from '../utils/commons'; +import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; export type ElementClickListener = (elements: Array<[GeometryValue, SeriesIdentifier]>) => void; export type ElementOverListener = (elements: Array<[GeometryValue, SeriesIdentifier]>) => void; @@ -42,7 +43,7 @@ export interface BasePointerEvent { */ export interface PointerOverEvent extends BasePointerEvent { type: typeof PointerEventType.Over; - scale: ScaleTypes; + scale: ScaleContinuousType | ScaleOrdinalType; /** * @todo * unit for event (i.e. `time`, `feet`, `count`, etc.) diff --git a/src/state/selectors/get_chart_rotation.ts b/src/state/selectors/get_chart_rotation.ts index 219d72e48c..44c16ea635 100644 --- a/src/state/selectors/get_chart_rotation.ts +++ b/src/state/selectors/get_chart_rotation.ts @@ -1,7 +1,7 @@ import createCachedSelector from 're-reselect'; import { getSettingsSpecSelector } from './get_settings_specs'; -import { Rotation } from '../../chart_types/xy_chart/utils/specs'; +import { Rotation } from '../../utils/commons'; import { getChartIdSelector } from './get_chart_id'; export const getChartRotationSelector = createCachedSelector( diff --git a/src/utils/accessor.ts b/src/utils/accessor.ts index b1853fcdff..c889d15bca 100644 --- a/src/utils/accessor.ts +++ b/src/utils/accessor.ts @@ -1,4 +1,4 @@ -import { Datum } from './domain'; +import { Datum } from './commons'; type UnaryAccessorFn = (datum: Datum) => any; type BinaryAccessorFn = (datum: Datum, index: number) => any; diff --git a/src/utils/commons.ts b/src/utils/commons.ts index ed35146e58..db7e2983e0 100644 --- a/src/utils/commons.ts +++ b/src/utils/commons.ts @@ -1,4 +1,19 @@ -import * as uuid from 'uuid'; +import { v1 as uuidV1 } from 'uuid'; +import { $Values } from 'utility-types'; + +export type Datum = any; +export type Rotation = 0 | 90 | -90 | 180; +export type Rendering = 'canvas' | 'svg'; +export type Color = string; + +export const Position = Object.freeze({ + Top: 'top' as 'top', + Bottom: 'bottom' as 'bottom', + Left: 'left' as 'left', + Right: 'right' as 'right', +}); + +export type Position = $Values; export function identity(value: T): T { return value; @@ -20,8 +35,8 @@ export function clamp(value: number, min: number, max: number): number { * it should begin with an letter to be HTML4 compliant. */ export function htmlIdGenerator(idPrefix?: string) { - const prefix = idPrefix || `i${uuid.v1()}`; - return (suffix?: string) => `${prefix}_${suffix || uuid.v1()}`; + const prefix = idPrefix || `i${uuidV1()}`; + return (suffix?: string) => `${prefix}_${suffix || uuidV1()}`; } /** diff --git a/src/utils/domain.ts b/src/utils/domain.ts index c71224b15f..46cbfe43bd 100644 --- a/src/utils/domain.ts +++ b/src/utils/domain.ts @@ -1,7 +1,7 @@ import { extent, sum } from 'd3-array'; import { nest } from 'd3-collection'; import { Accessor, AccessorFn } from './accessor'; -import { ScaleType } from './scales/scales'; +import { ScaleType } from '../scales'; export type Domain = any[]; @@ -110,5 +110,3 @@ export function computeStackedContinuousDomain( const cumulativeSumAccessor = (d: any) => d.value; return computeContinuousDataDomain(groups, cumulativeSumAccessor, scaleToExtent); } - -export type Datum = any; diff --git a/src/utils/events.ts b/src/utils/events.ts index 2dbd31d7f8..c268b48ee4 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -1,5 +1,5 @@ import { PointerEvent, isPointerOverEvent, PointerOverEvent } from '../specs'; -import { Scale } from './scales/scales'; +import { Scale } from '../scales'; export function isValidPointerOverEvent( mainScale: Scale, diff --git a/stories/annotations.tsx b/stories/annotations.tsx index ca34520c40..2d09cc27de 100644 --- a/stories/annotations.tsx +++ b/stories/annotations.tsx @@ -21,7 +21,7 @@ import { Icon } from '../src/components/icons/icon'; import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana'; import { getChartRotationKnob, arrayKnobs } from './common'; import { BandedAccessorType } from '../src/utils/geometry'; -import { Position } from '../src/chart_types/xy_chart/utils/specs'; +import { Position } from '../src/utils/commons'; const dateFormatter = timeFormatter('HH:mm:ss'); diff --git a/stories/utils/utils.ts b/stories/utils/utils.ts index 1d45847672..3003841db8 100644 --- a/stories/utils/utils.ts +++ b/stories/utils/utils.ts @@ -1,6 +1,6 @@ import { arrayToLookup, hueInterpolator } from '../../src/chart_types/partition_chart/layout/utils/calcs'; import { palettes } from '../../src/mocks/hierarchical/palettes'; -import { Datum } from '../../src/utils/domain'; +import { Datum } from '../../src/utils/commons'; import { countryDimension, productDimension, regionDimension } from '../../src/mocks/hierarchical/dimension_codes'; export const productLookup = arrayToLookup((d: Datum) => d.sitc1, productDimension); From 6546553a6f0ac57d6818963c527635f7121738d7 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Fri, 21 Feb 2020 14:28:32 -0600 Subject: [PATCH 06/18] refactor: color picker display logic --- .playground/playground.tsx | 82 +++++++++------- package.json | 1 + src/components/chart.tsx | 10 +- .../legend/__snapshots__/legend.test.tsx.snap | 8 +- src/components/legend/legend.test.tsx | 95 ++++++++++++------- src/components/legend/legend.tsx | 4 +- src/components/legend/legend_item.tsx | 72 ++++++-------- src/specs/settings.tsx | 8 +- yarn.lock | 7 ++ 9 files changed, 166 insertions(+), 121 deletions(-) diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 3201f53cca..78ea377cb7 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -1,42 +1,58 @@ import React from 'react'; -import { Chart, ScaleType, Settings, BarSeries, DataGenerator } from '../src'; -import { RenderColorPicker } from '../src/components/legend/legend_item'; +import { Chart, ScaleType, Settings, BarSeries, DataGenerator, LegendColorPickerFn, Axis } from '../src'; + +const dg = new DataGenerator(); export class Playground extends React.Component<{}, { isSunburstShown: boolean }> { - render() { - const dg = new DataGenerator(); - const data = dg.generateGroupedSeries(10, 4, 'split'); - const renderColorPicker: RenderColorPicker = (onChange, onClose, isOpen, button) => - isOpen ? ( -
- Custom Color Picker - - - {button} -
- ) : ( - { button } - ); + state: any = { + colors: ['red'], + }; + data = dg.generateGroupedSeries(10, 4, 'split'); + customColor = '#0c7b93'; + container?: HTMLDivElement; + + legendColorPickerFn: LegendColorPickerFn = (anchor, onClose) => { return ( -
- - - - +
+ Custom Color Picker + +
); + }; + + render() { + return ( + <> +
+ + + + + + +
+ + ); } } diff --git a/package.json b/package.json index ae13fde770..7a2f93406d 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@babel/preset-react": "^7.8.3", "@commitlint/cli": "^8.1.0", "@commitlint/config-conventional": "^8.1.0", + "@elastic/datemath": "^5.0.2", "@elastic/eui": "^16.0.1", "@mdx-js/loader": "^1.5.5", "@semantic-release/changelog": "^3.0.6", diff --git a/src/components/chart.tsx b/src/components/chart.tsx index d5a86145ef..5c702bc249 100644 --- a/src/components/chart.tsx +++ b/src/components/chart.tsx @@ -1,7 +1,7 @@ import React, { CSSProperties, createRef } from 'react'; import classNames from 'classnames'; import { Provider } from 'react-redux'; -import { createStore, Store } from 'redux'; +import { createStore, Store, Unsubscribe } from 'redux'; import uuid from 'uuid'; import { SpecsParser } from '../specs/specs_parser'; import { ChartResizer } from './chart_resizer'; @@ -51,6 +51,7 @@ export class Chart extends React.Component { static defaultProps: ChartProps = { renderer: 'canvas', }; + private unsubscribeToStore: Unsubscribe; private chartStore: Store; private chartContainerRef: React.RefObject; private chartStageRef: React.RefObject; @@ -77,11 +78,12 @@ export class Chart extends React.Component { const onElementOutCaller = createOnElementOutCaller(); const onBrushEndCaller = createOnBrushEndCaller(); const onPointerMoveCaller = createOnPointerMoveCaller(); - this.chartStore.subscribe(() => { + this.unsubscribeToStore = this.chartStore.subscribe(() => { const state = this.chartStore.getState(); if (!isInitialized(state)) { return; } + const settings = getSettingsSpecSelector(state); if (this.state.legendPosition !== settings.legendPosition) { this.setState({ @@ -100,6 +102,10 @@ export class Chart extends React.Component { }); } + componentWillUnmount() { + this.unsubscribeToStore(); + } + dispatchExternalPointerEvent(event: PointerEvent) { this.chartStore.dispatch(onExternalPointerEvent(event)); } diff --git a/src/components/legend/__snapshots__/legend.test.tsx.snap b/src/components/legend/__snapshots__/legend.test.tsx.snap index 4cb65e293e..f397578099 100644 --- a/src/components/legend/__snapshots__/legend.test.tsx.snap +++ b/src/components/legend/__snapshots__/legend.test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Legend #renderColorPicker should match snapshot after onChange is called 1`] = `"
splita
3.80782526887084
splitb
7.1961229105542746
splitc
4.847071810176317
splitd
7.310527355059277
"`; +exports[`Legend #legendColorPicker should match snapshot after onChange is called 1`] = `"
splita
splitb
splitc
splitd
"`; -exports[`Legend #renderColorPicker should match snapshot after onClose is called 1`] = `"
splita
3.80782526887084
splitb
7.1961229105542746
splitc
4.847071810176317
splitd
7.310527355059277
"`; +exports[`Legend #legendColorPicker should match snapshot after onClose is called 1`] = `"
splita
splitb
splitc
splitd
"`; -exports[`Legend #renderColorPicker should render colorPicker when color is clicked 1`] = `"
Custom Color Picker
splita
3.80782526887084
"`; +exports[`Legend #legendColorPicker should render colorPicker when color is clicked 1`] = `"
Custom Color Picker
"`; -exports[`Legend #renderColorPicker should render colorPicker when color is clicked 2`] = `"
Custom Color Picker
splita
3.80782526887084
splitb
7.1961229105542746
splitc
4.847071810176317
splitd
7.310527355059277
"`; +exports[`Legend #legendColorPicker should render colorPicker when color is clicked 2`] = `"
splita
Custom Color Picker
splitb
splitc
splitd
"`; diff --git a/src/components/legend/legend.test.tsx b/src/components/legend/legend.test.tsx index 6f5ed8c1ca..e85233932b 100644 --- a/src/components/legend/legend.test.tsx +++ b/src/components/legend/legend.test.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { Component } from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { Chart } from '../chart'; -import { Settings, BarSeries } from '../../specs'; +import { Settings, BarSeries, LegendColorPickerFn } from '../../specs'; import { ScaleType } from '../../scales'; import { Legend } from './legend'; -import { LegendListItem, RenderColorPicker } from './legend_item'; +import { LegendListItem } from './legend_item'; import { SeededDataGenerator } from '../../mocks/utils'; const dg = new SeededDataGenerator(); @@ -132,42 +132,67 @@ describe('Legend', () => { }); }); - describe('#renderColorPicker', () => { + describe('#legendColorPicker', () => { + class LegendColorPickerMock extends Component< + { onLegendItemClick: () => void; customColor: string }, + { colors: string[] } + > { + state = { + colors: ['red'], + }; + + pickerElement = document.createElement('div'); + data = dg.generateGroupedSeries(10, 4, 'split'); + + legendColorPickerFn: LegendColorPickerFn = (anchor, onClose) => { + return ( +
+ Custom Color Picker + + +
+ ); + }; + + render() { + return ( + + + + + ); + } + } + let wrapper: ReactWrapper; const customColor = '#0c7b93'; const onLegendItemClick = jest.fn(); - const data = dg.generateGroupedSeries(10, 4, 'split'); - const renderColorPicker: RenderColorPicker = (onChange, onClose, isOpen, button) => - isOpen ? ( -
- Custom Color Picker - - - {button} -
- ) : ( - { button } - ); beforeEach(() => { - wrapper = mount( - - - - , - ); + wrapper = mount(); }); const clickFirstColor = () => { @@ -243,7 +268,7 @@ describe('Legend', () => { expect(legendItems).toHaveLength(4); legendItems.forEach((legendItem, i) => { // toggle click is only enabled on the title - legendItem.find('.echLegendItem__title').simulate('click'); + legendItem.find('.echLegendItem__label').simulate('click'); expect(onLegendItemClick).toBeCalledTimes(i + 1); }); }); diff --git a/src/components/legend/legend.tsx b/src/components/legend/legend.tsx index 661d2e4ffe..1a9b315abb 100644 --- a/src/components/legend/legend.tsx +++ b/src/components/legend/legend.tsx @@ -138,7 +138,7 @@ class LegendComponent extends React.Component { } const { key, displayValue, banded } = item; const { legendItemTooltipValues, settings } = this.props; - const { showLegendExtra, legendPosition, renderColorPicker } = settings; + const { showLegendExtra, legendPosition, legendColorPicker } = settings; const legendValues = this.getLegendValues(legendItemTooltipValues, key, banded); return legendValues.map((value, index) => { const yAccessor: BandedAccessorType = index === 0 ? BandedAccessorType.Y1 : BandedAccessorType.Y0; @@ -146,7 +146,7 @@ class LegendComponent extends React.Component { void, - onClose: () => void, - isOpen: boolean, - button: NonNullable, -) => ReactNode; - interface LegendItemProps { legendItem: LegendItem; extra: string; label?: string; legendPosition: Position; - renderColorPicker?: RenderColorPicker; showExtra: boolean; + legendColorPicker?: LegendColorPickerFn; onLegendItemClickListener?: LegendItemListener; onLegendItemOutListener?: BasicListener; onLegendItemOverListener?: LegendItemListener; @@ -72,11 +65,11 @@ function renderLabel( interface LegendItemState { isOpen: boolean; - color?: string; } export class LegendListItem extends Component { static displayName = 'LegendItem'; + ref = createRef(); state: LegendItemState = { isOpen: false, @@ -97,7 +90,7 @@ export class LegendListItem extends Component } if (isSeriesVisible) { - const changable = Boolean(this.props.renderColorPicker); + const changable = Boolean(this.props.legendColorPicker); const colorClasses = classNames('echLegendItem__color', { 'echLegendItem__color--changable': changable, }); @@ -108,7 +101,7 @@ export class LegendListItem extends Component changable ? (e) => { e.stopPropagation(); - this.setToggleIsOpen(); + this.toggleIsOpen(); } : undefined } @@ -129,48 +122,43 @@ export class LegendListItem extends Component ); }; - renderItem = () => { + renderColorPicker() { + const { legendColorPicker } = this.props; + if (legendColorPicker && this.state.isOpen && this.ref.current) { + return legendColorPicker(this.ref.current, this.toggleIsOpen); + } + } + + render() { const { extra, legendItem, legendPosition, label, showExtra, onLegendItemClickListener } = this.props; const { color, isSeriesVisible, seriesIdentifier, isLegendItemVisible } = legendItem; const onLabelClick = this.onVisibilityClick(seriesIdentifier); const hasLabelClickListener = Boolean(onLegendItemClickListener); const itemClassNames = classNames('echLegendItem', `echLegendItem--${legendPosition}`, { - 'echLegendItem--isHidden': !isSeriesVisible, + 'echLegendItem--hidden': !isSeriesVisible, 'echLegendItem__extra--hidden': !isLegendItemVisible, }); return ( -
- {this.renderColor(this.state.color ?? color, isSeriesVisible)} - {renderLabel(onLabelClick, hasLabelClickListener, label)} - {showExtra && renderExtra(extra, isSeriesVisible)} -
+ <> +
+ {this.renderColor(color, isSeriesVisible)} + {renderLabel(onLabelClick, hasLabelClickListener, label)} + {showExtra && renderExtra(extra, isSeriesVisible)} +
+ {this.renderColorPicker()} + ); - }; - - render() { - const { renderColorPicker } = this.props; - if (renderColorPicker && this.state.isOpen) { - return renderColorPicker(this.onColorChange, this.setToggleIsOpen, this.state.isOpen, this.renderItem()); - } - - return this.renderItem(); } - setToggleIsOpen = () => { - this.setState({ isOpen: !this.state.isOpen }); - }; - - onColorChange = (color: string) => { - this.setState({ - color, - isOpen: !this.state.isOpen, - }); + toggleIsOpen = () => { + this.setState(({ isOpen }) => ({ isOpen: !isOpen })); }; onLegendItemMouseOver = () => { diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index b98efc7843..75cb80c558 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -7,11 +7,11 @@ import { Spec } from '.'; import { LIGHT_THEME } from '../utils/themes/light_theme'; import { ChartTypes } from '../chart_types'; import { GeometryValue } from '../utils/geometry'; -import { RenderColorPicker } from '../components/legend/legend_item'; import { XYChartSeriesIdentifier, SeriesIdentifier } from '../chart_types/xy_chart/utils/series'; +import { Accessor } from '../utils/accessor'; import { Position, Rendering, Rotation } from '../utils/commons'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; -import { Accessor } from '../utils/accessor'; +import { ReactNode } from 'react'; export type ElementClickListener = (elements: Array<[GeometryValue, XYChartSeriesIdentifier]>) => void; export type ElementOverListener = (elements: Array<[GeometryValue, XYChartSeriesIdentifier]>) => void; @@ -112,6 +112,8 @@ export interface TooltipProps { unit?: string; } +export type LegendColorPickerFn = (anchor: HTMLDivElement, onClose: () => void) => ReactNode; + export interface SettingsSpec extends Spec { /** * Partial theme to be merged with base @@ -162,7 +164,7 @@ export interface SettingsSpec extends Spec { onRenderChange?: RenderChangeListener; xDomain?: Domain | DomainRange; resizeDebounce?: number; - renderColorPicker?: RenderColorPicker; + legendColorPicker?: LegendColorPickerFn; } export type DefaultSettingsProps = diff --git a/yarn.lock b/yarn.lock index 0aa8f9cf49..bfe647fe48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2670,6 +2670,13 @@ resolved "https://registry.yarnpkg.com/@egoist/vue-to-react/-/vue-to-react-1.1.0.tgz#83c884b8608e8ee62e76c03e91ce9c26063a91ad" integrity sha512-MwfwXHDh6ptZGLEtNLPXp2Wghteav7mzpT2Mcwl3NZWKF814i5hhHnNkVrcQQEuxUroSWQqzxLkMKSb+nhPang== +"@elastic/datemath@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@elastic/datemath/-/datemath-5.0.2.tgz#1e62fe7137acd6ebcde9a794ef22b91820c9e6cf" + integrity sha512-MYU7KedGPMYu3ljgrO3tY8I8rD73lvBCltd78k5avDIv/6gMbuhKXsMhkEPbb9angs9hR/2ADk0QcGbVxUBXUw== + dependencies: + tslib "^1.9.3" + "@elastic/eui@^16.0.1": version "16.0.1" resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-16.0.1.tgz#8b3d358d1574f4168fd276c5cb190361c477f0b0" From 57f5f6405fe331966f053f90f6d816f5cf5854e7 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Mon, 24 Feb 2020 08:53:52 -0600 Subject: [PATCH 07/18] fix: bypass flacky legend dimensions in snapshot --- .../legend/__snapshots__/legend.test.tsx.snap | 4 ++-- src/components/legend/legend.test.tsx | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/legend/__snapshots__/legend.test.tsx.snap b/src/components/legend/__snapshots__/legend.test.tsx.snap index f397578099..5518bb8fa5 100644 --- a/src/components/legend/__snapshots__/legend.test.tsx.snap +++ b/src/components/legend/__snapshots__/legend.test.tsx.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Legend #legendColorPicker should match snapshot after onChange is called 1`] = `"
splita
splitb
splitc
splitd
"`; +exports[`Legend #legendColorPicker should match snapshot after onChange is called 1`] = `"
splita
splitb
splitc
splitd
"`; -exports[`Legend #legendColorPicker should match snapshot after onClose is called 1`] = `"
splita
splitb
splitc
splitd
"`; +exports[`Legend #legendColorPicker should match snapshot after onClose is called 1`] = `"
splita
splitb
splitc
splitd
"`; exports[`Legend #legendColorPicker should render colorPicker when color is clicked 1`] = `"
Custom Color Picker
"`; diff --git a/src/components/legend/legend.test.tsx b/src/components/legend/legend.test.tsx index e85233932b..509dbcfdf8 100644 --- a/src/components/legend/legend.test.tsx +++ b/src/components/legend/legend.test.tsx @@ -220,7 +220,12 @@ describe('Legend', () => { .simulate('click') .first(); - expect(wrapper.find(Legend).html()).toMatchSnapshot(); + expect( + wrapper + .find(LegendListItem) + .map((e) => e.html()) + .join(''), + ).toMatchSnapshot(); }); it('should set isOpen to false after onChange is called', () => { @@ -248,7 +253,12 @@ describe('Legend', () => { .find('#close') .simulate('click') .first(); - expect(wrapper.find(Legend).html()).toMatchSnapshot(); + expect( + wrapper + .find(LegendListItem) + .map((e) => e.html()) + .join(''), + ).toMatchSnapshot(); }); it('should set isOpen to false after onClose is called', () => { From 5bf4016d21570c68e139470954baaa5292f8d579 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Mon, 24 Feb 2020 10:28:44 -0600 Subject: [PATCH 08/18] tests: fix last test --- src/components/legend/legend.test.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/legend/legend.test.tsx b/src/components/legend/legend.test.tsx index 509dbcfdf8..8bcc862e4d 100644 --- a/src/components/legend/legend.test.tsx +++ b/src/components/legend/legend.test.tsx @@ -210,7 +210,12 @@ describe('Legend', () => { it('should render colorPicker when color is clicked', () => { clickFirstColor(); expect(wrapper.find('#colorPicker').html()).toMatchSnapshot(); - expect(wrapper.find(Legend).html()).toMatchSnapshot(); + expect( + wrapper + .find(LegendListItem) + .map((e) => e.html()) + .join(''), + ).toMatchSnapshot(); }); it('should match snapshot after onChange is called', () => { From 71ee64c452e33dd5ac7cd1041506e2d0d8418591 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Mon, 24 Feb 2020 11:11:27 -0600 Subject: [PATCH 09/18] fix: test snapshot --- src/components/legend/__snapshots__/legend.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/legend/__snapshots__/legend.test.tsx.snap b/src/components/legend/__snapshots__/legend.test.tsx.snap index 5518bb8fa5..b4f88a8934 100644 --- a/src/components/legend/__snapshots__/legend.test.tsx.snap +++ b/src/components/legend/__snapshots__/legend.test.tsx.snap @@ -6,4 +6,4 @@ exports[`Legend #legendColorPicker should match snapshot after onClose is called exports[`Legend #legendColorPicker should render colorPicker when color is clicked 1`] = `"
Custom Color Picker
"`; -exports[`Legend #legendColorPicker should render colorPicker when color is clicked 2`] = `"
splita
Custom Color Picker
splitb
splitc
splitd
"`; +exports[`Legend #legendColorPicker should render colorPicker when color is clicked 2`] = `"
splita
Custom Color Picker
splitb
splitc
splitd
"`; From 1db0634afa026453336ed76be107e2fcb655a446 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Tue, 25 Feb 2020 11:20:20 -0600 Subject: [PATCH 10/18] chore: fix pr comments --- .../get_tooltip_values_highlighted_geoms.ts | 4 -- src/components/legend/_legend_item.scss | 5 +- src/components/legend/legend.test.tsx | 1 - src/components/legend/legend_item.tsx | 47 ++++++++++--------- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts index 87841f0592..d237ec2ef1 100644 --- a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts +++ b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts @@ -36,10 +36,6 @@ const EMPTY_VALUES = Object.freeze({ highlightedGeometries: [], }); -export interface TooltipData { - header: TooltipValue | null; - values: TooltipValue[]; -} export interface TooltipAndHighlightedGeoms { tooltip: TooltipInfo; highlightedGeometries: IndexedGeometry[]; diff --git a/src/components/legend/_legend_item.scss b/src/components/legend/_legend_item.scss index 1de0f9ccba..36e2cbf702 100644 --- a/src/components/legend/_legend_item.scss +++ b/src/components/legend/_legend_item.scss @@ -15,10 +15,6 @@ $legendItemVerticalPadding: $echLegendRowGap / 2; } } - &__label:hover { - text-decoration: underline; - } - &__color { margin-right: $euiSizeXS; } @@ -38,6 +34,7 @@ $legendItemVerticalPadding: $echLegendRowGap / 2; &:hover { cursor: pointer; + text-decoration: underline; } } diff --git a/src/components/legend/legend.test.tsx b/src/components/legend/legend.test.tsx index 8bcc862e4d..393f73e5a4 100644 --- a/src/components/legend/legend.test.tsx +++ b/src/components/legend/legend.test.tsx @@ -141,7 +141,6 @@ describe('Legend', () => { colors: ['red'], }; - pickerElement = document.createElement('div'); data = dg.generateGroupedSeries(10, 4, 'split'); legendColorPickerFn: LegendColorPickerFn = (anchor, onClose) => { diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index 41207648e3..4bf44dd99f 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -79,6 +79,14 @@ export class LegendListItem extends Component return !deepEqual(this.props, nextProps) || !deepEqual(this.state, nextState); } + handleColorClick = (changable: boolean) => + changable + ? (event: React.MouseEvent) => { + event.stopPropagation(); + this.toggleIsOpen(); + } + : undefined; + /** * Create a div for the color/eye icon * @param color @@ -89,35 +97,28 @@ export class LegendListItem extends Component return null; } - if (isSeriesVisible) { - const changable = Boolean(this.props.legendColorPicker); - const colorClasses = classNames('echLegendItem__color', { - 'echLegendItem__color--changable': changable, - }); - + if (!isSeriesVisible) { return ( -
{ - e.stopPropagation(); - this.toggleIsOpen(); - } - : undefined - } - className={colorClasses} - aria-label="series color" - title="series color" - > - +
+ {/* changing the default viewBox for the eyeClosed icon to keep the same dimensions */} +
); } - // changing the default viewBox for the eyeClosed icon to keep the same dimensions + const changable = Boolean(this.props.legendColorPicker); + const colorClasses = classNames('echLegendItem__color', { + 'echLegendItem__color--changable': changable, + }); + return ( -
- +
+
); }; From 9e6bfdb4b75f710251b1cfc1254f29ba7b7f1822 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Tue, 25 Feb 2020 18:45:49 -0600 Subject: [PATCH 11/18] refactor: update logic for storybook example --- .eslintrc.js | 1 + .playground/playground.tsx | 61 ++++++++++----------------- .storybook/style.scss | 2 + src/components/legend/legend.test.tsx | 4 +- src/components/legend/legend_item.tsx | 18 +++++--- src/specs/settings.tsx | 13 ++++-- stories/legend/9_color_picker.tsx | 53 +++++++++++++++++++++++ stories/legend/legend.stories.tsx | 1 + 8 files changed, 105 insertions(+), 48 deletions(-) create mode 100644 stories/legend/9_color_picker.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 14770614e0..9cb8514086 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -69,6 +69,7 @@ module.exports = { '@typescript-eslint/ban-ts-ignore': 'off', '@typescript-eslint/no-inferrable-types': 'off', 'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never' }], + 'react/prop-types': 0, }, settings: { 'import/resolver': { diff --git a/.playground/playground.tsx b/.playground/playground.tsx index e79632daab..0201be20ce 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -1,54 +1,39 @@ import React from 'react'; -import { Chart, ScaleType, Settings, BarSeries, DataGenerator, LegendColorPickerFn, Axis } from '../src'; -const dg = new DataGenerator(); +import { Chart, LineSeries, ScaleType, Position, Axis } from '../src'; +import { SeededDataGenerator } from '../src/mocks/utils'; export class Playground extends React.Component<{}, { isSunburstShown: boolean }> { - state: any = { - colors: ['red'], - }; - - data = dg.generateGroupedSeries(10, 4, 'split'); - customColor = '#0c7b93'; - container?: HTMLDivElement; - - legendColorPickerFn: LegendColorPickerFn = (anchor, onClose) => { - return ( -
- Custom Color Picker - - -
- ); - }; - render() { + const dg = new SeededDataGenerator(); + const data = dg.generateGroupedSeries(10, 2).map((item) => ({ + ...item, + y1: item.y + 100, + })); + return ( <>
- - - - + + + +
diff --git a/.storybook/style.scss b/.storybook/style.scss index f7df75d346..8d9d656642 100644 --- a/.storybook/style.scss +++ b/.storybook/style.scss @@ -1,3 +1,5 @@ +@import '../node_modules/@elastic/eui/src/theme_light.scss'; + .story-chart { box-sizing: border-box; background: white; diff --git a/src/components/legend/legend.test.tsx b/src/components/legend/legend.test.tsx index db31a4ffa6..74d1a5117d 100644 --- a/src/components/legend/legend.test.tsx +++ b/src/components/legend/legend.test.tsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { Chart } from '../chart'; -import { Settings, BarSeries, LegendColorPickerFn } from '../../specs'; +import { Settings, BarSeries, LegendColorPicker } from '../../specs'; import { ScaleType } from '../../scales'; import { Legend } from './legend'; import { LegendListItem } from './legend_item'; @@ -143,7 +143,7 @@ describe('Legend', () => { data = dg.generateGroupedSeries(10, 4, 'split'); - legendColorPickerFn: LegendColorPickerFn = (anchor, onClose) => { + legendColorPickerFn: LegendColorPicker = ({ onClose }) => { return (
Custom Color Picker diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index 4bf44dd99f..9e216d3c34 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import React, { Component, createRef } from 'react'; import { deepEqual } from '../../utils/fast_deep_equal'; import { Icon } from '../icons/icon'; -import { LegendItemListener, BasicListener, LegendColorPickerFn } from '../../specs/settings'; +import { LegendItemListener, BasicListener, LegendColorPicker } from '../../specs/settings'; import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; import { onLegendItemOutAction, onLegendItemOverAction } from '../../state/actions/legend'; import { Position } from '../../utils/commons'; @@ -14,7 +14,7 @@ interface LegendItemProps { label?: string; legendPosition: Position; showExtra: boolean; - legendColorPicker?: LegendColorPickerFn; + legendColorPicker?: LegendColorPicker; onLegendItemClickListener?: LegendItemListener; onLegendItemOutListener?: BasicListener; onLegendItemOverListener?: LegendItemListener; @@ -124,9 +124,17 @@ export class LegendListItem extends Component }; renderColorPicker() { - const { legendColorPicker } = this.props; - if (legendColorPicker && this.state.isOpen && this.ref.current) { - return legendColorPicker(this.ref.current, this.toggleIsOpen); + const { legendColorPicker: ColorPicker, legendItem } = this.props; + const { seriesIdentifier, color } = legendItem; + if (ColorPicker && this.state.isOpen && this.ref.current) { + return ( + + ); } } diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 75cb80c558..7a6fef38cc 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -1,4 +1,6 @@ +import { ComponentType } from 'react'; import { $Values } from 'utility-types'; + import { DomainRange } from '../chart_types/xy_chart/utils/specs'; import { PartialTheme, Theme } from '../utils/themes/theme'; import { Domain } from '../utils/domain'; @@ -11,7 +13,6 @@ import { XYChartSeriesIdentifier, SeriesIdentifier } from '../chart_types/xy_cha import { Accessor } from '../utils/accessor'; import { Position, Rendering, Rotation } from '../utils/commons'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; -import { ReactNode } from 'react'; export type ElementClickListener = (elements: Array<[GeometryValue, XYChartSeriesIdentifier]>) => void; export type ElementOverListener = (elements: Array<[GeometryValue, XYChartSeriesIdentifier]>) => void; @@ -112,7 +113,13 @@ export interface TooltipProps { unit?: string; } -export type LegendColorPickerFn = (anchor: HTMLDivElement, onClose: () => void) => ReactNode; +export interface LegendColorPickerProps { + anchor: HTMLDivElement; + color: string; + onClose: () => void; + seriesIdentifier: XYChartSeriesIdentifier; +} +export type LegendColorPicker = ComponentType; export interface SettingsSpec extends Spec { /** @@ -164,7 +171,7 @@ export interface SettingsSpec extends Spec { onRenderChange?: RenderChangeListener; xDomain?: Domain | DomainRange; resizeDebounce?: number; - legendColorPicker?: LegendColorPickerFn; + legendColorPicker?: LegendColorPicker; } export type DefaultSettingsProps = diff --git a/stories/legend/9_color_picker.tsx b/stories/legend/9_color_picker.tsx new file mode 100644 index 0000000000..2ffa0c61b9 --- /dev/null +++ b/stories/legend/9_color_picker.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { number } from '@storybook/addon-knobs'; + +import { EuiColorPicker, EuiWrappingPopover } from '@elastic/eui'; + +import { Axis, BarSeries, Chart, Position, ScaleType, Settings, PartialTheme, LegendColorPicker } from '../../src/'; +import { SeededDataGenerator } from '../../src/mocks/utils'; + +const dg = new SeededDataGenerator(); + +export const example = () => { + const theme: PartialTheme = { + legend: { + spacingBuffer: number('legend buffer value', 80), + }, + }; + const data = dg.generateGroupedSeries(5, 2); + const [colors, setColors] = useState>({}); + + const renderColorPicker: LegendColorPicker = ({ anchor, color, onClose, seriesIdentifier }) => { + const handleChange = (color: string) => { + onClose(); + setColors({ + ...colors, + [seriesIdentifier.key]: color, + }); + }; + return ( + + + + ); + }; + + return ( + + + + Number(d).toFixed(2)} /> + + colors[key] ?? null} + /> + + ); +}; diff --git a/stories/legend/legend.stories.tsx b/stories/legend/legend.stories.tsx index 908565bd29..44dd12573e 100644 --- a/stories/legend/legend.stories.tsx +++ b/stories/legend/legend.stories.tsx @@ -15,3 +15,4 @@ export { example as changingSpecs } from './5_changing_specs'; export { example as hideLegendItemsBySeries } from './6_hide_legend'; export { example as displayValuesInLegendElements } from './7_display_values'; export { example as legendSpacingBuffer } from './8_spacing_buffer'; +export { example as colorPicker } from './9_color_picker'; From 1cfb6494ac9a1de094d7f18b6af6ab68c2c7453c Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Wed, 26 Feb 2020 15:46:05 -0600 Subject: [PATCH 12/18] refactor: generic key types to be more specific --- src/chart_types/xy_chart/legend/legend.ts | 11 +++-- .../xy_chart/renderer/canvas/axes/index.ts | 2 +- .../xy_chart/state/chart_state.tsx | 3 +- .../state/selectors/compute_legend.ts | 3 +- .../selectors/get_legend_tooltip_values.ts | 3 +- .../state/selectors/get_series_color_map.ts | 4 +- .../state/selectors/merge_y_custom_domains.ts | 10 ++-- src/chart_types/xy_chart/state/utils.ts | 19 ++++---- src/chart_types/xy_chart/tooltip/tooltip.ts | 6 +-- src/chart_types/xy_chart/utils/series.ts | 25 +++++----- src/components/legend/legend.tsx | 7 +-- src/state/chart_state.ts | 46 +++++++++++++------ src/state/selectors/get_legend_items.ts | 5 +- .../selectors/get_legend_items_values.ts | 5 +- 14 files changed, 92 insertions(+), 57 deletions(-) diff --git a/src/chart_types/xy_chart/legend/legend.ts b/src/chart_types/xy_chart/legend/legend.ts index defdb8ca8b..34cbe31b62 100644 --- a/src/chart_types/xy_chart/legend/legend.ts +++ b/src/chart_types/xy_chart/legend/legend.ts @@ -6,6 +6,7 @@ import { getSortedDataSeriesColorsValuesMap, getSeriesName, XYChartSeriesIdentifier, + SeriesKey, } from '../utils/series'; import { AxisSpec, BasicSeriesSpec, Postfixes, isAreaSeriesSpec, isBarSeriesSpec } from '../utils/specs'; import { Y0_ACCESSOR_POSTFIX, Y1_ACCESSOR_POSTFIX } from '../tooltip/tooltip'; @@ -17,7 +18,7 @@ interface FormattedLastValues { } export type LegendItem = Postfixes & { - key: string; + key: SeriesKey; color: string; name: string; seriesIdentifier: XYChartSeriesIdentifier; @@ -54,14 +55,14 @@ export function getItemLabel( } export function computeLegend( - seriesCollection: Map, - seriesColors: Map, + seriesCollection: Map, + seriesColors: Map, specs: BasicSeriesSpec[], defaultColor: string, axesSpecs: AxisSpec[], deselectedDataSeries: XYChartSeriesIdentifier[] = [], -): Map { - const legendItems: Map = new Map(); +): Map { + const legendItems: Map = new Map(); const sortedCollection = getSortedDataSeriesColorsValuesMap(seriesCollection); sortedCollection.forEach((series, key) => { diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/index.ts b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts index 6d457607e8..c7c063e41d 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/index.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts @@ -24,7 +24,7 @@ export interface AxesProps { axesVisibleTicks: Map; axesSpecs: AxisSpec[]; axesTicksDimensions: Map; - axesPositions: Map; + axesPositions: Map; axisStyle: AxisConfig; debug: boolean; chartDimensions: Dimensions; diff --git a/src/chart_types/xy_chart/state/chart_state.tsx b/src/chart_types/xy_chart/state/chart_state.tsx index 1c0f4497f4..4120f752ee 100644 --- a/src/chart_types/xy_chart/state/chart_state.tsx +++ b/src/chart_types/xy_chart/state/chart_state.tsx @@ -18,6 +18,7 @@ import { getTooltipInfoSelector } from './selectors/get_tooltip_values_highlight import { htmlIdGenerator } from '../../../utils/commons'; import { Tooltip } from '../../../components/tooltip'; import { getTooltipAnchorPositionSelector } from './selectors/get_tooltip_position'; +import { SeriesKey } from '../utils/series'; export class XYAxisChartState implements InternalChartState { chartType = ChartTypes.XYAxis; @@ -34,7 +35,7 @@ export class XYAxisChartState implements InternalChartState { getLegendItems(globalState: GlobalChartState) { return computeLegendSelector(globalState); } - getLegendItemsValues(globalState: GlobalChartState): Map { + getLegendItemsValues(globalState: GlobalChartState): Map { return getLegendTooltipValuesSelector(globalState); } chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject) { diff --git a/src/chart_types/xy_chart/state/selectors/compute_legend.ts b/src/chart_types/xy_chart/state/selectors/compute_legend.ts index b91f665dff..c9611483d5 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_legend.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_legend.ts @@ -6,6 +6,7 @@ import { getSeriesColorsSelector } from './get_series_color_map'; import { computeLegend, LegendItem } from '../../legend/legend'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { SeriesKey } from '../../utils/series'; const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; @@ -25,7 +26,7 @@ export const computeLegendSelector = createCachedSelector( seriesColors, axesSpecs, deselectedDataSeries, - ): Map => { + ): Map => { return computeLegend( seriesDomainsAndData.seriesCollection, seriesColors, diff --git a/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts b/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts index 35a72e898f..df8a904028 100644 --- a/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts +++ b/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts @@ -2,10 +2,11 @@ import createCachedSelector from 're-reselect'; import { getSeriesTooltipValues, TooltipLegendValue } from '../../tooltip/tooltip'; import { getTooltipInfoSelector } from './get_tooltip_values_highlighted_geoms'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { SeriesKey } from '../../utils/series'; export const getLegendTooltipValuesSelector = createCachedSelector( [getTooltipInfoSelector], - ({ values }): Map => { + ({ values }): Map => { return getSeriesTooltipValues(values); }, )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts b/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts index a38a8ae028..8633fc0e96 100644 --- a/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts +++ b/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts @@ -1,14 +1,14 @@ import createCachedSelector from 're-reselect'; import { computeSeriesDomainsSelector } from './compute_series_domains'; import { getSeriesSpecsSelector } from './get_specs'; -import { getSeriesColors } from '../../utils/series'; +import { getSeriesColors, SeriesKey } from '../../utils/series'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getCustomSeriesColors } from '../utils'; export const getSeriesColorsSelector = createCachedSelector( [getSeriesSpecsSelector, computeSeriesDomainsSelector, getChartThemeSelector], - (seriesSpecs, seriesDomainsAndData, chartTheme): Map => { + (seriesSpecs, seriesDomainsAndData, chartTheme): Map => { const updatedCustomSeriesColors = getCustomSeriesColors(seriesSpecs, seriesDomainsAndData.seriesCollection); const seriesColorMap = getSeriesColors( diff --git a/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts b/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts index c37c9b47fa..fa76565cd2 100644 --- a/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts +++ b/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts @@ -5,16 +5,20 @@ import { AxisSpec, DomainRange } from '../../utils/specs'; import { Rotation } from '../../../../utils/commons'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { GroupId } from '../../../../utils/ids'; export const mergeYCustomDomainsByGroupIdSelector = createCachedSelector( [getAxisSpecsSelector, getSettingsSpecSelector], - (axisSpecs, settingsSpec): Map => { + (axisSpecs, settingsSpec): Map => { return mergeYCustomDomainsByGroupId(axisSpecs, settingsSpec ? settingsSpec.rotation : 0); }, )(getChartIdSelector); -export function mergeYCustomDomainsByGroupId(axesSpecs: AxisSpec[], chartRotation: Rotation): Map { - const domainsByGroupId = new Map(); +export function mergeYCustomDomainsByGroupId( + axesSpecs: AxisSpec[], + chartRotation: Rotation, +): Map { + const domainsByGroupId = new Map(); axesSpecs.forEach((spec: AxisSpec) => { const { id, groupId, domain } = spec; diff --git a/src/chart_types/xy_chart/state/utils.ts b/src/chart_types/xy_chart/state/utils.ts index 21edbd2fec..661cce3e91 100644 --- a/src/chart_types/xy_chart/state/utils.ts +++ b/src/chart_types/xy_chart/state/utils.ts @@ -14,6 +14,7 @@ import { getSeriesKey, RawDataSeries, XYChartSeriesIdentifier, + SeriesKey, } from '../utils/series'; import { AreaSeriesSpec, @@ -91,7 +92,7 @@ export interface SeriesDomainsAndData { stacked: FormattedDataSeries[]; nonStacked: FormattedDataSeries[]; }; - seriesCollection: Map; + seriesCollection: Map; } /** @@ -122,9 +123,9 @@ export function updateDeselectedDataSeries( */ export function getCustomSeriesColors( seriesSpecs: BasicSeriesSpec[], - seriesCollection: Map, -): Map { - const updatedCustomSeriesColors = new Map(); + seriesCollection: Map, +): Map { + const updatedCustomSeriesColors = new Map(); const counters = new Map(); seriesCollection.forEach(({ seriesIdentifier }, seriesKey) => { @@ -159,8 +160,8 @@ export interface LastValues { function getLastValues(formattedDataSeries: { stacked: FormattedDataSeries[]; nonStacked: FormattedDataSeries[]; -}): Map { - const lastValues = new Map(); +}): Map { + const lastValues = new Map(); // we need to get the latest formattedDataSeries.stacked.forEach((ds) => { @@ -231,7 +232,7 @@ export function computeSeriesDomains( // we need to get the last values from the formatted dataseries // because we change the format if we are on percentage mode const lastValues = getLastValues(formattedDataSeries); - const updatedSeriesCollection = new Map(); + const updatedSeriesCollection = new Map(); seriesCollection.forEach((value, key) => { const lastValue = lastValues.get(key); const updatedColorSet: SeriesCollectionValue = { @@ -257,7 +258,7 @@ export function computeSeriesGeometries( stacked: FormattedDataSeries[]; nonStacked: FormattedDataSeries[]; }, - seriesColorMap: Map, + seriesColorMap: Map, chartTheme: Theme, chartDims: Dimensions, chartRotation: Rotation, @@ -443,7 +444,7 @@ function renderGeometries( xScale: Scale, yScale: Scale, seriesSpecs: BasicSeriesSpec[], - seriesColorsMap: Map, + seriesColorsMap: Map, defaultColor: string, axesSpecs: AxisSpec[], chartTheme: Theme, diff --git a/src/chart_types/xy_chart/tooltip/tooltip.ts b/src/chart_types/xy_chart/tooltip/tooltip.ts index 76ee34ea9c..bd80731086 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -8,7 +8,7 @@ import { } from '../utils/specs'; import { IndexedGeometry, BandedAccessorType } from '../../../utils/geometry'; import { getAccessorFormatLabel } from '../../../utils/accessor'; -import { getSeriesName } from '../utils/series'; +import { getSeriesName, SeriesKey } from '../utils/series'; import { TooltipValue } from '../../../specs'; export interface TooltipLegendValue { @@ -22,9 +22,9 @@ export const Y1_ACCESSOR_POSTFIX = ' - upper'; export function getSeriesTooltipValues( tooltipValues: TooltipValue[], defaultValue?: string, -): Map { +): Map { // map from seriesKey to TooltipLegendValue - const seriesTooltipValues = new Map(); + const seriesTooltipValues = new Map(); tooltipValues.forEach(({ value, seriesIdentifier, valueAccessor }) => { const seriesValue = defaultValue ? defaultValue : value; diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index 582c130f11..7b0bffd096 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -47,9 +47,12 @@ export interface DataSeriesDatum { /** the list of filled values because missing or nulls */ filled?: FilledValues; } + +export type SeriesKey = string; + export type SeriesIdentifier = { specId: SpecId; - key: string; + key: SeriesKey; }; export interface XYChartSeriesIdentifier extends SeriesIdentifier { @@ -113,7 +116,7 @@ export function splitSeries({ xValues: Set; } { const isMultipleY = yAccessors && yAccessors.length > 1; - const series = new Map(); + const series = new Map(); const colorsValues = new Set(); const xValues = new Set(); @@ -166,7 +169,7 @@ export function getSeriesKey({ * along with the series key */ function updateSeriesMap( - seriesMap: Map, + seriesMap: Map, splitAccessors: Map, accessor: any, datum: RawDataSeriesDatum, @@ -339,11 +342,11 @@ export function getSplittedSeries( deselectedDataSeries: XYChartSeriesIdentifier[] = [], ): { splittedSeries: Map; - seriesCollection: Map; + seriesCollection: Map; xValues: Set; } { const splittedSeries = new Map(); - const seriesCollection = new Map(); + const seriesCollection = new Map(); const xValues: Set = new Set(); let isOrdinalScale = false; for (const spec of seriesSpecs) { @@ -464,8 +467,8 @@ function getSortIndex({ specSortIndex }: SeriesCollectionValue, total: number): } export function getSortedDataSeriesColorsValuesMap( - seriesCollection: Map, -): Map { + seriesCollection: Map, +): Map { const seriesColorsArray = [...seriesCollection]; seriesColorsArray.sort(([, specA], [, specB]) => { return getSortIndex(specA, seriesCollection.size) - getSortIndex(specB, seriesCollection.size); @@ -475,11 +478,11 @@ export function getSortedDataSeriesColorsValuesMap( } export function getSeriesColors( - seriesCollection: Map, + seriesCollection: Map, chartColors: ColorConfig, - customColors: Map, -): Map { - const seriesColorMap = new Map(); + customColors: Map, +): Map { + const seriesColorMap = new Map(); let counter = 0; seriesCollection.forEach((_, seriesKey) => { diff --git a/src/components/legend/legend.tsx b/src/components/legend/legend.tsx index 1a9b315abb..74ecafed62 100644 --- a/src/components/legend/legend.tsx +++ b/src/components/legend/legend.tsx @@ -23,9 +23,10 @@ import { } from '../../state/actions/legend'; import { SettingsSpec } from '../../specs'; import { BandedAccessorType } from '../../utils/geometry'; +import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; interface LegendStateProps { - legendItems: Map; + legendItems: Map; legendPosition: Position; legendItemTooltipValues: Map; showLegend: boolean; @@ -119,8 +120,8 @@ class LegendComponent extends React.Component { }; private getLegendValues( - tooltipValues: Map | undefined, - key: string, + tooltipValues: Map | undefined, + key: SeriesKey, banded: boolean = false, ): any[] { const values = tooltipValues && tooltipValues.get(key); diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 1cb0c4d29e..647deebf51 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -2,7 +2,7 @@ import { SPEC_PARSED, SPEC_UNMOUNTED, UPSERT_SPEC, REMOVE_SPEC, SPEC_PARSING } f import { interactionsReducer } from './reducers/interactions'; import { ChartTypes } from '../chart_types'; import { XYAxisChartState } from '../chart_types/xy_chart/state/chart_state'; -import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; +import { XYChartSeriesIdentifier, SeriesKey } from '../chart_types/xy_chart/utils/series'; import { Spec, PointerEvent } from '../specs'; import { DEFAULT_SETTINGS_SPEC } from '../specs/settings'; import { Dimensions } from '../utils/dimensions'; @@ -54,12 +54,12 @@ export interface InternalChartState { * return the list of legend items * @param globalState */ - getLegendItems(globalState: GlobalChartState): Map; + getLegendItems(globalState: GlobalChartState): Map; /** * return the list of values for each legend item * @param globalState */ - getLegendItemsValues(globalState: GlobalChartState): Map; + getLegendItemsValues(globalState: GlobalChartState): Map; /** * return the CSS pointer cursor depending on the internal chart state * @param globalState @@ -117,25 +117,45 @@ export interface ExternalEventsState { } export interface GlobalChartState { - // an unique ID for each chart used by re-reselect to memoize selector per chart + /** + * a unique ID for each chart used by re-reselect to memoize selector per chart + */ chartId: string; - // true when all all the specs are parsed ad stored into the specs object + /** + * true when all all the specs are parsed ad stored into the specs object + */ specsInitialized: boolean; - // true if the chart is rendered on dom + /** + * true if the chart is rendered on dom + */ chartRendered: boolean; - // incremental count of the chart rendering + /** + * incremental count of the chart rendering + */ chartRenderedCount: number; - // the map of parsed specs + /** + * the map of parsed specs + */ specs: SpecList; - // the chart type depending on the used specs + /** + * the chart type depending on the used specs + */ chartType: ChartTypes | null; - // a chart-type-dependant class that is used to render and share chart-type dependant functions + /** + * a chart-type-dependant class that is used to render and share chart-type dependant functions + */ internalChartState: InternalChartState | null; - // the dimensions of the parent container, including the legend + /** + * the dimensions of the parent container, including the legend + */ parentDimensions: Dimensions; - // the state of the interactions + /** + * the state of the interactions + */ interactions: InteractionsState; - // external event state + /** + * external event state + */ externalEvents: ExternalEventsState; } diff --git a/src/state/selectors/get_legend_items.ts b/src/state/selectors/get_legend_items.ts index d3e1888e4c..8b9064cb80 100644 --- a/src/state/selectors/get_legend_items.ts +++ b/src/state/selectors/get_legend_items.ts @@ -1,8 +1,9 @@ import { GlobalChartState } from '../chart_state'; import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; +import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; -const EMPTY_LEGEND_LIST = new Map(); -export const getLegendItemsSelector = (state: GlobalChartState): Map => { +const EMPTY_LEGEND_LIST = new Map(); +export const getLegendItemsSelector = (state: GlobalChartState): Map => { if (state.internalChartState) { return state.internalChartState.getLegendItems(state); } else { diff --git a/src/state/selectors/get_legend_items_values.ts b/src/state/selectors/get_legend_items_values.ts index 9a41a4f8be..5b06c13136 100644 --- a/src/state/selectors/get_legend_items_values.ts +++ b/src/state/selectors/get_legend_items_values.ts @@ -1,8 +1,9 @@ import { TooltipLegendValue } from '../../chart_types/xy_chart/tooltip/tooltip'; import { GlobalChartState } from '../chart_state'; +import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; -const EMPTY_ITEM_LIST = new Map(); -export const getLegendItemsValuesSelector = (state: GlobalChartState): Map => { +const EMPTY_ITEM_LIST = new Map(); +export const getLegendItemsValuesSelector = (state: GlobalChartState): Map => { if (state.internalChartState) { return state.internalChartState.getLegendItemsValues(state); } else { From 51284ff9f61b2246f02c0c4e7ebddcddcad60a30 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Wed, 26 Feb 2020 16:02:47 -0600 Subject: [PATCH 13/18] refactor: color state management logic --- .../state/selectors/get_series_color_map.ts | 10 ++- src/chart_types/xy_chart/utils/series.test.ts | 77 +++++++++++-------- src/chart_types/xy_chart/utils/series.ts | 43 ++++++++++- src/components/legend/legend.tsx | 10 +++ src/components/legend/legend_item.tsx | 23 +++++- src/specs/settings.tsx | 16 ++++ src/state/actions/colors.ts | 35 +++++++++ src/state/actions/index.ts | 4 +- src/state/chart_state.ts | 44 +++++++++++ stories/legend/9_color_picker.tsx | 41 +++++----- 10 files changed, 246 insertions(+), 57 deletions(-) create mode 100644 src/state/actions/colors.ts diff --git a/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts b/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts index 8633fc0e96..f8205b7e28 100644 --- a/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts +++ b/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts @@ -5,16 +5,22 @@ import { getSeriesColors, SeriesKey } from '../../utils/series'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getCustomSeriesColors } from '../utils'; +import { GlobalChartState } from '../../../../state/chart_state'; + +function getColorOverrides({ colors }: GlobalChartState) { + return colors; +} export const getSeriesColorsSelector = createCachedSelector( - [getSeriesSpecsSelector, computeSeriesDomainsSelector, getChartThemeSelector], - (seriesSpecs, seriesDomainsAndData, chartTheme): Map => { + [getSeriesSpecsSelector, computeSeriesDomainsSelector, getChartThemeSelector, getColorOverrides], + (seriesSpecs, seriesDomainsAndData, chartTheme, colorOverrides): Map => { const updatedCustomSeriesColors = getCustomSeriesColors(seriesSpecs, seriesDomainsAndData.seriesCollection); const seriesColorMap = getSeriesColors( seriesDomainsAndData.seriesCollection, chartTheme.colors, updatedCustomSeriesColors, + colorOverrides, ); return seriesColorMap; }, diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index ad45e2c06d..b3efc60902 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -484,32 +484,16 @@ describe('Series', () => { ); expect(stackedDataSeries.stacked).toMatchSnapshot(); }); - test('should get series color map', () => { - const spec1: BasicSeriesSpec = { - specType: SpecTypes.Series, - chartType: ChartTypes.XYAxis, - id: 'spec1', - groupId: 'group', - seriesType: SeriesTypes.Line, - yScaleType: ScaleType.Log, - xScaleType: ScaleType.Linear, - xAccessor: 'x', - yAccessors: ['y'], - yScaleToDataExtent: false, - data: TestDataset.BARCHART_1Y0G, - hideInLegend: false, - }; - - const specs = new Map(); - specs.set(spec1.id, spec1); - const dataSeriesValuesA: SeriesCollectionValue = { + describe('#getSeriesColors', () => { + const seriesKey = 'mock_series_key'; + const mockSeries: SeriesCollectionValue = { seriesIdentifier: { specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), seriesKeys: ['a', 'b', 'c'], - key: '', + key: seriesKey, }, }; @@ -519,22 +503,53 @@ describe('Series', () => { }; const seriesColors = new Map(); - seriesColors.set('spec1', dataSeriesValuesA); + seriesColors.set(seriesKey, mockSeries); const emptyCustomColors = new Map(); + const persistedColor = 'persisted_color'; + const customColor = 'custom_color'; + const customColors: Map = new Map(); + customColors.set(seriesKey, customColor); + const emptyColorOverrides = { + persisted: {}, + temporary: {}, + }; + const persistedOverrides = { + persisted: { [seriesKey]: persistedColor }, + temporary: {}, + }; - const defaultColorMap = getSeriesColors(seriesColors, chartColors, emptyCustomColors); - const expectedDefaultColorMap = new Map(); - expectedDefaultColorMap.set('spec1', 'elastic_charts_c1'); - expect(defaultColorMap).toEqual(expectedDefaultColorMap); + it('should return deafult color', () => { + const result = getSeriesColors(seriesColors, chartColors, emptyCustomColors, emptyColorOverrides); + const expected = new Map(); + expected.set(seriesKey, 'elastic_charts_c1'); + expect(result).toEqual(expected); + }); - const customColors: Map = new Map(); - customColors.set('spec1', 'custom_color'); + it('should return persisted color', () => { + const result = getSeriesColors(seriesColors, chartColors, customColors, persistedOverrides); + const expected = new Map(); + expected.set(seriesKey, persistedColor); + expect(result).toEqual(expected); + }); + + it('should return custom color', () => { + const result = getSeriesColors(seriesColors, chartColors, customColors, persistedOverrides); + const expected = new Map(); + expected.set(seriesKey, customColor); + expect(result).toEqual(expected); + }); - const customizedColorMap = getSeriesColors(seriesColors, chartColors, customColors); - const expectedCustomizedColorMap = new Map(); - expectedCustomizedColorMap.set('spec1', 'custom_color'); - expect(customizedColorMap).toEqual(expectedCustomizedColorMap); + it('should return temporary color', () => { + const temporaryColor = 'persisted-color'; + const result = getSeriesColors(seriesColors, chartColors, customColors, { + ...persistedOverrides, + temporary: { [seriesKey]: temporaryColor }, + }); + const expected = new Map(); + expected.set(seriesKey, temporaryColor); + expect(result).toEqual(expected); + }); }); test('should only include deselectedDataSeries when splitting series if deselectedDataSeries is defined', () => { const specId = 'splitSpec'; diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index 7b0bffd096..0a2e9a5daa 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -8,6 +8,7 @@ import { formatStackedDataSeriesValues } from './stacked_series_utils'; import { ScaleType } from '../../../scales'; import { LastValues } from '../state/utils'; import { Datum } from '../../../utils/commons'; +import { ColorOverrides } from '../../../state/chart_state'; export const SERIES_DELIMITER = ' - '; @@ -477,17 +478,55 @@ export function getSortedDataSeriesColorsValuesMap( return new Map([...seriesColorsArray]); } +/** + * Helper function to get highest override color. + * + * from highest to lowest: `temporary`, `customSeriesColors` then `persisted` + * + * @param key + * @param customColors + * @param overrides + */ +function getHighestOverride( + key: string, + customColors: Map, + overrides: ColorOverrides, +): string | undefined { + let color: string | undefined = overrides.temporary[key]; + + if (color) { + return color; + } + + color = customColors.get(key); + + if (color) { + return color; + } + + return overrides.persisted[key]; +} + +/** + * Returns color for a series given all color hierarchies + * + * @param seriesCollection + * @param chartColors + * @param customColors + * @param overrides + */ export function getSeriesColors( seriesCollection: Map, chartColors: ColorConfig, customColors: Map, + overrides: ColorOverrides, ): Map { const seriesColorMap = new Map(); let counter = 0; seriesCollection.forEach((_, seriesKey) => { - const customSeriesColor: string | undefined = customColors.get(seriesKey); - const color = customSeriesColor || chartColors.vizColors[counter % chartColors.vizColors.length]; + const colorOverride = getHighestOverride(seriesKey, customColors, overrides); + const color = colorOverride || chartColors.vizColors[counter % chartColors.vizColors.length]; seriesColorMap.set(seriesKey, color); counter++; diff --git a/src/components/legend/legend.tsx b/src/components/legend/legend.tsx index 74ecafed62..6037f7bc52 100644 --- a/src/components/legend/legend.tsx +++ b/src/components/legend/legend.tsx @@ -21,6 +21,7 @@ import { onLegendItemOutAction, onLegendItemOverAction, } from '../../state/actions/legend'; +import { clearTemporaryColors, setTemporaryColor, setPersistedColor } from '../../state/actions/colors'; import { SettingsSpec } from '../../specs'; import { BandedAccessorType } from '../../utils/geometry'; import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; @@ -41,6 +42,9 @@ interface LegendDispatchProps { onLegendItemOutAction: typeof onLegendItemOutAction; onLegendItemOverAction: typeof onLegendItemOverAction; onToggleDeselectSeriesAction: typeof onToggleDeselectSeriesAction; + clearTemporaryColors: typeof clearTemporaryColors; + setTemporaryColor: typeof setTemporaryColor; + setPersistedColor: typeof setPersistedColor; } type LegendProps = LegendStateProps & LegendDispatchProps; @@ -155,6 +159,9 @@ class LegendComponent extends React.Component { toggleDeselectSeriesAction={this.props.onToggleDeselectSeriesAction} legendItemOutAction={this.props.onLegendItemOutAction} legendItemOverAction={this.props.onLegendItemOverAction} + clearTemporaryColors={this.props.clearTemporaryColors} + setTemporaryColor={this.props.setTemporaryColor} + setPersistedColor={this.props.setPersistedColor} onLegendItemOverListener={settings.onLegendItemOver} onLegendItemOutListener={settings.onLegendItemOut} onLegendItemClickListener={settings.onLegendItemClick} @@ -171,6 +178,9 @@ const mapDispatchToProps = (dispatch: Dispatch): LegendDispatchProps => onToggleDeselectSeriesAction, onLegendItemOutAction, onLegendItemOverAction, + clearTemporaryColors, + setTemporaryColor, + setPersistedColor, }, dispatch, ); diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index 9e216d3c34..ab1db9d4c4 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -6,7 +6,7 @@ import { LegendItemListener, BasicListener, LegendColorPicker } from '../../spec import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; import { onLegendItemOutAction, onLegendItemOverAction } from '../../state/actions/legend'; import { Position } from '../../utils/commons'; -import { XYChartSeriesIdentifier } from '../../chart_types/xy_chart/utils/series'; +import { XYChartSeriesIdentifier, SeriesKey } from '../../chart_types/xy_chart/utils/series'; interface LegendItemProps { legendItem: LegendItem; @@ -20,6 +20,9 @@ interface LegendItemProps { onLegendItemOverListener?: LegendItemListener; legendItemOutAction: typeof onLegendItemOutAction; legendItemOverAction: typeof onLegendItemOverAction; + clearTemporaryColors: () => void; + setTemporaryColor: (key: SeriesKey, color: string) => void; + setPersistedColor: (key: SeriesKey, color: string) => void; toggleDeselectSeriesAction: (legendItemId: XYChartSeriesIdentifier) => void; } @@ -124,14 +127,28 @@ export class LegendListItem extends Component }; renderColorPicker() { - const { legendColorPicker: ColorPicker, legendItem } = this.props; + const { + legendColorPicker: ColorPicker, + legendItem, + clearTemporaryColors, + setTemporaryColor, + setPersistedColor, + } = this.props; const { seriesIdentifier, color } = legendItem; + + const handleClose = () => { + setPersistedColor(seriesIdentifier.key, color); + clearTemporaryColors(); + this.toggleIsOpen(); + }; + if (ColorPicker && this.state.isOpen && this.ref.current) { return ( setTemporaryColor(seriesIdentifier.key, color)} seriesIdentifier={seriesIdentifier} /> ); diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 7a6fef38cc..998e15e697 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -114,9 +114,25 @@ export interface TooltipProps { } export interface LegendColorPickerProps { + /** + * Anchor used to position picker + */ anchor: HTMLDivElement; + /** + * Current color of the given series + */ color: string; + /** + * Callback to close color picker and set persistent color + */ onClose: () => void; + /** + * Callback to update temporary color state + */ + onChange: (color: string) => void; + /** + * Series id for the active series + */ seriesIdentifier: XYChartSeriesIdentifier; } export type LegendColorPicker = ComponentType; diff --git a/src/state/actions/colors.ts b/src/state/actions/colors.ts new file mode 100644 index 0000000000..e74877c0d6 --- /dev/null +++ b/src/state/actions/colors.ts @@ -0,0 +1,35 @@ +import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; + +export const CLEAR_TEMPORARY_COLORS = 'CLEAR_TEMPORARY_COLORS'; +export const SET_TEMPORARY_COLOR = 'SET_TEMPORARY_COLOR'; +export const SET_PERSISTED_COLOR = 'SET_PERSISTED_COLOR'; + +interface ClearTemporaryColors { + type: typeof CLEAR_TEMPORARY_COLORS; +} + +interface SetTemporaryColor { + type: typeof SET_TEMPORARY_COLOR; + key: SeriesKey; + color: string; +} + +interface SetPersistedColor { + type: typeof SET_PERSISTED_COLOR; + key: SeriesKey; + color: string; +} + +export function clearTemporaryColors(): ClearTemporaryColors { + return { type: CLEAR_TEMPORARY_COLORS }; +} + +export function setTemporaryColor(key: SeriesKey, color: string): SetTemporaryColor { + return { type: SET_TEMPORARY_COLOR, key, color }; +} + +export function setPersistedColor(key: SeriesKey, color: string): SetPersistedColor { + return { type: SET_PERSISTED_COLOR, key, color }; +} + +export type ColorsActions = ClearTemporaryColors | SetTemporaryColor | SetPersistedColor; diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index 7044bba8b6..64f2c49148 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -4,6 +4,7 @@ import { ChartSettingsActions } from './chart_settings'; import { LegendActions } from './legend'; import { EventsActions } from './events'; import { MouseActions } from './mouse'; +import { ColorsActions } from './colors'; export type StateActions = | SpecActions @@ -11,4 +12,5 @@ export type StateActions = | ChartSettingsActions | LegendActions | EventsActions - | MouseActions; + | MouseActions + | ColorsActions; diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 647deebf51..9490098ef6 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -1,4 +1,5 @@ import { SPEC_PARSED, SPEC_UNMOUNTED, UPSERT_SPEC, REMOVE_SPEC, SPEC_PARSING } from './actions/specs'; +import { SET_PERSISTED_COLOR, SET_TEMPORARY_COLOR, CLEAR_TEMPORARY_COLORS } from './actions/colors'; import { interactionsReducer } from './reducers/interactions'; import { ChartTypes } from '../chart_types'; import { XYAxisChartState } from '../chart_types/xy_chart/state/chart_state'; @@ -116,6 +117,11 @@ export interface ExternalEventsState { pointer: PointerEvent | null; } +export interface ColorOverrides { + temporary: Record; + persisted: Record; +} + export interface GlobalChartState { /** * a unique ID for each chart used by re-reselect to memoize selector per chart @@ -157,6 +163,10 @@ export interface GlobalChartState { * external event state */ externalEvents: ExternalEventsState; + /** + * Color map used to persist color picker changes + */ + colors: ColorOverrides; } export const getInitialState = (chartId: string): GlobalChartState => ({ @@ -167,6 +177,10 @@ export const getInitialState = (chartId: string): GlobalChartState => ({ specs: { [DEFAULT_SETTINGS_SPEC.id]: DEFAULT_SETTINGS_SPEC, }, + colors: { + temporary: {}, + persisted: {}, + }, chartType: null, internalChartState: null, interactions: { @@ -287,6 +301,36 @@ export const chartStoreReducer = (chartId: string) => { }, }, }; + case CLEAR_TEMPORARY_COLORS: + return { + ...state, + colors: { + ...state.colors, + temporary: {}, + }, + }; + case SET_TEMPORARY_COLOR: + return { + ...state, + colors: { + ...state.colors, + temporary: { + ...state.colors.temporary, + [action.key]: action.color, + }, + }, + }; + case SET_PERSISTED_COLOR: + return { + ...state, + colors: { + ...state.colors, + persisted: { + ...state.colors.persisted, + [action.key]: action.color, + }, + }, + }; default: return { ...state, diff --git a/stories/legend/9_color_picker.tsx b/stories/legend/9_color_picker.tsx index 2ffa0c61b9..90f855e062 100644 --- a/stories/legend/9_color_picker.tsx +++ b/stories/legend/9_color_picker.tsx @@ -1,40 +1,45 @@ import React, { useState } from 'react'; -import { number } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; -import { EuiColorPicker, EuiWrappingPopover } from '@elastic/eui'; +import { EuiColorPicker, EuiWrappingPopover, EuiButton, EuiSpacer } from '@elastic/eui'; -import { Axis, BarSeries, Chart, Position, ScaleType, Settings, PartialTheme, LegendColorPicker } from '../../src/'; -import { SeededDataGenerator } from '../../src/mocks/utils'; +import { Axis, BarSeries, Chart, Position, ScaleType, Settings, LegendColorPicker } from '../../src/'; +import { BARCHART_1Y1G } from '../../src/utils/data_samples/test_dataset'; +import { SeriesKey } from '../../src/chart_types/xy_chart/utils/series'; -const dg = new SeededDataGenerator(); +const onChangeAction = action('onChange'); +const onCloseAction = action('onClose'); export const example = () => { - const theme: PartialTheme = { - legend: { - spacingBuffer: number('legend buffer value', 80), - }, - }; - const data = dg.generateGroupedSeries(5, 2); - const [colors, setColors] = useState>({}); + const [colors, setColors] = useState>({}); - const renderColorPicker: LegendColorPicker = ({ anchor, color, onClose, seriesIdentifier }) => { - const handleChange = (color: string) => { + const renderColorPicker: LegendColorPicker = ({ anchor, color, onClose, seriesIdentifier, onChange }) => { + const handleClose = () => { onClose(); + onCloseAction(); setColors({ ...colors, [seriesIdentifier.key]: color, }); }; + const handleChange = (color: string) => { + onChangeAction(color); + onChange(color); + }; return ( - - + + + + + Done + ); }; return ( - + Number(d).toFixed(2)} /> @@ -45,7 +50,7 @@ export const example = () => { xAccessor="x" yAccessors={['y']} splitSeriesAccessors={['g']} - data={data} + data={BARCHART_1Y1G} customSeriesColors={({ key }) => colors[key] ?? null} /> From 403018868b3d6f0b96215f9d4096e93c39942695 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Wed, 26 Feb 2020 17:30:13 -0600 Subject: [PATCH 14/18] chore: fix merge conflicts --- src/chart_types/xy_chart/state/utils.test.ts | 97 +- src/chart_types/xy_chart/state/utils.ts | 16 +- .../utils/__snapshots__/series.test.ts.snap | 869 ++++++++++++++++++ src/chart_types/xy_chart/utils/series.test.ts | 30 +- src/chart_types/xy_chart/utils/series.ts | 6 +- src/chart_types/xy_chart/utils/specs.ts | 8 +- src/components/legend/legend.test.tsx | 2 +- src/index.ts | 7 +- src/utils/accessor.ts | 19 +- src/utils/themes/theme.ts | 6 +- stories/legend/9_color_picker.tsx | 2 +- .../stylings/8_custom_series_colors_array.tsx | 2 +- .../9_custom_series_colors_function.tsx | 10 +- wiki/overview.md | 2 +- 14 files changed, 1031 insertions(+), 45 deletions(-) diff --git a/src/chart_types/xy_chart/state/utils.test.ts b/src/chart_types/xy_chart/state/utils.test.ts index 4e4986e22a..f91633b926 100644 --- a/src/chart_types/xy_chart/state/utils.test.ts +++ b/src/chart_types/xy_chart/state/utils.test.ts @@ -36,8 +36,14 @@ import { MockSeriesCollection } from '../../../mocks/series/series_identifiers'; import { SeededDataGenerator } from '../../../mocks/utils'; import { SeriesCollectionValue, getSeriesIndex, getSeriesColors } from '../utils/series'; import { SpecTypes } from '../../../specs/settings'; +import { ColorOverrides } from '../../../state/chart_state'; describe('Chart State utils', () => { + const emptySeriesOverrides: ColorOverrides = { + temporary: {}, + persisted: {}, + }; + it('should compute and format specifications for non stacked chart', () => { const spec1: BasicSeriesSpec = { chartType: ChartTypes.XYAxis, @@ -337,7 +343,7 @@ describe('Chart State utils', () => { }); describe('series collection is not empty', () => { - it('it should return an empty map if no customSeriesColors', () => { + it('it should return an empty map if no color', () => { const barSpec1 = MockSeriesSpec.bar({ id: specId1, data }); const barSpec2 = MockSeriesSpec.bar({ id: specId2, data }); const barSeriesSpecs = MockSeriesSpecs.fromSpecs([barSpec1, barSpec2]); @@ -347,9 +353,20 @@ describe('Chart State utils', () => { expect(actual.size).toBe(0); }); + it('it should return string color value', () => { + const color = 'green'; + const barSpec1 = MockSeriesSpec.bar({ id: specId1, data, color }); + const barSpec2 = MockSeriesSpec.bar({ id: specId2, data }); + const barSeriesSpecs = MockSeriesSpecs.fromSpecs([barSpec1, barSpec2]); + const barSeriesCollection = MockSeriesCollection.fromSpecs(barSeriesSpecs); + const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection); + + expect([...actual.values()]).toEqualArrayOf(color); + }); + describe('with customSeriesColors array', () => { const customSeriesColors = ['red', 'blue', 'green']; - const barSpec1 = MockSeriesSpec.bar({ id: specId1, data, customSeriesColors }); + const barSpec1 = MockSeriesSpec.bar({ id: specId1, data, color: customSeriesColors }); const barSpec2 = MockSeriesSpec.bar({ id: specId2, data }); const barSeriesSpecs = MockSeriesSpecs.fromSpecs([barSpec1, barSpec2]); const barSeriesCollection = MockSeriesCollection.fromSpecs(barSeriesSpecs); @@ -369,15 +386,15 @@ describe('Chart State utils', () => { }); }); - describe('with customSeriesColors function', () => { - const customSeriesColors: SeriesColorAccessorFn = ({ yAccessor, splitAccessors }) => { + describe('with color function', () => { + const color: SeriesColorAccessorFn = ({ yAccessor, splitAccessors }) => { if (yAccessor === 'y' && splitAccessors.get('g') === 'b') { return 'aquamarine'; } return null; }; - const barSpec1 = MockSeriesSpec.bar({ id: specId1, yAccessors: ['y'], data, customSeriesColors }); + const barSpec1 = MockSeriesSpec.bar({ id: specId1, yAccessors: ['y'], data, color }); const barSpec2 = MockSeriesSpec.bar({ id: specId2, data }); const barSeriesSpecs = MockSeriesSpecs.fromSpecs([barSpec1, barSpec2]); const barSeriesCollection = MockSeriesCollection.fromSpecs(barSeriesSpecs); @@ -449,7 +466,12 @@ describe('Chart State utils', () => { const chartTheme = { ...LIGHT_THEME, colors: chartColors }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, @@ -506,7 +528,12 @@ describe('Chart State utils', () => { const chartTheme = { ...LIGHT_THEME, colors: chartColors }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, @@ -565,7 +592,12 @@ describe('Chart State utils', () => { const chartTheme = { ...LIGHT_THEME, colors: chartColors }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, @@ -651,7 +683,12 @@ describe('Chart State utils', () => { const chartTheme = { ...LIGHT_THEME, colors: chartColors }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, @@ -724,7 +761,12 @@ describe('Chart State utils', () => { const chartTheme = { ...LIGHT_THEME, colors: chartColors }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, @@ -797,7 +839,12 @@ describe('Chart State utils', () => { const chartTheme = { ...LIGHT_THEME, colors: chartColors }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, @@ -878,7 +925,12 @@ describe('Chart State utils', () => { const chartTheme = { ...LIGHT_THEME, colors: chartColors }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, @@ -971,7 +1023,12 @@ describe('Chart State utils', () => { const chartTheme = { ...LIGHT_THEME, colors: chartColors }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, @@ -1057,7 +1114,12 @@ describe('Chart State utils', () => { const chartTheme = { ...LIGHT_THEME, colors: chartColors }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, @@ -1122,7 +1184,12 @@ describe('Chart State utils', () => { }; const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId, undefined); - const seriesColorMap = getSeriesColors(seriesDomains.seriesCollection, chartColors, new Map()); + const seriesColorMap = getSeriesColors( + seriesDomains.seriesCollection, + chartColors, + new Map(), + emptySeriesOverrides, + ); const geometries = computeSeriesGeometries( seriesSpecs, seriesDomains.xDomain, diff --git a/src/chart_types/xy_chart/state/utils.ts b/src/chart_types/xy_chart/state/utils.ts index 661cce3e91..f398efda55 100644 --- a/src/chart_types/xy_chart/state/utils.ts +++ b/src/chart_types/xy_chart/state/utils.ts @@ -131,18 +131,20 @@ export function getCustomSeriesColors( seriesCollection.forEach(({ seriesIdentifier }, seriesKey) => { const spec = getSpecsById(seriesSpecs, seriesIdentifier.specId); - if (!spec || !spec.customSeriesColors) { + if (!spec || !spec.color) { return; } let color: string | undefined | null; - if (!color && spec.customSeriesColors) { - const counter = counters.get(seriesIdentifier.specId) || 0; - color = Array.isArray(spec.customSeriesColors) - ? spec.customSeriesColors[counter % spec.customSeriesColors.length] - : spec.customSeriesColors(seriesIdentifier); - counters.set(seriesIdentifier.specId, counter + 1); + if (!color && spec.color) { + if (typeof spec.color === 'string') { + color = spec.color; + } else { + const counter = counters.get(seriesIdentifier.specId) || 0; + color = Array.isArray(spec.color) ? spec.color[counter % spec.color.length] : spec.color(seriesIdentifier); + counters.set(seriesIdentifier.specId, counter + 1); + } } if (color) { diff --git a/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap b/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap index 6484ea9d0c..47f1020b1b 100644 --- a/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap +++ b/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap @@ -18275,6 +18275,875 @@ Array [ ] `; +exports[`Series functional accessors Can split dataset into 2Y2G series 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "x": 6, + "y0": null, + "y1": 7, + }, + ], + "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "direct-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "x": 0, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "x": 2, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "x": 6, + "y0": null, + "y1": 3, + }, + ], + "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "direct-cdn", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "x": 6, + "y0": null, + "y1": 7, + }, + ], + "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "indirect-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "x": 0, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "x": 2, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "x": 6, + "y0": null, + "y1": 3, + }, + ], + "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "indirect-cdn", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "direct-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "x": 0, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "x": 1, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "x": 3, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "x": 6, + "y0": null, + "y1": 4, + }, + ], + "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "direct-cdn", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "indirect-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "x": 0, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "x": 1, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "x": 3, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "x": 6, + "y0": null, + "y1": 4, + }, + ], + "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "indirect-cdn", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y2", + }, +] +`; + +exports[`Series functional accessors Can split dataset with custom _all xAccessor 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "x": "_all", + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "x": "_all", + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "x": "_all", + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "x": "_all", + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "x": "_all", + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "x": "_all", + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "x": "_all", + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "x": "_all", + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "x": "_all", + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "x": "_all", + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "x": "_all", + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "x": "_all", + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "x": "_all", + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "x": "_all", + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "x": "_all", + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "x": "_all", + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "x": "_all", + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "x": "_all", + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "x": "_all", + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "x": "_all", + "y0": null, + "y1": 6, + }, + ], + "key": "spec{spec1}yAccessor{y1}splitAccessors{}", + "seriesKeys": Array [ + "y1", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y1", + }, +] +`; + exports[`Series should compute data series for stacked specs 1`] = ` Array [ Object { diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index b3efc60902..9310adb5aa 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -20,6 +20,7 @@ import { SpecTypes } from '../../../specs/settings'; import { MockSeriesSpec } from '../../../mocks/specs'; import { SeededDataGenerator } from '../../../mocks/utils'; import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { AccessorFn } from '../../../utils/accessor'; const dg = new SeededDataGenerator(); @@ -527,7 +528,7 @@ describe('Series', () => { }); it('should return persisted color', () => { - const result = getSeriesColors(seriesColors, chartColors, customColors, persistedOverrides); + const result = getSeriesColors(seriesColors, chartColors, emptyCustomColors, persistedOverrides); const expected = new Map(); expected.set(seriesKey, persistedColor); expect(result).toEqual(expected); @@ -897,4 +898,31 @@ describe('Series', () => { }); }); }); + + describe('functional accessors', () => { + test('Can split dataset into 2Y2G series', () => { + const xAccessor: AccessorFn = (d) => d.x; + const splittedSeries = splitSeries({ + id: 'spec1', + data: TestDataset.BARCHART_2Y2G, + xAccessor, + yAccessors: ['y1', 'y2'], + splitSeriesAccessors: ['g1', 'g2'], + }); + expect(splittedSeries.rawDataSeries.length).toBe(8); + expect(splittedSeries.rawDataSeries).toMatchSnapshot(); + }); + + test('Can split dataset with custom _all xAccessor', () => { + const xAccessor: AccessorFn = () => '_all'; + const splittedSeries = splitSeries({ + id: 'spec1', + data: TestDataset.BARCHART_2Y2G, + xAccessor, + yAccessors: ['y1'], + }); + expect(splittedSeries.rawDataSeries.length).toBe(1); + expect(splittedSeries.rawDataSeries).toMatchSnapshot(); + }); + }); }); diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index 0a2e9a5daa..9d1070c70b 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -1,5 +1,5 @@ import { ColorConfig } from '../../../utils/themes/theme'; -import { Accessor } from '../../../utils/accessor'; +import { Accessor, getAccessorValue, AccessorFn } from '../../../utils/accessor'; import { GroupId, SpecId } from '../../../utils/ids'; import { splitSpecsByGroupId, YBasicSeriesSpec } from '../domains/y_domain'; import { formatNonStackedDataSeriesValues } from './nonstacked_series_utils'; @@ -217,11 +217,11 @@ function getSplitAccessors(datum: Datum, accessors: Accessor[] = []): Map string | null; -export type CustomSeriesColors = SeriesColorsArray | SeriesColorAccessorFn; +export type SeriesColorAccessor = string | SeriesColorsArray | SeriesColorAccessorFn; export interface SeriesAccessors { /** The field name of the x value on Datum object */ - xAccessor: Accessor; + xAccessor: Accessor | AccessorFn; /** An array of field names one per y metric value */ yAccessors: Accessor[]; /** An optional accessor of the y0 value: base point for area/bar charts */ diff --git a/src/components/legend/legend.test.tsx b/src/components/legend/legend.test.tsx index 74d1a5117d..252cae2fb5 100644 --- a/src/components/legend/legend.test.tsx +++ b/src/components/legend/legend.test.tsx @@ -178,7 +178,7 @@ describe('Legend', () => { xAccessor="x" yAccessors={['y']} splitSeriesAccessors={['g']} - customSeriesColors={this.state.colors} + color={this.state.colors} data={this.data} /> diff --git a/src/index.ts b/src/index.ts index fd39490345..556a7b7d6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ export { SeriesIdentifier, XYChartSeriesIdentifier } from './chart_types/xy_char export { AnnotationDomainType, AnnotationDomainTypes, - CustomSeriesColors, + SeriesColorAccessor, SeriesColorsArray, SeriesColorAccessorFn, HistogramModeAlignment, @@ -30,6 +30,11 @@ export { RectAnnotationDatum, RectAnnotationSpec, SeriesTypes, + SeriesName, + SeriesNameFn, + SeriesNameAccessor, + SeriesNameConfig, + SeriesNameConfigOptions, } from './chart_types/xy_chart/utils/specs'; export { AnnotationTooltipFormatter } from './chart_types/xy_chart/annotations/annotation_utils'; export { GeometryValue } from './utils/geometry'; diff --git a/src/utils/accessor.ts b/src/utils/accessor.ts index c889d15bca..af009169c6 100644 --- a/src/utils/accessor.ts +++ b/src/utils/accessor.ts @@ -5,8 +5,9 @@ type BinaryAccessorFn = (datum: Datum, index: number) => any; export type AccessorFn = UnaryAccessorFn; export type IndexedAccessorFn = UnaryAccessorFn | BinaryAccessorFn; -export type AccessorString = string | number; -export type Accessor = AccessorString; +type AccessorObjectKey = string; +type AccessorArrayIndex = number; +export type Accessor = AccessorObjectKey | AccessorArrayIndex; /** * Accessor format for _banded_ series as postfix string or accessor function @@ -39,3 +40,17 @@ export function getAccessorFormatLabel(accessor: AccessorFormat, label: string): return accessor(label); } + +/** + * Helper function to get accessor value from string, number or function + * + * @param {Datum} datum + * @param {AccessorString|AccessorFn} accessor + */ +export function getAccessorValue(datum: Datum, accessor: Accessor | AccessorFn) { + if (typeof accessor === 'function') { + return accessor(datum); + } + + return datum[accessor]; +} diff --git a/src/utils/themes/theme.ts b/src/utils/themes/theme.ts index 7f7a40fc6b..55d5c1f7dc 100644 --- a/src/utils/themes/theme.ts +++ b/src/utils/themes/theme.ts @@ -131,7 +131,7 @@ export interface Theme { * * __Note:__ This is not used to set the color of a specific series. As such, any changes to the styles will not be reflected in the tooltip, legend, etc.. * - * You may use `CustomSeriesColors` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. + * You may use `SeriesColorAccessor` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. */ lineSeriesStyle: LineSeriesStyle; /** @@ -139,7 +139,7 @@ export interface Theme { * * __Note:__ This is not used to set the color of a specific series. As such, any changes to the styles will not be reflected in the tooltip, legend, etc.. * - * You may use `CustomSeriesColors` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. + * You may use `SeriesColorAccessor` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. */ areaSeriesStyle: AreaSeriesStyle; /** @@ -147,7 +147,7 @@ export interface Theme { * * __Note:__ This is not used to set the color of a specific series. As such, any changes to the styles will not be reflected in the tooltip, legend, etc.. * - * You may use `CustomSeriesColors` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. + * You may use `SeriesColorAccessor` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. */ barSeriesStyle: BarSeriesStyle; arcSeriesStyle: ArcSeriesStyle; diff --git a/stories/legend/9_color_picker.tsx b/stories/legend/9_color_picker.tsx index 90f855e062..4d0ffd8bc8 100644 --- a/stories/legend/9_color_picker.tsx +++ b/stories/legend/9_color_picker.tsx @@ -51,7 +51,7 @@ export const example = () => { yAccessors={['y']} splitSeriesAccessors={['g']} data={BARCHART_1Y1G} - customSeriesColors={({ key }) => colors[key] ?? null} + color={({ key }) => colors[key] ?? null} /> ); diff --git a/stories/stylings/8_custom_series_colors_array.tsx b/stories/stylings/8_custom_series_colors_array.tsx index c52ca08e46..485ad96d1b 100644 --- a/stories/stylings/8_custom_series_colors_array.tsx +++ b/stories/stylings/8_custom_series_colors_array.tsx @@ -18,7 +18,7 @@ export const example = () => { xAccessor="x" yAccessors={['y1']} splitSeriesAccessors={['g1', 'g2']} - customSeriesColors={['red', 'orange', 'blue', 'green', 'black', 'lightgrey']} + color={['red', 'orange', 'blue', 'green', 'black', 'lightgrey']} data={TestDatasets.BARCHART_2Y2G} /> diff --git a/stories/stylings/9_custom_series_colors_function.tsx b/stories/stylings/9_custom_series_colors_function.tsx index 54b38a274c..479dd83461 100644 --- a/stories/stylings/9_custom_series_colors_function.tsx +++ b/stories/stylings/9_custom_series_colors_function.tsx @@ -1,12 +1,12 @@ import { color } from '@storybook/addon-knobs'; import React from 'react'; -import { Axis, BarSeries, Chart, CustomSeriesColors, LineSeries, Position, ScaleType, Settings } from '../../src'; +import { Axis, BarSeries, Chart, SeriesColorAccessor, LineSeries, Position, ScaleType, Settings } from '../../src'; import * as TestDatasets from '../../src/utils/data_samples/test_dataset'; export const example = () => { const barColor = color('barSeriesColor', '#000'); - const barSeriesColorAccessor: CustomSeriesColors = ({ specId, yAccessor, splitAccessors }) => { + const barSeriesColorAccessor: SeriesColorAccessor = ({ specId, yAccessor, splitAccessors }) => { if ( specId === 'bars' && yAccessor === 'y1' && @@ -21,7 +21,7 @@ export const example = () => { }; const lineColor = color('linelineSeriesColor', '#ff0'); - const lineSeriesColorAccessor: CustomSeriesColors = ({ specId, yAccessor, splitAccessors }) => { + const lineSeriesColorAccessor: SeriesColorAccessor = ({ specId, yAccessor, splitAccessors }) => { // eslint-disable-next-line react/prop-types if (specId === 'lines' && yAccessor === 'y1' && splitAccessors.size === 0) { return lineColor; @@ -42,7 +42,7 @@ export const example = () => { xAccessor="x" yAccessors={['y1', 'y2']} splitSeriesAccessors={['g1', 'g2']} - customSeriesColors={barSeriesColorAccessor} + color={barSeriesColorAccessor} data={TestDatasets.BARCHART_2Y2G} /> { yScaleType={ScaleType.Linear} xAccessor="x" yAccessors={['y']} - customSeriesColors={lineSeriesColorAccessor} + color={lineSeriesColorAccessor} data={[ { x: 0, y: 3 }, { x: 1, y: 2 }, diff --git a/wiki/overview.md b/wiki/overview.md index f8bfc5b4d9..4d54bca087 100644 --- a/wiki/overview.md +++ b/wiki/overview.md @@ -127,7 +127,7 @@ export interface SeriesSpec { /** The type of series you are looking to render */ seriesType: SeriesTypes; /** Set colors for specific series */ - customSeriesColors?: CustomSeriesColors; + color?: SeriesColorAccessor; /** If the series should appear in the legend * @default false */ From 310240f23a7ce51fcc971a67303b899eec976c42 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Wed, 26 Feb 2020 17:30:55 -0600 Subject: [PATCH 15/18] tests: generalize common page object methods --- integration/page_objects/common.ts | 157 ++++++++++++++++++++++------- 1 file changed, 120 insertions(+), 37 deletions(-) diff --git a/integration/page_objects/common.ts b/integration/page_objects/common.ts index 9f0669a073..3fbceca1bb 100644 --- a/integration/page_objects/common.ts +++ b/integration/page_objects/common.ts @@ -15,13 +15,44 @@ interface ScreenshotDOMElementOptions { path?: string; } +type ScreenshotElementAtUrlOptions = ScreenshotDOMElementOptions & { + /** + * timeout for waiting on element to appear in DOM + * + * @default JEST_TIMEOUT + */ + timeout?: number; + /** + * any desired action to be performed after loading url, prior to screenshot + */ + action?: () => void | Promise; + /** + * Selector used to wait on DOM element + */ + waitSelector?: string; +}; + class CommonPage { + readonly chartWaitSelector = '.echChartStatus[data-ech-render-complete=true]'; + readonly chartSelector = '.echChart'; + + /** + * Parse url from knob storybook url to iframe storybook url + * + * @param url + */ static parseUrl(url: string): string { const { query } = Url.parse(url); return `${baseUrl}?${query}${query ? '&' : ''}knob-debug=false`; } - async getBoundingClientRect(selector = '.echChart') { + + /** + * Get getBoundingClientRect of selected element + * + * @param selector + */ + async getBoundingClientRect(selector: string) { return await page.evaluate((selector) => { const element = document.querySelector(selector); @@ -34,12 +65,16 @@ class CommonPage { return { left: x, top: y, width, height, id: element.id }; }, selector); } + /** - * Capture screenshot or chart element only + * Capture screenshot of selected element only + * + * @param selector + * @param options */ - async screenshotDOMElement(selector = '.echChart', opts?: ScreenshotDOMElementOptions) { - const padding: number = opts && opts.padding ? opts.padding : 0; - const path: string | undefined = opts && opts.path ? opts.path : undefined; + async screenshotDOMElement(selector: string, options?: ScreenshotDOMElementOptions) { + const padding: number = options && options.padding ? options.padding : 0; + const path: string | undefined = options && options.path ? options.path : undefined; const rect = await this.getBoundingClientRect(selector); return page.screenshot({ @@ -53,69 +88,117 @@ class CommonPage { }); } - async moveMouseRelativeToDOMElement(mousePosition: { x: number; y: number }, selector = '.echChart') { - const chartContainer = await this.getBoundingClientRect(selector); - await page.mouse.move(chartContainer.left + mousePosition.x, chartContainer.top + mousePosition.y); + /** + * Move mouse relative to element + * + * @param mousePosition + * @param selector + */ + async moveMouseRelativeToDOMElement(mousePosition: { x: number; y: number }, selector: string) { + const element = await this.getBoundingClientRect(selector); + await page.mouse.move(element.left + mousePosition.x, element.top + mousePosition.y); + } + + /** + * Click mouse relative to element + * + * @param mousePosition + * @param selector + */ + async clickMouseRelativeToDOMElement(mousePosition: { x: number; y: number }, selector: string) { + const element = await this.getBoundingClientRect(selector); + await page.mouse.click(element.left + mousePosition.x, element.top + mousePosition.y); } /** - * Expect a chart given a url from storybook. + * Expect an element given a url and selector from storybook * * - Note: No need to fix host or port. They will be set automatically. * * @param url Storybook url from knobs section + * @param selector selector of element to screenshot + * @param options */ - async expectChartAtUrlToMatchScreenshot(url: string) { + async expectElementAtUrlToMatchScreenshot( + url: string, + selector: string = 'body', + options?: ScreenshotElementAtUrlOptions, + ) { try { - await this.loadChartFromURL(url); - await this.waitForElement(); + await this.loadElementFromURL(url, options?.waitSelector ?? selector, options?.timeout); - const chart = await this.screenshotDOMElement(); + if (options?.action) { + await options.action(); + } + + const element = await this.screenshotDOMElement(selector, options); - if (!chart) { - throw new Error(`Error: Unable to find chart element\n\n\t${url}`); + if (!element) { + throw new Error(`Error: Unable to find element\n\n\t${url}`); } - expect(chart).toMatchImageSnapshot(); + expect(element).toMatchImageSnapshot(); } catch (error) { throw new Error(error); } } /** - * Expect a chart given a url from storybook. - * - * - Note: No need to fix host or port. They will be set automatically. + * Expect a chart given a url from storybook * * @param url Storybook url from knobs section + * @param options */ - async expectChartWithMouseAtUrlToMatchScreenshot(url: string, mousePosition: { x: number; y: number }) { - try { - await this.loadChartFromURL(url); - await this.waitForElement(); - await this.moveMouseRelativeToDOMElement(mousePosition); - const chart = await this.screenshotDOMElement(); - if (!chart) { - throw new Error(`Error: Unable to find chart element\n\n\t${url}`); - } + async expectChartAtUrlToMatchScreenshot(url: string, options?: ScreenshotElementAtUrlOptions) { + await this.expectElementAtUrlToMatchScreenshot(url, this.chartSelector, { + waitSelector: this.chartWaitSelector, + ...options, + }); + } - expect(chart).toMatchImageSnapshot(); - } catch (error) { - throw new Error(`${error}\n\n${url}`); - } + /** + * Expect a chart given a url from storybook with mouse move + * + * @param url Storybook url from knobs section + * @param mousePosition - postion of mouse relative to chart + * @param options + */ + async expectChartWithMouseAtUrlToMatchScreenshot( + url: string, + mousePosition: { x: number; y: number }, + options?: Omit, + ) { + const action = async () => await this.moveMouseRelativeToDOMElement(mousePosition, this.chartSelector); + await this.expectChartAtUrlToMatchScreenshot(url, { + ...options, + action, + }); } - async loadChartFromURL(url: string) { + + /** + * Loads storybook page from raw url, and waits for element + * + * @param url Storybook url from knobs section + * @param waitSelector selector of element to wait to appear in DOM + * @param timeout timeout for waiting on element to appear in DOM + */ + async loadElementFromURL(url: string, waitSelector?: string, timeout?: number) { const cleanUrl = CommonPage.parseUrl(url); await page.goto(cleanUrl); - this.waitForElement(); + + if (waitSelector) { + await this.waitForElement(waitSelector, timeout); + } } + /** * Wait for an element to be on the DOM - * @param {string} [selector] the DOM selector to wait for, default to '.echChartStatus[data-ech-render-complete=true]' + * + * @param {string} [waitSelector] the DOM selector to wait for, default to '.echChartStatus[data-ech-render-complete=true]' * @param {number} [timeout] - the timeout for the operation, default to 10000ms */ - async waitForElement(selector = '.echChartStatus[data-ech-render-complete=true]', timeout = JEST_TIMEOUT) { - await page.waitForSelector(selector, { timeout }); + async waitForElement(waitSelector: string, timeout = JEST_TIMEOUT) { + await page.waitForSelector(waitSelector, { timeout }); } } From e376620f57d9a4ea4874dd1a481cd600146153b8 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Wed, 26 Feb 2020 17:42:27 -0600 Subject: [PATCH 16/18] test: add legend color picker interaction VRT --- ...ender-color-picker-on-mouse-click-1-snap.png | Bin 0 -> 36933 bytes integration/tests/legend_stories.test.ts | 12 ++++++++++++ 2 files changed, 12 insertions(+) create mode 100644 integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-render-color-picker-on-mouse-click-1-snap.png diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-render-color-picker-on-mouse-click-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-render-color-picker-on-mouse-click-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..9820ff06ff1f6f0c962d203cbb609f080868b8fd GIT binary patch literal 36933 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

)T>N=^;)#)w(akE@qIfup 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 literal 0 HcmV?d00001 diff --git a/integration/tests/legend_stories.test.ts b/integration/tests/legend_stories.test.ts index 0c40236201..9fcb5bf3f4 100644 --- a/integration/tests/legend_stories.test.ts +++ b/integration/tests/legend_stories.test.ts @@ -21,4 +21,16 @@ describe('Legend stories', () => { 'http://localhost:9001/?path=/story/legend--legend-spacing-buffer&knob-legend buffer value=0', ); }); + + it('should render color picker on mouse click', async () => { + const action = async () => await common.clickMouseRelativeToDOMElement({ x: 0, y: 0 }, '.echLegendItem__color'); + await common.expectElementAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/legend--color-picker', + 'body', + { + action, + waitSelector: common.chartWaitSelector, + }, + ); + }); }); From 4386a19359e2b88e033b87b09435500bf36c7b13 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Thu, 27 Feb 2020 10:43:42 -0600 Subject: [PATCH 17/18] fix: vrt flakyness and errors --- integration/jest.config.js | 2 ++ integration/page_objects/common.ts | 8 ++++++++ ...r-picker-visually-looks-correct-1-snap.png | Bin 0 -> 12474 bytes ...der-color-picker-on-mouse-click-1-snap.png | Bin 36933 -> 37899 bytes integration/tests/legend_stories.test.ts | 1 + 5 files changed, 11 insertions(+) create mode 100644 integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-color-picker-visually-looks-correct-1-snap.png diff --git a/integration/jest.config.js b/integration/jest.config.js index cb02bbd111..a21d53a49a 100644 --- a/integration/jest.config.js +++ b/integration/jest.config.js @@ -8,6 +8,8 @@ module.exports = Object.assign( 'ts-jest': { tsConfig: '/tsconfig.json', }, + window: {}, + HTMLElement: {}, }, }, jestPuppeteerDocker, diff --git a/integration/page_objects/common.ts b/integration/page_objects/common.ts index 3fbceca1bb..15b69cc122 100644 --- a/integration/page_objects/common.ts +++ b/integration/page_objects/common.ts @@ -30,6 +30,10 @@ type ScreenshotElementAtUrlOptions = ScreenshotDOMElementOptions & { * Selector used to wait on DOM element */ waitSelector?: string; + /** + * Delay to take screenshot after element is visiable + */ + delay?: number; }; class CommonPage { @@ -131,6 +135,10 @@ class CommonPage { await options.action(); } + if (options?.delay) { + await page.waitFor(options.delay); + } + const element = await this.screenshotDOMElement(selector, options); if (!element) { diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-color-picker-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-color-picker-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..98c49da8def68eaac6b6dde3c8b3d7aa16ada898 GIT binary patch literal 12474 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 N>XQ1!f(zGg{~u|cL&pFB literal 0 HcmV?d00001 diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-render-color-picker-on-mouse-click-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-render-color-picker-on-mouse-click-1-snap.png index 9820ff06ff1f6f0c962d203cbb609f080868b8fd..4bb4f8c9c208c85280d3cf551a85500745556539 100644 GIT binary patch literal 37899 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 literal 36933 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

)T>N=^;)#)w(akE@qIfup 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 diff --git a/integration/tests/legend_stories.test.ts b/integration/tests/legend_stories.test.ts index 9fcb5bf3f4..a2b76b89ef 100644 --- a/integration/tests/legend_stories.test.ts +++ b/integration/tests/legend_stories.test.ts @@ -30,6 +30,7 @@ describe('Legend stories', () => { { action, waitSelector: common.chartWaitSelector, + delay: 500, // needed for popover animation to complete }, ); }); From 5cf51e4e143e3ef1ed0c2e3e1226799a7d51d308 Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Thu, 27 Feb 2020 13:55:01 -0600 Subject: [PATCH 18/18] chore: update pr comments, repalce string types with Color --- integration/jest.config.js | 7 +++++++ ...der-color-picker-on-mouse-click-1-snap.png | Bin 37899 -> 39642 bytes .../partition_chart/layout/utils/calcs.ts | 3 ++- .../xy_chart/annotations/annotation_utils.ts | 4 ++-- src/chart_types/xy_chart/legend/legend.ts | 6 +++--- .../xy_chart/rendering/rendering.ts | 10 +++++----- .../state/selectors/get_series_color_map.ts | 3 ++- src/chart_types/xy_chart/state/utils.ts | 12 ++++++------ src/chart_types/xy_chart/utils/series.ts | 16 ++++++++-------- src/components/legend/legend_item.tsx | 13 +++++++------ src/specs/settings.tsx | 8 ++++---- src/state/actions/colors.ts | 9 +++++---- src/state/chart_state.ts | 5 +++-- src/utils/geometry.ts | 11 ++++++----- stories/legend/9_color_picker.tsx | 16 +++++++++++++--- 15 files changed, 73 insertions(+), 50 deletions(-) diff --git a/integration/jest.config.js b/integration/jest.config.js index a21d53a49a..1b2ce938fe 100644 --- a/integration/jest.config.js +++ b/integration/jest.config.js @@ -8,6 +8,13 @@ module.exports = Object.assign( 'ts-jest': { tsConfig: '/tsconfig.json', }, + /* + * The window and HTMLElement globals are required to use @elastic/eui with VRT + * + * The jest-puppeteer-docker env extends a node test environment and not jsdom test environment. + * Some EUI components that are included in the bundle, but not used, require the jsdom setup. + * To bypass these errors we are just mocking both as empty objects. + */ window: {}, HTMLElement: {}, }, diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-render-color-picker-on-mouse-click-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-render-color-picker-on-mouse-click-1-snap.png index 4bb4f8c9c208c85280d3cf551a85500745556539..5dc72e320160864ff5cca6e95bcec95075d6674b 100644 GIT binary patch delta 19748 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 delta 17991 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 diff --git a/src/chart_types/partition_chart/layout/utils/calcs.ts b/src/chart_types/partition_chart/layout/utils/calcs.ts index d3f1ce6861..355c0ed371 100644 --- a/src/chart_types/partition_chart/layout/utils/calcs.ts +++ b/src/chart_types/partition_chart/layout/utils/calcs.ts @@ -1,5 +1,6 @@ import { Ratio } from '../types/geometry_types'; import { RgbTuple, stringToRGB } from './d3_utils'; +import { Color } from '../../../../utils/commons'; export function hueInterpolator(colors: RgbTuple[]) { return (d: number) => { @@ -26,7 +27,7 @@ export function arrayToLookup(keyFun: Function, array: Array) { return Object.assign({}, ...array.map((d) => ({ [keyFun(d)]: d }))); } -export function colorIsDark(color: string) { +export function colorIsDark(color: Color) { // fixme this assumes a white or very light background const rgba = stringToRGB(color); const { r, g, b, opacity } = rgba; diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.ts b/src/chart_types/xy_chart/annotations/annotation_utils.ts index fd7463e263..088f4d164d 100644 --- a/src/chart_types/xy_chart/annotations/annotation_utils.ts +++ b/src/chart_types/xy_chart/annotations/annotation_utils.ts @@ -23,7 +23,7 @@ import { AnnotationRectProps, computeRectAnnotationDimensions, } from './rect_annotation_tooltip'; -import { Rotation, Position } from '../../../utils/commons'; +import { Rotation, Position, Color } from '../../../utils/commons'; export type AnnotationTooltipFormatter = (details?: string) => JSX.Element | null; @@ -54,7 +54,7 @@ export interface AnnotationMarker { icon: JSX.Element; position: { top: number; left: number }; dimension: { width: number; height: number }; - color: string; + color: Color; } export type AnnotationDimensions = AnnotationLineProps[] | AnnotationRectProps[]; diff --git a/src/chart_types/xy_chart/legend/legend.ts b/src/chart_types/xy_chart/legend/legend.ts index 34cbe31b62..72c9a8ec04 100644 --- a/src/chart_types/xy_chart/legend/legend.ts +++ b/src/chart_types/xy_chart/legend/legend.ts @@ -1,5 +1,5 @@ import { getAxesSpecForSpecId, LastValues, getSpecsById } from '../state/utils'; -import { identity } from '../../../utils/commons'; +import { identity, Color } from '../../../utils/commons'; import { SeriesCollectionValue, getSeriesIndex, @@ -19,7 +19,7 @@ interface FormattedLastValues { export type LegendItem = Postfixes & { key: SeriesKey; - color: string; + color: Color; name: string; seriesIdentifier: XYChartSeriesIdentifier; isSeriesVisible?: boolean; @@ -56,7 +56,7 @@ export function getItemLabel( export function computeLegend( seriesCollection: Map, - seriesColors: Map, + seriesColors: Map, specs: BasicSeriesSpec[], defaultColor: string, axesSpecs: AxisSpec[], diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index f74410db0b..a1d087b2ec 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -22,7 +22,7 @@ import { ClippedRanges, BandedAccessorType, } from '../../../utils/geometry'; -import { mergePartial } from '../../../utils/commons'; +import { mergePartial, Color } from '../../../utils/commons'; import { LegendItem } from '../legend/legend'; export function mutableIndexedGeometryMapUpsert( @@ -91,7 +91,7 @@ function renderPoints( dataSeries: DataSeries, xScale: Scale, yScale: Scale, - color: string, + color: Color, hasY0Accessors: boolean, styleAccessor?: PointStyleAccessor, ): { @@ -170,7 +170,7 @@ export function renderBars( dataSeries: DataSeries, xScale: Scale, yScale: Scale, - color: string, + color: Color, sharedSeriesStyle: BarSeriesStyle, displayValueSettings?: DisplayValueSpec, styleAccessor?: BarStyleAccessor, @@ -310,7 +310,7 @@ export function renderLine( dataSeries: DataSeries, xScale: Scale, yScale: Scale, - color: string, + color: Color, curve: CurveType, hasY0Accessors: boolean, xScaleOffset: number, @@ -399,7 +399,7 @@ export function renderArea( dataSeries: DataSeries, xScale: Scale, yScale: Scale, - color: string, + color: Color, curve: CurveType, hasY0Accessors: boolean, xScaleOffset: number, diff --git a/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts b/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts index f8205b7e28..dab4568b7a 100644 --- a/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts +++ b/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts @@ -6,6 +6,7 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getCustomSeriesColors } from '../utils'; import { GlobalChartState } from '../../../../state/chart_state'; +import { Color } from '../../../../utils/commons'; function getColorOverrides({ colors }: GlobalChartState) { return colors; @@ -13,7 +14,7 @@ function getColorOverrides({ colors }: GlobalChartState) { export const getSeriesColorsSelector = createCachedSelector( [getSeriesSpecsSelector, computeSeriesDomainsSelector, getChartThemeSelector, getColorOverrides], - (seriesSpecs, seriesDomainsAndData, chartTheme, colorOverrides): Map => { + (seriesSpecs, seriesDomainsAndData, chartTheme, colorOverrides): Map => { const updatedCustomSeriesColors = getCustomSeriesColors(seriesSpecs, seriesDomainsAndData.seriesCollection); const seriesColorMap = getSeriesColors( diff --git a/src/chart_types/xy_chart/state/utils.ts b/src/chart_types/xy_chart/state/utils.ts index f398efda55..13d83a74a7 100644 --- a/src/chart_types/xy_chart/state/utils.ts +++ b/src/chart_types/xy_chart/state/utils.ts @@ -33,7 +33,7 @@ import { SeriesTypes, } from '../utils/specs'; import { ColorConfig, Theme } from '../../../utils/themes/theme'; -import { identity, mergePartial, Rotation } from '../../../utils/commons'; +import { identity, mergePartial, Rotation, Color } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; import { Domain } from '../../../utils/domain'; import { GroupId, SpecId } from '../../../utils/ids'; @@ -124,8 +124,8 @@ export function updateDeselectedDataSeries( export function getCustomSeriesColors( seriesSpecs: BasicSeriesSpec[], seriesCollection: Map, -): Map { - const updatedCustomSeriesColors = new Map(); +): Map { + const updatedCustomSeriesColors = new Map(); const counters = new Map(); seriesCollection.forEach(({ seriesIdentifier }, seriesKey) => { @@ -135,7 +135,7 @@ export function getCustomSeriesColors( return; } - let color: string | undefined | null; + let color: Color | undefined | null; if (!color && spec.color) { if (typeof spec.color === 'string') { @@ -260,7 +260,7 @@ export function computeSeriesGeometries( stacked: FormattedDataSeries[]; nonStacked: FormattedDataSeries[]; }, - seriesColorMap: Map, + seriesColorMap: Map, chartTheme: Theme, chartDims: Dimensions, chartRotation: Rotation, @@ -446,7 +446,7 @@ function renderGeometries( xScale: Scale, yScale: Scale, seriesSpecs: BasicSeriesSpec[], - seriesColorsMap: Map, + seriesColorsMap: Map, defaultColor: string, axesSpecs: AxisSpec[], chartTheme: Theme, diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index 9d1070c70b..3cb0c84aa1 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -7,7 +7,7 @@ import { BasicSeriesSpec, SeriesTypes, SeriesSpecs, SeriesNameConfigOptions } fr import { formatStackedDataSeriesValues } from './stacked_series_utils'; import { ScaleType } from '../../../scales'; import { LastValues } from '../state/utils'; -import { Datum } from '../../../utils/commons'; +import { Datum, Color } from '../../../utils/commons'; import { ColorOverrides } from '../../../state/chart_state'; export const SERIES_DELIMITER = ' - '; @@ -481,7 +481,7 @@ export function getSortedDataSeriesColorsValuesMap( /** * Helper function to get highest override color. * - * from highest to lowest: `temporary`, `customSeriesColors` then `persisted` + * from highest to lowest: `temporary`, `seriesSpec.color` then `persisted` * * @param key * @param customColors @@ -489,10 +489,10 @@ export function getSortedDataSeriesColorsValuesMap( */ function getHighestOverride( key: string, - customColors: Map, + customColors: Map, overrides: ColorOverrides, -): string | undefined { - let color: string | undefined = overrides.temporary[key]; +): Color | undefined { + let color: Color | undefined = overrides.temporary[key]; if (color) { return color; @@ -518,10 +518,10 @@ function getHighestOverride( export function getSeriesColors( seriesCollection: Map, chartColors: ColorConfig, - customColors: Map, + customColors: Map, overrides: ColorOverrides, -): Map { - const seriesColorMap = new Map(); +): Map { + const seriesColorMap = new Map(); let counter = 0; seriesCollection.forEach((_, seriesKey) => { diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index ab1db9d4c4..7d342b3e86 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -5,8 +5,9 @@ import { Icon } from '../icons/icon'; import { LegendItemListener, BasicListener, LegendColorPicker } from '../../specs/settings'; import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; import { onLegendItemOutAction, onLegendItemOverAction } from '../../state/actions/legend'; -import { Position } from '../../utils/commons'; -import { XYChartSeriesIdentifier, SeriesKey } from '../../chart_types/xy_chart/utils/series'; +import { Position, Color } from '../../utils/commons'; +import { XYChartSeriesIdentifier } from '../../chart_types/xy_chart/utils/series'; +import { clearTemporaryColors, setTemporaryColor, setPersistedColor } from '../../state/actions/colors'; interface LegendItemProps { legendItem: LegendItem; @@ -20,9 +21,9 @@ interface LegendItemProps { onLegendItemOverListener?: LegendItemListener; legendItemOutAction: typeof onLegendItemOutAction; legendItemOverAction: typeof onLegendItemOverAction; - clearTemporaryColors: () => void; - setTemporaryColor: (key: SeriesKey, color: string) => void; - setPersistedColor: (key: SeriesKey, color: string) => void; + clearTemporaryColors: typeof clearTemporaryColors; + setTemporaryColor: typeof setTemporaryColor; + setPersistedColor: typeof setPersistedColor; toggleDeselectSeriesAction: (legendItemId: XYChartSeriesIdentifier) => void; } @@ -148,7 +149,7 @@ export class LegendListItem extends Component anchor={this.ref.current} color={color} onClose={handleClose} - onChange={(color: string) => setTemporaryColor(seriesIdentifier.key, color)} + onChange={(color: Color) => setTemporaryColor(seriesIdentifier.key, color)} seriesIdentifier={seriesIdentifier} /> ); diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 998e15e697..68120c9b15 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -11,7 +11,7 @@ import { ChartTypes } from '../chart_types'; import { GeometryValue } from '../utils/geometry'; import { XYChartSeriesIdentifier, SeriesIdentifier } from '../chart_types/xy_chart/utils/series'; import { Accessor } from '../utils/accessor'; -import { Position, Rendering, Rotation } from '../utils/commons'; +import { Position, Rendering, Rotation, Color } from '../utils/commons'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; export type ElementClickListener = (elements: Array<[GeometryValue, XYChartSeriesIdentifier]>) => void; @@ -85,7 +85,7 @@ export interface TooltipValue { /** * The color of the graphic mark (by default the color of the series) */ - color: string; + color: Color; /** * True if the mouse is over the graphic mark connected to the tooltip */ @@ -121,7 +121,7 @@ export interface LegendColorPickerProps { /** * Current color of the given series */ - color: string; + color: Color; /** * Callback to close color picker and set persistent color */ @@ -129,7 +129,7 @@ export interface LegendColorPickerProps { /** * Callback to update temporary color state */ - onChange: (color: string) => void; + onChange: (color: Color) => void; /** * Series id for the active series */ diff --git a/src/state/actions/colors.ts b/src/state/actions/colors.ts index e74877c0d6..61e571a240 100644 --- a/src/state/actions/colors.ts +++ b/src/state/actions/colors.ts @@ -1,4 +1,5 @@ import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; +import { Color } from '../../utils/commons'; export const CLEAR_TEMPORARY_COLORS = 'CLEAR_TEMPORARY_COLORS'; export const SET_TEMPORARY_COLOR = 'SET_TEMPORARY_COLOR'; @@ -11,24 +12,24 @@ interface ClearTemporaryColors { interface SetTemporaryColor { type: typeof SET_TEMPORARY_COLOR; key: SeriesKey; - color: string; + color: Color; } interface SetPersistedColor { type: typeof SET_PERSISTED_COLOR; key: SeriesKey; - color: string; + color: Color; } export function clearTemporaryColors(): ClearTemporaryColors { return { type: CLEAR_TEMPORARY_COLORS }; } -export function setTemporaryColor(key: SeriesKey, color: string): SetTemporaryColor { +export function setTemporaryColor(key: SeriesKey, color: Color): SetTemporaryColor { return { type: SET_TEMPORARY_COLOR, key, color }; } -export function setPersistedColor(key: SeriesKey, color: string): SetPersistedColor { +export function setPersistedColor(key: SeriesKey, color: Color): SetPersistedColor { return { type: SET_PERSISTED_COLOR, key, color }; } diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 9490098ef6..c392883e36 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -18,6 +18,7 @@ import { RefObject } from 'react'; import { PartitionState } from '../chart_types/partition_chart/state/chart_state'; import { TooltipInfo } from '../components/tooltip/types'; import { TooltipAnchorPosition } from '../components/tooltip/utils'; +import { Color } from '../utils/commons'; export type BackwardRef = () => React.RefObject; @@ -118,8 +119,8 @@ export interface ExternalEventsState { } export interface ColorOverrides { - temporary: Record; - persisted: Record; + temporary: Record; + persisted: Record; } export interface GlobalChartState { diff --git a/src/utils/geometry.ts b/src/utils/geometry.ts index 8c0cded6c7..47da1b8342 100644 --- a/src/utils/geometry.ts +++ b/src/utils/geometry.ts @@ -1,6 +1,7 @@ import { $Values } from 'utility-types'; import { BarSeriesStyle, PointStyle, AreaStyle, LineStyle, ArcStyle } from './themes/theme'; import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; +import { Color } from './commons'; /** * The accessor type @@ -31,7 +32,7 @@ export interface PointGeometry { x: number; y: number; radius: number; - color: string; + color: Color; transform: { x: number; y: number; @@ -45,7 +46,7 @@ export interface BarGeometry { y: number; width: number; height: number; - color: string; + color: Color; displayValue?: { text: any; width: number; @@ -61,7 +62,7 @@ export interface BarGeometry { export interface LineGeometry { line: string; points: PointGeometry[]; - color: string; + color: Color; transform: { x: number; y: number; @@ -79,7 +80,7 @@ export interface AreaGeometry { area: string; lines: string[]; points: PointGeometry[]; - color: string; + color: Color; transform: { x: number; y: number; @@ -97,7 +98,7 @@ export interface AreaGeometry { export interface ArcGeometry { arc: string; - color: string; + color: Color; seriesIdentifier: XYChartSeriesIdentifier; seriesArcStyle: ArcStyle; transform: { diff --git a/stories/legend/9_color_picker.tsx b/stories/legend/9_color_picker.tsx index 4d0ffd8bc8..f961dcda33 100644 --- a/stories/legend/9_color_picker.tsx +++ b/stories/legend/9_color_picker.tsx @@ -6,12 +6,13 @@ import { EuiColorPicker, EuiWrappingPopover, EuiButton, EuiSpacer } from '@elast import { Axis, BarSeries, Chart, Position, ScaleType, Settings, LegendColorPicker } from '../../src/'; import { BARCHART_1Y1G } from '../../src/utils/data_samples/test_dataset'; import { SeriesKey } from '../../src/chart_types/xy_chart/utils/series'; +import { Color } from '../../src/utils/commons'; const onChangeAction = action('onChange'); const onCloseAction = action('onClose'); export const example = () => { - const [colors, setColors] = useState>({}); + const [colors, setColors] = useState>({}); const renderColorPicker: LegendColorPicker = ({ anchor, color, onClose, seriesIdentifier, onChange }) => { const handleClose = () => { @@ -22,9 +23,9 @@ export const example = () => { [seriesIdentifier.key]: color, }); }; - const handleChange = (color: string) => { - onChangeAction(color); + const handleChange = (color: Color) => { onChange(color); + onChangeAction(color); }; return ( @@ -56,3 +57,12 @@ export const example = () => { ); }; + +example.story = { + parameters: { + info: { + text: + 'Elastic charts will maintain the color selection in memory beyond chart updates. However, to persist colors beyond browser refresh the consumer would need to manage the color state and use the color prop on the SeriesSpec to assign a color via a SeriesColorAccessor.', + }, + }, +};