@gravity-ui/charts 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/Tooltip/DefaultContent.js +18 -3
- package/dist/cjs/constants/defaults/series-options.js +12 -0
- package/dist/cjs/constants/index.d.ts +1 -0
- package/dist/cjs/constants/index.js +1 -0
- package/dist/cjs/hooks/useSeries/prepare-radar.d.ts +16 -0
- package/dist/cjs/hooks/useSeries/prepare-radar.js +63 -0
- package/dist/cjs/hooks/useSeries/prepareSeries.js +8 -0
- package/dist/cjs/hooks/useSeries/types.d.ts +35 -2
- package/dist/cjs/hooks/useShapes/index.d.ts +2 -1
- package/dist/cjs/hooks/useShapes/index.js +12 -0
- package/dist/cjs/hooks/useShapes/marker.d.ts +3 -2
- package/dist/cjs/hooks/useShapes/radar/index.d.ts +12 -0
- package/dist/cjs/hooks/useShapes/radar/index.js +136 -0
- package/dist/cjs/hooks/useShapes/radar/prepare-data.d.ts +9 -0
- package/dist/cjs/hooks/useShapes/radar/prepare-data.js +155 -0
- package/dist/cjs/hooks/useShapes/radar/types.d.ts +58 -0
- package/dist/cjs/hooks/useShapes/radar/types.js +1 -0
- package/dist/cjs/hooks/useShapes/styles.css +4 -0
- package/dist/cjs/types/chart/radar.d.ts +50 -0
- package/dist/cjs/types/chart/radar.js +1 -0
- package/dist/cjs/types/chart/series.d.ts +17 -2
- package/dist/cjs/types/chart/tooltip.d.ts +8 -1
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/cjs/types/index.js +1 -0
- package/dist/cjs/utils/chart/get-closest-data.js +39 -2
- package/dist/cjs/utils/chart/index.js +1 -1
- package/dist/esm/components/Tooltip/DefaultContent.js +18 -3
- package/dist/esm/constants/defaults/series-options.js +12 -0
- package/dist/esm/constants/index.d.ts +1 -0
- package/dist/esm/constants/index.js +1 -0
- package/dist/esm/hooks/useSeries/prepare-radar.d.ts +16 -0
- package/dist/esm/hooks/useSeries/prepare-radar.js +63 -0
- package/dist/esm/hooks/useSeries/prepareSeries.js +8 -0
- package/dist/esm/hooks/useSeries/types.d.ts +35 -2
- package/dist/esm/hooks/useShapes/index.d.ts +2 -1
- package/dist/esm/hooks/useShapes/index.js +12 -0
- package/dist/esm/hooks/useShapes/marker.d.ts +3 -2
- package/dist/esm/hooks/useShapes/radar/index.d.ts +12 -0
- package/dist/esm/hooks/useShapes/radar/index.js +136 -0
- package/dist/esm/hooks/useShapes/radar/prepare-data.d.ts +9 -0
- package/dist/esm/hooks/useShapes/radar/prepare-data.js +155 -0
- package/dist/esm/hooks/useShapes/radar/types.d.ts +58 -0
- package/dist/esm/hooks/useShapes/radar/types.js +1 -0
- package/dist/esm/hooks/useShapes/styles.css +4 -0
- package/dist/esm/types/chart/radar.d.ts +50 -0
- package/dist/esm/types/chart/radar.js +1 -0
- package/dist/esm/types/chart/series.d.ts +17 -2
- package/dist/esm/types/chart/tooltip.d.ts +8 -1
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/utils/chart/get-closest-data.js +39 -2
- package/dist/esm/utils/chart/index.js +1 -1
- package/package.json +1 -1
|
@@ -28,14 +28,17 @@ const getRowData = (fieldName, data, axis) => {
|
|
|
28
28
|
const getXRowData = (data, xAxis) => getRowData('x', data, xAxis);
|
|
29
29
|
const getYRowData = (data, yAxis) => getRowData('y', data, yAxis);
|
|
30
30
|
const getMeasureValue = (data, xAxis, yAxis) => {
|
|
31
|
-
var _a, _b;
|
|
31
|
+
var _a, _b, _c, _d;
|
|
32
32
|
if (data.every((item) => ['pie', 'treemap', 'waterfall', 'sankey'].includes(item.series.type))) {
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
|
+
if (data.some((item) => item.series.type === 'radar')) {
|
|
36
|
+
return (_b = (_a = data[0].category) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null;
|
|
37
|
+
}
|
|
35
38
|
if (data.some((item) => item.series.type === 'bar-y')) {
|
|
36
|
-
return getYRowData((
|
|
39
|
+
return getYRowData((_c = data[0]) === null || _c === void 0 ? void 0 : _c.data, yAxis);
|
|
37
40
|
}
|
|
38
|
-
return getXRowData((
|
|
41
|
+
return getXRowData((_d = data[0]) === null || _d === void 0 ? void 0 : _d.data, xAxis);
|
|
39
42
|
};
|
|
40
43
|
export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
41
44
|
const measureValue = getMeasureValue(hovered, xAxis, yAxis);
|
|
@@ -109,6 +112,18 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
|
|
|
109
112
|
": ",
|
|
110
113
|
value)));
|
|
111
114
|
}
|
|
115
|
+
case 'radar': {
|
|
116
|
+
const radarSeries = series;
|
|
117
|
+
const seriesData = data;
|
|
118
|
+
const value = (React.createElement(React.Fragment, null,
|
|
119
|
+
React.createElement("span", null,
|
|
120
|
+
radarSeries.name || radarSeries.id,
|
|
121
|
+
"\u00A0"),
|
|
122
|
+
React.createElement("span", null, seriesData.value)));
|
|
123
|
+
return (React.createElement("div", { key: id, className: b('content-row') },
|
|
124
|
+
React.createElement("div", { className: b('color'), style: { backgroundColor: color } }),
|
|
125
|
+
React.createElement("div", null, closest ? React.createElement("b", null, value) : React.createElement("span", null, value))));
|
|
126
|
+
}
|
|
112
127
|
default: {
|
|
113
128
|
return null;
|
|
114
129
|
}
|
|
@@ -89,6 +89,18 @@ export const seriesOptionsDefaults = {
|
|
|
89
89
|
},
|
|
90
90
|
},
|
|
91
91
|
},
|
|
92
|
+
radar: {
|
|
93
|
+
states: {
|
|
94
|
+
hover: {
|
|
95
|
+
enabled: true,
|
|
96
|
+
brightness: 0.3,
|
|
97
|
+
},
|
|
98
|
+
inactive: {
|
|
99
|
+
enabled: true,
|
|
100
|
+
opacity: 0.5,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
92
104
|
waterfall: {
|
|
93
105
|
barMaxWidth: 50,
|
|
94
106
|
barPadding: 0.1,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChartSeriesOptions, RadarSeries } from '../../types';
|
|
2
|
+
import type { PreparedLegend, PreparedRadarSeries } from './types';
|
|
3
|
+
type PrepareRadarSeriesArgs = {
|
|
4
|
+
series: RadarSeries[];
|
|
5
|
+
seriesOptions?: ChartSeriesOptions;
|
|
6
|
+
legend: PreparedLegend;
|
|
7
|
+
};
|
|
8
|
+
export declare const DEFAULT_MARKER: {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
radius: number;
|
|
11
|
+
symbol: `${import("../../constants").SymbolType}`;
|
|
12
|
+
borderColor: string;
|
|
13
|
+
borderWidth: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function prepareRadarSeries(args: PrepareRadarSeriesArgs): PreparedRadarSeries[];
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { scaleOrdinal } from 'd3';
|
|
2
|
+
import get from 'lodash/get';
|
|
3
|
+
import merge from 'lodash/merge';
|
|
4
|
+
import { DEFAULT_PALETTE } from '../../constants';
|
|
5
|
+
import { getUniqId } from '../../utils';
|
|
6
|
+
import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE, DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS, } from './constants';
|
|
7
|
+
import { prepareLegendSymbol } from './utils';
|
|
8
|
+
export const DEFAULT_MARKER = Object.assign(Object.assign({}, DEFAULT_POINT_MARKER_OPTIONS), { enabled: true, radius: 2 });
|
|
9
|
+
function prepareMarker(series, seriesOptions) {
|
|
10
|
+
var _a;
|
|
11
|
+
const seriesHoverState = get(seriesOptions, 'radar.states.hover');
|
|
12
|
+
const markerNormalState = Object.assign({}, DEFAULT_MARKER, (_a = seriesOptions === null || seriesOptions === void 0 ? void 0 : seriesOptions.radar) === null || _a === void 0 ? void 0 : _a.marker, series.marker);
|
|
13
|
+
const hoveredMarkerDefaultOptions = {
|
|
14
|
+
enabled: true,
|
|
15
|
+
radius: markerNormalState.radius + 1,
|
|
16
|
+
borderWidth: 1,
|
|
17
|
+
borderColor: '#ffffff',
|
|
18
|
+
halo: DEFAULT_HALO_OPTIONS,
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
states: {
|
|
22
|
+
normal: markerNormalState,
|
|
23
|
+
hover: merge(hoveredMarkerDefaultOptions, seriesHoverState === null || seriesHoverState === void 0 ? void 0 : seriesHoverState.marker),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function prepareRadarSeries(args) {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
const { series: radarSeries, seriesOptions, legend } = args;
|
|
30
|
+
const colorScale = scaleOrdinal(radarSeries.map((s, index) => { var _a; return (_a = s.name) !== null && _a !== void 0 ? _a : `Series ${index + 1}`; }), DEFAULT_PALETTE);
|
|
31
|
+
const categories = (_b = (_a = radarSeries.find((s) => s.categories)) === null || _a === void 0 ? void 0 : _a.categories) !== null && _b !== void 0 ? _b : [];
|
|
32
|
+
return radarSeries.map((series, index) => {
|
|
33
|
+
var _a, _b, _c, _d, _e;
|
|
34
|
+
const name = (_a = series.name) !== null && _a !== void 0 ? _a : `Series ${index + 1}`;
|
|
35
|
+
const color = (_b = series.color) !== null && _b !== void 0 ? _b : colorScale(name);
|
|
36
|
+
const preparedSeries = {
|
|
37
|
+
type: 'radar',
|
|
38
|
+
data: series.data,
|
|
39
|
+
categories,
|
|
40
|
+
id: getUniqId(),
|
|
41
|
+
name,
|
|
42
|
+
color,
|
|
43
|
+
visible: typeof series.visible === 'boolean' ? series.visible : true,
|
|
44
|
+
legend: {
|
|
45
|
+
enabled: get(series, 'legend.enabled', legend.enabled),
|
|
46
|
+
symbol: prepareLegendSymbol(series),
|
|
47
|
+
},
|
|
48
|
+
borderColor: series.borderColor || color,
|
|
49
|
+
borderWidth: (_c = series.borderWidth) !== null && _c !== void 0 ? _c : 1,
|
|
50
|
+
fillOpacity: (_d = series.fillOpacity) !== null && _d !== void 0 ? _d : 0.25,
|
|
51
|
+
dataLabels: {
|
|
52
|
+
enabled: get(series, 'dataLabels.enabled', true),
|
|
53
|
+
style: Object.assign({}, DEFAULT_DATALABELS_STYLE, (_e = series.dataLabels) === null || _e === void 0 ? void 0 : _e.style),
|
|
54
|
+
padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING),
|
|
55
|
+
allowOverlap: get(series, 'dataLabels.allowOverlap', false),
|
|
56
|
+
html: get(series, 'dataLabels.html', false),
|
|
57
|
+
},
|
|
58
|
+
cursor: get(series, 'cursor', null),
|
|
59
|
+
marker: prepareMarker(series, seriesOptions),
|
|
60
|
+
};
|
|
61
|
+
return preparedSeries;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -4,6 +4,7 @@ import { prepareBarXSeries } from './prepare-bar-x';
|
|
|
4
4
|
import { prepareBarYSeries } from './prepare-bar-y';
|
|
5
5
|
import { prepareLineSeries } from './prepare-line';
|
|
6
6
|
import { preparePieSeries } from './prepare-pie';
|
|
7
|
+
import { prepareRadarSeries } from './prepare-radar';
|
|
7
8
|
import { prepareSankeySeries } from './prepare-sankey';
|
|
8
9
|
import { prepareScatterSeries } from './prepare-scatter';
|
|
9
10
|
import { prepareTreemap } from './prepare-treemap';
|
|
@@ -75,6 +76,13 @@ export function prepareSeries(args) {
|
|
|
75
76
|
legend,
|
|
76
77
|
});
|
|
77
78
|
}
|
|
79
|
+
case 'radar': {
|
|
80
|
+
return prepareRadarSeries({
|
|
81
|
+
series: series,
|
|
82
|
+
seriesOptions,
|
|
83
|
+
legend,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
78
86
|
default: {
|
|
79
87
|
throw new ChartError({
|
|
80
88
|
message: `Series type "${type}" does not support data preparation for series that do not support the presence of axes`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DashStyle, LayoutAlgorithm, LineCap, SeriesOptionsDefaults, SymbolType } from '../../constants';
|
|
2
|
-
import type { AreaSeries, AreaSeriesData, BarXSeries, BarXSeriesData, BarYSeries, BarYSeriesData, BaseTextStyle, ChartLegend, ConnectorCurve, ConnectorShape, LineSeries, LineSeriesData, PathLegendSymbolOptions, PieSeries, PieSeriesData, RectLegendSymbolOptions, SankeySeries, SankeySeriesData, ScatterSeries, ScatterSeriesData, SymbolLegendSymbolOptions, TreemapSeries, TreemapSeriesData, WaterfallSeries, WaterfallSeriesData } from '../../types';
|
|
2
|
+
import type { AreaSeries, AreaSeriesData, BarXSeries, BarXSeriesData, BarYSeries, BarYSeriesData, BaseTextStyle, ChartLegend, ConnectorCurve, ConnectorShape, LineSeries, LineSeriesData, PathLegendSymbolOptions, PieSeries, PieSeriesData, RadarSeries, RadarSeriesCategory, RadarSeriesData, RectLegendSymbolOptions, SankeySeries, SankeySeriesData, ScatterSeries, ScatterSeriesData, SymbolLegendSymbolOptions, TreemapSeries, TreemapSeriesData, WaterfallSeries, WaterfallSeriesData } from '../../types';
|
|
3
3
|
export type RectLegendSymbol = {
|
|
4
4
|
shape: 'rect';
|
|
5
5
|
} & Required<RectLegendSymbolOptions>;
|
|
@@ -258,7 +258,40 @@ export type PreparedSankeySeries = {
|
|
|
258
258
|
style: BaseTextStyle;
|
|
259
259
|
};
|
|
260
260
|
} & BasePreparedSeries & Omit<SankeySeries, keyof BasePreparedSeries>;
|
|
261
|
-
export type
|
|
261
|
+
export type PreparedRadarSeries = {
|
|
262
|
+
type: RadarSeries['type'];
|
|
263
|
+
data: RadarSeriesData[];
|
|
264
|
+
categories: RadarSeriesCategory[];
|
|
265
|
+
borderColor: string;
|
|
266
|
+
borderWidth: number;
|
|
267
|
+
fillOpacity: number;
|
|
268
|
+
dataLabels: {
|
|
269
|
+
enabled: boolean;
|
|
270
|
+
style: BaseTextStyle;
|
|
271
|
+
padding: number;
|
|
272
|
+
allowOverlap: boolean;
|
|
273
|
+
html: boolean;
|
|
274
|
+
};
|
|
275
|
+
marker: {
|
|
276
|
+
states: {
|
|
277
|
+
normal: {
|
|
278
|
+
symbol: `${SymbolType}`;
|
|
279
|
+
enabled: boolean;
|
|
280
|
+
radius: number;
|
|
281
|
+
borderWidth: number;
|
|
282
|
+
borderColor: string;
|
|
283
|
+
};
|
|
284
|
+
hover: {
|
|
285
|
+
enabled: boolean;
|
|
286
|
+
radius: number;
|
|
287
|
+
borderWidth: number;
|
|
288
|
+
borderColor: string;
|
|
289
|
+
halo: PreparedHaloOptions;
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
} & BasePreparedSeries;
|
|
294
|
+
export type PreparedSeries = PreparedScatterSeries | PreparedBarXSeries | PreparedBarYSeries | PreparedPieSeries | PreparedLineSeries | PreparedAreaSeries | PreparedTreemapSeries | PreparedWaterfallSeries | PreparedSankeySeries | PreparedRadarSeries;
|
|
262
295
|
export type PreparedSeriesOptions = SeriesOptionsDefaults;
|
|
263
296
|
export type StackedSeries = BarXSeries | AreaSeries | BarYSeries;
|
|
264
297
|
export {};
|
|
@@ -8,13 +8,14 @@ import type { PreparedBarXData } from './bar-x';
|
|
|
8
8
|
import type { PreparedBarYData } from './bar-y/types';
|
|
9
9
|
import type { PreparedLineData } from './line/types';
|
|
10
10
|
import type { PreparedPieData } from './pie/types';
|
|
11
|
+
import type { PreparedRadarData } from './radar/types';
|
|
11
12
|
import type { PreparedSankeyData } from './sankey/types';
|
|
12
13
|
import type { PreparedScatterData } from './scatter/types';
|
|
13
14
|
export type { PreparedBarXData } from './bar-x';
|
|
14
15
|
export type { PreparedScatterData } from './scatter/types';
|
|
15
16
|
import type { PreparedWaterfallData } from './waterfall';
|
|
16
17
|
import './styles.css';
|
|
17
|
-
export type ShapeData = PreparedBarXData | PreparedBarYData | PreparedScatterData | PreparedLineData | PreparedPieData | PreparedAreaData | PreparedWaterfallData | PreparedSankeyData;
|
|
18
|
+
export type ShapeData = PreparedBarXData | PreparedBarYData | PreparedScatterData | PreparedLineData | PreparedPieData | PreparedAreaData | PreparedWaterfallData | PreparedSankeyData | PreparedRadarData;
|
|
18
19
|
type Args = {
|
|
19
20
|
boundsWidth: number;
|
|
20
21
|
boundsHeight: number;
|
|
@@ -10,6 +10,8 @@ import { LineSeriesShapes } from './line';
|
|
|
10
10
|
import { prepareLineData } from './line/prepare-data';
|
|
11
11
|
import { PieSeriesShapes } from './pie';
|
|
12
12
|
import { preparePieData } from './pie/prepare-data';
|
|
13
|
+
import { RadarSeriesShapes } from './radar';
|
|
14
|
+
import { prepareRadarData } from './radar/prepare-data';
|
|
13
15
|
import { SankeySeriesShape } from './sankey';
|
|
14
16
|
import { prepareSankeyData } from './sankey/prepare-data';
|
|
15
17
|
import { ScatterSeriesShape, prepareScatterData } from './scatter';
|
|
@@ -149,6 +151,16 @@ export const useShapes = (args) => {
|
|
|
149
151
|
shapesData.push(preparedData);
|
|
150
152
|
break;
|
|
151
153
|
}
|
|
154
|
+
case 'radar': {
|
|
155
|
+
const preparedData = prepareRadarData({
|
|
156
|
+
series: chartSeries,
|
|
157
|
+
boundsWidth,
|
|
158
|
+
boundsHeight,
|
|
159
|
+
});
|
|
160
|
+
acc.push(React.createElement(RadarSeriesShapes, { key: "radar", dispatcher: dispatcher, series: preparedData, seriesOptions: seriesOptions, htmlLayout: htmlLayout }));
|
|
161
|
+
shapesData.push(...preparedData);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
152
164
|
default: {
|
|
153
165
|
throw new ChartError({
|
|
154
166
|
message: `The display method is not defined for a series with type "${seriesType}"`,
|
|
@@ -2,9 +2,10 @@ import type { BaseType, Selection } from 'd3';
|
|
|
2
2
|
import { SymbolType } from '../../constants';
|
|
3
3
|
import type { MarkerData as AreaMarkerData } from './area/types';
|
|
4
4
|
import type { MarkerData as LineMarkerData } from './line/types';
|
|
5
|
+
import type { RadarMarkerData } from './radar/types';
|
|
5
6
|
import type { MarkerData as ScatterMarkerData } from './scatter/types';
|
|
6
|
-
type MarkerData = LineMarkerData | AreaMarkerData | ScatterMarkerData;
|
|
7
|
-
export declare function renderMarker<T extends MarkerData>(selection: Selection<BaseType
|
|
7
|
+
type MarkerData = LineMarkerData | AreaMarkerData | ScatterMarkerData | RadarMarkerData;
|
|
8
|
+
export declare function renderMarker<T extends MarkerData>(selection: Selection<BaseType, T, BaseType, unknown>): Selection<BaseType, T, BaseType, unknown>;
|
|
8
9
|
export declare function getMarkerVisibility(d: MarkerData): "" | "hidden";
|
|
9
10
|
export declare function getMarkerHaloVisibility(d: MarkerData): "" | "hidden";
|
|
10
11
|
export declare function setMarker<T extends BaseType, D extends MarkerData>(selection: Selection<T, D, BaseType | null, unknown>, state: 'normal' | 'hover'): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Dispatch } from 'd3';
|
|
3
|
+
import type { PreparedSeriesOptions } from '../../useSeries/types';
|
|
4
|
+
import type { PreparedRadarData } from './types';
|
|
5
|
+
type PrepareRadarSeriesArgs = {
|
|
6
|
+
dispatcher: Dispatch<object>;
|
|
7
|
+
series: PreparedRadarData[];
|
|
8
|
+
seriesOptions: PreparedSeriesOptions;
|
|
9
|
+
htmlLayout: HTMLElement | null;
|
|
10
|
+
};
|
|
11
|
+
export declare function RadarSeriesShapes(args: PrepareRadarSeriesArgs): React.JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { color, line, select } from 'd3';
|
|
3
|
+
import get from 'lodash/get';
|
|
4
|
+
import { block } from '../../../utils';
|
|
5
|
+
import { HtmlLayer } from '../HtmlLayer';
|
|
6
|
+
import { getMarkerHaloVisibility, getMarkerVisibility, renderMarker, selectMarkerHalo, selectMarkerSymbol, setMarker, } from '../marker';
|
|
7
|
+
import { setActiveState } from '../utils';
|
|
8
|
+
const b = block('radar');
|
|
9
|
+
export function RadarSeriesShapes(args) {
|
|
10
|
+
const { dispatcher, series: preparedData, seriesOptions, htmlLayout } = args;
|
|
11
|
+
const ref = React.useRef(null);
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
if (!ref.current) {
|
|
14
|
+
return () => { };
|
|
15
|
+
}
|
|
16
|
+
const svgElement = select(ref.current);
|
|
17
|
+
svgElement.selectAll('*').remove();
|
|
18
|
+
const areaSelector = `.${b('area')}`;
|
|
19
|
+
const radarSelection = svgElement
|
|
20
|
+
.selectAll('radar')
|
|
21
|
+
.data(preparedData)
|
|
22
|
+
.join('g')
|
|
23
|
+
.attr('id', (radarData) => radarData.id)
|
|
24
|
+
.attr('class', b('item'))
|
|
25
|
+
.attr('cursor', (radarData) => radarData.cursor);
|
|
26
|
+
// render axes
|
|
27
|
+
radarSelection
|
|
28
|
+
.selectAll(`.${b('axis')}`)
|
|
29
|
+
.data((radarData) => radarData.axes)
|
|
30
|
+
.join('line')
|
|
31
|
+
.attr('class', b('axis'))
|
|
32
|
+
.attr('x1', (d) => d.radar.center[0])
|
|
33
|
+
.attr('y1', (d) => d.radar.center[1])
|
|
34
|
+
.attr('x2', (d) => d.point[0])
|
|
35
|
+
.attr('y2', (d) => d.point[1])
|
|
36
|
+
.attr('stroke', (d) => d.strokeColor)
|
|
37
|
+
.attr('stroke-width', (d) => d.strokeWidth);
|
|
38
|
+
// render grid lines
|
|
39
|
+
radarSelection
|
|
40
|
+
.selectAll(`.${b('grid')}`)
|
|
41
|
+
.data((radarData) => radarData.grid)
|
|
42
|
+
.join('path')
|
|
43
|
+
.attr('class', b('grid'))
|
|
44
|
+
.attr('d', (d) => `${line()(d.path)} Z`)
|
|
45
|
+
.attr('fill', 'none')
|
|
46
|
+
.attr('stroke', (d) => d.strokeColor)
|
|
47
|
+
.attr('stroke-width', (d) => d.strokeWidth);
|
|
48
|
+
// render radar area
|
|
49
|
+
const shapesSelection = radarSelection
|
|
50
|
+
.selectAll(areaSelector)
|
|
51
|
+
.data((radarData) => radarData.shapes)
|
|
52
|
+
.join('g')
|
|
53
|
+
.attr('class', b('area'));
|
|
54
|
+
shapesSelection
|
|
55
|
+
.append('path')
|
|
56
|
+
.attr('d', (d) => d.path)
|
|
57
|
+
.attr('fill', (d) => d.color)
|
|
58
|
+
.attr('fill-opacity', (d) => d.fillOpacity)
|
|
59
|
+
.attr('stroke', (d) => d.borderColor)
|
|
60
|
+
.attr('stroke-width', (d) => d.borderWidth);
|
|
61
|
+
// render markers
|
|
62
|
+
const markerSelection = shapesSelection
|
|
63
|
+
.selectAll('marker')
|
|
64
|
+
.data((radarData) => radarData.points)
|
|
65
|
+
.join('g')
|
|
66
|
+
.call(renderMarker);
|
|
67
|
+
// Render labels
|
|
68
|
+
radarSelection
|
|
69
|
+
.selectAll('text')
|
|
70
|
+
.data((radarData) => radarData.labels)
|
|
71
|
+
.join('text')
|
|
72
|
+
.text((d) => d.text)
|
|
73
|
+
.attr('class', b('label'))
|
|
74
|
+
.attr('x', (d) => d.x)
|
|
75
|
+
.attr('y', (d) => d.y)
|
|
76
|
+
.attr('text-anchor', (d) => d.textAnchor)
|
|
77
|
+
.style('font-size', (d) => d.style.fontSize)
|
|
78
|
+
.style('font-weight', (d) => d.style.fontWeight || null)
|
|
79
|
+
.style('fill', (d) => d.style.fontColor || null);
|
|
80
|
+
// Handle hover events
|
|
81
|
+
const eventName = `hover-shape.radar`;
|
|
82
|
+
const hoverOptions = get(seriesOptions, 'radar.states.hover');
|
|
83
|
+
const inactiveOptions = get(seriesOptions, 'radar.states.inactive');
|
|
84
|
+
dispatcher.on(eventName, (data) => {
|
|
85
|
+
const closest = data === null || data === void 0 ? void 0 : data.find((d) => d.closest);
|
|
86
|
+
const selectedSeries = closest === null || closest === void 0 ? void 0 : closest.series;
|
|
87
|
+
const selectedSeriesData = closest === null || closest === void 0 ? void 0 : closest.data;
|
|
88
|
+
const selectedSeriesId = selectedSeries === null || selectedSeries === void 0 ? void 0 : selectedSeries.id;
|
|
89
|
+
const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
|
|
90
|
+
const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
|
|
91
|
+
shapesSelection.datum((d, i, elements) => {
|
|
92
|
+
var _a;
|
|
93
|
+
const hovered = Boolean(hoverEnabled && ((_a = d.series) === null || _a === void 0 ? void 0 : _a.id) === selectedSeriesId);
|
|
94
|
+
if (d.hovered !== hovered) {
|
|
95
|
+
d.hovered = hovered;
|
|
96
|
+
select(elements[i]).attr('fill', () => {
|
|
97
|
+
var _a;
|
|
98
|
+
const initialColor = d.color;
|
|
99
|
+
if (d.hovered) {
|
|
100
|
+
return (((_a = color(initialColor)) === null || _a === void 0 ? void 0 : _a.brighter(hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.brightness).toString()) || initialColor);
|
|
101
|
+
}
|
|
102
|
+
return initialColor;
|
|
103
|
+
});
|
|
104
|
+
if (hovered) {
|
|
105
|
+
select(elements[i]).raise();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
setActiveState({
|
|
109
|
+
element: elements[i],
|
|
110
|
+
state: inactiveOptions,
|
|
111
|
+
active: Boolean(!inactiveEnabled || !selectedSeriesId || selectedSeriesId === d.series.id),
|
|
112
|
+
datum: d,
|
|
113
|
+
});
|
|
114
|
+
markerSelection.datum((markerData, index, markers) => {
|
|
115
|
+
const hoveredState = Boolean(hoverEnabled && markerData.data === selectedSeriesData);
|
|
116
|
+
if (markerData.hovered !== hoveredState) {
|
|
117
|
+
markerData.hovered = hoveredState;
|
|
118
|
+
const elementSelection = select(markers[index]);
|
|
119
|
+
elementSelection.attr('visibility', getMarkerVisibility(markerData));
|
|
120
|
+
selectMarkerHalo(elementSelection).attr('visibility', getMarkerHaloVisibility);
|
|
121
|
+
selectMarkerSymbol(elementSelection).call(setMarker, hoveredState ? 'hover' : 'normal');
|
|
122
|
+
}
|
|
123
|
+
return markerData;
|
|
124
|
+
});
|
|
125
|
+
return d;
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
return () => {
|
|
129
|
+
dispatcher.on(eventName, null);
|
|
130
|
+
};
|
|
131
|
+
}, [dispatcher, preparedData, seriesOptions]);
|
|
132
|
+
const htmlElements = preparedData.map((d) => d.htmlLabels).flat();
|
|
133
|
+
return (React.createElement(React.Fragment, null,
|
|
134
|
+
React.createElement("g", { ref: ref, className: b() }),
|
|
135
|
+
React.createElement(HtmlLayer, { preparedData: { htmlElements }, htmlLayout: htmlLayout })));
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PreparedRadarSeries } from '../../useSeries/types';
|
|
2
|
+
import type { PreparedRadarData } from './types';
|
|
3
|
+
type Args = {
|
|
4
|
+
series: PreparedRadarSeries[];
|
|
5
|
+
boundsWidth: number;
|
|
6
|
+
boundsHeight: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function prepareRadarData(args: Args): PreparedRadarData[];
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { curveLinearClosed, line, range, scaleLinear } from 'd3';
|
|
2
|
+
import { getLabelsSize } from '../../../utils';
|
|
3
|
+
export function prepareRadarData(args) {
|
|
4
|
+
const { series: preparedSeries, boundsWidth, boundsHeight } = args;
|
|
5
|
+
const maxRadius = Math.min(boundsWidth, boundsHeight) / 2;
|
|
6
|
+
const center = [boundsWidth / 2, boundsHeight / 2];
|
|
7
|
+
const result = [];
|
|
8
|
+
const gridStepsCount = 5;
|
|
9
|
+
const radius = maxRadius * 0.8; // Leave some space for labels
|
|
10
|
+
// Create scale for values
|
|
11
|
+
const valueScale = scaleLinear()
|
|
12
|
+
.domain([0, Math.max(...preparedSeries.map((s) => s.data.map((d) => d.value)).flat())])
|
|
13
|
+
.range([0, radius]);
|
|
14
|
+
const [, finalRadius] = valueScale.range();
|
|
15
|
+
const data = {
|
|
16
|
+
type: 'radar',
|
|
17
|
+
id: preparedSeries[0].id,
|
|
18
|
+
center,
|
|
19
|
+
radius: finalRadius,
|
|
20
|
+
shapes: [],
|
|
21
|
+
labels: [],
|
|
22
|
+
axes: [],
|
|
23
|
+
htmlLabels: [],
|
|
24
|
+
grid: [],
|
|
25
|
+
cursor: preparedSeries[0].cursor,
|
|
26
|
+
};
|
|
27
|
+
const categories = preparedSeries[0].categories;
|
|
28
|
+
const axisStrokeColor = 'var(--g-color-line-generic)';
|
|
29
|
+
const axisStrokeWidth = 1;
|
|
30
|
+
// Create axes based on categories
|
|
31
|
+
const axesCount = categories.length;
|
|
32
|
+
const angleStep = (2 * Math.PI) / axesCount;
|
|
33
|
+
data.axes = categories.map((_category, index) => {
|
|
34
|
+
const angle = index * angleStep - Math.PI / 2; // Start from top (negative PI/2)
|
|
35
|
+
return {
|
|
36
|
+
point: [
|
|
37
|
+
center[0] + Math.cos(angle) * data.radius,
|
|
38
|
+
center[1] + Math.sin(angle) * data.radius,
|
|
39
|
+
],
|
|
40
|
+
radar: data,
|
|
41
|
+
strokeColor: axisStrokeColor,
|
|
42
|
+
strokeWidth: axisStrokeWidth,
|
|
43
|
+
angle,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const gridStepInc = data.radius / gridStepsCount;
|
|
47
|
+
const gridSteps = range(gridStepInc, data.radius + gridStepInc, gridStepInc);
|
|
48
|
+
gridSteps.forEach((gridStep) => {
|
|
49
|
+
const gridLines = {
|
|
50
|
+
path: [],
|
|
51
|
+
strokeColor: axisStrokeColor,
|
|
52
|
+
strokeWidth: axisStrokeWidth,
|
|
53
|
+
};
|
|
54
|
+
categories.forEach((_category, index) => {
|
|
55
|
+
const angle = index * angleStep - Math.PI / 2; // Start from top (negative PI/2)
|
|
56
|
+
gridLines.path.push([
|
|
57
|
+
center[0] + Math.cos(angle) * gridStep,
|
|
58
|
+
center[1] + Math.sin(angle) * gridStep,
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
data.grid.push(gridLines);
|
|
62
|
+
});
|
|
63
|
+
const radarAreaLine = line().curve(curveLinearClosed);
|
|
64
|
+
preparedSeries.forEach((series) => {
|
|
65
|
+
var _a;
|
|
66
|
+
const { dataLabels } = series;
|
|
67
|
+
const markers = [];
|
|
68
|
+
categories.forEach((category, index) => {
|
|
69
|
+
var _a;
|
|
70
|
+
const dataItem = series.data[index];
|
|
71
|
+
const angle = index * angleStep - Math.PI / 2; // Start from top (negative PI/2)
|
|
72
|
+
const pointValueScale = scaleLinear()
|
|
73
|
+
.domain([
|
|
74
|
+
0,
|
|
75
|
+
(_a = category.maxValue) !== null && _a !== void 0 ? _a : Math.max(...preparedSeries.map((s) => s.data[index].value)),
|
|
76
|
+
])
|
|
77
|
+
.range([0, radius]);
|
|
78
|
+
const pointRadius = pointValueScale(dataItem.value);
|
|
79
|
+
const x = center[0] + Math.cos(angle) * pointRadius;
|
|
80
|
+
const y = center[1] + Math.sin(angle) * pointRadius;
|
|
81
|
+
markers.push({
|
|
82
|
+
point: {
|
|
83
|
+
x,
|
|
84
|
+
y,
|
|
85
|
+
series,
|
|
86
|
+
data: dataItem,
|
|
87
|
+
},
|
|
88
|
+
index,
|
|
89
|
+
position: [x, y],
|
|
90
|
+
color: series.color,
|
|
91
|
+
opacity: 1,
|
|
92
|
+
radius: 2,
|
|
93
|
+
data: dataItem,
|
|
94
|
+
series: series,
|
|
95
|
+
hovered: false,
|
|
96
|
+
active: false,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
data.shapes.push({
|
|
100
|
+
borderWidth: series.borderWidth,
|
|
101
|
+
borderColor: series.borderColor,
|
|
102
|
+
fillOpacity: series.fillOpacity,
|
|
103
|
+
points: markers,
|
|
104
|
+
path: (_a = radarAreaLine(markers.map((p) => p.position))) !== null && _a !== void 0 ? _a : '',
|
|
105
|
+
series: series,
|
|
106
|
+
color: series.color,
|
|
107
|
+
hovered: false,
|
|
108
|
+
active: true,
|
|
109
|
+
});
|
|
110
|
+
// Create labels if enabled
|
|
111
|
+
if (dataLabels.enabled) {
|
|
112
|
+
const { style } = dataLabels;
|
|
113
|
+
const shouldUseHtml = dataLabels.html;
|
|
114
|
+
data.labels = categories.map((category, index) => {
|
|
115
|
+
const text = category.key;
|
|
116
|
+
const labelSize = getLabelsSize({ labels: [text], style });
|
|
117
|
+
const angle = index * angleStep - Math.PI / 2;
|
|
118
|
+
// Position label slightly outside the point
|
|
119
|
+
const labelRadius = data.radius + 10;
|
|
120
|
+
let x = center[0] + Math.cos(angle) * labelRadius;
|
|
121
|
+
let y = center[1] + Math.sin(angle) * labelRadius;
|
|
122
|
+
if (shouldUseHtml) {
|
|
123
|
+
x = x < center[0] ? x - labelSize.maxWidth : x;
|
|
124
|
+
y = y - labelSize.maxHeight;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
y = y < center[1] ? y - labelSize.maxHeight : y;
|
|
128
|
+
}
|
|
129
|
+
x = Math.max(-boundsWidth / 2, x);
|
|
130
|
+
return {
|
|
131
|
+
text,
|
|
132
|
+
x,
|
|
133
|
+
y,
|
|
134
|
+
style,
|
|
135
|
+
size: { width: labelSize.maxWidth, height: labelSize.maxHeight },
|
|
136
|
+
maxWidth: labelSize.maxWidth,
|
|
137
|
+
textAnchor: angle > Math.PI / 2 && angle < (3 * Math.PI) / 2 ? 'end' : 'start',
|
|
138
|
+
series: { id: series.id },
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
// Create HTML labels if needed
|
|
142
|
+
if (dataLabels.html) {
|
|
143
|
+
data.htmlLabels = data.labels.map((label) => ({
|
|
144
|
+
x: label.x,
|
|
145
|
+
y: label.y,
|
|
146
|
+
content: label.text,
|
|
147
|
+
size: label.size,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return data;
|
|
152
|
+
});
|
|
153
|
+
result.push(data);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { HtmlItem, LabelData, RadarSeriesData } from '../../../types';
|
|
2
|
+
import type { PreparedRadarSeries } from '../../useSeries/types';
|
|
3
|
+
export type RadarLabelData = LabelData & {
|
|
4
|
+
maxWidth: number;
|
|
5
|
+
};
|
|
6
|
+
export type RadarAxisData = {
|
|
7
|
+
point: [number, number];
|
|
8
|
+
angle: number;
|
|
9
|
+
strokeColor: string;
|
|
10
|
+
strokeWidth: number;
|
|
11
|
+
radar: PreparedRadarData;
|
|
12
|
+
};
|
|
13
|
+
export type RadarGridData = {
|
|
14
|
+
path: [number, number][];
|
|
15
|
+
strokeColor: string;
|
|
16
|
+
strokeWidth: number;
|
|
17
|
+
};
|
|
18
|
+
export type PointData = {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
data: RadarSeriesData;
|
|
22
|
+
series: PreparedRadarSeries;
|
|
23
|
+
};
|
|
24
|
+
export type RadarMarkerData = {
|
|
25
|
+
point: PointData;
|
|
26
|
+
radius: number;
|
|
27
|
+
position: [number, number];
|
|
28
|
+
index: number;
|
|
29
|
+
color: string;
|
|
30
|
+
opacity: number;
|
|
31
|
+
data: RadarSeriesData;
|
|
32
|
+
series: PreparedRadarSeries;
|
|
33
|
+
hovered: boolean;
|
|
34
|
+
active: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type RadarShapeData = {
|
|
37
|
+
points: RadarMarkerData[];
|
|
38
|
+
path: string;
|
|
39
|
+
color: string;
|
|
40
|
+
series: PreparedRadarSeries;
|
|
41
|
+
hovered: boolean;
|
|
42
|
+
active: boolean;
|
|
43
|
+
borderColor: string;
|
|
44
|
+
borderWidth: number;
|
|
45
|
+
fillOpacity: number;
|
|
46
|
+
};
|
|
47
|
+
export type PreparedRadarData = {
|
|
48
|
+
type: 'radar';
|
|
49
|
+
id: string;
|
|
50
|
+
shapes: RadarShapeData[];
|
|
51
|
+
labels: RadarLabelData[];
|
|
52
|
+
axes: RadarAxisData[];
|
|
53
|
+
grid: RadarGridData[];
|
|
54
|
+
center: [number, number];
|
|
55
|
+
radius: number;
|
|
56
|
+
htmlLabels: HtmlItem[];
|
|
57
|
+
cursor: string | null;
|
|
58
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|