diff --git a/packages/osd-charts/api/charts.api.md b/packages/osd-charts/api/charts.api.md index b07f33284220..30aeeff2f012 100644 --- a/packages/osd-charts/api/charts.api.md +++ b/packages/osd-charts/api/charts.api.md @@ -646,6 +646,8 @@ export interface DebugState { // // (undocumented) bars?: DebugStateBar[]; + // Warning: (ae-forgotten-export) The symbol "HeatmapDebugState" needs to be exported by the entry point index.d.ts + heatmap?: HeatmapDebugState; // Warning: (ae-forgotten-export) The symbol "DebugStateLegend" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1104,8 +1106,8 @@ export interface HeatmapSpec extends Spec { data: Datum[]; // (undocumented) highlightedData?: { - x: any[]; - y: any[]; + x: Array; + y: Array; }; // (undocumented) name?: string; diff --git a/packages/osd-charts/package.json b/packages/osd-charts/package.json index 3f841876e66c..458643027ab8 100644 --- a/packages/osd-charts/package.json +++ b/packages/osd-charts/package.json @@ -17,6 +17,7 @@ "scripts": { "autoprefix:css": "echo 'Autoprefixing...' && yarn postcss dist/*.css --no-map --use autoprefixer -d dist", "api:check": "yarn build:ts && yarn api:extract", + "api:check:local": "yarn api:check --local", "api:extract": "yarn api-extractor run --verbose", "backport": "backport", "build": "yarn build:ts && yarn build:css", diff --git a/packages/osd-charts/src/chart_types/heatmap/specs/heatmap.ts b/packages/osd-charts/src/chart_types/heatmap/specs/heatmap.ts index 85e36653da2c..4aff119ffe0d 100644 --- a/packages/osd-charts/src/chart_types/heatmap/specs/heatmap.ts +++ b/packages/osd-charts/src/chart_types/heatmap/specs/heatmap.ts @@ -68,7 +68,7 @@ export interface HeatmapSpec extends Spec { ySortPredicate: Predicate; xScaleType: SeriesScales['xScaleType']; config: RecursivePartial; - highlightedData?: { x: any[]; y: any[] }; + highlightedData?: { x: Array; y: Array }; name?: string; } diff --git a/packages/osd-charts/src/chart_types/heatmap/state/chart_state.tsx b/packages/osd-charts/src/chart_types/heatmap/state/chart_state.tsx index 7513589a8634..d8028363171b 100644 --- a/packages/osd-charts/src/chart_types/heatmap/state/chart_state.tsx +++ b/packages/osd-charts/src/chart_types/heatmap/state/chart_state.tsx @@ -25,7 +25,6 @@ import { Tooltip } from '../../../components/tooltip'; import { InternalChartState, GlobalChartState, BackwardRef } from '../../../state/chart_state'; import { getChartContainerDimensionsSelector } from '../../../state/selectors/get_chart_container_dimensions'; import { InitStatus } from '../../../state/selectors/get_internal_is_intialized'; -import { DebugState } from '../../../state/types'; import { Dimensions } from '../../../utils/dimensions'; import { Heatmap } from '../renderer/canvas/connected_component'; import { HighlighterFromBrush } from '../renderer/dom/highlighter_brush'; @@ -33,6 +32,7 @@ import { computeChartDimensionsSelector } from './selectors/compute_chart_dimens import { computeLegendSelector } from './selectors/compute_legend'; import { getBrushAreaSelector } from './selectors/get_brush_area'; import { getPointerCursorSelector } from './selectors/get_cursor_pointer'; +import { getDebugStateSelector } from './selectors/get_debug_state'; import { getLegendItemsLabelsSelector } from './selectors/get_legend_items_labels'; import { getTooltipAnchorSelector } from './selectors/get_tooltip_anchor'; import { getSpecOrNull } from './selectors/heatmap_spec'; @@ -126,9 +126,8 @@ export class HeatmapState implements InternalChartState { return getBrushAreaSelector(globalState); } - // TODO - getDebugState(): DebugState { - return {}; + getDebugState(globalState: GlobalChartState) { + return getDebugStateSelector(globalState); } eventCallbacks(globalState: GlobalChartState) { diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts new file mode 100644 index 000000000000..630d4823ec85 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import createCachedSelector from 're-reselect'; + +import { RGBtoString } from '../../../../common/color_library_wrappers'; +import { LegendItem } from '../../../../common/legend'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { DebugState, DebugStateLegend } from '../../../../state/types'; +import { Position } from '../../../../utils/common'; +import { computeLegendSelector } from './compute_legend'; +import { geometries } from './geometries'; +import { getHighlightedAreaSelector, getHighlightedDataSelector } from './get_highlighted_area'; +import { getPickedCells } from './get_picked_cells'; + +/** + * Returns a stringified version of the `debugState` + * @internal + */ +export const getDebugStateSelector = createCachedSelector( + [geometries, computeLegendSelector, getHighlightedAreaSelector, getPickedCells, getHighlightedDataSelector], + (geoms, legend, pickedArea, pickedCells, highlightedData): DebugState => { + return { + // Common debug state + legend: getLegendState(legend), + axes: { + x: [ + { + id: 'x', + position: Position.Left, + labels: geoms.heatmapViewModel.xValues.map(({ text }) => text), + values: geoms.heatmapViewModel.xValues.map(({ value }) => value), + // vertical lines + gridlines: geoms.heatmapViewModel.gridLines.x.map((line) => ({ x: line.x1, y: line.y2 })), + }, + ], + y: [ + { + id: 'y', + position: Position.Bottom, + labels: geoms.heatmapViewModel.yValues.map(({ text }) => text), + values: geoms.heatmapViewModel.yValues.map(({ value }) => value), + // horizontal lines + gridlines: geoms.heatmapViewModel.gridLines.y.map((line) => ({ x: line.x2, y: line.y1 })), + }, + ], + }, + // Heatmap debug state + heatmap: { + cells: geoms.heatmapViewModel.cells.map(({ x, y, fill, formatted, value }) => ({ + x, + y, + fill: RGBtoString(fill.color), + formatted, + value, + })), + selection: { + area: pickedArea, + data: highlightedData, + }, + }, + }; + }, +)(getChartIdSelector); + +function getLegendState(legendItems: LegendItem[]): DebugStateLegend { + const items = legendItems + .filter(({ isSeriesHidden }) => !isSeriesHidden) + .map(({ label: name, color, seriesIdentifiers: [{ key }] }) => ({ + key, + name, + color, + })); + + return { items }; +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts index dc93e5233f64..f7a906b65494 100644 --- a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts @@ -24,7 +24,20 @@ import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { isBrushingSelector } from './is_brushing'; /** - * + * @internal + */ +export const getHighlightedDataSelector = createCachedSelector( + [getHeatmapSpecSelector, isBrushingSelector], + (spec, isBrushing) => { + if (!spec.highlightedData || isBrushing) { + return null; + } + return spec.highlightedData; + }, +)(getChartIdSelector); + +/** + * Returns rect position of the highlighted selection. * @internal */ export const getHighlightedAreaSelector = createCachedSelector( diff --git a/packages/osd-charts/src/state/types.ts b/packages/osd-charts/src/state/types.ts index 43b0a5b43014..b2c6b88dfa6f 100644 --- a/packages/osd-charts/src/state/types.ts +++ b/packages/osd-charts/src/state/types.ts @@ -17,8 +17,9 @@ * under the License. */ -import { Position } from '../utils/common'; -import { GeometryValue } from '../utils/geometry'; +import type { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types'; +import type { Position } from '../utils/common'; +import type { GeometryValue } from '../utils/geometry'; export interface DebugStateAxis { id: string; @@ -78,6 +79,16 @@ export type DebugStateBar = DebugStateBase & { labels: any[]; }; +type CellDebug = Pick & { fill: string }; + +type HeatmapDebugState = { + cells: CellDebug[]; + selection: { + area: { x: number; y: number; width: number; height: number } | null; + data: { x: Array; y: Array } | null; + }; +}; + /** * Describes _visible_ chart state for use in functional tests * @@ -89,4 +100,6 @@ export interface DebugState { areas?: DebugStateArea[]; lines?: DebugStateLine[]; bars?: DebugStateBar[]; + /** Heatmap chart debug state */ + heatmap?: HeatmapDebugState; } diff --git a/packages/osd-charts/stories/heatmap/1_basic.tsx b/packages/osd-charts/stories/heatmap/1_basic.tsx index 73a2581aa129..77363283db29 100644 --- a/packages/osd-charts/stories/heatmap/1_basic.tsx +++ b/packages/osd-charts/stories/heatmap/1_basic.tsx @@ -17,54 +17,106 @@ * under the License. */ import { action } from '@storybook/addon-actions'; -import React from 'react'; +import { boolean, button } from '@storybook/addon-knobs'; +import React, { useCallback, useMemo, useState } from 'react'; +import { debounce } from 'ts-debounce'; -import { Chart, Heatmap, niceTimeFormatter, RecursivePartial, ScaleType, Settings } from '../../src'; +import { + Chart, + DebugState, + Heatmap, + HeatmapElementEvent, + niceTimeFormatter, + RecursivePartial, + ScaleType, + Settings, +} from '../../src'; import { Config } from '../../src/chart_types/heatmap/layout/types/config_types'; import { SWIM_LANE_DATA } from '../../src/utils/data_samples/test_anomaly_swim_lane'; export const Example = () => { - const config: RecursivePartial = { - grid: { - cellHeight: { - min: 20, - }, - stroke: { - width: 1, - color: '#D3DAE6', + const [selection, setSelection] = useState<{ x: (string | number)[]; y: (string | number)[] } | undefined>(); + + const persistCellsSelection = boolean('Persist cells selection', true); + const debugState = boolean('Enable debug state', true); + const dataStateAction = action('DataState'); + + const handler = useCallback(() => { + setSelection(undefined); + }, []); + + button('Clear cells selection', handler); + + const config: RecursivePartial = useMemo( + () => ({ + grid: { + cellHeight: { + min: 20, + }, + stroke: { + width: 1, + color: '#D3DAE6', + }, }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 3, - label: { - visible: false, + cell: { + maxWidth: 'fill', + maxHeight: 3, + label: { + visible: false, + }, + border: { + stroke: '#D3DAE6', + strokeWidth: 0, + }, }, - border: { - stroke: '#D3DAE6', - strokeWidth: 0, + yAxisLabel: { + visible: true, + width: 'auto', + padding: { left: 10, right: 10 }, }, - }, - yAxisLabel: { - visible: true, - width: 'auto', - padding: { left: 10, right: 10 }, - }, - xAxisLabel: { - formatter: (value: string | number) => { - return niceTimeFormatter([1572825600000, 1572912000000])(value, { timeZone: 'UTC' }); + xAxisLabel: { + formatter: (value: string | number) => { + return niceTimeFormatter([1572825600000, 1572912000000])(value, { timeZone: 'UTC' }); + }, }, - }, - }; + onBrushEnd: ((e) => { + setSelection({ x: e.x, y: e.y }); + }) as Config['onBrushEnd'], + }), + [], + ); + + const logDebugstate = debounce(() => { + if (!debugState) return; + + const statusEl = document.querySelector('.echChartStatus'); + + if (statusEl) { + const dataState = statusEl.dataset.echDebugState + ? (JSON.parse(statusEl.dataset.echDebugState) as DebugState) + : null; + dataStateAction(dataState); + } + }, 100); + + // @ts-ignore + const onElementClick: ElementClickListener = useCallback((e: HeatmapElementEvent[]) => { + const cell = e[0][0]; + // @ts-ignore + setSelection({ x: [cell.datum.x, cell.datum.x], y: [cell.datum.y] }); + }, []); + return ( { ranges={[0, 3, 25, 50, 75]} colors={['#ffffff', '#d2e9f7', '#8bc8fb', '#fdec25', '#fba740', '#fe5050']} data={SWIM_LANE_DATA.map((v) => ({ ...v, time: v.time * 1000 }))} - // highlightedData={{ x: [], y: [] }} xAccessor={(d) => d.time} yAccessor={(d) => d.laneLabel} valueAccessor={(d) => d.value} @@ -80,6 +131,7 @@ export const Example = () => { ySortPredicate="numAsc" xScaleType={ScaleType.Time} config={config} + highlightedData={persistCellsSelection ? selection : undefined} /> );