Skip to content

Commit

Permalink
fix: review fixes 2
Browse files Browse the repository at this point in the history
  • Loading branch information
korvin89 committed Feb 7, 2024
1 parent cd55353 commit ff997d7
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 133 deletions.
53 changes: 27 additions & 26 deletions src/plugins/d3/__stories__/treemap/Playground.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,39 @@ import type {ChartKitRef} from '../../../../types';
import type {ChartKitWidgetData} from '../../../../types/widget-data';
import {D3Plugin} from '../..';

const prepareData = (): ChartKitWidgetData['series']['data'] => {
return [
{
type: 'treemap',
name: 'Example',
dataLabels: {
enabled: true,
},
layoutAlgorithm: 'binary',
levels: [{index: 1}, {index: 2}, {index: 3}],
const prepareData = (): ChartKitWidgetData => {
return {
series: {
data: [
{name: 'One', value: 15},
{name: 'Two', value: 10},
{name: 'Three', value: 15},
{name: 'Four'},
{name: 'Four-1', value: 5, parentId: 'Four'},
{name: 'Four-2', parentId: 'Four'},
{name: 'Four-3', value: 4, parentId: 'Four'},
{name: 'Four-2-1', value: 5, parentId: 'Four-2'},
{name: 'Four-2-2', value: 7, parentId: 'Four-2'},
{name: 'Four-2-3', value: 10, parentId: 'Four-2'},
{
type: 'treemap',
name: 'Example',
dataLabels: {
enabled: true,
},
layoutAlgorithm: 'binary',
levels: [{index: 1}, {index: 2}, {index: 3}],
data: [
{name: 'One', value: 15},
{name: 'Two', value: 10},
{name: 'Three', value: 15},
{name: 'Four'},
{name: 'Four-1', value: 5, parentId: 'Four'},
{name: 'Four-2', parentId: 'Four'},
{name: 'Four-3', value: 4, parentId: 'Four'},
{name: 'Four-2-1', value: 5, parentId: 'Four-2'},
{name: 'Four-2-2', value: 7, parentId: 'Four-2'},
{name: 'Four-2-3', value: 10, parentId: 'Four-2'},
],
},
],
},
];
};
};

const ChartStory = ({data}: {data: ChartKitWidgetData['series']['data']}) => {
const ChartStory = ({data}: {data: ChartKitWidgetData}) => {
const [shown, setShown] = React.useState(false);
const chartkitRef = React.useRef<ChartKitRef>();
const widgetData: ChartKitWidgetData = {
series: {data},
};

if (!shown) {
settings.set({plugins: [D3Plugin]});
Expand All @@ -47,7 +48,7 @@ const ChartStory = ({data}: {data: ChartKitWidgetData['series']['data']}) => {

return (
<div style={{height: '300px', width: '100%'}}>
<ChartKit ref={chartkitRef} type="d3" data={widgetData} />
<ChartKit ref={chartkitRef} type="d3" data={data} />
</div>
);
};
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/d3/renderer/hooks/useSeries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const useSeries = (args: Args) => {

const handleLegendItemClick: OnLegendItemClick = React.useCallback(
({name, metaKey}) => {
const allItems = getAllLegendItems(preparedSeries);
const onlyItemSelected =
activeLegendItems.length === 1 && activeLegendItems.includes(name);
let nextActiveLegendItems: string[];
Expand All @@ -94,8 +95,10 @@ export const useSeries = (args: Args) => {
nextActiveLegendItems = activeLegendItems.filter((item) => item !== name);
} else if (metaKey && !activeLegendItems.includes(name)) {
nextActiveLegendItems = activeLegendItems.concat(name);
} else if (onlyItemSelected && allItems.length === 1) {
nextActiveLegendItems = [];
} else if (onlyItemSelected) {
nextActiveLegendItems = getAllLegendItems(preparedSeries);
nextActiveLegendItems = allItems;
} else {
nextActiveLegendItems = [name];
}
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/d3/renderer/hooks/useShapes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ export const useShapes = (args: Args) => {
// We should have exactly one series with "treemap" type
// Otherwise data validation should emit an error
series: chartSeries[0] as PreparedTreemapSeries,
width: boundsWidth,
height: boundsHeight,
});
acc.push(
<TreemapSeriesShape
Expand All @@ -216,8 +218,6 @@ export const useShapes = (args: Args) => {
preparedData={preparedData}
seriesOptions={seriesOptions}
svgContainer={svgContainer}
width={boundsWidth}
height={boundsHeight}
/>,
);
}
Expand Down
87 changes: 18 additions & 69 deletions src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,26 @@
import React from 'react';
import {
color,
pointer,
select,
treemap,
treemapBinary,
treemapDice,
treemapSlice,
treemapSliceDice,
treemapSquarify,
} from 'd3';
import {color, pointer, select} from 'd3';
import type {BaseType, Dispatch, HierarchyRectangularNode} from 'd3';
import get from 'lodash/get';

import {LayoutAlgorithm} from '../../../../../../constants';
import type {TooltipDataChunkTreemap} from '../../../../../../types';
import type {TooltipDataChunkTreemap, TreemapSeriesData} from '../../../../../../types';
import {setEllipsisForOverflowTexts} from '../../../utils';
import {block} from '../../../../../../utils/cn';

import {PreparedSeriesOptions} from '../../useSeries/types';
import type {PreparedTreemapData, PreparedTreemapSeriesData, TreemapLabelData} from './types';
import {getLabelData} from './utils';
import type {PreparedTreemapData, TreemapLabelData} from './types';

const b = block('d3-treemap');
const DEFAULT_PADDING = 1;

type ShapeProps = {
dispatcher: Dispatch<object>;
preparedData: PreparedTreemapData;
seriesOptions: PreparedSeriesOptions;
svgContainer: SVGSVGElement | null;
width: number;
height: number;
};

export const TreemapSeriesShape = (props: ShapeProps) => {
const {dispatcher, preparedData, seriesOptions, svgContainer, width, height} = props;
const {dispatcher, preparedData, seriesOptions, svgContainer} = props;
const ref = React.useRef<SVGGElement>(null);

React.useEffect(() => {
Expand All @@ -45,40 +30,10 @@ export const TreemapSeriesShape = (props: ShapeProps) => {

const svgElement = select(ref.current);
svgElement.selectAll('*').remove();
const {hierarchy, series} = preparedData;
const treemapInstance = treemap<PreparedTreemapSeriesData>();

switch (series.layoutAlgorithm) {
case LayoutAlgorithm.Binary: {
treemapInstance.tile(treemapBinary);
break;
}
case LayoutAlgorithm.Dice: {
treemapInstance.tile(treemapDice);
break;
}
case LayoutAlgorithm.Slice: {
treemapInstance.tile(treemapSlice);
break;
}
case LayoutAlgorithm.SliceDice: {
treemapInstance.tile(treemapSliceDice);
break;
}
case LayoutAlgorithm.Squarify: {
treemapInstance.tile(treemapSquarify);
break;
}
}

const root = treemapInstance.size([width, height]).paddingInner((d) => {
const levelOptions = series.levels?.find((l) => l.index === d.depth + 1);
return levelOptions?.padding ?? DEFAULT_PADDING;
})(hierarchy);

const {labelData, leaves, series} = preparedData;
const leaf = svgElement
.selectAll('g')
.data(root.leaves())
.data(leaves)
.join('g')
.attr('transform', (d) => `translate(${d.x0},${d.y0})`);
const rectSelection = leaf
Expand All @@ -94,10 +49,6 @@ export const TreemapSeriesShape = (props: ShapeProps) => {
})
.attr('width', (d) => d.x1 - d.x0)
.attr('height', (d) => d.y1 - d.y0);

const labelData: TreemapLabelData[] = series.dataLabels?.enabled
? getLabelData(leaf.data())
: [];
const labelSelection = svgElement
.selectAll<SVGTextElement, typeof labelData>('tspan')
.data(labelData)
Expand All @@ -116,10 +67,9 @@ export const TreemapSeriesShape = (props: ShapeProps) => {
const inactiveOptions = get(seriesOptions, 'treemap.states.inactive');
svgElement
.on('mousemove', (e) => {
const hoveredRect = select<
BaseType,
HierarchyRectangularNode<PreparedTreemapSeriesData>
>(e.target);
const hoveredRect = select<BaseType, HierarchyRectangularNode<TreemapSeriesData>>(
e.target,
);
const datum = hoveredRect.datum();
dispatcher.call(
'hover-shape',
Expand All @@ -135,14 +85,13 @@ export const TreemapSeriesShape = (props: ShapeProps) => {
dispatcher.on(eventName, (data?: TooltipDataChunkTreemap[]) => {
const hoverEnabled = hoverOptions?.enabled;
const inactiveEnabled = inactiveOptions?.enabled;
const selectedId = (data?.[0].data as PreparedTreemapSeriesData | undefined)?._nodeId;
const hoveredData = data?.[0].data;
rectSelection.datum((d, index, list) => {
const currentRect = select<
BaseType,
HierarchyRectangularNode<PreparedTreemapSeriesData>
>(list[index]);
const hovered = Boolean(hoverEnabled && d.data._nodeId === selectedId);
const inactive = Boolean(inactiveEnabled && selectedId && !hovered);
const currentRect = select<BaseType, HierarchyRectangularNode<TreemapSeriesData>>(
list[index],
);
const hovered = Boolean(hoverEnabled && hoveredData === d.data);
const inactive = Boolean(inactiveEnabled && hoveredData && !hovered);
currentRect
.attr('fill', (currentD) => {
const levelOptions = series.levels?.find((l) => l.index === currentD.depth);
Expand All @@ -167,8 +116,8 @@ export const TreemapSeriesShape = (props: ShapeProps) => {
});
labelSelection.datum((d, index, list) => {
const currentLabel = select<BaseType, TreemapLabelData>(list[index]);
const hovered = Boolean(hoverEnabled && d.id === selectedId);
const inactive = Boolean(inactiveEnabled && selectedId && !hovered);
const hovered = Boolean(hoverEnabled && hoveredData === d.nodeData);
const inactive = Boolean(inactiveEnabled && hoveredData && !hovered);
currentLabel.attr('opacity', () => {
if (inactive) {
return inactiveOptions?.opacity || null;
Expand All @@ -182,7 +131,7 @@ export const TreemapSeriesShape = (props: ShapeProps) => {
return () => {
dispatcher.on(eventName, null);
};
}, [dispatcher, preparedData, seriesOptions, svgContainer, width, height]);
}, [dispatcher, preparedData, seriesOptions, svgContainer]);

return <g ref={ref} className={b()} />;
};
82 changes: 71 additions & 11 deletions src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,86 @@
import {stratify} from 'd3';
import {
stratify,
treemap,
treemapBinary,
treemapDice,
treemapSlice,
treemapSliceDice,
treemapSquarify,
} from 'd3';
import type {HierarchyRectangularNode} from 'd3';

import {getRandomCKId} from '../../../../../../utils';
import {LayoutAlgorithm} from '../../../../../../constants';
import type {TreemapSeriesData} from '../../../../../../types';

import type {PreparedTreemapSeries} from '../../useSeries/types';
import type {PreparedTreemapData, PreparedTreemapSeriesData} from './types';
import type {PreparedTreemapData, TreemapLabelData} from './types';

export function prepareTreemapData(args: {series: PreparedTreemapSeries}): PreparedTreemapData {
const {series} = args;
const DEFAULT_PADDING = 1;

function getLabelData(data: HierarchyRectangularNode<TreemapSeriesData>[]): TreemapLabelData[] {
return data.map((d) => {
const text = d.data.name;

return {
text,
x: d.x0,
y: d.y0,
width: d.x1 - d.x0,
nodeData: d.data,
};
});
}

export function prepareTreemapData(args: {
series: PreparedTreemapSeries;
width: number;
height: number;
}): PreparedTreemapData {
const {series, width, height} = args;
const dataWithRootNode = getSeriesDataWithRootNode(series);
const hierarchy = stratify<PreparedTreemapSeriesData>()
const hierarchy = stratify<TreemapSeriesData>()
.id((d) => d.id || d.name)
.parentId((d) => d.parentId)(dataWithRootNode)
.sum((d) => d.value || 0);
const treemapInstance = treemap<TreemapSeriesData>();

switch (series.layoutAlgorithm) {
case LayoutAlgorithm.Binary: {
treemapInstance.tile(treemapBinary);
break;
}
case LayoutAlgorithm.Dice: {
treemapInstance.tile(treemapDice);
break;
}
case LayoutAlgorithm.Slice: {
treemapInstance.tile(treemapSlice);
break;
}
case LayoutAlgorithm.SliceDice: {
treemapInstance.tile(treemapSliceDice);
break;
}
case LayoutAlgorithm.Squarify: {
treemapInstance.tile(treemapSquarify);
break;
}
}

const root = treemapInstance.size([width, height]).paddingInner((d) => {
const levelOptions = series.levels?.find((l) => l.index === d.depth + 1);
return levelOptions?.padding ?? DEFAULT_PADDING;
})(hierarchy);
const leaves = root.leaves();
const labelData: TreemapLabelData[] = series.dataLabels?.enabled ? getLabelData(leaves) : [];

return {hierarchy, series};
return {labelData, leaves, series};
}

function getSeriesDataWithRootNode(series: PreparedTreemapSeries) {
return series.data.reduce<PreparedTreemapSeriesData[]>(
return series.data.reduce<TreemapSeriesData[]>(
(acc, d) => {
const dataChunk = Object.assign({_nodeId: getRandomCKId()}, d);
const dataChunk = Object.assign({}, d);

if (!dataChunk.parentId) {
dataChunk.parentId = series.id;
Expand All @@ -29,7 +90,6 @@ function getSeriesDataWithRootNode(series: PreparedTreemapSeries) {

return acc;
},
// We do not need _nodeId in root
[{name: series.name, id: series.id} as PreparedTreemapSeriesData],
[{name: series.name, id: series.id}],
);
}
Loading

0 comments on commit ff997d7

Please sign in to comment.