Skip to content

Commit

Permalink
feat(partition): add debuggable state (#1117)
Browse files Browse the repository at this point in the history
Add the debug state for partition charts with the following type signature:

fix #917
  • Loading branch information
markov00 authored Apr 15, 2021
1 parent c1b59f2 commit d7fc206
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 8 deletions.
4 changes: 4 additions & 0 deletions api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,10 @@ export interface DebugState {
//
// (undocumented)
lines?: DebugStateLine[];
// Warning: (ae-forgotten-export) The symbol "PartitionDebugState" needs to be exported by the entry point index.d.ts
//
// (undocumented)
partition?: PartitionDebugState[];
}

// @public (undocumented)
Expand Down
6 changes: 3 additions & 3 deletions src/chart_types/partition_chart/state/chart_state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { DebugState } from '../../../state/types';
import { Dimensions } from '../../../utils/dimensions';
import { render } from '../renderer/dom/layered_partition_chart';
import { computeLegendSelector } from './selectors/compute_legend';
import { getDebugStateSelector } from './selectors/get_debug_state';
import { getLegendItemsExtra } from './selectors/get_legend_items_extra';
import { getLegendItemsLabels } from './selectors/get_legend_items_labels';
import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible';
Expand Down Expand Up @@ -130,8 +131,7 @@ export class PartitionState implements InternalChartState {
return null;
}

// TODO
getDebugState(): DebugState {
return {};
getDebugState(state: GlobalChartState): DebugState {
return getDebugStateSelector(state);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* 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 { Store } from 'redux';

import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs';
import { MockStore } from '../../../../mocks/store/store';
import {
HeatmapElementEvent,
LayerValue,
PartitionElementEvent,
XYChartElementEvent,
} from '../../../../specs/settings';
import { onMouseDown, onMouseUp, onPointerMove } from '../../../../state/actions/mouse';
import { GlobalChartState } from '../../../../state/chart_state';
import { DebugState, PartitionDebugState, SinglePartitionDebugState } from '../../../../state/types';
import { PartitionLayout } from '../../layout/types/config_types';
import { isSunburst } from '../../layout/viewmodel/viewmodel';
import { getDebugStateSelector } from './get_debug_state';
import { createOnElementClickCaller } from './on_element_click_caller';

describe.each([
[PartitionLayout.sunburst, 9, 9],
[PartitionLayout.treemap, 9, 6],
[PartitionLayout.flame, 9, 6],
[PartitionLayout.icicle, 9, 6],
[PartitionLayout.mosaic, 9, 6],
])('Partition - debug state %s', (partitionLayout, numberOfElements, numberOfCalls) => {
type TestDatum = { cat1: string; cat2: string; val: number };
const specJSON = {
config: {
partitionLayout,
},
data: [
{ cat1: 'Asia', cat2: 'Japan', val: 1 },
{ cat1: 'Asia', cat2: 'China', val: 1 },
{ cat1: 'Europe', cat2: 'Germany', val: 1 },
{ cat1: 'Europe', cat2: 'Italy', val: 1 },
{ cat1: 'North America', cat2: 'United States', val: 1 },
{ cat1: 'North America', cat2: 'Canada', val: 1 },
],
valueAccessor: (d: TestDatum) => d.val,
layers: [
{
groupByRollup: (d: TestDatum) => d.cat1,
},
{
groupByRollup: (d: TestDatum) => d.cat2,
},
],
};
let store: Store<GlobalChartState>;
let onClickListener: jest.Mock<
undefined,
Array<(XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent)[]>
>;
let debugState: DebugState;

beforeEach(() => {
onClickListener = jest.fn((): undefined => undefined);
store = MockStore.default({ width: 500, height: 500, top: 0, left: 0 });
const onElementClickCaller = createOnElementClickCaller();
store.subscribe(() => {
onElementClickCaller(store.getState());
});
MockStore.addSpecs(
[
MockSeriesSpec.sunburst(specJSON),
MockGlobalSpec.settings({ debugState: true, onElementClick: onClickListener }),
],
store,
);
debugState = getDebugStateSelector(store.getState());
});

it('can compute debug state', () => {
// small multiple panels
expect(debugState.partition).toHaveLength(1);
// partition sectors
expect(debugState.partition![0].partitions).toHaveLength(numberOfElements);
});

it('can click on every sector', () => {
const [{ partitions }] = debugState.partition as PartitionDebugState[];
let counter = 0;
for (let index = 0; index < partitions.length; index++) {
const partition = partitions[index];
if (!isSunburst(partitionLayout) && partition.depth < 2) {
continue;
}
expectCorrectClickInfo(store, onClickListener, partition, counter);
counter++;
}
expect(onClickListener).toBeCalledTimes(numberOfCalls);
});
});

function expectCorrectClickInfo(
store: Store<GlobalChartState>,
onClickListener: jest.Mock<undefined, Array<(XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent)[]>>,
partition: SinglePartitionDebugState,
index: number,
) {
const {
depth,
value,
name,
coords: [x, y],
} = partition;

store.dispatch(onPointerMove({ x, y }, index * 3));
store.dispatch(onMouseDown({ x, y }, index * 3 + 1));
store.dispatch(onMouseUp({ x, y }, index * 3 + 2));

expect(onClickListener).toBeCalledTimes(index + 1);
const obj = onClickListener.mock.calls[index][0][0][0] as LayerValue[];
// pick the last element of the path
expect(obj[obj.length - 1]).toMatchObject({
depth,
groupByRollup: name,
value,
});
}
72 changes: 72 additions & 0 deletions src/chart_types/partition_chart/state/selectors/get_debug_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 { TAU } from '../../../../common/constants';
import { Pixels, PointObject } from '../../../../common/geometry';
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { DebugState, PartitionDebugState } from '../../../../state/types';
import { QuadViewModel } from '../../layout/types/viewmodel_types';
import { isSunburst } from '../../layout/viewmodel/viewmodel';
import { partitionMultiGeometries } from './geometries';

/** @internal */
export const getDebugStateSelector = createCachedSelector(
[partitionMultiGeometries],
(geoms): DebugState => {
return {
partition: geoms.reduce<PartitionDebugState[]>((acc, { panelTitle, config, quadViewModel, diskCenter }) => {
const partitions: PartitionDebugState['partitions'] = quadViewModel.map((model) => {
const { dataName, depth, fillColor, value } = model;
return {
name: dataName,
depth,
color: fillColor,
value,
coords: isSunburst(config.partitionLayout)
? getCoordsForSector(model, diskCenter)
: getCoordsForRectangle(model, diskCenter),
};
});
acc.push({
panelTitle,
partitions,
});
return acc;
}, []),
};
},
)(getChartIdSelector);

function getCoordsForSector({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] {
const X0 = x0 - TAU / 4;
const X1 = x1 - TAU / 4;
const cr = y0px + (y1px - y0px) / 2;
const angle = X0 + (X1 - X0) / 2;
const x = Math.round(Math.cos(angle) * cr + diskCenter.x);
const y = Math.round(Math.sin(angle) * cr + diskCenter.y);
return [x, y];
}

function getCoordsForRectangle({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] {
const y = Math.round(y0px + (y1px - y0px) / 2 + diskCenter.y);
const x = Math.round(x0 + (x1 - x0) / 2 + diskCenter.x);
return [x, y];
}
6 changes: 3 additions & 3 deletions src/components/chart_status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ class ChartStatusComponent extends React.Component<ChartStatusStateProps> {
}

const mapStateToProps = (state: GlobalChartState): ChartStatusStateProps => {
const settings = getSettingsSpecSelector(state);
const { onRenderChange, debugState } = getSettingsSpecSelector(state);

return {
rendered: state.chartRendered,
renderedCount: state.chartRenderedCount,
onRenderChange: settings.onRenderChange,
debugState: settings.debugState ? getDebugStateSelector(state) : null,
onRenderChange,
debugState: debugState ? getDebugStateSelector(state) : null,
};
};

Expand Down
17 changes: 17 additions & 0 deletions src/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import type { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types';
import { Pixels } from '../common/geometry';
import type { Position } from '../utils/common';
import type { GeometryValue } from '../utils/geometry';

Expand Down Expand Up @@ -97,6 +98,21 @@ type HeatmapDebugState = {
};
};

/** @public */
export type SinglePartitionDebugState = {
name: string;
depth: number;
color: string;
value: number;
coords: [Pixels, Pixels];
};

/** @public */
export type PartitionDebugState = {
panelTitle: string;
partitions: Array<SinglePartitionDebugState>;
};

/**
* Describes _visible_ chart state for use in functional tests
*
Expand All @@ -111,4 +127,5 @@ export interface DebugState {
bars?: DebugStateBar[];
/** Heatmap chart debug state */
heatmap?: HeatmapDebugState;
partition?: PartitionDebugState[];
}
3 changes: 2 additions & 1 deletion stories/sunburst/10_2_slice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@

import React from 'react';

import { Chart, Datum, Partition, PartitionLayout } from '../../src';
import { Chart, Datum, Partition, PartitionLayout, Settings } from '../../src';
import { config } from '../../src/chart_types/partition_chart/layout/config';
import { mocks } from '../../src/mocks/hierarchical';
import { indexInterpolatedFillColor, interpolatorCET2s, productLookup } from '../utils/utils';

export const Example = () => (
<Chart className="story-chart">
<Settings debugState />
<Partition
id="spec_1"
data={mocks.pie.slice(0, 2)}
Expand Down
2 changes: 1 addition & 1 deletion stories/treemap/1_one_layer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const defaultFillColor = (colorMaker: any) => (d: any, i: number, a: any[]) => c

export const Example = () => (
<Chart className="story-chart">
<Settings theme={STORYBOOK_LIGHT_THEME} />
<Settings theme={STORYBOOK_LIGHT_THEME} debugState />
<Partition
id="spec_1"
data={mocks.pie}
Expand Down

0 comments on commit d7fc206

Please sign in to comment.