diff --git a/src/plugins/d3/__stories__/treemap/Playground.stories.tsx b/src/plugins/d3/__stories__/treemap/Playground.stories.tsx index 68bacbfe..ca332ad8 100644 --- a/src/plugins/d3/__stories__/treemap/Playground.stories.tsx +++ b/src/plugins/d3/__stories__/treemap/Playground.stories.tsx @@ -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(); - const widgetData: ChartKitWidgetData = { - series: {data}, - }; if (!shown) { settings.set({plugins: [D3Plugin]}); @@ -47,7 +48,7 @@ const ChartStory = ({data}: {data: ChartKitWidgetData['series']['data']}) => { return (
- +
); }; diff --git a/src/plugins/d3/renderer/hooks/useSeries/index.ts b/src/plugins/d3/renderer/hooks/useSeries/index.ts index 4e6ebd96..06443317 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/index.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/index.ts @@ -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[]; @@ -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]; } diff --git a/src/plugins/d3/renderer/hooks/useShapes/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/index.tsx index c0c3a5c8..f6f0f57d 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/index.tsx @@ -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( { preparedData={preparedData} seriesOptions={seriesOptions} svgContainer={svgContainer} - width={boundsWidth} - height={boundsHeight} />, ); } diff --git a/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx index 4f39db46..b7482c4e 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx @@ -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; 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(null); React.useEffect(() => { @@ -45,40 +30,10 @@ export const TreemapSeriesShape = (props: ShapeProps) => { const svgElement = select(ref.current); svgElement.selectAll('*').remove(); - const {hierarchy, series} = preparedData; - const treemapInstance = treemap(); - - 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 @@ -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('tspan') .data(labelData) @@ -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 - >(e.target); + const hoveredRect = select>( + e.target, + ); const datum = hoveredRect.datum(); dispatcher.call( 'hover-shape', @@ -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 - >(list[index]); - const hovered = Boolean(hoverEnabled && d.data._nodeId === selectedId); - const inactive = Boolean(inactiveEnabled && selectedId && !hovered); + const currentRect = select>( + 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); @@ -167,8 +116,8 @@ export const TreemapSeriesShape = (props: ShapeProps) => { }); labelSelection.datum((d, index, list) => { const currentLabel = select(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; @@ -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 ; }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts index debdab6f..305948bf 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts @@ -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[]): 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() + const hierarchy = stratify() .id((d) => d.id || d.name) .parentId((d) => d.parentId)(dataWithRootNode) .sum((d) => d.value || 0); + const treemapInstance = treemap(); + + 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( + return series.data.reduce( (acc, d) => { - const dataChunk = Object.assign({_nodeId: getRandomCKId()}, d); + const dataChunk = Object.assign({}, d); if (!dataChunk.parentId) { dataChunk.parentId = series.id; @@ -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}], ); } diff --git a/src/plugins/d3/renderer/hooks/useShapes/treemap/types.ts b/src/plugins/d3/renderer/hooks/useShapes/treemap/types.ts index 9cf2d74f..d5fab99d 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/treemap/types.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/treemap/types.ts @@ -1,20 +1,18 @@ -import type {HierarchyNode} from 'd3'; -import {TreemapSeriesData} from '../../../../../../types'; -import {PreparedTreemapSeries} from '../../useSeries/types'; +import type {HierarchyRectangularNode} from 'd3'; -export type PreparedTreemapSeriesData = TreemapSeriesData & { - _nodeId: string; -}; - -export type PreparedTreemapData = { - hierarchy: HierarchyNode; - series: PreparedTreemapSeries; -}; +import type {TreemapSeriesData} from '../../../../../../types'; +import type {PreparedTreemapSeries} from '../../useSeries/types'; export type TreemapLabelData = { - id: string; text: string; x: number; y: number; width: number; + nodeData: TreemapSeriesData; +}; + +export type PreparedTreemapData = { + labelData: TreemapLabelData[]; + leaves: HierarchyRectangularNode>[]; + series: PreparedTreemapSeries; }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/treemap/utils.ts b/src/plugins/d3/renderer/hooks/useShapes/treemap/utils.ts deleted file mode 100644 index 34aa0574..00000000 --- a/src/plugins/d3/renderer/hooks/useShapes/treemap/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type {HierarchyRectangularNode} from 'd3'; - -import type {TreemapLabelData, PreparedTreemapSeriesData} from './types'; - -export function getLabelData( - data: HierarchyRectangularNode[], -): TreemapLabelData[] { - return data.map((d) => { - const text = d.data.name; - return {text, id: d.data._nodeId, x: d.x0, y: d.y0, width: d.x1 - d.x0}; - }); -}