@cloud-ru/uikit-product-charts 0.13.12
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/CHANGELOG.md +843 -0
- package/LICENSE +201 -0
- package/README.md +8 -0
- package/package.json +67 -0
- package/src/components/BagelChart/BagelChart.tsx +47 -0
- package/src/components/BagelChart/index.ts +1 -0
- package/src/components/BagelChart/styles.module.scss +29 -0
- package/src/components/BagelChart/utils.ts +14 -0
- package/src/components/HeatMapChart/HeatMapChart.tsx +154 -0
- package/src/components/HeatMapChart/constants.ts +4 -0
- package/src/components/HeatMapChart/helpers/constants.ts +6 -0
- package/src/components/HeatMapChart/helpers/getComputedColor.ts +5 -0
- package/src/components/HeatMapChart/helpers/getContrastColor.ts +16 -0
- package/src/components/HeatMapChart/helpers/getStyles.ts +34 -0
- package/src/components/HeatMapChart/helpers/getTickValues.ts +63 -0
- package/src/components/HeatMapChart/helpers/index.ts +4 -0
- package/src/components/HeatMapChart/index.ts +2 -0
- package/src/components/HeatMapChart/styles.module.scss +96 -0
- package/src/components/HeatMapChart/types.ts +40 -0
- package/src/components/InteractiveChart/InteractiveChart.tsx +75 -0
- package/src/components/InteractiveChart/configurations/boxPlot.ts +75 -0
- package/src/components/InteractiveChart/configurations/defaultPlot.ts +63 -0
- package/src/components/InteractiveChart/configurations/index.ts +2 -0
- package/src/components/InteractiveChart/constants.ts +19 -0
- package/src/components/InteractiveChart/helpers/pathRenderer.ts +48 -0
- package/src/components/InteractiveChart/hooks/useComputedColors.ts +32 -0
- package/src/components/InteractiveChart/hooks/useLayer.ts +33 -0
- package/src/components/InteractiveChart/index.ts +2 -0
- package/src/components/InteractiveChart/plugins/boxPlotPlugin.ts +132 -0
- package/src/components/InteractiveChart/plugins/columnHighlightPlugin.ts +78 -0
- package/src/components/InteractiveChart/plugins/legendAsTooltipPlugin.ts +69 -0
- package/src/components/InteractiveChart/plugins/wheelZoomPlugin.ts +115 -0
- package/src/components/InteractiveChart/styles.module.scss +39 -0
- package/src/components/InteractiveChart/types.ts +7 -0
- package/src/components/PieChart/Legend/Legend.tsx +71 -0
- package/src/components/PieChart/Legend/index.ts +1 -0
- package/src/components/PieChart/Legend/styles.module.scss +45 -0
- package/src/components/PieChart/Pie.tsx +71 -0
- package/src/components/PieChart/PieChart.tsx +154 -0
- package/src/components/PieChart/index.ts +2 -0
- package/src/components/PieChart/styles.module.scss +56 -0
- package/src/components/PieChart/types.ts +42 -0
- package/src/components/index.ts +4 -0
- package/src/constants/colors.ts +70 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
|
|
5
|
+
import uPlot from 'uplot';
|
|
6
|
+
import { ColorMap, OTHER_COLORS } from '../../../constants/colors';
|
|
7
|
+
|
|
8
|
+
export function legendAsTooltipPlugin({
|
|
9
|
+
className = '',
|
|
10
|
+
computedColors,
|
|
11
|
+
}: {
|
|
12
|
+
className?: string;
|
|
13
|
+
computedColors: ColorMap;
|
|
14
|
+
}) {
|
|
15
|
+
const backgroundColor = computedColors[OTHER_COLORS.TooltipBackgroundColor];
|
|
16
|
+
const color = computedColors[OTHER_COLORS.TooltipColor];
|
|
17
|
+
|
|
18
|
+
function init(u: uPlot, opts) {
|
|
19
|
+
const legendEl = u.root.querySelector(`.u-legend`) as Element;
|
|
20
|
+
|
|
21
|
+
legendEl.classList.remove('u-inline');
|
|
22
|
+
className && legendEl.classList.add(className);
|
|
23
|
+
|
|
24
|
+
uPlot.assign(legendEl.style, {
|
|
25
|
+
borderRadius: '4px',
|
|
26
|
+
textAlign: 'left',
|
|
27
|
+
pointerEvents: 'none',
|
|
28
|
+
display: 'none',
|
|
29
|
+
position: 'absolute',
|
|
30
|
+
left: 0,
|
|
31
|
+
top: 0,
|
|
32
|
+
zIndex: 100,
|
|
33
|
+
backgroundColor,
|
|
34
|
+
color,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// hide series color markers
|
|
38
|
+
const idents = legendEl.querySelectorAll('.u-marker');
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < idents.length; i++) idents[i].style.display = 'none';
|
|
41
|
+
|
|
42
|
+
const overEl = u.over;
|
|
43
|
+
overEl.style.overflow = 'visible';
|
|
44
|
+
|
|
45
|
+
// move legend into plot bounds
|
|
46
|
+
overEl.appendChild(legendEl);
|
|
47
|
+
|
|
48
|
+
// show/hide tooltip on enter/exit
|
|
49
|
+
overEl.addEventListener('mouseenter', () => {
|
|
50
|
+
legendEl.style.display = null;
|
|
51
|
+
});
|
|
52
|
+
overEl.addEventListener('mouseleave', () => {
|
|
53
|
+
legendEl.style.display = 'none';
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function update(u) {
|
|
58
|
+
const { left, top } = u.cursor;
|
|
59
|
+
const legendEl = u.root.querySelector(`.u-legend`) as Element;
|
|
60
|
+
legendEl.style.transform = 'translate(' + (left + 15) + 'px, ' + top + 'px)';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
hooks: {
|
|
65
|
+
init: init,
|
|
66
|
+
setCursor: update,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
|
|
5
|
+
export function wheelZoomPlugin(opts) {
|
|
6
|
+
const factor = opts.factor || 0.75;
|
|
7
|
+
|
|
8
|
+
let xMin, xMax, yMin, yMax, xRange, yRange;
|
|
9
|
+
|
|
10
|
+
function clamp(nRange, nMin, nMax, fRange, fMin, fMax) {
|
|
11
|
+
if (nRange > fRange) {
|
|
12
|
+
nMin = fMin;
|
|
13
|
+
nMax = fMax;
|
|
14
|
+
} else if (nMin < fMin) {
|
|
15
|
+
nMin = fMin;
|
|
16
|
+
nMax = fMin + nRange;
|
|
17
|
+
} else if (nMax > fMax) {
|
|
18
|
+
nMax = fMax;
|
|
19
|
+
nMin = fMax - nRange;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return [nMin, nMax];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
hooks: {
|
|
27
|
+
ready: u => {
|
|
28
|
+
xMin = u.scales.x.min;
|
|
29
|
+
xMax = u.scales.x.max;
|
|
30
|
+
yMin = u.scales.y.min;
|
|
31
|
+
yMax = u.scales.y.max;
|
|
32
|
+
|
|
33
|
+
xRange = xMax - xMin;
|
|
34
|
+
yRange = yMax - yMin;
|
|
35
|
+
|
|
36
|
+
const plot = u.root.querySelector('.u-over');
|
|
37
|
+
const rect = plot.getBoundingClientRect();
|
|
38
|
+
|
|
39
|
+
// wheel drag pan
|
|
40
|
+
plot.addEventListener('mousedown', e => {
|
|
41
|
+
if (e.button == 1) {
|
|
42
|
+
// plot.style.cursor = "move";
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
|
|
45
|
+
const left0 = e.clientX;
|
|
46
|
+
// let top0 = e.clientY;
|
|
47
|
+
|
|
48
|
+
const scXMin0 = u.scales.x.min;
|
|
49
|
+
const scXMax0 = u.scales.x.max;
|
|
50
|
+
|
|
51
|
+
const xUnitsPerPx = u.posToVal(1, 'x') - u.posToVal(0, 'x');
|
|
52
|
+
|
|
53
|
+
function onmove(e) {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
|
|
56
|
+
const left1 = e.clientX;
|
|
57
|
+
// let top1 = e.clientY;
|
|
58
|
+
|
|
59
|
+
const dx = xUnitsPerPx * (left1 - left0);
|
|
60
|
+
|
|
61
|
+
u.setScale('x', {
|
|
62
|
+
min: scXMin0 - dx,
|
|
63
|
+
max: scXMax0 - dx,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onup(e) {
|
|
68
|
+
document.removeEventListener('mousemove', onmove);
|
|
69
|
+
document.removeEventListener('mouseup', onup);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
document.addEventListener('mousemove', onmove);
|
|
73
|
+
document.addEventListener('mouseup', onup);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// wheel scroll zoom
|
|
78
|
+
plot.addEventListener('wheel', e => {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
|
|
81
|
+
const { left, top } = u.cursor;
|
|
82
|
+
|
|
83
|
+
const leftPct = left / rect.width;
|
|
84
|
+
const btmPct = 1 - top / rect.height;
|
|
85
|
+
const xVal = u.posToVal(left, 'x');
|
|
86
|
+
const yVal = u.posToVal(top, 'y');
|
|
87
|
+
const oxRange = u.scales.x.max - u.scales.x.min;
|
|
88
|
+
const oyRange = u.scales.y.max - u.scales.y.min;
|
|
89
|
+
|
|
90
|
+
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
|
91
|
+
let nxMin = xVal - leftPct * nxRange;
|
|
92
|
+
let nxMax = nxMin + nxRange;
|
|
93
|
+
[nxMin, nxMax] = clamp(nxRange, nxMin, nxMax, xRange, xMin, xMax);
|
|
94
|
+
|
|
95
|
+
const nyRange = e.deltaY < 0 ? oyRange * factor : oyRange / factor;
|
|
96
|
+
let nyMin = yVal - btmPct * nyRange;
|
|
97
|
+
let nyMax = nyMin + nyRange;
|
|
98
|
+
[nyMin, nyMax] = clamp(nyRange, nyMin, nyMax, yRange, yMin, yMax);
|
|
99
|
+
|
|
100
|
+
u.batch(() => {
|
|
101
|
+
u.setScale('x', {
|
|
102
|
+
min: nxMin,
|
|
103
|
+
max: nxMax,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
u.setScale('y', {
|
|
107
|
+
min: nyMin,
|
|
108
|
+
max: nyMax,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables';
|
|
2
|
+
|
|
3
|
+
/* stylelint-disable-next-line selector-pseudo-class-no-unknown*/
|
|
4
|
+
:global {
|
|
5
|
+
.interactive-chart-wrapper {
|
|
6
|
+
& .uplot {
|
|
7
|
+
font-family: inherit;
|
|
8
|
+
background-color: styles-theme-variables.$sys-neutral-background1-level;
|
|
9
|
+
padding: 16px 0;
|
|
10
|
+
border-radius: 8px;
|
|
11
|
+
color: styles-theme-variables.$sys-neutral-text-main;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
& .uplot .u-title {
|
|
15
|
+
font-size: 20px;
|
|
16
|
+
text-align: unset;
|
|
17
|
+
padding-left: 24px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* mouse cross hair */
|
|
21
|
+
& .u-hz .u-cursor-x,
|
|
22
|
+
& .u-vt .u-cursor-y,
|
|
23
|
+
& .u-hz .u-cursor-y,
|
|
24
|
+
& .u-vt .u-cursor-x {
|
|
25
|
+
border-color: styles-theme-variables.$sys-neutral-decor-default;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Selected area */
|
|
29
|
+
& .u-select {
|
|
30
|
+
background-color: styles-theme-variables.$sys-neutral-decor-disabled;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
& table .u-series .u-marker {
|
|
34
|
+
width: 16px;
|
|
35
|
+
height: 16px;
|
|
36
|
+
border-radius: 16px;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ValueOf } from '@snack-uikit/utils';
|
|
2
|
+
|
|
3
|
+
import { DRAW_STYLES, LINE_INTERPOLATIONS, PLOT_TYPES } from './constants';
|
|
4
|
+
|
|
5
|
+
export type PlotType = ValueOf<typeof PLOT_TYPES>;
|
|
6
|
+
export type LineInterpolation = ValueOf<typeof LINE_INTERPOLATIONS>;
|
|
7
|
+
export type DrawStyle = ValueOf<typeof DRAW_STYLES>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Fragment, MouseEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Divider } from '@snack-uikit/divider';
|
|
4
|
+
import { Link } from '@snack-uikit/link';
|
|
5
|
+
import { Typography } from '@snack-uikit/typography';
|
|
6
|
+
|
|
7
|
+
import { TextLike } from '../types';
|
|
8
|
+
import styles from './styles.module.scss';
|
|
9
|
+
|
|
10
|
+
type LegendItem = {
|
|
11
|
+
label: TextLike;
|
|
12
|
+
value: TextLike;
|
|
13
|
+
color?: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type LegendItemProps = LegendItem & {
|
|
18
|
+
size: 's' | 'm' | 'l';
|
|
19
|
+
onItemClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function LegendItem({ color, label, value, size, onItemClick }: LegendItemProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div className={styles.legendItemWrapper}>
|
|
25
|
+
<span className={styles.legendItemTitle}>
|
|
26
|
+
{color && <span className={styles.dot} style={{ '--color': color }} />}
|
|
27
|
+
<Link onClick={onItemClick} text={String(label)} size={size} />
|
|
28
|
+
</span>
|
|
29
|
+
|
|
30
|
+
<span className={styles.legendValue}>{value}</span>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type LegendProps = {
|
|
36
|
+
data: Array<LegendItem>;
|
|
37
|
+
typographySize: 's' | 'm' | 'l';
|
|
38
|
+
legendTitle?: string;
|
|
39
|
+
onItemClick?: (event: MouseEvent<HTMLAnchorElement>, data: LegendItem) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function Legend({ data, legendTitle, typographySize, onItemClick }: LegendProps) {
|
|
43
|
+
return (
|
|
44
|
+
<div className={styles.legend}>
|
|
45
|
+
{legendTitle && (
|
|
46
|
+
<>
|
|
47
|
+
<Typography purpose={'label'} family={'sans'} size={typographySize}>
|
|
48
|
+
{legendTitle}
|
|
49
|
+
</Typography>
|
|
50
|
+
<div className={styles.legendDividerWrapper}>
|
|
51
|
+
<Divider />
|
|
52
|
+
</div>
|
|
53
|
+
</>
|
|
54
|
+
)}
|
|
55
|
+
{data.map((item, index) => (
|
|
56
|
+
<Fragment key={`legend_${item.label}_${index}`}>
|
|
57
|
+
<LegendItem
|
|
58
|
+
{...item}
|
|
59
|
+
size={typographySize}
|
|
60
|
+
onItemClick={onItemClick ? event => onItemClick(event, item) : undefined}
|
|
61
|
+
/>
|
|
62
|
+
{index !== data.length - 1 && (
|
|
63
|
+
<div className={styles.legendDividerWrapper}>
|
|
64
|
+
<Divider />
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</Fragment>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Legend';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables';
|
|
2
|
+
|
|
3
|
+
.legend {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
color: styles-theme-variables.$sys-neutral-text-support;
|
|
7
|
+
padding-right: 8px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.legendItemWrapper {
|
|
11
|
+
color: styles-theme-variables.$sys-neutral-text-main;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
flex-direction: row;
|
|
15
|
+
gap: 8px;
|
|
16
|
+
justify-content: space-between;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.dot {
|
|
20
|
+
display: inline-block;
|
|
21
|
+
min-width: 8px;
|
|
22
|
+
min-height: 8px;
|
|
23
|
+
width: 8px;
|
|
24
|
+
height: 8px;
|
|
25
|
+
border-radius: 100%;
|
|
26
|
+
|
|
27
|
+
margin-right: 12px;
|
|
28
|
+
background-color: var(--color);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.legendDividerWrapper {
|
|
32
|
+
margin: 16px 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.legendItemTitle {
|
|
36
|
+
@include styles-theme-variables.composite-var(styles-theme-variables.$sans-body-m);
|
|
37
|
+
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.legendValue {
|
|
43
|
+
@include styles-theme-variables.composite-var(styles-theme-variables.$sans-label-l);
|
|
44
|
+
}
|
|
45
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { arc, pie, PieArcDatum } from 'd3-shape';
|
|
2
|
+
import { CSSProperties, Fragment, MouseEvent } from 'react';
|
|
3
|
+
|
|
4
|
+
import { DataType, LabelRenderFunction } from './types';
|
|
5
|
+
|
|
6
|
+
type PieProps = {
|
|
7
|
+
data: DataType[];
|
|
8
|
+
label: LabelRenderFunction<DataType>;
|
|
9
|
+
onMouseOut: () => void;
|
|
10
|
+
onMouseOver: (event: MouseEvent<SVGPathElement>, dataIndex: number) => void;
|
|
11
|
+
onMouseDown: (event: MouseEvent<SVGPathElement>, dataIndex: number) => void;
|
|
12
|
+
radius: number;
|
|
13
|
+
innerRadius: number;
|
|
14
|
+
segmentsShift: number;
|
|
15
|
+
segmentsStyle: CSSProperties;
|
|
16
|
+
hoveredIndex?: number;
|
|
17
|
+
style: CSSProperties;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function Pie({
|
|
21
|
+
data,
|
|
22
|
+
hoveredIndex,
|
|
23
|
+
radius,
|
|
24
|
+
innerRadius,
|
|
25
|
+
segmentsShift,
|
|
26
|
+
onMouseOut,
|
|
27
|
+
label,
|
|
28
|
+
style,
|
|
29
|
+
segmentsStyle,
|
|
30
|
+
onMouseOver,
|
|
31
|
+
onMouseDown,
|
|
32
|
+
}: PieProps) {
|
|
33
|
+
const pieSegments = pie<DataType>()
|
|
34
|
+
.sort(null)
|
|
35
|
+
.value((d: DataType) => d.value)(data);
|
|
36
|
+
|
|
37
|
+
const getHoveredPath = arc<PieArcDatum<DataType>>()
|
|
38
|
+
.outerRadius(radius + 1)
|
|
39
|
+
.innerRadius(innerRadius + 1)
|
|
40
|
+
.startAngle(d => d.startAngle + Math.PI / 2)
|
|
41
|
+
.endAngle(d => d.endAngle + Math.PI / 2)
|
|
42
|
+
.padAngle(segmentsShift);
|
|
43
|
+
|
|
44
|
+
const getPath = arc<PieArcDatum<DataType>>()
|
|
45
|
+
.outerRadius(radius)
|
|
46
|
+
.innerRadius(innerRadius)
|
|
47
|
+
.startAngle(d => d.startAngle + Math.PI / 2)
|
|
48
|
+
.endAngle(d => d.endAngle + Math.PI / 2)
|
|
49
|
+
.padAngle(segmentsShift);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<svg viewBox='0 0 100 100' width='100%' height='100%' style={style}>
|
|
53
|
+
<g transform='translate(50,50)'>
|
|
54
|
+
{pieSegments.map((segment, index) => (
|
|
55
|
+
<Fragment key={index}>
|
|
56
|
+
<path
|
|
57
|
+
onMouseOver={e => onMouseOver(e, index)}
|
|
58
|
+
onMouseOut={onMouseOut}
|
|
59
|
+
onMouseDown={e => onMouseDown(e, index)}
|
|
60
|
+
fill={segment.data.color}
|
|
61
|
+
d={hoveredIndex === index ? String(getHoveredPath(segment)) : String(getPath(segment))}
|
|
62
|
+
style={segmentsStyle}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
{label({ dataEntry: segment.data, dataIndex: index })}
|
|
66
|
+
</Fragment>
|
|
67
|
+
))}
|
|
68
|
+
</g>
|
|
69
|
+
</svg>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { truncateString } from '@cloud-ru/ft-formatters';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import { MouseEvent, useCallback, useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { extractSupportProps, WithSupportProps } from '@cloud-ru/uikit-product-utils';
|
|
6
|
+
import { Scroll } from '@snack-uikit/scroll';
|
|
7
|
+
import { Typography } from '@snack-uikit/typography';
|
|
8
|
+
|
|
9
|
+
import { CHART_SERIES_COLORS, SERIES_COLORS, SeriesColor } from '../../constants/colors';
|
|
10
|
+
import { Legend } from './Legend';
|
|
11
|
+
import { Pie } from './Pie';
|
|
12
|
+
import styles from './styles.module.scss';
|
|
13
|
+
import { DataType, LabelRenderFunction, LegendType, PieChartProps } from './types';
|
|
14
|
+
|
|
15
|
+
export function PieChart({
|
|
16
|
+
options: { width, height, title, legendTitle, typographySize = 'l' },
|
|
17
|
+
data,
|
|
18
|
+
aggregatedLegend,
|
|
19
|
+
onPieSegmentClick,
|
|
20
|
+
onLegendItemClick,
|
|
21
|
+
className,
|
|
22
|
+
...rest
|
|
23
|
+
}: WithSupportProps<PieChartProps>) {
|
|
24
|
+
const [hovered, setHovered] = useState<number | undefined>(undefined);
|
|
25
|
+
const colorizedData: DataType[] = useMemo(
|
|
26
|
+
() =>
|
|
27
|
+
data.map((x, index) => {
|
|
28
|
+
const colorsKey = Object.values<SeriesColor>(SERIES_COLORS);
|
|
29
|
+
const chartColor = CHART_SERIES_COLORS[colorsKey[index % colorsKey.length]];
|
|
30
|
+
return {
|
|
31
|
+
...x,
|
|
32
|
+
color: x.color || chartColor,
|
|
33
|
+
};
|
|
34
|
+
}),
|
|
35
|
+
[data],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const pieStyles = useMemo(() => ({ overflow: 'overlay' }), []);
|
|
39
|
+
const segmentStyles = useMemo(() => ({ transition: 'all .3s', cursor: 'pointer' }), []);
|
|
40
|
+
const onMouseOverCallback = useCallback((_: unknown, index: number) => setHovered(index), []);
|
|
41
|
+
const onMouseOutCallback = useCallback(() => setHovered(undefined), []);
|
|
42
|
+
const onMouseDownCallback = useCallback(
|
|
43
|
+
(event: MouseEvent<SVGPathElement>, index: number) => {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
|
|
46
|
+
if (onPieSegmentClick) {
|
|
47
|
+
onPieSegmentClick(data[index]);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
[data, onPieSegmentClick],
|
|
51
|
+
);
|
|
52
|
+
const onColorizedLegendItemClick = useMemo(() => {
|
|
53
|
+
if (!onLegendItemClick) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (event: MouseEvent<HTMLAnchorElement>, item: LegendType) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
onLegendItemClick(item);
|
|
60
|
+
};
|
|
61
|
+
}, [onLegendItemClick]);
|
|
62
|
+
const onAggregatedItemClick = useMemo(() => {
|
|
63
|
+
const handleClick = aggregatedLegend?.onAggregatedLegendItemClick;
|
|
64
|
+
if (!handleClick) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (event: MouseEvent<HTMLAnchorElement>, item: LegendType) => {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
handleClick(item);
|
|
71
|
+
};
|
|
72
|
+
}, [aggregatedLegend]);
|
|
73
|
+
|
|
74
|
+
const labelRenderer = useCallback<LabelRenderFunction<DataType>>(
|
|
75
|
+
({ dataEntry, dataIndex }) => (
|
|
76
|
+
<>
|
|
77
|
+
<text
|
|
78
|
+
className={styles.svgText}
|
|
79
|
+
x={0}
|
|
80
|
+
y={-4}
|
|
81
|
+
data-hovered={hovered === dataIndex || undefined}
|
|
82
|
+
key={`${dataIndex}_label`}
|
|
83
|
+
>
|
|
84
|
+
{truncateString(String(dataEntry.label), 15)}
|
|
85
|
+
</text>
|
|
86
|
+
<text
|
|
87
|
+
className={styles.svgText}
|
|
88
|
+
x={0}
|
|
89
|
+
y={4}
|
|
90
|
+
data-hovered={hovered === dataIndex || undefined}
|
|
91
|
+
data-bolder='true'
|
|
92
|
+
key={`${dataIndex}_value`}
|
|
93
|
+
>
|
|
94
|
+
{dataEntry.value}
|
|
95
|
+
</text>
|
|
96
|
+
</>
|
|
97
|
+
),
|
|
98
|
+
[hovered],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
{...extractSupportProps(rest)}
|
|
104
|
+
className={cn(className, styles.wrapper)}
|
|
105
|
+
style={{ '--width': width ? `${width}px` : undefined, '--height': height ? `${height}px` : undefined }}
|
|
106
|
+
>
|
|
107
|
+
<Typography purpose={'title'} family={'sans'} size={typographySize} className={styles.title}>
|
|
108
|
+
{title}
|
|
109
|
+
</Typography>
|
|
110
|
+
|
|
111
|
+
<div className={styles.contentWrapper}>
|
|
112
|
+
<div className={styles.legendWrapper}>
|
|
113
|
+
<Scroll size={'s'}>
|
|
114
|
+
<Legend
|
|
115
|
+
data={colorizedData}
|
|
116
|
+
legendTitle={legendTitle}
|
|
117
|
+
onItemClick={onColorizedLegendItemClick}
|
|
118
|
+
typographySize={typographySize}
|
|
119
|
+
/>
|
|
120
|
+
</Scroll>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className={styles.pieWrapper}>
|
|
124
|
+
<Pie
|
|
125
|
+
style={pieStyles}
|
|
126
|
+
radius={46}
|
|
127
|
+
innerRadius={23}
|
|
128
|
+
label={labelRenderer}
|
|
129
|
+
data={colorizedData}
|
|
130
|
+
segmentsStyle={segmentStyles}
|
|
131
|
+
segmentsShift={0.015}
|
|
132
|
+
hoveredIndex={hovered}
|
|
133
|
+
onMouseOver={onMouseOverCallback}
|
|
134
|
+
onMouseOut={onMouseOutCallback}
|
|
135
|
+
onMouseDown={onMouseDownCallback}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{aggregatedLegend && (
|
|
140
|
+
<div className={styles.legendWrapper}>
|
|
141
|
+
<Scroll size={'s'}>
|
|
142
|
+
<Legend
|
|
143
|
+
data={aggregatedLegend.data}
|
|
144
|
+
legendTitle={aggregatedLegend.title}
|
|
145
|
+
onItemClick={onAggregatedItemClick}
|
|
146
|
+
typographySize={typographySize}
|
|
147
|
+
/>
|
|
148
|
+
</Scroll>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables';
|
|
2
|
+
|
|
3
|
+
.wrapper {
|
|
4
|
+
display: flex;
|
|
5
|
+
width: var(--width, 100%);
|
|
6
|
+
height: var(--height, 100%);
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
background-color: styles-theme-variables.$sys-neutral-background1-level;
|
|
10
|
+
border-radius: 8px;
|
|
11
|
+
padding: 24px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.title {
|
|
15
|
+
padding-bottom: 24px;
|
|
16
|
+
color: styles-theme-variables.$sys-neutral-text-main;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.contentWrapper {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: row;
|
|
22
|
+
gap: 24px;
|
|
23
|
+
height: calc(100% - 50px);
|
|
24
|
+
justify-content: space-between;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.legendWrapper {
|
|
28
|
+
min-width: 25%;
|
|
29
|
+
max-width: 33%;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.pieWrapper {
|
|
33
|
+
height: 100%;
|
|
34
|
+
text-align: center;
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.svgText {
|
|
41
|
+
text-anchor: middle;
|
|
42
|
+
visibility: hidden;
|
|
43
|
+
width: 30px;
|
|
44
|
+
font-size: 5px;
|
|
45
|
+
word-break: break-all;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
fill: styles-theme-variables.$sys-neutral-text-main;
|
|
48
|
+
|
|
49
|
+
&[data-hovered] {
|
|
50
|
+
visibility: visible;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&[data-bolder] {
|
|
54
|
+
font-weight: 700;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type TextLike = string | number;
|
|
4
|
+
|
|
5
|
+
export type LegendType = {
|
|
6
|
+
label: TextLike;
|
|
7
|
+
value: TextLike;
|
|
8
|
+
id?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type DataType = {
|
|
12
|
+
label: TextLike;
|
|
13
|
+
value: number;
|
|
14
|
+
id?: string;
|
|
15
|
+
color?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type PieChartProps = {
|
|
19
|
+
data: DataType[];
|
|
20
|
+
options: {
|
|
21
|
+
title: string;
|
|
22
|
+
width?: number;
|
|
23
|
+
height?: number;
|
|
24
|
+
legendTitle?: string;
|
|
25
|
+
typographySize?: 's' | 'm' | 'l';
|
|
26
|
+
};
|
|
27
|
+
onPieSegmentClick?: (data: DataType) => void;
|
|
28
|
+
onLegendItemClick?: (data: LegendType) => void;
|
|
29
|
+
aggregatedLegend?: {
|
|
30
|
+
data: LegendType[];
|
|
31
|
+
title: string;
|
|
32
|
+
onAggregatedLegendItemClick?: (data: LegendType) => void;
|
|
33
|
+
};
|
|
34
|
+
className?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type LabelRenderProps<DataType> = {
|
|
38
|
+
dataEntry: DataType;
|
|
39
|
+
dataIndex: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type LabelRenderFunction<DataType> = (labelRenderProps: LabelRenderProps<DataType>) => ReactNode;
|