@gravity-ui/charts 1.45.0 → 1.46.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/ChartInner/index.js +2 -2
- package/dist/cjs/components/ChartInner/styles.css +2 -2
- package/dist/cjs/components/ChartInner/useChartInnerProps.js +1 -1
- package/dist/cjs/components/ChartInner/utils/title.d.ts +2 -1
- package/dist/cjs/components/ChartInner/utils/title.js +51 -14
- package/dist/cjs/components/Title/index.d.ts +4 -2
- package/dist/cjs/components/Title/index.js +9 -2
- package/dist/cjs/core/types/chart/title.d.ts +18 -0
- package/dist/cjs/hooks/types.d.ts +6 -3
- package/dist/cjs/hooks/useShapes/HtmlLayer.js +4 -3
- package/dist/cjs/hooks/useShapes/line/index.js +3 -3
- package/dist/cjs/types/chart-ui.d.ts +2 -0
- package/dist/esm/components/ChartInner/index.js +2 -2
- package/dist/esm/components/ChartInner/styles.css +2 -2
- package/dist/esm/components/ChartInner/useChartInnerProps.js +1 -1
- package/dist/esm/components/ChartInner/utils/title.d.ts +2 -1
- package/dist/esm/components/ChartInner/utils/title.js +51 -14
- package/dist/esm/components/Title/index.d.ts +4 -2
- package/dist/esm/components/Title/index.js +9 -2
- package/dist/esm/core/types/chart/title.d.ts +18 -0
- package/dist/esm/hooks/types.d.ts +6 -3
- package/dist/esm/hooks/useShapes/HtmlLayer.js +4 -3
- package/dist/esm/hooks/useShapes/line/index.js +3 -3
- package/dist/esm/types/chart-ui.d.ts +2 -0
- package/package.json +1 -1
|
@@ -256,7 +256,7 @@ export const ChartInner = (props) => {
|
|
|
256
256
|
React.createElement("rect", { x: 0, y: 0, width: boundsWidth, height: boundsHeight })),
|
|
257
257
|
React.createElement("clipPath", { id: getClipPathIdByBounds({ clipPathId, bounds: 'horizontal' }) },
|
|
258
258
|
React.createElement("rect", { x: 0, y: -boundsHeight, width: boundsWidth, height: boundsHeight * 3 }))),
|
|
259
|
-
preparedTitle && React.createElement(Title, Object.assign({}, preparedTitle)),
|
|
259
|
+
preparedTitle && React.createElement(Title, Object.assign({}, preparedTitle, { htmlLayout: htmlLayout })),
|
|
260
260
|
React.createElement("g", { transform: `translate(0, ${boundsOffsetTop})` }, preparedSplit === null || preparedSplit === void 0 ? void 0 : preparedSplit.plots.map((plot, index) => {
|
|
261
261
|
return React.createElement(PlotTitle, { key: `plot-${index}`, title: plot.title });
|
|
262
262
|
})),
|
|
@@ -285,7 +285,7 @@ export const ChartInner = (props) => {
|
|
|
285
285
|
// when starting to select an area, the tooltip remains in the position where the selection began
|
|
286
286
|
onPointerMove: throttledHandlePointerMove, onPointerLeave: handlePointerLeave, onTouchStart: throttledHandleTouchMove, onTouchMove: throttledHandleTouchMove, onClick: handleChartClick }, initialized ? chartContent : null),
|
|
287
287
|
React.createElement("div", { className: b('html-layer'), ref: setHtmlLayout, style: {
|
|
288
|
-
'--g-html-layout-transform': `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
|
|
288
|
+
'--g-html-layout-plot-transform': `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
|
|
289
289
|
} }),
|
|
290
290
|
Object.keys(zoomState).length > 0 && (preparedChart === null || preparedChart === void 0 ? void 0 : preparedChart.zoom) && (React.createElement(Button, { className: b('reset-zoom-button'), onClick: () => {
|
|
291
291
|
var _a;
|
|
@@ -53,6 +53,7 @@ export function useChartInnerProps(props) {
|
|
|
53
53
|
const preparedTitle = await getPreparedTitle({
|
|
54
54
|
title: data.title,
|
|
55
55
|
chartWidth: width,
|
|
56
|
+
chartHeight: height,
|
|
56
57
|
chartMargin: (_a = data.chart) === null || _a === void 0 ? void 0 : _a.margin,
|
|
57
58
|
});
|
|
58
59
|
const preparedChart = getPreparedChart({
|
|
@@ -203,7 +204,6 @@ export function useChartInnerProps(props) {
|
|
|
203
204
|
getYAxisWidth,
|
|
204
205
|
legendConfig,
|
|
205
206
|
});
|
|
206
|
-
//end
|
|
207
207
|
const newStateValue = {
|
|
208
208
|
allPreparedSeries,
|
|
209
209
|
boundsHeight,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { PreparedTitle } from '../../../hooks/types';
|
|
2
2
|
import type { ChartData, ChartMargin } from '../../../types';
|
|
3
|
-
export declare const getPreparedTitle: ({ title, chartWidth, chartMargin, }: {
|
|
3
|
+
export declare const getPreparedTitle: ({ title, chartWidth, chartHeight, chartMargin, }: {
|
|
4
4
|
title: ChartData["title"];
|
|
5
5
|
chartWidth: number;
|
|
6
|
+
chartHeight: number;
|
|
6
7
|
chartMargin?: Partial<ChartMargin>;
|
|
7
8
|
}) => Promise<PreparedTitle | undefined>;
|
|
@@ -1,26 +1,64 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getTextSizeFn, getTextWithElipsis, wrapText } from '../../../core/utils';
|
|
1
|
+
import { calculateNumericProperty, getLabelsSize, getTextSizeFn, getTextWithElipsis, wrapText, } from '../../../core/utils';
|
|
3
2
|
const DEFAULT_TITLE_FONT_SIZE = '15px';
|
|
4
3
|
const DEFAULT_TITLE_MARGIN = 10;
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const DEFAULT_TITLE_MAX_HEIGHT = '50%';
|
|
5
|
+
export const getPreparedTitle = async ({ title, chartWidth, chartHeight, chartMargin, }) => {
|
|
6
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
7
|
+
const titleText = title === null || title === void 0 ? void 0 : title.text;
|
|
8
|
+
if (!titleText) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
7
11
|
const chartMarginTop = (_a = chartMargin === null || chartMargin === void 0 ? void 0 : chartMargin.top) !== null && _a !== void 0 ? _a : 0;
|
|
8
12
|
const chartMarginLeft = (_b = chartMargin === null || chartMargin === void 0 ? void 0 : chartMargin.left) !== null && _b !== void 0 ? _b : 0;
|
|
9
13
|
const chartMarginRight = (_c = chartMargin === null || chartMargin === void 0 ? void 0 : chartMargin.right) !== null && _c !== void 0 ? _c : 0;
|
|
10
|
-
const titleText = get(title, 'text');
|
|
11
14
|
const titleStyle = {
|
|
12
15
|
fontSize: (_e = (_d = title === null || title === void 0 ? void 0 : title.style) === null || _d === void 0 ? void 0 : _d.fontSize) !== null && _e !== void 0 ? _e : DEFAULT_TITLE_FONT_SIZE,
|
|
13
16
|
fontWeight: (_g = (_f = title === null || title === void 0 ? void 0 : title.style) === null || _f === void 0 ? void 0 : _f.fontWeight) !== null && _g !== void 0 ? _g : 'var(--g-text-subheader-font-weight)',
|
|
14
17
|
fontColor: (_j = (_h = title === null || title === void 0 ? void 0 : title.style) === null || _h === void 0 ? void 0 : _h.fontColor) !== null && _j !== void 0 ? _j : 'var(--g-color-text-primary)',
|
|
15
18
|
};
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
const usableWidth = chartWidth - chartMarginLeft - chartMarginRight;
|
|
20
|
+
const titleMargin = (_k = title === null || title === void 0 ? void 0 : title.margin) !== null && _k !== void 0 ? _k : DEFAULT_TITLE_MARGIN;
|
|
21
|
+
if (title === null || title === void 0 ? void 0 : title.html) {
|
|
22
|
+
const titleSize = await getLabelsSize({
|
|
23
|
+
labels: [titleText],
|
|
24
|
+
style: Object.assign(Object.assign({}, titleStyle), { maxWidth: `${usableWidth}px` }),
|
|
25
|
+
html: true,
|
|
26
|
+
});
|
|
27
|
+
const resolvedMaxHeight = calculateNumericProperty({
|
|
28
|
+
value: (_l = title.maxHeight) !== null && _l !== void 0 ? _l : DEFAULT_TITLE_MAX_HEIGHT,
|
|
29
|
+
base: chartHeight,
|
|
30
|
+
});
|
|
31
|
+
const titleHeight = resolvedMaxHeight === undefined
|
|
32
|
+
? titleSize.maxHeight
|
|
33
|
+
: Math.min(titleSize.maxHeight, resolvedMaxHeight);
|
|
34
|
+
const qa = title === null || title === void 0 ? void 0 : title.qa;
|
|
35
|
+
const qaAttr = qa ? ` data-qa="${qa}"` : '';
|
|
36
|
+
const maxHeightStyle = resolvedMaxHeight === undefined ? '' : ` max-height: ${resolvedMaxHeight}px;`;
|
|
37
|
+
const htmlContent = `<div${qaAttr} style="max-width: ${usableWidth}px; overflow: hidden;${maxHeightStyle}">${titleText}</div>`;
|
|
38
|
+
const titleWidth = Math.min(titleSize.maxWidth, usableWidth);
|
|
39
|
+
return {
|
|
40
|
+
text: titleText,
|
|
41
|
+
style: titleStyle,
|
|
42
|
+
height: titleHeight,
|
|
43
|
+
margin: titleMargin,
|
|
44
|
+
qa,
|
|
45
|
+
html: true,
|
|
46
|
+
htmlElements: [
|
|
47
|
+
{
|
|
48
|
+
x: chartMarginLeft + (usableWidth - titleWidth) / 2,
|
|
49
|
+
y: chartMarginTop,
|
|
50
|
+
content: htmlContent,
|
|
51
|
+
size: { width: titleWidth, height: titleHeight },
|
|
52
|
+
style: titleStyle,
|
|
53
|
+
scope: 'chart',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
18
57
|
}
|
|
58
|
+
const xCenter = chartMarginLeft + usableWidth / 2;
|
|
19
59
|
const getTitleTextSize = getTextSizeFn({ style: titleStyle });
|
|
20
|
-
const maxRowCount = (
|
|
60
|
+
const maxRowCount = (_m = title === null || title === void 0 ? void 0 : title.maxRowCount) !== null && _m !== void 0 ? _m : 1;
|
|
21
61
|
const contentRows = [];
|
|
22
|
-
const usableWidth = chartWidth - chartMarginLeft - chartMarginRight;
|
|
23
|
-
const xCenter = chartMarginLeft + usableWidth / 2;
|
|
24
62
|
if (maxRowCount > 1) {
|
|
25
63
|
let titleTextRows = await wrapText({
|
|
26
64
|
text: titleText,
|
|
@@ -33,7 +71,7 @@ export const getPreparedTitle = async ({ title, chartWidth, chartMargin, }) => {
|
|
|
33
71
|
acc.push(row);
|
|
34
72
|
}
|
|
35
73
|
else {
|
|
36
|
-
acc[maxRowCount - 1].text
|
|
74
|
+
acc[maxRowCount - 1] = Object.assign(Object.assign({}, acc[maxRowCount - 1]), { text: acc[maxRowCount - 1].text + row.text });
|
|
37
75
|
}
|
|
38
76
|
return acc;
|
|
39
77
|
}, []);
|
|
@@ -71,12 +109,11 @@ export const getPreparedTitle = async ({ title, chartWidth, chartMargin, }) => {
|
|
|
71
109
|
});
|
|
72
110
|
}
|
|
73
111
|
const totalTextHeight = contentRows.reduce((acc, row) => acc + row.size.height, 0);
|
|
74
|
-
const titleHeight = totalTextHeight;
|
|
75
112
|
return {
|
|
76
113
|
text: titleText,
|
|
77
114
|
style: titleStyle,
|
|
78
|
-
height:
|
|
79
|
-
margin:
|
|
115
|
+
height: totalTextHeight,
|
|
116
|
+
margin: titleMargin,
|
|
80
117
|
qa: title === null || title === void 0 ? void 0 : title.qa,
|
|
81
118
|
contentRows,
|
|
82
119
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { PreparedTitle } from '../../hooks';
|
|
3
|
-
type Props = PreparedTitle
|
|
4
|
-
|
|
3
|
+
type Props = PreparedTitle & {
|
|
4
|
+
htmlLayout?: HTMLElement | null;
|
|
5
|
+
};
|
|
6
|
+
export declare const Title: (props: Props) => React.JSX.Element | null;
|
|
5
7
|
export {};
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { HtmlLayer } from '../../hooks/useShapes/HtmlLayer';
|
|
2
3
|
export const Title = (props) => {
|
|
3
|
-
const { style, qa, contentRows } = props;
|
|
4
|
+
const { style, qa, contentRows, html, htmlElements, htmlLayout } = props;
|
|
5
|
+
if (html) {
|
|
6
|
+
if (!htmlLayout || !htmlElements) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return React.createElement(HtmlLayer, { htmlLayout: htmlLayout, preparedData: { htmlElements } });
|
|
10
|
+
}
|
|
4
11
|
return (React.createElement("text", { dominantBaseline: "hanging", textAnchor: "middle", style: {
|
|
5
12
|
fill: style === null || style === void 0 ? void 0 : style.fontColor,
|
|
6
13
|
fontSize: style === null || style === void 0 ? void 0 : style.fontSize,
|
|
7
14
|
fontWeight: style === null || style === void 0 ? void 0 : style.fontWeight,
|
|
8
|
-
}, "data-qa": qa }, contentRows.map((row, i) => (React.createElement("tspan", { key: i, x: row.x, y: row.y, dominantBaseline: "hanging", dangerouslySetInnerHTML: { __html: row.text } })))));
|
|
15
|
+
}, "data-qa": qa }, contentRows === null || contentRows === void 0 ? void 0 : contentRows.map((row, i) => (React.createElement("tspan", { key: i, x: row.x, y: row.y, dominantBaseline: "hanging", dangerouslySetInnerHTML: { __html: row.text } })))));
|
|
9
16
|
};
|
|
@@ -5,6 +5,7 @@ export interface ChartTitle {
|
|
|
5
5
|
/**
|
|
6
6
|
* Maximum number of text rows. If the text exceeds this limit, it is truncated with an ellipsis.
|
|
7
7
|
* Default: 1
|
|
8
|
+
* Not applicable when `html: true` — HTML content manages its own layout.
|
|
8
9
|
*/
|
|
9
10
|
maxRowCount?: number;
|
|
10
11
|
/**
|
|
@@ -17,4 +18,21 @@ export interface ChartTitle {
|
|
|
17
18
|
* It is assigned as a data-qa attribute to an element.
|
|
18
19
|
*/
|
|
19
20
|
qa?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Enables HTML rendering for the chart title.
|
|
23
|
+
* When true, the title is rendered as an HTML element on top of the SVG
|
|
24
|
+
* instead of an SVG text node. This allows using arbitrary HTML tags
|
|
25
|
+
* (e.g. links, styled spans) that cannot be embedded in SVG.
|
|
26
|
+
* The element will be displayed outside the box of the SVG element.
|
|
27
|
+
*/
|
|
28
|
+
html?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Maximum height of the title area. Accepts a pixel value (`number` or `"100px"`)
|
|
31
|
+
* or a percentage string (`"50%"`) relative to the full chart height.
|
|
32
|
+
* When the title content exceeds this height, it is clipped.
|
|
33
|
+
* Only applicable when `html: true`.
|
|
34
|
+
*
|
|
35
|
+
* Default: 50%
|
|
36
|
+
*/
|
|
37
|
+
maxHeight?: string | number;
|
|
20
38
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TextRowData } from '../components/types';
|
|
2
|
-
import type { ChartBrush, ChartData, ChartMargin, ChartTitle, ChartZoom, DeepRequired } from '../types';
|
|
2
|
+
import type { BaseTextStyle, ChartBrush, ChartData, ChartMargin, ChartTitle, ChartZoom, DeepRequired } from '../types';
|
|
3
|
+
import type { HtmlItem } from '../types/chart-ui';
|
|
3
4
|
export type PreparedZoom = DeepRequired<Omit<ChartZoom, 'enabled' | 'brush'>> & DeepRequired<{
|
|
4
5
|
brush: ChartBrush;
|
|
5
6
|
}>;
|
|
@@ -7,10 +8,12 @@ export type PreparedChart = {
|
|
|
7
8
|
margin: ChartMargin;
|
|
8
9
|
zoom: PreparedZoom | null;
|
|
9
10
|
};
|
|
10
|
-
export type PreparedTitle = Omit<ChartTitle, 'margin'> & {
|
|
11
|
+
export type PreparedTitle = Omit<ChartTitle, 'margin' | 'style'> & {
|
|
11
12
|
height: number;
|
|
12
13
|
margin: number;
|
|
13
|
-
|
|
14
|
+
style: BaseTextStyle;
|
|
15
|
+
contentRows?: TextRowData[];
|
|
16
|
+
htmlElements?: HtmlItem[];
|
|
14
17
|
};
|
|
15
18
|
export type PreparedTooltip = ChartData['tooltip'] & {
|
|
16
19
|
enabled: boolean;
|
|
@@ -19,8 +19,9 @@ export const HtmlLayer = (props) => {
|
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
return (React.createElement(Portal, { container: htmlLayout }, items.map((item, index) => {
|
|
22
|
-
var _a, _b, _c;
|
|
23
|
-
const
|
|
24
|
-
|
|
22
|
+
var _a, _b, _c, _d;
|
|
23
|
+
const scope = (_a = item.scope) !== null && _a !== void 0 ? _a : 'plot';
|
|
24
|
+
const style = Object.assign(Object.assign({}, item.style), { color: (_c = (_b = item.style) === null || _b === void 0 ? void 0 : _b.color) !== null && _c !== void 0 ? _c : (_d = item.style) === null || _d === void 0 ? void 0 : _d.fontColor, position: 'absolute', left: item.x, top: item.y });
|
|
25
|
+
return (React.createElement("div", { className: b('html-layer-item', { plot: scope === 'plot' }), key: index, dangerouslySetInnerHTML: { __html: item.content }, style: style }));
|
|
25
26
|
})));
|
|
26
27
|
};
|
|
@@ -81,8 +81,8 @@ export const LineSeriesShapes = (args) => {
|
|
|
81
81
|
function handleShapeHover(data) {
|
|
82
82
|
hoveredDataRef.current = data;
|
|
83
83
|
const selected = (data === null || data === void 0 ? void 0 : data.filter((d) => d.series.type === 'line')) || [];
|
|
84
|
+
const selectedDataItems = selected.map((d) => d.data);
|
|
84
85
|
const selectedSeriesIds = selected.map((d) => { var _a; return (_a = d.series) === null || _a === void 0 ? void 0 : _a.id; });
|
|
85
|
-
const closestChunk = selected.find((d) => d.closest);
|
|
86
86
|
lineSelection.datum((d, index, list) => {
|
|
87
87
|
const elementSelection = select(list[index]);
|
|
88
88
|
const hovered = Boolean(hoverEnabled && selectedSeriesIds.includes(d.id));
|
|
@@ -118,7 +118,7 @@ export const LineSeriesShapes = (args) => {
|
|
|
118
118
|
});
|
|
119
119
|
markerSelection.datum((d, index, list) => {
|
|
120
120
|
const elementSelection = select(list[index]);
|
|
121
|
-
const hovered = Boolean(hoverEnabled && d.point.data
|
|
121
|
+
const hovered = Boolean(hoverEnabled && selectedDataItems.includes(d.point.data));
|
|
122
122
|
if (d.hovered !== hovered) {
|
|
123
123
|
d.hovered = hovered;
|
|
124
124
|
elementSelection.attr('visibility', getMarkerVisibility(d));
|
|
@@ -141,7 +141,7 @@ export const LineSeriesShapes = (args) => {
|
|
|
141
141
|
hoverMarkersSvgElement.selectAll('*').remove();
|
|
142
142
|
if (hoverEnabled && selected.length > 0) {
|
|
143
143
|
const hoverOnlyMarkers = [];
|
|
144
|
-
for (const chunk of selected
|
|
144
|
+
for (const chunk of selected) {
|
|
145
145
|
const seriesData = preparedData.find((pd) => pd.id === chunk.series.id);
|
|
146
146
|
if (!seriesData) {
|
|
147
147
|
continue;
|
|
@@ -24,6 +24,8 @@ export interface HtmlItem {
|
|
|
24
24
|
height: number;
|
|
25
25
|
};
|
|
26
26
|
style?: BaseTextStyle & React.CSSProperties;
|
|
27
|
+
/** Coordinate space for positioning: 'plot' uses the plot area origin, 'chart' uses the full chart origin. Defaults to 'plot'. */
|
|
28
|
+
scope?: 'plot' | 'chart';
|
|
27
29
|
}
|
|
28
30
|
export interface ShapeDataWithLabels {
|
|
29
31
|
svgLabels: LabelData[];
|
|
@@ -256,7 +256,7 @@ export const ChartInner = (props) => {
|
|
|
256
256
|
React.createElement("rect", { x: 0, y: 0, width: boundsWidth, height: boundsHeight })),
|
|
257
257
|
React.createElement("clipPath", { id: getClipPathIdByBounds({ clipPathId, bounds: 'horizontal' }) },
|
|
258
258
|
React.createElement("rect", { x: 0, y: -boundsHeight, width: boundsWidth, height: boundsHeight * 3 }))),
|
|
259
|
-
preparedTitle && React.createElement(Title, Object.assign({}, preparedTitle)),
|
|
259
|
+
preparedTitle && React.createElement(Title, Object.assign({}, preparedTitle, { htmlLayout: htmlLayout })),
|
|
260
260
|
React.createElement("g", { transform: `translate(0, ${boundsOffsetTop})` }, preparedSplit === null || preparedSplit === void 0 ? void 0 : preparedSplit.plots.map((plot, index) => {
|
|
261
261
|
return React.createElement(PlotTitle, { key: `plot-${index}`, title: plot.title });
|
|
262
262
|
})),
|
|
@@ -285,7 +285,7 @@ export const ChartInner = (props) => {
|
|
|
285
285
|
// when starting to select an area, the tooltip remains in the position where the selection began
|
|
286
286
|
onPointerMove: throttledHandlePointerMove, onPointerLeave: handlePointerLeave, onTouchStart: throttledHandleTouchMove, onTouchMove: throttledHandleTouchMove, onClick: handleChartClick }, initialized ? chartContent : null),
|
|
287
287
|
React.createElement("div", { className: b('html-layer'), ref: setHtmlLayout, style: {
|
|
288
|
-
'--g-html-layout-transform': `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
|
|
288
|
+
'--g-html-layout-plot-transform': `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
|
|
289
289
|
} }),
|
|
290
290
|
Object.keys(zoomState).length > 0 && (preparedChart === null || preparedChart === void 0 ? void 0 : preparedChart.zoom) && (React.createElement(Button, { className: b('reset-zoom-button'), onClick: () => {
|
|
291
291
|
var _a;
|
|
@@ -53,6 +53,7 @@ export function useChartInnerProps(props) {
|
|
|
53
53
|
const preparedTitle = await getPreparedTitle({
|
|
54
54
|
title: data.title,
|
|
55
55
|
chartWidth: width,
|
|
56
|
+
chartHeight: height,
|
|
56
57
|
chartMargin: (_a = data.chart) === null || _a === void 0 ? void 0 : _a.margin,
|
|
57
58
|
});
|
|
58
59
|
const preparedChart = getPreparedChart({
|
|
@@ -203,7 +204,6 @@ export function useChartInnerProps(props) {
|
|
|
203
204
|
getYAxisWidth,
|
|
204
205
|
legendConfig,
|
|
205
206
|
});
|
|
206
|
-
//end
|
|
207
207
|
const newStateValue = {
|
|
208
208
|
allPreparedSeries,
|
|
209
209
|
boundsHeight,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { PreparedTitle } from '../../../hooks/types';
|
|
2
2
|
import type { ChartData, ChartMargin } from '../../../types';
|
|
3
|
-
export declare const getPreparedTitle: ({ title, chartWidth, chartMargin, }: {
|
|
3
|
+
export declare const getPreparedTitle: ({ title, chartWidth, chartHeight, chartMargin, }: {
|
|
4
4
|
title: ChartData["title"];
|
|
5
5
|
chartWidth: number;
|
|
6
|
+
chartHeight: number;
|
|
6
7
|
chartMargin?: Partial<ChartMargin>;
|
|
7
8
|
}) => Promise<PreparedTitle | undefined>;
|
|
@@ -1,26 +1,64 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getTextSizeFn, getTextWithElipsis, wrapText } from '../../../core/utils';
|
|
1
|
+
import { calculateNumericProperty, getLabelsSize, getTextSizeFn, getTextWithElipsis, wrapText, } from '../../../core/utils';
|
|
3
2
|
const DEFAULT_TITLE_FONT_SIZE = '15px';
|
|
4
3
|
const DEFAULT_TITLE_MARGIN = 10;
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const DEFAULT_TITLE_MAX_HEIGHT = '50%';
|
|
5
|
+
export const getPreparedTitle = async ({ title, chartWidth, chartHeight, chartMargin, }) => {
|
|
6
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
7
|
+
const titleText = title === null || title === void 0 ? void 0 : title.text;
|
|
8
|
+
if (!titleText) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
7
11
|
const chartMarginTop = (_a = chartMargin === null || chartMargin === void 0 ? void 0 : chartMargin.top) !== null && _a !== void 0 ? _a : 0;
|
|
8
12
|
const chartMarginLeft = (_b = chartMargin === null || chartMargin === void 0 ? void 0 : chartMargin.left) !== null && _b !== void 0 ? _b : 0;
|
|
9
13
|
const chartMarginRight = (_c = chartMargin === null || chartMargin === void 0 ? void 0 : chartMargin.right) !== null && _c !== void 0 ? _c : 0;
|
|
10
|
-
const titleText = get(title, 'text');
|
|
11
14
|
const titleStyle = {
|
|
12
15
|
fontSize: (_e = (_d = title === null || title === void 0 ? void 0 : title.style) === null || _d === void 0 ? void 0 : _d.fontSize) !== null && _e !== void 0 ? _e : DEFAULT_TITLE_FONT_SIZE,
|
|
13
16
|
fontWeight: (_g = (_f = title === null || title === void 0 ? void 0 : title.style) === null || _f === void 0 ? void 0 : _f.fontWeight) !== null && _g !== void 0 ? _g : 'var(--g-text-subheader-font-weight)',
|
|
14
17
|
fontColor: (_j = (_h = title === null || title === void 0 ? void 0 : title.style) === null || _h === void 0 ? void 0 : _h.fontColor) !== null && _j !== void 0 ? _j : 'var(--g-color-text-primary)',
|
|
15
18
|
};
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
const usableWidth = chartWidth - chartMarginLeft - chartMarginRight;
|
|
20
|
+
const titleMargin = (_k = title === null || title === void 0 ? void 0 : title.margin) !== null && _k !== void 0 ? _k : DEFAULT_TITLE_MARGIN;
|
|
21
|
+
if (title === null || title === void 0 ? void 0 : title.html) {
|
|
22
|
+
const titleSize = await getLabelsSize({
|
|
23
|
+
labels: [titleText],
|
|
24
|
+
style: Object.assign(Object.assign({}, titleStyle), { maxWidth: `${usableWidth}px` }),
|
|
25
|
+
html: true,
|
|
26
|
+
});
|
|
27
|
+
const resolvedMaxHeight = calculateNumericProperty({
|
|
28
|
+
value: (_l = title.maxHeight) !== null && _l !== void 0 ? _l : DEFAULT_TITLE_MAX_HEIGHT,
|
|
29
|
+
base: chartHeight,
|
|
30
|
+
});
|
|
31
|
+
const titleHeight = resolvedMaxHeight === undefined
|
|
32
|
+
? titleSize.maxHeight
|
|
33
|
+
: Math.min(titleSize.maxHeight, resolvedMaxHeight);
|
|
34
|
+
const qa = title === null || title === void 0 ? void 0 : title.qa;
|
|
35
|
+
const qaAttr = qa ? ` data-qa="${qa}"` : '';
|
|
36
|
+
const maxHeightStyle = resolvedMaxHeight === undefined ? '' : ` max-height: ${resolvedMaxHeight}px;`;
|
|
37
|
+
const htmlContent = `<div${qaAttr} style="max-width: ${usableWidth}px; overflow: hidden;${maxHeightStyle}">${titleText}</div>`;
|
|
38
|
+
const titleWidth = Math.min(titleSize.maxWidth, usableWidth);
|
|
39
|
+
return {
|
|
40
|
+
text: titleText,
|
|
41
|
+
style: titleStyle,
|
|
42
|
+
height: titleHeight,
|
|
43
|
+
margin: titleMargin,
|
|
44
|
+
qa,
|
|
45
|
+
html: true,
|
|
46
|
+
htmlElements: [
|
|
47
|
+
{
|
|
48
|
+
x: chartMarginLeft + (usableWidth - titleWidth) / 2,
|
|
49
|
+
y: chartMarginTop,
|
|
50
|
+
content: htmlContent,
|
|
51
|
+
size: { width: titleWidth, height: titleHeight },
|
|
52
|
+
style: titleStyle,
|
|
53
|
+
scope: 'chart',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
18
57
|
}
|
|
58
|
+
const xCenter = chartMarginLeft + usableWidth / 2;
|
|
19
59
|
const getTitleTextSize = getTextSizeFn({ style: titleStyle });
|
|
20
|
-
const maxRowCount = (
|
|
60
|
+
const maxRowCount = (_m = title === null || title === void 0 ? void 0 : title.maxRowCount) !== null && _m !== void 0 ? _m : 1;
|
|
21
61
|
const contentRows = [];
|
|
22
|
-
const usableWidth = chartWidth - chartMarginLeft - chartMarginRight;
|
|
23
|
-
const xCenter = chartMarginLeft + usableWidth / 2;
|
|
24
62
|
if (maxRowCount > 1) {
|
|
25
63
|
let titleTextRows = await wrapText({
|
|
26
64
|
text: titleText,
|
|
@@ -33,7 +71,7 @@ export const getPreparedTitle = async ({ title, chartWidth, chartMargin, }) => {
|
|
|
33
71
|
acc.push(row);
|
|
34
72
|
}
|
|
35
73
|
else {
|
|
36
|
-
acc[maxRowCount - 1].text
|
|
74
|
+
acc[maxRowCount - 1] = Object.assign(Object.assign({}, acc[maxRowCount - 1]), { text: acc[maxRowCount - 1].text + row.text });
|
|
37
75
|
}
|
|
38
76
|
return acc;
|
|
39
77
|
}, []);
|
|
@@ -71,12 +109,11 @@ export const getPreparedTitle = async ({ title, chartWidth, chartMargin, }) => {
|
|
|
71
109
|
});
|
|
72
110
|
}
|
|
73
111
|
const totalTextHeight = contentRows.reduce((acc, row) => acc + row.size.height, 0);
|
|
74
|
-
const titleHeight = totalTextHeight;
|
|
75
112
|
return {
|
|
76
113
|
text: titleText,
|
|
77
114
|
style: titleStyle,
|
|
78
|
-
height:
|
|
79
|
-
margin:
|
|
115
|
+
height: totalTextHeight,
|
|
116
|
+
margin: titleMargin,
|
|
80
117
|
qa: title === null || title === void 0 ? void 0 : title.qa,
|
|
81
118
|
contentRows,
|
|
82
119
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { PreparedTitle } from '../../hooks';
|
|
3
|
-
type Props = PreparedTitle
|
|
4
|
-
|
|
3
|
+
type Props = PreparedTitle & {
|
|
4
|
+
htmlLayout?: HTMLElement | null;
|
|
5
|
+
};
|
|
6
|
+
export declare const Title: (props: Props) => React.JSX.Element | null;
|
|
5
7
|
export {};
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { HtmlLayer } from '../../hooks/useShapes/HtmlLayer';
|
|
2
3
|
export const Title = (props) => {
|
|
3
|
-
const { style, qa, contentRows } = props;
|
|
4
|
+
const { style, qa, contentRows, html, htmlElements, htmlLayout } = props;
|
|
5
|
+
if (html) {
|
|
6
|
+
if (!htmlLayout || !htmlElements) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return React.createElement(HtmlLayer, { htmlLayout: htmlLayout, preparedData: { htmlElements } });
|
|
10
|
+
}
|
|
4
11
|
return (React.createElement("text", { dominantBaseline: "hanging", textAnchor: "middle", style: {
|
|
5
12
|
fill: style === null || style === void 0 ? void 0 : style.fontColor,
|
|
6
13
|
fontSize: style === null || style === void 0 ? void 0 : style.fontSize,
|
|
7
14
|
fontWeight: style === null || style === void 0 ? void 0 : style.fontWeight,
|
|
8
|
-
}, "data-qa": qa }, contentRows.map((row, i) => (React.createElement("tspan", { key: i, x: row.x, y: row.y, dominantBaseline: "hanging", dangerouslySetInnerHTML: { __html: row.text } })))));
|
|
15
|
+
}, "data-qa": qa }, contentRows === null || contentRows === void 0 ? void 0 : contentRows.map((row, i) => (React.createElement("tspan", { key: i, x: row.x, y: row.y, dominantBaseline: "hanging", dangerouslySetInnerHTML: { __html: row.text } })))));
|
|
9
16
|
};
|
|
@@ -5,6 +5,7 @@ export interface ChartTitle {
|
|
|
5
5
|
/**
|
|
6
6
|
* Maximum number of text rows. If the text exceeds this limit, it is truncated with an ellipsis.
|
|
7
7
|
* Default: 1
|
|
8
|
+
* Not applicable when `html: true` — HTML content manages its own layout.
|
|
8
9
|
*/
|
|
9
10
|
maxRowCount?: number;
|
|
10
11
|
/**
|
|
@@ -17,4 +18,21 @@ export interface ChartTitle {
|
|
|
17
18
|
* It is assigned as a data-qa attribute to an element.
|
|
18
19
|
*/
|
|
19
20
|
qa?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Enables HTML rendering for the chart title.
|
|
23
|
+
* When true, the title is rendered as an HTML element on top of the SVG
|
|
24
|
+
* instead of an SVG text node. This allows using arbitrary HTML tags
|
|
25
|
+
* (e.g. links, styled spans) that cannot be embedded in SVG.
|
|
26
|
+
* The element will be displayed outside the box of the SVG element.
|
|
27
|
+
*/
|
|
28
|
+
html?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Maximum height of the title area. Accepts a pixel value (`number` or `"100px"`)
|
|
31
|
+
* or a percentage string (`"50%"`) relative to the full chart height.
|
|
32
|
+
* When the title content exceeds this height, it is clipped.
|
|
33
|
+
* Only applicable when `html: true`.
|
|
34
|
+
*
|
|
35
|
+
* Default: 50%
|
|
36
|
+
*/
|
|
37
|
+
maxHeight?: string | number;
|
|
20
38
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TextRowData } from '../components/types';
|
|
2
|
-
import type { ChartBrush, ChartData, ChartMargin, ChartTitle, ChartZoom, DeepRequired } from '../types';
|
|
2
|
+
import type { BaseTextStyle, ChartBrush, ChartData, ChartMargin, ChartTitle, ChartZoom, DeepRequired } from '../types';
|
|
3
|
+
import type { HtmlItem } from '../types/chart-ui';
|
|
3
4
|
export type PreparedZoom = DeepRequired<Omit<ChartZoom, 'enabled' | 'brush'>> & DeepRequired<{
|
|
4
5
|
brush: ChartBrush;
|
|
5
6
|
}>;
|
|
@@ -7,10 +8,12 @@ export type PreparedChart = {
|
|
|
7
8
|
margin: ChartMargin;
|
|
8
9
|
zoom: PreparedZoom | null;
|
|
9
10
|
};
|
|
10
|
-
export type PreparedTitle = Omit<ChartTitle, 'margin'> & {
|
|
11
|
+
export type PreparedTitle = Omit<ChartTitle, 'margin' | 'style'> & {
|
|
11
12
|
height: number;
|
|
12
13
|
margin: number;
|
|
13
|
-
|
|
14
|
+
style: BaseTextStyle;
|
|
15
|
+
contentRows?: TextRowData[];
|
|
16
|
+
htmlElements?: HtmlItem[];
|
|
14
17
|
};
|
|
15
18
|
export type PreparedTooltip = ChartData['tooltip'] & {
|
|
16
19
|
enabled: boolean;
|
|
@@ -19,8 +19,9 @@ export const HtmlLayer = (props) => {
|
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
return (React.createElement(Portal, { container: htmlLayout }, items.map((item, index) => {
|
|
22
|
-
var _a, _b, _c;
|
|
23
|
-
const
|
|
24
|
-
|
|
22
|
+
var _a, _b, _c, _d;
|
|
23
|
+
const scope = (_a = item.scope) !== null && _a !== void 0 ? _a : 'plot';
|
|
24
|
+
const style = Object.assign(Object.assign({}, item.style), { color: (_c = (_b = item.style) === null || _b === void 0 ? void 0 : _b.color) !== null && _c !== void 0 ? _c : (_d = item.style) === null || _d === void 0 ? void 0 : _d.fontColor, position: 'absolute', left: item.x, top: item.y });
|
|
25
|
+
return (React.createElement("div", { className: b('html-layer-item', { plot: scope === 'plot' }), key: index, dangerouslySetInnerHTML: { __html: item.content }, style: style }));
|
|
25
26
|
})));
|
|
26
27
|
};
|
|
@@ -81,8 +81,8 @@ export const LineSeriesShapes = (args) => {
|
|
|
81
81
|
function handleShapeHover(data) {
|
|
82
82
|
hoveredDataRef.current = data;
|
|
83
83
|
const selected = (data === null || data === void 0 ? void 0 : data.filter((d) => d.series.type === 'line')) || [];
|
|
84
|
+
const selectedDataItems = selected.map((d) => d.data);
|
|
84
85
|
const selectedSeriesIds = selected.map((d) => { var _a; return (_a = d.series) === null || _a === void 0 ? void 0 : _a.id; });
|
|
85
|
-
const closestChunk = selected.find((d) => d.closest);
|
|
86
86
|
lineSelection.datum((d, index, list) => {
|
|
87
87
|
const elementSelection = select(list[index]);
|
|
88
88
|
const hovered = Boolean(hoverEnabled && selectedSeriesIds.includes(d.id));
|
|
@@ -118,7 +118,7 @@ export const LineSeriesShapes = (args) => {
|
|
|
118
118
|
});
|
|
119
119
|
markerSelection.datum((d, index, list) => {
|
|
120
120
|
const elementSelection = select(list[index]);
|
|
121
|
-
const hovered = Boolean(hoverEnabled && d.point.data
|
|
121
|
+
const hovered = Boolean(hoverEnabled && selectedDataItems.includes(d.point.data));
|
|
122
122
|
if (d.hovered !== hovered) {
|
|
123
123
|
d.hovered = hovered;
|
|
124
124
|
elementSelection.attr('visibility', getMarkerVisibility(d));
|
|
@@ -141,7 +141,7 @@ export const LineSeriesShapes = (args) => {
|
|
|
141
141
|
hoverMarkersSvgElement.selectAll('*').remove();
|
|
142
142
|
if (hoverEnabled && selected.length > 0) {
|
|
143
143
|
const hoverOnlyMarkers = [];
|
|
144
|
-
for (const chunk of selected
|
|
144
|
+
for (const chunk of selected) {
|
|
145
145
|
const seriesData = preparedData.find((pd) => pd.id === chunk.series.id);
|
|
146
146
|
if (!seriesData) {
|
|
147
147
|
continue;
|
|
@@ -24,6 +24,8 @@ export interface HtmlItem {
|
|
|
24
24
|
height: number;
|
|
25
25
|
};
|
|
26
26
|
style?: BaseTextStyle & React.CSSProperties;
|
|
27
|
+
/** Coordinate space for positioning: 'plot' uses the plot area origin, 'chart' uses the full chart origin. Defaults to 'plot'. */
|
|
28
|
+
scope?: 'plot' | 'chart';
|
|
27
29
|
}
|
|
28
30
|
export interface ShapeDataWithLabels {
|
|
29
31
|
svgLabels: LabelData[];
|