@gravity-ui/charts 1.47.0 → 1.48.1
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/AxisX/prepare-axis-data.js +9 -6
- package/dist/cjs/components/AxisY/prepare-axis-data.js +11 -4
- package/dist/cjs/core/axes/types.d.ts +4 -2
- package/dist/cjs/core/axes/x-axis.js +2 -0
- package/dist/cjs/core/axes/y-axis.js +2 -0
- package/dist/cjs/core/series/prepare-scatter.js +11 -3
- package/dist/cjs/core/series/types.d.ts +8 -0
- package/dist/cjs/core/shapes/area/prepare-data.js +2 -49
- package/dist/cjs/core/shapes/bar-x/prepare-data.js +49 -35
- package/dist/cjs/core/shapes/line/prepare-data.js +13 -58
- package/dist/cjs/core/shapes/scatter/prepare-data.d.ts +5 -2
- package/dist/cjs/core/shapes/scatter/prepare-data.js +43 -4
- package/dist/cjs/core/shapes/scatter/renderer.d.ts +2 -2
- package/dist/cjs/core/shapes/scatter/renderer.js +9 -1
- package/dist/cjs/core/shapes/scatter/types.d.ts +6 -1
- package/dist/cjs/core/types/chart/axis.d.ts +20 -0
- package/dist/cjs/core/types/chart/scatter.d.ts +2 -0
- package/dist/cjs/core/utils/data-labels.d.ts +46 -0
- package/dist/cjs/core/utils/data-labels.js +64 -0
- package/dist/cjs/core/utils/index.d.ts +1 -0
- package/dist/cjs/core/utils/index.js +1 -0
- package/dist/cjs/core/utils/ticks/datetime.js +7 -0
- package/dist/cjs/hooks/useShapes/index.js +5 -3
- package/dist/cjs/hooks/useShapes/scatter/index.d.ts +2 -2
- package/dist/cjs/hooks/useShapes/scatter/index.js +4 -1
- package/dist/cjs/hooks/useShapes/styles.css +8 -25
- package/dist/esm/components/AxisX/prepare-axis-data.js +9 -6
- package/dist/esm/components/AxisY/prepare-axis-data.js +11 -4
- package/dist/esm/core/axes/types.d.ts +4 -2
- package/dist/esm/core/axes/x-axis.js +2 -0
- package/dist/esm/core/axes/y-axis.js +2 -0
- package/dist/esm/core/series/prepare-scatter.js +11 -3
- package/dist/esm/core/series/types.d.ts +8 -0
- package/dist/esm/core/shapes/area/prepare-data.js +2 -49
- package/dist/esm/core/shapes/bar-x/prepare-data.js +49 -35
- package/dist/esm/core/shapes/line/prepare-data.js +13 -58
- package/dist/esm/core/shapes/scatter/prepare-data.d.ts +5 -2
- package/dist/esm/core/shapes/scatter/prepare-data.js +43 -4
- package/dist/esm/core/shapes/scatter/renderer.d.ts +2 -2
- package/dist/esm/core/shapes/scatter/renderer.js +9 -1
- package/dist/esm/core/shapes/scatter/types.d.ts +6 -1
- package/dist/esm/core/types/chart/axis.d.ts +20 -0
- package/dist/esm/core/types/chart/scatter.d.ts +2 -0
- package/dist/esm/core/utils/data-labels.d.ts +46 -0
- package/dist/esm/core/utils/data-labels.js +64 -0
- package/dist/esm/core/utils/index.d.ts +1 -0
- package/dist/esm/core/utils/index.js +1 -0
- package/dist/esm/core/utils/ticks/datetime.js +7 -0
- package/dist/esm/hooks/useShapes/index.js +5 -3
- package/dist/esm/hooks/useShapes/scatter/index.d.ts +2 -2
- package/dist/esm/hooks/useShapes/scatter/index.js +4 -1
- package/dist/esm/hooks/useShapes/styles.css +8 -25
- package/package.json +4 -1
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
import { prepareAnnotation } from '../../series/prepare-annotation';
|
|
2
|
-
import { filterOverlappingLabels,
|
|
3
|
-
import { getFormattedValue } from '../../utils/format';
|
|
2
|
+
import { filterOverlappingLabels, preparePointDataLabels } from '../../utils';
|
|
4
3
|
import { getXValue, getYValue, markHiddenPointsOutOfYRange } from '../utils';
|
|
5
|
-
async function getHtmlLabel(point, series, xMax) {
|
|
6
|
-
var _a;
|
|
7
|
-
const content = String((_a = point.data.label) !== null && _a !== void 0 ? _a : point.data.y);
|
|
8
|
-
const size = await getLabelsSize({ labels: [content], html: true });
|
|
9
|
-
const width = size.maxWidth;
|
|
10
|
-
return {
|
|
11
|
-
x: Math.min(xMax - size.maxWidth, Math.max(0, point.x - width / 2)),
|
|
12
|
-
y: Math.max(0, point.y - series.dataLabels.padding - size.maxHeight),
|
|
13
|
-
content,
|
|
14
|
-
size: { width, height: size.maxHeight },
|
|
15
|
-
style: series.dataLabels.style,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
4
|
export const prepareLineData = async (args) => {
|
|
19
|
-
var _a, _b, _c, _d, _e, _f
|
|
5
|
+
var _a, _b, _c, _d, _e, _f;
|
|
20
6
|
const { series, seriesOptions, xAxis, yAxis, xScale, yScale, split, isOutsideBounds, isRangeSlider, otherLayers, } = args;
|
|
21
7
|
const [_xMin, xRangeMax] = xScale.range();
|
|
22
8
|
const xMax = xRangeMax;
|
|
@@ -58,46 +44,15 @@ export const prepareLineData = async (args) => {
|
|
|
58
44
|
let htmlElements = [];
|
|
59
45
|
let svgLabels = [];
|
|
60
46
|
if (s.dataLabels.enabled && !isRangeSlider) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
const getTextSize = getTextSizeFn({ style: s.dataLabels.style });
|
|
73
|
-
for (let index = 0; index < points.length; index++) {
|
|
74
|
-
const point = points[index];
|
|
75
|
-
if (point.y !== null &&
|
|
76
|
-
point.x !== null &&
|
|
77
|
-
!isOutsideBounds(point.x, point.y)) {
|
|
78
|
-
const labelValue = (_e = point.data.label) !== null && _e !== void 0 ? _e : point.data.y;
|
|
79
|
-
const text = getFormattedValue(Object.assign({ value: labelValue }, s.dataLabels));
|
|
80
|
-
const labelSize = await getTextSize(text);
|
|
81
|
-
const style = s.dataLabels.style;
|
|
82
|
-
const y = Math.max(yAxisTop, point.y -
|
|
83
|
-
s.dataLabels.padding -
|
|
84
|
-
labelSize.height +
|
|
85
|
-
labelSize.hangingOffset);
|
|
86
|
-
const x = Math.min(xMax - labelSize.width, Math.max(0, point.x - labelSize.width / 2));
|
|
87
|
-
const labelData = {
|
|
88
|
-
text,
|
|
89
|
-
x,
|
|
90
|
-
y,
|
|
91
|
-
style,
|
|
92
|
-
size: labelSize,
|
|
93
|
-
textAnchor: 'start',
|
|
94
|
-
series: s,
|
|
95
|
-
active: true,
|
|
96
|
-
};
|
|
97
|
-
svgLabels.push(labelData);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
47
|
+
const labelsData = await preparePointDataLabels({
|
|
48
|
+
series: s,
|
|
49
|
+
points,
|
|
50
|
+
xMax,
|
|
51
|
+
yAxisTop,
|
|
52
|
+
isOutsideBounds,
|
|
53
|
+
});
|
|
54
|
+
svgLabels = labelsData.svgLabels;
|
|
55
|
+
htmlElements = labelsData.htmlLabels;
|
|
101
56
|
}
|
|
102
57
|
if (!s.dataLabels.allowOverlap) {
|
|
103
58
|
svgLabels = filterOverlappingLabels(svgLabels, otherLayers.map((l) => l.svgLabels).flat());
|
|
@@ -148,11 +103,11 @@ export const prepareLineData = async (args) => {
|
|
|
148
103
|
id: s.id,
|
|
149
104
|
htmlLabels: htmlElements,
|
|
150
105
|
color: s.color,
|
|
151
|
-
lineWidth: (
|
|
106
|
+
lineWidth: (_e = (isRangeSlider ? s.rangeSlider.lineWidth : undefined)) !== null && _e !== void 0 ? _e : s.lineWidth,
|
|
152
107
|
dashStyle: s.dashStyle,
|
|
153
108
|
linecap: s.linecap,
|
|
154
109
|
linejoin: s.linejoin,
|
|
155
|
-
opacity: (
|
|
110
|
+
opacity: (_f = (isRangeSlider ? s.rangeSlider.opacity : undefined)) !== null && _f !== void 0 ? _f : s.opacity,
|
|
156
111
|
};
|
|
157
112
|
acc.push(result);
|
|
158
113
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { PreparedXAxis, PreparedYAxis } from '../../axes/types';
|
|
2
|
+
import type { PreparedSplit } from '../../layout/split-types';
|
|
2
3
|
import type { ChartScale } from '../../scales/types';
|
|
3
4
|
import type { PreparedScatterSeries } from '../../series/types';
|
|
4
|
-
import type {
|
|
5
|
+
import type { PreparedScatterShapeData } from './types';
|
|
5
6
|
export declare function prepareScatterData(args: {
|
|
6
7
|
series: PreparedScatterSeries[];
|
|
7
8
|
xAxis: PreparedXAxis;
|
|
8
9
|
xScale: ChartScale;
|
|
9
10
|
yAxis: PreparedYAxis[];
|
|
10
11
|
yScale: (ChartScale | undefined)[];
|
|
12
|
+
split: PreparedSplit;
|
|
11
13
|
isOutsideBounds: (x: number, y: number) => boolean;
|
|
12
|
-
|
|
14
|
+
isRangeSlider?: boolean;
|
|
15
|
+
}): Promise<PreparedScatterShapeData>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import get from 'lodash/get';
|
|
2
2
|
import { getXValue, getYValue } from '../../shapes/utils';
|
|
3
|
-
import { getDataCategoryValue } from '../../utils';
|
|
3
|
+
import { filterOverlappingLabels, getDataCategoryValue, preparePointDataLabels } from '../../utils';
|
|
4
4
|
function getFilteredLinearScatterData(data) {
|
|
5
5
|
return data.filter((d) => typeof d.x === 'number' && typeof d.y === 'number');
|
|
6
6
|
}
|
|
@@ -32,9 +32,12 @@ function getFilteredCategoryScatterData(args) {
|
|
|
32
32
|
return xInRange && yInRange;
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
export function prepareScatterData(args) {
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
export async function prepareScatterData(args) {
|
|
36
|
+
var _a;
|
|
37
|
+
const { series, xAxis, xScale, yAxis, yScale, split, isOutsideBounds, isRangeSlider } = args;
|
|
38
|
+
const [_xMin, xRangeMax] = xScale.range();
|
|
39
|
+
const xMax = xRangeMax;
|
|
40
|
+
const markers = series.reduce((acc, s) => {
|
|
38
41
|
const yAxisIndex = get(s, 'yAxis', 0);
|
|
39
42
|
const seriesYAxis = yAxis[yAxisIndex];
|
|
40
43
|
const seriesYScale = yScale[yAxisIndex];
|
|
@@ -74,4 +77,40 @@ export function prepareScatterData(args) {
|
|
|
74
77
|
});
|
|
75
78
|
return acc;
|
|
76
79
|
}, []);
|
|
80
|
+
const allSvgLabels = [];
|
|
81
|
+
const allHtmlLabels = [];
|
|
82
|
+
if (!isRangeSlider) {
|
|
83
|
+
for (const s of series) {
|
|
84
|
+
if (!s.dataLabels.enabled) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const yAxisIndex = get(s, 'yAxis', 0);
|
|
88
|
+
const seriesYAxis = yAxis[yAxisIndex];
|
|
89
|
+
const seriesYScale = yScale[yAxisIndex];
|
|
90
|
+
if (!seriesYScale) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const yAxisTop = ((_a = split.plots[seriesYAxis.plotIndex]) === null || _a === void 0 ? void 0 : _a.top) || 0;
|
|
94
|
+
const seriesPoints = markers
|
|
95
|
+
.filter((m) => m.point.series.id === s.id && !m.clipped)
|
|
96
|
+
.map((m) => m.point);
|
|
97
|
+
const { svgLabels, htmlLabels } = await preparePointDataLabels({
|
|
98
|
+
series: s,
|
|
99
|
+
points: seriesPoints,
|
|
100
|
+
xMax,
|
|
101
|
+
yAxisTop,
|
|
102
|
+
isOutsideBounds,
|
|
103
|
+
anchorYOffset: s.marker.states.normal.radius,
|
|
104
|
+
});
|
|
105
|
+
if (s.dataLabels.allowOverlap) {
|
|
106
|
+
allSvgLabels.push(...svgLabels);
|
|
107
|
+
allHtmlLabels.push(...htmlLabels);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
allSvgLabels.push(...filterOverlappingLabels(svgLabels, allSvgLabels));
|
|
111
|
+
allHtmlLabels.push(...filterOverlappingLabels(htmlLabels, allHtmlLabels));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { markers, svgLabels: allSvgLabels, htmlLabels: allHtmlLabels };
|
|
77
116
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Dispatch } from 'd3-dispatch';
|
|
2
2
|
import type { PreparedSeriesOptions } from '../../series/types';
|
|
3
|
-
import type {
|
|
3
|
+
import type { PreparedScatterShapeData } from './types';
|
|
4
4
|
export declare function renderScatter(elements: {
|
|
5
5
|
plot: SVGGElement;
|
|
6
|
-
}, preparedData:
|
|
6
|
+
}, preparedData: PreparedScatterShapeData, seriesOptions: PreparedSeriesOptions, dispatcher?: Dispatch<object>): () => void;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { select } from 'd3-selection';
|
|
2
2
|
import get from 'lodash/get';
|
|
3
|
+
import { block } from '../../../utils';
|
|
3
4
|
import { getMarkerHaloVisibility, renderMarker, selectMarkerHalo, selectMarkerSymbol, setMarker, } from '../../shapes/marker';
|
|
4
5
|
import { setActiveState, shapeKey } from '../../shapes/utils';
|
|
6
|
+
import { renderDataLabels } from '../data-labels';
|
|
7
|
+
const b = block('scatter');
|
|
5
8
|
export function renderScatter(elements, preparedData, seriesOptions, dispatcher) {
|
|
6
9
|
const svgElement = select(elements.plot);
|
|
7
10
|
const hoverOptions = get(seriesOptions, 'scatter.states.hover');
|
|
@@ -9,11 +12,16 @@ export function renderScatter(elements, preparedData, seriesOptions, dispatcher)
|
|
|
9
12
|
svgElement.selectAll('*').remove();
|
|
10
13
|
const selection = svgElement
|
|
11
14
|
.selectAll('path')
|
|
12
|
-
.data(preparedData, shapeKey)
|
|
15
|
+
.data(preparedData.markers, shapeKey)
|
|
13
16
|
.join('g')
|
|
14
17
|
.call(renderMarker)
|
|
15
18
|
.attr('opacity', (d) => d.point.opacity)
|
|
16
19
|
.attr('cursor', (d) => d.point.series.cursor);
|
|
20
|
+
renderDataLabels({
|
|
21
|
+
container: svgElement,
|
|
22
|
+
data: preparedData.svgLabels,
|
|
23
|
+
className: b('label'),
|
|
24
|
+
});
|
|
17
25
|
const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
|
|
18
26
|
const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
|
|
19
27
|
function handleShapeHover(data) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HtmlItem, ScatterSeriesData } from '../../../types';
|
|
1
|
+
import type { HtmlItem, LabelData, ScatterSeriesData } from '../../../types';
|
|
2
2
|
import type { PreparedScatterSeries } from '../../series/types';
|
|
3
3
|
type PointData = {
|
|
4
4
|
x: number;
|
|
@@ -16,4 +16,9 @@ export type MarkerData = {
|
|
|
16
16
|
clipped: boolean;
|
|
17
17
|
};
|
|
18
18
|
export type PreparedScatterData = MarkerData;
|
|
19
|
+
export type PreparedScatterShapeData = {
|
|
20
|
+
markers: PreparedScatterData[];
|
|
21
|
+
svgLabels: LabelData[];
|
|
22
|
+
htmlLabels: HtmlItem[];
|
|
23
|
+
};
|
|
19
24
|
export {};
|
|
@@ -317,6 +317,7 @@ export interface AxisPlotShape extends AxisPlot {
|
|
|
317
317
|
plotHeight: number;
|
|
318
318
|
}) => string;
|
|
319
319
|
}
|
|
320
|
+
export type PlotBandAlign = 'start' | 'end';
|
|
320
321
|
export interface AxisPlotBand extends AxisPlot {
|
|
321
322
|
/**
|
|
322
323
|
* The start position of the plot band in axis units.
|
|
@@ -336,6 +337,25 @@ export interface AxisPlotBand extends AxisPlot {
|
|
|
336
337
|
* If the value is `Infinity` or `null`, it will be treated as the end of the axis.
|
|
337
338
|
*/
|
|
338
339
|
to: number | string | null;
|
|
340
|
+
/**
|
|
341
|
+
* Anchor side on the perpendicular axis when `size` is set.
|
|
342
|
+
*
|
|
343
|
+
* - `'start'` — the band sticks to the main axis line (bottom for an X axis,
|
|
344
|
+
* left for a left Y axis, right for a right Y axis).
|
|
345
|
+
* - `'end'` — the band sticks to the opposite side of the plot area.
|
|
346
|
+
*
|
|
347
|
+
* Has no effect without `size`.
|
|
348
|
+
* @default 'start'
|
|
349
|
+
*/
|
|
350
|
+
align?: PlotBandAlign;
|
|
351
|
+
/**
|
|
352
|
+
* Perpendicular extent of the band.
|
|
353
|
+
*
|
|
354
|
+
* Accepts a pixel number (`40`), a pixel string (`"40px"`), or a percentage of
|
|
355
|
+
* the perpendicular plot extent (`"25%"`). When omitted, the band spans the
|
|
356
|
+
* full perpendicular extent of the plot area (default behavior).
|
|
357
|
+
*/
|
|
358
|
+
size?: number | string;
|
|
339
359
|
}
|
|
340
360
|
export interface AxisCrosshair extends Pick<AxisPlotLine, 'color' | 'dashStyle' | 'opacity' | 'layerPlacement' | 'width'> {
|
|
341
361
|
/**
|
|
@@ -23,6 +23,8 @@ export interface ScatterSeriesData<T = MeaningfulAny> extends BaseSeriesData<T>
|
|
|
23
23
|
* @deprecated use `x` or `y` instead
|
|
24
24
|
*/
|
|
25
25
|
category?: string;
|
|
26
|
+
/** Data label value of the point. If not specified, the y value is used. */
|
|
27
|
+
label?: string | number;
|
|
26
28
|
/** Individual radius for the point. */
|
|
27
29
|
radius?: number;
|
|
28
30
|
/** Individual opacity for the point. */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { HtmlItem, LabelData } from '../../types';
|
|
2
|
+
import type { BaseTextStyle, ValueFormat } from '../types/chart/base';
|
|
3
|
+
type PointLabelSeries = {
|
|
4
|
+
id: string;
|
|
5
|
+
dataLabels: {
|
|
6
|
+
style: BaseTextStyle;
|
|
7
|
+
html: boolean;
|
|
8
|
+
padding: number;
|
|
9
|
+
format?: ValueFormat;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
type LabelPoint = {
|
|
13
|
+
x: number | null;
|
|
14
|
+
y: number | null;
|
|
15
|
+
data: {
|
|
16
|
+
label?: string | number | null;
|
|
17
|
+
y?: string | number | null;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Shared "above-point" dataLabels algorithm used by line, area, and scatter series.
|
|
22
|
+
*
|
|
23
|
+
* For each visible point it:
|
|
24
|
+
* 1. Formats the value via getFormattedValue
|
|
25
|
+
* 2. Measures the label size (HTML → getLabelsSize, SVG → getTextSizeFn)
|
|
26
|
+
* 3. Positions the label centered above the point, clamped to chart bounds
|
|
27
|
+
*
|
|
28
|
+
* `anchorYOffset` shifts the vertical anchor from the point center upward by the given
|
|
29
|
+
* number of pixels (e.g. marker radius for scatter), so padding is measured from the
|
|
30
|
+
* marker edge rather than its center. The top-boundary clamp also respects this offset
|
|
31
|
+
* so the label never drops below the anchor.
|
|
32
|
+
*
|
|
33
|
+
* Overlap filtering is intentionally left to the caller.
|
|
34
|
+
*/
|
|
35
|
+
export declare function preparePointDataLabels<S extends PointLabelSeries, P extends LabelPoint>({ series, points, xMax, yAxisTop, isOutsideBounds, anchorYOffset, }: {
|
|
36
|
+
series: S;
|
|
37
|
+
points: P[];
|
|
38
|
+
xMax: number;
|
|
39
|
+
yAxisTop: number;
|
|
40
|
+
isOutsideBounds: (x: number, y: number) => boolean;
|
|
41
|
+
anchorYOffset?: number;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
svgLabels: LabelData[];
|
|
44
|
+
htmlLabels: HtmlItem[];
|
|
45
|
+
}>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getFormattedValue } from './format';
|
|
2
|
+
import { getLabelsSize, getTextSizeFn } from './text';
|
|
3
|
+
/**
|
|
4
|
+
* Shared "above-point" dataLabels algorithm used by line, area, and scatter series.
|
|
5
|
+
*
|
|
6
|
+
* For each visible point it:
|
|
7
|
+
* 1. Formats the value via getFormattedValue
|
|
8
|
+
* 2. Measures the label size (HTML → getLabelsSize, SVG → getTextSizeFn)
|
|
9
|
+
* 3. Positions the label centered above the point, clamped to chart bounds
|
|
10
|
+
*
|
|
11
|
+
* `anchorYOffset` shifts the vertical anchor from the point center upward by the given
|
|
12
|
+
* number of pixels (e.g. marker radius for scatter), so padding is measured from the
|
|
13
|
+
* marker edge rather than its center. The top-boundary clamp also respects this offset
|
|
14
|
+
* so the label never drops below the anchor.
|
|
15
|
+
*
|
|
16
|
+
* Overlap filtering is intentionally left to the caller.
|
|
17
|
+
*/
|
|
18
|
+
export async function preparePointDataLabels({ series, points, xMax, yAxisTop, isOutsideBounds, anchorYOffset = 0, }) {
|
|
19
|
+
var _a;
|
|
20
|
+
const svgLabels = [];
|
|
21
|
+
const htmlLabels = [];
|
|
22
|
+
const getTextSize = getTextSizeFn({ style: series.dataLabels.style });
|
|
23
|
+
for (let i = 0; i < points.length; i++) {
|
|
24
|
+
const point = points[i];
|
|
25
|
+
if (point.y === null || point.x === null || isOutsideBounds(point.x, point.y)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const text = getFormattedValue(Object.assign({ value: (_a = point.data.label) !== null && _a !== void 0 ? _a : point.data.y }, series.dataLabels));
|
|
29
|
+
const anchorY = point.y - anchorYOffset;
|
|
30
|
+
if (series.dataLabels.html) {
|
|
31
|
+
const size = await getLabelsSize({
|
|
32
|
+
labels: [text],
|
|
33
|
+
style: series.dataLabels.style,
|
|
34
|
+
html: true,
|
|
35
|
+
});
|
|
36
|
+
const width = size.maxWidth;
|
|
37
|
+
const height = size.maxHeight;
|
|
38
|
+
htmlLabels.push({
|
|
39
|
+
x: Math.min(xMax - width, Math.max(0, point.x - width / 2)),
|
|
40
|
+
y: Math.max(yAxisTop, anchorY - series.dataLabels.padding - height),
|
|
41
|
+
content: text,
|
|
42
|
+
size: { width, height },
|
|
43
|
+
style: series.dataLabels.style,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const labelSize = await getTextSize(text);
|
|
48
|
+
svgLabels.push({
|
|
49
|
+
text,
|
|
50
|
+
x: Math.min(xMax - labelSize.width, Math.max(0, point.x - labelSize.width / 2)),
|
|
51
|
+
y: Math.max(yAxisTop, anchorY -
|
|
52
|
+
series.dataLabels.padding -
|
|
53
|
+
labelSize.height +
|
|
54
|
+
labelSize.hangingOffset),
|
|
55
|
+
style: series.dataLabels.style,
|
|
56
|
+
size: labelSize,
|
|
57
|
+
textAnchor: 'start',
|
|
58
|
+
series,
|
|
59
|
+
active: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { svgLabels, htmlLabels };
|
|
64
|
+
}
|
|
@@ -21,6 +21,10 @@ const tickIntervals = [
|
|
|
21
21
|
[utcMonth, 3, 3 * MONTH],
|
|
22
22
|
[utcYear, 1, YEAR],
|
|
23
23
|
];
|
|
24
|
+
// utcDay.every(2) resets its day counter at the start of each month (field = getUTCDate() - 1),
|
|
25
|
+
// so in a 31-day month the last tick lands on day 31 and the next tick is day 1 of the following
|
|
26
|
+
// month — only 1 day apart. Filtering by absolute Unix day number avoids the monthly reset.
|
|
27
|
+
const utcEvery2Days = utcDay.filter((d) => Math.floor(d.getTime() / DAY) % 2 === 0);
|
|
24
28
|
function getDateTimeTickInterval(start, stop, count) {
|
|
25
29
|
const target = Math.abs(stop - start) / count;
|
|
26
30
|
const i = bisector(([, , step]) => step).right(tickIntervals, target);
|
|
@@ -31,6 +35,9 @@ function getDateTimeTickInterval(start, stop, count) {
|
|
|
31
35
|
return utcMillisecond.every(Math.max(tickStep(start, stop, count), 1));
|
|
32
36
|
}
|
|
33
37
|
const [t, step] = tickIntervals[target / tickIntervals[i - 1][2] < tickIntervals[i][2] / target ? i - 1 : i];
|
|
38
|
+
if (t === utcDay && step === 2) {
|
|
39
|
+
return utcEvery2Days;
|
|
40
|
+
}
|
|
34
41
|
return t.every(step);
|
|
35
42
|
}
|
|
36
43
|
/**
|
|
@@ -146,18 +146,20 @@ export async function getShapes(args) {
|
|
|
146
146
|
}
|
|
147
147
|
case SERIES_TYPE.Scatter: {
|
|
148
148
|
if (xAxis && xScale && (yScale === null || yScale === void 0 ? void 0 : yScale.length)) {
|
|
149
|
-
const
|
|
149
|
+
const scatterShapeData = await prepareScatterData({
|
|
150
150
|
series: chartSeries,
|
|
151
151
|
xAxis,
|
|
152
152
|
xScale,
|
|
153
153
|
yAxis,
|
|
154
154
|
yScale,
|
|
155
|
+
split,
|
|
155
156
|
isOutsideBounds,
|
|
157
|
+
isRangeSlider,
|
|
156
158
|
});
|
|
157
159
|
shapes[index] = (React.createElement(ScatterSeriesShape, { key: SERIES_TYPE.Scatter, clipPathId: shouldUseClipPathId(SERIES_TYPE.Scatter, clipPathBySeriesType)
|
|
158
160
|
? clipPathId
|
|
159
|
-
: undefined, dispatcher: dispatcher, preparedData:
|
|
160
|
-
shapesData.splice(index, 0, ...
|
|
161
|
+
: undefined, dispatcher: dispatcher, preparedData: scatterShapeData, seriesOptions: seriesOptions, htmlLayout: htmlLayout }));
|
|
162
|
+
shapesData.splice(index, 0, ...scatterShapeData.markers);
|
|
161
163
|
}
|
|
162
164
|
break;
|
|
163
165
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Dispatch } from 'd3-dispatch';
|
|
3
3
|
import type { PreparedSeriesOptions } from '../../../core/series/types';
|
|
4
|
-
import type {
|
|
4
|
+
import type { PreparedScatterShapeData } from '../../../core/shapes/scatter/types';
|
|
5
5
|
export { prepareScatterData } from '../../../core/shapes/scatter/prepare-data';
|
|
6
6
|
type ScatterSeriesShapeProps = {
|
|
7
7
|
htmlLayout: HTMLElement | null;
|
|
8
|
-
preparedData:
|
|
8
|
+
preparedData: PreparedScatterShapeData;
|
|
9
9
|
seriesOptions: PreparedSeriesOptions;
|
|
10
10
|
clipPathId?: string;
|
|
11
11
|
dispatcher?: Dispatch<object>;
|
|
@@ -13,7 +13,10 @@ export function ScatterSeriesShape(props) {
|
|
|
13
13
|
}
|
|
14
14
|
return renderScatter({ plot: ref.current }, preparedData, seriesOptions, dispatcher);
|
|
15
15
|
}, [dispatcher, preparedData, seriesOptions]);
|
|
16
|
+
const htmlLayerData = React.useMemo(() => {
|
|
17
|
+
return { htmlElements: preparedData.htmlLabels };
|
|
18
|
+
}, [preparedData]);
|
|
16
19
|
return (React.createElement(React.Fragment, null,
|
|
17
20
|
React.createElement("g", { ref: ref, className: b(), clipPath: clipPathId ? `url(#${clipPathId})` : undefined }),
|
|
18
|
-
React.createElement(HtmlLayer, { preparedData:
|
|
21
|
+
React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
|
|
19
22
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
.gcharts-line__label,
|
|
2
2
|
.gcharts-area__label,
|
|
3
3
|
.gcharts-radar__label,
|
|
4
|
+
.gcharts-bar-x__label,
|
|
5
|
+
.gcharts-bar-y__label,
|
|
4
6
|
.gcharts-heatmap__label,
|
|
5
|
-
.gcharts-funnel__label
|
|
7
|
+
.gcharts-funnel__label,
|
|
8
|
+
.gcharts-treemap__label,
|
|
9
|
+
.gcharts-pie__label,
|
|
10
|
+
.gcharts-scatter__label {
|
|
11
|
+
user-select: none;
|
|
12
|
+
pointer-events: none;
|
|
6
13
|
dominant-baseline: hanging;
|
|
7
14
|
}
|
|
8
15
|
|
|
@@ -13,30 +20,6 @@
|
|
|
13
20
|
.gcharts-pie__segment {
|
|
14
21
|
stroke: var(--g-color-base-background);
|
|
15
22
|
}
|
|
16
|
-
.gcharts-pie__label {
|
|
17
|
-
font-size: 11px;
|
|
18
|
-
font-weight: bold;
|
|
19
|
-
fill: var(--g-color-text-complementary);
|
|
20
|
-
dominant-baseline: hanging;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.gcharts-bar-x__label {
|
|
24
|
-
user-select: none;
|
|
25
|
-
fill: var(--g-color-text-complementary);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.gcharts-bar-y__label {
|
|
29
|
-
user-select: none;
|
|
30
|
-
fill: var(--g-color-text-complementary);
|
|
31
|
-
dominant-baseline: hanging;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.gcharts-treemap__label {
|
|
35
|
-
user-select: none;
|
|
36
|
-
pointer-events: none;
|
|
37
|
-
fill: var(--g-color-text-complementary);
|
|
38
|
-
dominant-baseline: hanging;
|
|
39
|
-
}
|
|
40
23
|
|
|
41
24
|
.gcharts-waterfall__connector {
|
|
42
25
|
stroke: var(--g-color-line-generic-active);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravity-ui/charts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.1",
|
|
4
4
|
"description": "A flexible JavaScript library for data visualization and chart rendering using React",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -146,6 +146,9 @@
|
|
|
146
146
|
"typescript": "^5.9.3",
|
|
147
147
|
"vite": "^7.3.1"
|
|
148
148
|
},
|
|
149
|
+
"overrides": {
|
|
150
|
+
"dompurify": "^3.4.0"
|
|
151
|
+
},
|
|
149
152
|
"peerDependencies": {
|
|
150
153
|
"@gravity-ui/uikit": "^7.0.0",
|
|
151
154
|
"react": ">=17.0.0",
|