Skip to content

Commit

Permalink
feat: debug state for the heatmap chart (opensearch-project#976)
Browse files Browse the repository at this point in the history
* feat: debug state

* feat: use common debug state

* feat: update charts.api.md, add api:check:local command

* feat: update storybook with cells seleciton actions

* feat: debug state control

* feat: log debug state

* feat: add highlighted data to the debug state
  • Loading branch information
darnautov authored Mar 11, 2021
1 parent 162b100 commit 9ba7666
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 42 deletions.
6 changes: 4 additions & 2 deletions packages/osd-charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1104,8 +1106,8 @@ export interface HeatmapSpec extends Spec {
data: Datum[];
// (undocumented)
highlightedData?: {
x: any[];
y: any[];
x: Array<string | number>;
y: Array<string | number>;
};
// (undocumented)
name?: string;
Expand Down
1 change: 1 addition & 0 deletions packages/osd-charts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export interface HeatmapSpec extends Spec {
ySortPredicate: Predicate;
xScaleType: SeriesScales['xScaleType'];
config: RecursivePartial<Config>;
highlightedData?: { x: any[]; y: any[] };
highlightedData?: { x: Array<string | number>; y: Array<string | number> };
name?: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ 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';
import { computeChartDimensionsSelector } from './selectors/compute_chart_dimensions';
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';
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 };
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
17 changes: 15 additions & 2 deletions packages/osd-charts/src/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,6 +79,16 @@ export type DebugStateBar = DebugStateBase & {
labels: any[];
};

type CellDebug = Pick<Cell, 'value' | 'formatted' | 'x' | 'y'> & { fill: string };

type HeatmapDebugState = {
cells: CellDebug[];
selection: {
area: { x: number; y: number; width: number; height: number } | null;
data: { x: Array<string | number>; y: Array<string | number> } | null;
};
};

/**
* Describes _visible_ chart state for use in functional tests
*
Expand All @@ -89,4 +100,6 @@ export interface DebugState {
areas?: DebugStateArea[];
lines?: DebugStateLine[];
bars?: DebugStateBar[];
/** Heatmap chart debug state */
heatmap?: HeatmapDebugState;
}
116 changes: 84 additions & 32 deletions packages/osd-charts/stories/heatmap/1_basic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,69 +17,121 @@
* 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<Config> = {
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<Config> = 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<HTMLDivElement>('.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 (
<Chart className="story-chart">
<Settings
onElementClick={action('onElementClick')}
onElementClick={onElementClick}
onRenderChange={logDebugstate}
showLegend
legendPosition="top"
onBrushEnd={action('onBrushEnd')}
brushAxis="both"
xDomain={{ min: 1572825600000, max: 1572912000000, minInterval: 1800000 }}
debugState={debugState}
/>
<Heatmap
id="heatmap1"
colorScale={ScaleType.Threshold}
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}
valueFormatter={(d) => d.toFixed(0.2)}
ySortPredicate="numAsc"
xScaleType={ScaleType.Time}
config={config}
highlightedData={persistCellsSelection ? selection : undefined}
/>
</Chart>
);
Expand Down

0 comments on commit 9ba7666

Please sign in to comment.