Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(D3 plugin): add treemap chart #408

Merged
merged 5 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/constants/widget-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const SeriesType = {
Line: 'line',
Pie: 'pie',
Scatter: 'scatter',
Treemap: 'treemap',
} as const;

export enum DashStyle {
Expand Down Expand Up @@ -35,3 +36,11 @@ export enum LineCap {
Square = 'square',
None = 'none',
}

export enum LayoutAlgorithm {
Binary = 'binary',
Dice = 'dice',
Slice = 'slice',
SliceDice = 'slice-dice',
Squarify = 'squarify',
}
4 changes: 3 additions & 1 deletion src/i18n/keysets/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"label_invalid-axis-linear-data-point": "It seems you are trying to use inappropriate data type for \"{{key}}\" value in series \"{{seriesName}}\" for axis with type \"linear\". Numbers and nulls are allowed.",
"label_invalid-pie-data-value": "It seems you are trying to use inappropriate data type for \"value\" value. Only numbers are allowed.",
"label_invalid-series-type": "It seems you haven't defined \"series.type\" property, or defined it incorrectly. Available values: [{{types}}].",
"label_invalid-series-property": "It seems you are trying to use inappropriate value for \"{{key}}\", or defined it incorrectly. Available values: [{{values}}]."
"label_invalid-series-property": "It seems you are trying to use inappropriate value for \"{{key}}\", or defined it incorrectly. Available values: [{{values}}].",
"label_invalid-treemap-redundant-value": "It seems you are trying to set \"value\" for container node. Check node with this properties: { id: \"{{id}}\", name: \"{{name}}\" }",
"label_invalid-treemap-missing-value": "It seems you are trying to use node without \"value\". Check node with this properties: { id: \"{{id}}\", name: \"{{name}}\" }"
},
"highcharts": {
"reset-zoom-title": "Reset zoom",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/keysets/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
"label_invalid-axis-linear-data-point": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"{{key}}\" в серии \"{{seriesName}}\" для оси с типом \"linear\". Допускается использование чисел и значений null.",
"label_invalid-pie-data-value": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"value\". Допускается только использование чисел.",
"label_invalid-series-type": "Похоже, что вы не указали значение \"series.type\" или указали его неверно. Доступные значения: [{{types}}].",
"label_invalid-series-property": "Похоже, что вы пытаетесь использовать недопустимое значение для \"{{key}}\", или указали его неверно. Доступные значения: [{{values}}]."
"label_invalid-series-property": "Похоже, что вы пытаетесь использовать недопустимое значение для \"{{key}}\", или указали его неверно. Доступные значения: [{{values}}].",
"label_invalid-treemap-redundant-value": "Похоже, что вы пытаетесь установить значение \"value\" для узла, используемого в качестве контейнера. Проверьте узел с этими свойствами: { id: \"{{id}}\", name: \"{{name}}\" }",
"label_invalid-treemap-missing-value": "Похоже, что вы пытаетесь использовать узел без значения \"value\". Проверьте узел с этими свойствами: { id: \"{{id}}\", name: \"{{name}}\" }"
},
"highcharts": {
"reset-zoom-title": "Сбросить увеличение",
Expand Down
69 changes: 69 additions & 0 deletions src/plugins/d3/__stories__/treemap/Playground.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import {StoryObj} from '@storybook/react';
import {Button} from '@gravity-ui/uikit';
import {settings} from '../../../../libs';
import {ChartKit} from '../../../../components/ChartKit';
import type {ChartKitRef} from '../../../../types';
import type {ChartKitWidgetData} from '../../../../types/widget-data';
import {D3Plugin} from '../..';

const prepareData = (): ChartKitWidgetData => {
return {
series: {
data: [
{
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}) => {
const [shown, setShown] = React.useState(false);
const chartkitRef = React.useRef<ChartKitRef>();

if (!shown) {
settings.set({plugins: [D3Plugin]});
return <Button onClick={() => setShown(true)}>Show chart</Button>;
}

return (
<div style={{height: '300px', width: '100%'}}>
<ChartKit ref={chartkitRef} type="d3" data={data} />
</div>
);
};

export const TreemapPlayground: StoryObj<typeof ChartStory> = {
name: 'Playground',
args: {data: prepareData()},
argTypes: {
data: {
control: 'object',
},
},
};

export default {
title: 'Plugins/D3/Treemap',
component: ChartStory,
};
2 changes: 0 additions & 2 deletions src/plugins/d3/renderer/components/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ type Props = {
};

export const Chart = (props: Props) => {
// FIXME: add data validation
const {width, height, data} = props;
const svgRef = React.useRef<SVGSVGElement>(null);
const dispatcher = React.useMemo(() => {
Expand All @@ -45,7 +44,6 @@ export const Chart = (props: Props) => {
() => getPreparedXAxis({xAxis: data.xAxis, width, series: data.series.data}),
[data, width],
);

const yAxis = React.useMemo(
() => getPreparedYAxis({series: data.series.data, yAxis: data.yAxis}),
[data, width],
Expand Down
11 changes: 8 additions & 3 deletions src/plugins/d3/renderer/components/Tooltip/DefaultContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React from 'react';
import get from 'lodash/get';
import {dateTime} from '@gravity-ui/date-utils';
import type {ChartKitWidgetSeriesData, TooltipDataChunk} from '../../../../../types';
import type {
ChartKitWidgetSeriesData,
TooltipDataChunk,
TreemapSeriesData,
} from '../../../../../types';
import {formatNumber} from '../../../../shared';
import type {PreparedAxis, PreparedPieSeries} from '../../hooks';
import {getDataCategoryValue} from '../../utils';
Expand Down Expand Up @@ -81,8 +85,9 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => {
</div>
);
}
case 'pie': {
const pieSeriesData = data as PreparedPieSeries;
case 'pie':
case 'treemap': {
const pieSeriesData = data as PreparedPieSeries | TreemapSeriesData;

return (
<div key={id}>
Expand Down
12 changes: 12 additions & 0 deletions src/plugins/d3/renderer/constants/defaults/series-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,16 @@ export const seriesOptionsDefaults: SeriesOptionsDefaults = {
},
},
},
treemap: {
states: {
hover: {
enabled: true,
brightness: 0.3,
},
inactive: {
enabled: false,
opacity: 0.5,
},
},
},
};
9 changes: 5 additions & 4 deletions src/plugins/d3/renderer/hooks/useSeries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ export const useSeries = (args: Args) => {
getActiveLegendItems(preparedSeries),
);
const chartSeries = React.useMemo<PreparedSeries[]>(() => {
return preparedSeries.map((singleSeries, i) => {
singleSeries.id = `Series ${i + 1}`;

return preparedSeries.map((singleSeries) => {
if (singleSeries.legend.enabled) {
return {
...singleSeries,
Expand All @@ -88,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 @@ -96,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
48 changes: 48 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/prepare-treemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type {ScaleOrdinal} from 'd3';
import get from 'lodash/get';

import {LayoutAlgorithm} from '../../../../../constants';
import type {ChartKitWidgetSeriesOptions, TreemapSeries} from '../../../../../types';
import {getRandomCKId} from '../../../../../utils';

import {DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE} from './constants';
import type {PreparedLegend, PreparedTreemapSeries} from './types';
import {prepareLegendSymbol} from './utils';

type PrepareTreemapSeriesArgs = {
colorScale: ScaleOrdinal<string, string>;
legend: PreparedLegend;
series: TreemapSeries[];
seriesOptions?: ChartKitWidgetSeriesOptions;
};

export function prepareTreemap(args: PrepareTreemapSeriesArgs) {
const {colorScale, legend, series} = args;

return series.map<PreparedTreemapSeries>((s) => {
const id = getRandomCKId();
const name = s.name || '';
const color = s.color || colorScale(name);

return {
color,
data: s.data,
dataLabels: {
enabled: get(s, 'dataLabels.enabled', true),
style: Object.assign({}, DEFAULT_DATALABELS_STYLE, s.dataLabels?.style),
padding: get(s, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING),
allowOverlap: get(s, 'dataLabels.allowOverlap', false),
},
id,
type: s.type,
name,
visible: get(s, 'visible', true),
legend: {
enabled: get(s, 'legend.enabled', legend.enabled),
symbol: prepareLegendSymbol(s),
},
levels: s.levels,
layoutAlgorithm: get(s, 'layoutAlgorithm', LayoutAlgorithm.Binary),
};
});
}
12 changes: 11 additions & 1 deletion src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ import type {
LineSeries,
PieSeries,
ScatterSeries,
TreemapSeries,
} from '../../../../../types';
import {ChartKitError} from '../../../../../libs';

import type {PreparedLegend, PreparedSeries} from './types';
import {prepareLineSeries} from './prepare-line';
import {prepareBarXSeries} from './prepare-bar-x';
import {prepareBarYSeries} from './prepare-bar-y';
import {ChartKitError} from '../../../../../libs';
import {preparePieSeries} from './prepare-pie';
import {prepareArea} from './prepare-area';
import {prepareScatterSeries} from './prepare-scatter';
import {prepareTreemap} from './prepare-treemap';

export function prepareSeries(args: {
type: ChartKitWidgetSeries['type'];
Expand Down Expand Up @@ -63,6 +65,14 @@ export function prepareSeries(args: {
colorScale,
});
}
case 'treemap': {
return prepareTreemap({
series: series as TreemapSeries[],
seriesOptions,
legend,
colorScale,
});
}
default: {
throw new ChartKitError({
message: `Series type "${type}" does not support data preparation for series that do not support the presence of axes`,
Expand Down
20 changes: 18 additions & 2 deletions src/plugins/d3/renderer/hooks/useSeries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {
SymbolLegendSymbolOptions,
AreaSeries,
AreaSeriesData,
TreemapSeries,
TreemapSeriesData,
} from '../../../../../types';
import type {SeriesOptionsDefaults} from '../../constants';
import {DashStyle, LineCap, SymbolType} from '../../../../../constants';
import {DashStyle, LayoutAlgorithm, LineCap, SymbolType} from '../../../../../constants';

export type RectLegendSymbol = {
shape: 'rect';
Expand Down Expand Up @@ -228,13 +230,27 @@ export type PreparedAreaSeries = {
};
} & BasePreparedSeries;

export type PreparedTreemapSeries = {
type: TreemapSeries['type'];
data: TreemapSeriesData[];
dataLabels: {
enabled: boolean;
style: BaseTextStyle;
padding: number;
allowOverlap: boolean;
};
layoutAlgorithm: `${LayoutAlgorithm}`;
} & BasePreparedSeries &
TreemapSeries;

export type PreparedSeries =
| PreparedScatterSeries
| PreparedBarXSeries
| PreparedBarYSeries
| PreparedPieSeries
| PreparedLineSeries
| PreparedAreaSeries;
| PreparedAreaSeries
| PreparedTreemapSeries;

export type PreparedSeriesOptions = SeriesOptionsDefaults;

Expand Down
23 changes: 22 additions & 1 deletion src/plugins/d3/renderer/hooks/useShapes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
PreparedLineSeries,
PreparedPieSeries,
PreparedScatterSeries,
PreparedTreemapSeries,
PreparedSeries,
PreparedSeriesOptions,
} from '../';
Expand All @@ -31,6 +32,8 @@ export type {PreparedScatterData} from './scatter/types';
import {AreaSeriesShapes} from './area';
import {prepareAreaData} from './area/prepare-data';
import type {PreparedAreaData} from './area/types';
import {TreemapSeriesShape} from './treemap';
import {prepareTreemapData} from './treemap/prepare-data';

import './styles.scss';

Expand Down Expand Up @@ -189,7 +192,6 @@ export const useShapes = (args: Args) => {
boundsWidth,
boundsHeight,
});

acc.push(
<PieSeriesShapes
key="pie"
Expand All @@ -199,6 +201,25 @@ export const useShapes = (args: Args) => {
svgContainer={svgContainer}
/>,
);
break;
}
case 'treemap': {
const preparedData = prepareTreemapData({
// 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
key="treemap"
dispatcher={dispatcher}
preparedData={preparedData}
seriesOptions={seriesOptions}
svgContainer={svgContainer}
/>,
);
}
}
return acc;
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/d3/renderer/hooks/useShapes/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,12 @@
alignment-baseline: after-edge;
}
}

.chartkit-d3-treemap {
&__label {
fill: var(--g-color-text-complementary);
alignment-baseline: text-before-edge;
user-select: none;
pointer-events: none;
}
}
Loading
Loading