@gravity-ui/chartkit 4.5.0 → 4.6.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/build/plugins/d3/renderer/D3Widget.js +11 -1
- package/build/plugins/d3/renderer/components/AxisX.d.ts +1 -2
- package/build/plugins/d3/renderer/components/AxisX.js +39 -61
- package/build/plugins/d3/renderer/components/AxisY.js +28 -31
- package/build/plugins/d3/renderer/components/Chart.js +19 -7
- package/build/plugins/d3/renderer/components/Legend.d.ts +5 -6
- package/build/plugins/d3/renderer/components/Legend.js +139 -84
- package/build/plugins/d3/renderer/components/styles.css +27 -0
- package/build/plugins/d3/renderer/constants/defaults/axis.d.ts +5 -0
- package/build/plugins/d3/renderer/constants/defaults/axis.js +5 -0
- package/build/plugins/d3/renderer/constants/defaults/index.d.ts +2 -0
- package/build/plugins/d3/renderer/constants/defaults/index.js +2 -0
- package/build/plugins/d3/renderer/constants/defaults/legend.d.ts +4 -0
- package/build/plugins/d3/renderer/constants/defaults/legend.js +8 -0
- package/build/plugins/d3/renderer/{constants.d.ts → constants/index.d.ts} +1 -1
- package/build/plugins/d3/renderer/{constants.js → constants/index.js} +1 -1
- package/build/plugins/d3/renderer/hooks/useAxisScales/index.d.ts +3 -1
- package/build/plugins/d3/renderer/hooks/useAxisScales/index.js +64 -62
- package/build/plugins/d3/renderer/hooks/useChartDimensions/index.d.ts +7 -4
- package/build/plugins/d3/renderer/hooks/useChartDimensions/index.js +65 -7
- package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.d.ts +6 -0
- package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.js +7 -0
- package/build/plugins/d3/renderer/hooks/useChartOptions/chart.d.ts +1 -3
- package/build/plugins/d3/renderer/hooks/useChartOptions/chart.js +9 -68
- package/build/plugins/d3/renderer/hooks/useChartOptions/index.d.ts +3 -1
- package/build/plugins/d3/renderer/hooks/useChartOptions/index.js +3 -8
- package/build/plugins/d3/renderer/hooks/useChartOptions/types.d.ts +3 -6
- package/build/plugins/d3/renderer/hooks/useChartOptions/x-axis.js +4 -2
- package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.d.ts +3 -2
- package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +31 -4
- package/build/plugins/d3/renderer/hooks/useSeries/constants.d.ts +1 -1
- package/build/plugins/d3/renderer/hooks/useSeries/constants.js +1 -1
- package/build/plugins/d3/renderer/hooks/useSeries/index.d.ts +19 -7
- package/build/plugins/d3/renderer/hooks/useSeries/index.js +26 -8
- package/build/plugins/d3/renderer/hooks/useSeries/prepare-legend.d.ts +27 -0
- package/build/plugins/d3/renderer/hooks/useSeries/prepare-legend.js +92 -0
- package/build/plugins/d3/renderer/hooks/useSeries/prepareSeries.d.ts +1 -2
- package/build/plugins/d3/renderer/hooks/useSeries/types.d.ts +26 -1
- package/build/plugins/d3/renderer/hooks/useShapes/bar-x.js +2 -1
- package/build/plugins/d3/renderer/utils/axis-generators/bottom.d.ts +21 -0
- package/build/plugins/d3/renderer/utils/axis-generators/bottom.js +104 -0
- package/build/plugins/d3/renderer/utils/axis-generators/index.d.ts +1 -0
- package/build/plugins/d3/renderer/utils/axis-generators/index.js +1 -0
- package/build/plugins/d3/renderer/utils/axis.d.ts +22 -0
- package/build/plugins/d3/renderer/utils/axis.js +43 -0
- package/build/plugins/d3/renderer/utils/index.d.ts +7 -4
- package/build/plugins/d3/renderer/utils/index.js +16 -6
- package/build/plugins/d3/renderer/utils/text.d.ts +20 -2
- package/build/plugins/d3/renderer/utils/text.js +51 -1
- package/build/plugins/d3/renderer/utils/time.d.ts +3 -0
- package/build/plugins/d3/renderer/utils/time.js +34 -0
- package/build/plugins/highcharts/renderer/components/HighchartsComponent.js +3 -3
- package/build/plugins/shared/format-number/format-number.d.ts +1 -0
- package/build/plugins/shared/format-number/format-number.js +19 -20
- package/build/types/widget-data/axis.d.ts +13 -1
- package/build/types/widget-data/legend.d.ts +24 -7
- package/build/utils/common.d.ts +1 -0
- package/build/utils/common.js +1 -1
- package/build/utils/index.d.ts +1 -1
- package/build/utils/index.js +1 -1
- package/package.json +1 -1
- package/build/plugins/d3/renderer/hooks/useChartOptions/legend.d.ts +0 -6
- package/build/plugins/d3/renderer/hooks/useChartOptions/legend.js +0 -12
|
@@ -3,9 +3,11 @@ import { group, scaleOrdinal } from 'd3';
|
|
|
3
3
|
import { DEFAULT_PALETTE } from '../../constants';
|
|
4
4
|
import { getSeriesNames } from '../../utils';
|
|
5
5
|
import { getActiveLegendItems, getAllLegendItems } from './utils';
|
|
6
|
+
import { getPreparedLegend, getLegendComponents } from './prepare-legend';
|
|
6
7
|
import { prepareSeries } from './prepareSeries';
|
|
7
8
|
export const useSeries = (args) => {
|
|
8
|
-
const { series: { data: series },
|
|
9
|
+
const { chartWidth, chartHeight, chartMargin, legend, preparedYAxis, series: { data: series }, } = args;
|
|
10
|
+
const preparedLegend = React.useMemo(() => getPreparedLegend({ legend, series }), [legend, series]);
|
|
9
11
|
const preparedSeries = React.useMemo(() => {
|
|
10
12
|
const seriesNames = getSeriesNames(series);
|
|
11
13
|
const colorScale = scaleOrdinal(seriesNames, DEFAULT_PALETTE);
|
|
@@ -14,12 +16,12 @@ export const useSeries = (args) => {
|
|
|
14
16
|
acc.push(...prepareSeries({
|
|
15
17
|
type: seriesType,
|
|
16
18
|
series: seriesList,
|
|
17
|
-
legend,
|
|
19
|
+
legend: preparedLegend,
|
|
18
20
|
colorScale,
|
|
19
21
|
}));
|
|
20
22
|
return acc;
|
|
21
23
|
}, []);
|
|
22
|
-
}, [series,
|
|
24
|
+
}, [series, preparedLegend]);
|
|
23
25
|
const [activeLegendItems, setActiveLegendItems] = React.useState(getActiveLegendItems(preparedSeries));
|
|
24
26
|
const chartSeries = React.useMemo(() => {
|
|
25
27
|
return preparedSeries.map((singleSeries) => {
|
|
@@ -29,10 +31,16 @@ export const useSeries = (args) => {
|
|
|
29
31
|
return singleSeries;
|
|
30
32
|
});
|
|
31
33
|
}, [preparedSeries, activeLegendItems]);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const { legendConfig, legendItems } = React.useMemo(() => {
|
|
35
|
+
return getLegendComponents({
|
|
36
|
+
chartHeight,
|
|
37
|
+
chartMargin,
|
|
38
|
+
chartWidth,
|
|
39
|
+
series: chartSeries,
|
|
40
|
+
preparedLegend,
|
|
41
|
+
preparedYAxis,
|
|
42
|
+
});
|
|
43
|
+
}, [chartWidth, chartHeight, chartMargin, chartSeries, preparedLegend, preparedYAxis]);
|
|
36
44
|
const handleLegendItemClick = React.useCallback(({ name, metaKey }) => {
|
|
37
45
|
const onlyItemSelected = activeLegendItems.length === 1 && activeLegendItems.includes(name);
|
|
38
46
|
let nextActiveLegendItems;
|
|
@@ -50,5 +58,15 @@ export const useSeries = (args) => {
|
|
|
50
58
|
}
|
|
51
59
|
setActiveLegendItems(nextActiveLegendItems);
|
|
52
60
|
}, [preparedSeries, activeLegendItems]);
|
|
53
|
-
|
|
61
|
+
// FIXME: remove effect. It initiates extra rerender
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
setActiveLegendItems(getActiveLegendItems(preparedSeries));
|
|
64
|
+
}, [preparedSeries]);
|
|
65
|
+
return {
|
|
66
|
+
legendItems,
|
|
67
|
+
legendConfig,
|
|
68
|
+
preparedLegend,
|
|
69
|
+
preparedSeries: chartSeries,
|
|
70
|
+
handleLegendItemClick,
|
|
71
|
+
};
|
|
54
72
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ChartKitWidgetData } from '../../../../../types/widget-data';
|
|
2
|
+
import type { PreparedAxis, PreparedChart } from '../useChartOptions/types';
|
|
3
|
+
import type { PreparedLegend, PreparedSeries, LegendItem } from './types';
|
|
4
|
+
export declare const getPreparedLegend: (args: {
|
|
5
|
+
legend: ChartKitWidgetData['legend'];
|
|
6
|
+
series: ChartKitWidgetData['series']['data'];
|
|
7
|
+
}) => PreparedLegend;
|
|
8
|
+
export declare const getLegendComponents: (args: {
|
|
9
|
+
chartWidth: number;
|
|
10
|
+
chartHeight: number;
|
|
11
|
+
chartMargin: PreparedChart['margin'];
|
|
12
|
+
series: PreparedSeries[];
|
|
13
|
+
preparedLegend: PreparedLegend;
|
|
14
|
+
preparedYAxis: PreparedAxis[];
|
|
15
|
+
}) => {
|
|
16
|
+
legendConfig: {
|
|
17
|
+
offset: {
|
|
18
|
+
left: number;
|
|
19
|
+
top: number;
|
|
20
|
+
};
|
|
21
|
+
pagination: {
|
|
22
|
+
limit: number;
|
|
23
|
+
maxPage: number;
|
|
24
|
+
} | undefined;
|
|
25
|
+
};
|
|
26
|
+
legendItems: LegendItem[][];
|
|
27
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import clone from 'lodash/clone';
|
|
2
|
+
import get from 'lodash/get';
|
|
3
|
+
import merge from 'lodash/merge';
|
|
4
|
+
import { select } from 'd3';
|
|
5
|
+
import { legendDefaults } from '../../constants';
|
|
6
|
+
import { getHorisontalSvgTextHeight } from '../../utils';
|
|
7
|
+
import { getBoundsWidth } from '../useChartDimensions';
|
|
8
|
+
export const getPreparedLegend = (args) => {
|
|
9
|
+
const { legend, series } = args;
|
|
10
|
+
const enabled = typeof (legend === null || legend === void 0 ? void 0 : legend.enabled) === 'boolean' ? legend === null || legend === void 0 ? void 0 : legend.enabled : series.length > 1;
|
|
11
|
+
const defaultItemStyle = clone(legendDefaults.itemStyle);
|
|
12
|
+
const itemStyle = get(legend, 'itemStyle');
|
|
13
|
+
const computedItemStyle = merge(defaultItemStyle, itemStyle);
|
|
14
|
+
const lineHeight = getHorisontalSvgTextHeight({ text: 'Tmp', style: computedItemStyle });
|
|
15
|
+
const height = enabled ? lineHeight : 0;
|
|
16
|
+
return {
|
|
17
|
+
align: get(legend, 'align', legendDefaults.align),
|
|
18
|
+
enabled,
|
|
19
|
+
height,
|
|
20
|
+
itemDistance: get(legend, 'itemDistance', legendDefaults.itemDistance),
|
|
21
|
+
itemStyle: computedItemStyle,
|
|
22
|
+
lineHeight,
|
|
23
|
+
margin: get(legend, 'margin', legendDefaults.margin),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const getFlattenLegendItems = (series) => {
|
|
27
|
+
return series.reduce((acc, s) => {
|
|
28
|
+
const legendEnabled = get(s, 'legend.enabled', true);
|
|
29
|
+
if (legendEnabled) {
|
|
30
|
+
acc.push(Object.assign(Object.assign({}, s), { symbol: s.legend.symbol }));
|
|
31
|
+
}
|
|
32
|
+
return acc;
|
|
33
|
+
}, []);
|
|
34
|
+
};
|
|
35
|
+
const getGroupedLegendItems = (args) => {
|
|
36
|
+
const { maxLegendWidth, items, preparedLegend } = args;
|
|
37
|
+
const result = [[]];
|
|
38
|
+
let textWidthsInLine = [0];
|
|
39
|
+
let lineIndex = 0;
|
|
40
|
+
items.forEach((item) => {
|
|
41
|
+
select(document.body)
|
|
42
|
+
.append('text')
|
|
43
|
+
.text(item.name)
|
|
44
|
+
.style('font-size', preparedLegend.itemStyle.fontSize)
|
|
45
|
+
.each(function () {
|
|
46
|
+
const resultItem = clone(item);
|
|
47
|
+
const textWidth = this.getBoundingClientRect().width;
|
|
48
|
+
resultItem.textWidth = textWidth;
|
|
49
|
+
textWidthsInLine.push(textWidth);
|
|
50
|
+
const textsWidth = textWidthsInLine.reduce((acc, width) => acc + width, 0);
|
|
51
|
+
result[lineIndex].push(resultItem);
|
|
52
|
+
const symbolsWidth = result[lineIndex].reduce((acc, { symbol }) => {
|
|
53
|
+
return acc + symbol.width + symbol.padding;
|
|
54
|
+
}, 0);
|
|
55
|
+
const distancesWidth = (result[lineIndex].length - 1) * preparedLegend.itemDistance;
|
|
56
|
+
const isOverfilled = maxLegendWidth < textsWidth + symbolsWidth + distancesWidth;
|
|
57
|
+
if (isOverfilled) {
|
|
58
|
+
result[lineIndex].pop();
|
|
59
|
+
lineIndex += 1;
|
|
60
|
+
textWidthsInLine = [textWidth];
|
|
61
|
+
const nextLineIndex = lineIndex;
|
|
62
|
+
result[nextLineIndex] = [];
|
|
63
|
+
result[nextLineIndex].push(resultItem);
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.remove();
|
|
67
|
+
});
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
70
|
+
export const getLegendComponents = (args) => {
|
|
71
|
+
const { chartWidth, chartHeight, chartMargin, series, preparedLegend, preparedYAxis } = args;
|
|
72
|
+
const maxLegendWidth = getBoundsWidth({ chartWidth, chartMargin, preparedYAxis });
|
|
73
|
+
const maxLegendHeight = (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
|
|
74
|
+
const flattenLegendItems = getFlattenLegendItems(series);
|
|
75
|
+
const items = getGroupedLegendItems({
|
|
76
|
+
maxLegendWidth,
|
|
77
|
+
items: flattenLegendItems,
|
|
78
|
+
preparedLegend,
|
|
79
|
+
});
|
|
80
|
+
let legendHeight = preparedLegend.lineHeight * items.length;
|
|
81
|
+
let pagination;
|
|
82
|
+
if (maxLegendHeight < legendHeight) {
|
|
83
|
+
const limit = Math.floor(maxLegendHeight / preparedLegend.lineHeight);
|
|
84
|
+
const maxPage = Math.ceil(items.length / limit);
|
|
85
|
+
pagination = { limit, maxPage };
|
|
86
|
+
legendHeight = maxLegendHeight;
|
|
87
|
+
}
|
|
88
|
+
preparedLegend.height = legendHeight;
|
|
89
|
+
const top = chartHeight - chartMargin.bottom - preparedLegend.height + preparedLegend.lineHeight / 2;
|
|
90
|
+
const offset = { left: chartMargin.left, top };
|
|
91
|
+
return { legendConfig: { offset, pagination }, legendItems: items };
|
|
92
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ScaleOrdinal } from 'd3';
|
|
2
2
|
import type { ChartKitWidgetSeries } from '../../../../../types/widget-data';
|
|
3
|
-
import type { PreparedLegend } from '
|
|
4
|
-
import type { PreparedSeries } from './types';
|
|
3
|
+
import type { PreparedLegend, PreparedSeries } from './types';
|
|
5
4
|
export declare function prepareSeries(args: {
|
|
6
5
|
type: ChartKitWidgetSeries['type'];
|
|
7
6
|
series: ChartKitWidgetSeries[];
|
|
@@ -1,8 +1,33 @@
|
|
|
1
|
-
import { BarXSeries, BarXSeriesData, BaseTextStyle, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData } from '../../../../../types/widget-data';
|
|
1
|
+
import { BarXSeries, BarXSeriesData, BaseTextStyle, ChartKitWidgetLegend, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData } from '../../../../../types/widget-data';
|
|
2
2
|
export type RectLegendSymbol = {
|
|
3
3
|
shape: 'rect';
|
|
4
4
|
} & Required<RectLegendSymbolOptions>;
|
|
5
5
|
export type PreparedLegendSymbol = RectLegendSymbol;
|
|
6
|
+
export type PreparedLegend = Required<ChartKitWidgetLegend> & {
|
|
7
|
+
height: number;
|
|
8
|
+
lineHeight: number;
|
|
9
|
+
};
|
|
10
|
+
export type OnLegendItemClick = (data: {
|
|
11
|
+
name: string;
|
|
12
|
+
metaKey: boolean;
|
|
13
|
+
}) => void;
|
|
14
|
+
export type LegendItem = {
|
|
15
|
+
color: string;
|
|
16
|
+
name: string;
|
|
17
|
+
symbol: PreparedLegendSymbol;
|
|
18
|
+
textWidth: number;
|
|
19
|
+
visible?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type LegendConfig = {
|
|
22
|
+
offset: {
|
|
23
|
+
left: number;
|
|
24
|
+
top: number;
|
|
25
|
+
};
|
|
26
|
+
pagination?: {
|
|
27
|
+
limit: number;
|
|
28
|
+
maxPage: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
6
31
|
type BasePreparedSeries = {
|
|
7
32
|
color: string;
|
|
8
33
|
name: string;
|
|
@@ -4,6 +4,7 @@ import get from 'lodash/get';
|
|
|
4
4
|
import { block } from '../../../../../utils/cn';
|
|
5
5
|
import { getDataCategoryValue } from '../../utils';
|
|
6
6
|
import { DEFAULT_BAR_X_SERIES_OPTIONS } from './defaults';
|
|
7
|
+
const MIN_RECT_WIDTH = 1;
|
|
7
8
|
const MIN_RECT_GAP = 1;
|
|
8
9
|
const MIN_GROUP_GAP = 1;
|
|
9
10
|
const DEFAULT_LABEL_PADDING = 7;
|
|
@@ -72,7 +73,7 @@ function prepareData(args) {
|
|
|
72
73
|
const groupGap = Math.max(bandWidth * groupPadding, MIN_GROUP_GAP);
|
|
73
74
|
const groupWidth = bandWidth - groupGap;
|
|
74
75
|
const rectGap = Math.max(bandWidth * barPadding, MIN_RECT_GAP);
|
|
75
|
-
const rectWidth = Math.min(groupWidth / maxGroupSize - rectGap, barMaxWidth);
|
|
76
|
+
const rectWidth = Math.max(MIN_RECT_WIDTH, Math.min(groupWidth / maxGroupSize - rectGap, barMaxWidth));
|
|
76
77
|
const result = [];
|
|
77
78
|
Object.entries(data).forEach(([xValue, val]) => {
|
|
78
79
|
const stacks = Object.values(val);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AxisDomain, AxisScale, Selection } from 'd3';
|
|
2
|
+
import { BaseTextStyle } from '../../../../../types';
|
|
3
|
+
type AxisBottomArgs = {
|
|
4
|
+
scale: AxisScale<AxisDomain>;
|
|
5
|
+
ticks: {
|
|
6
|
+
count?: number;
|
|
7
|
+
maxTickCount: number;
|
|
8
|
+
labelFormat: (value: any) => string;
|
|
9
|
+
labelsPaddings?: number;
|
|
10
|
+
labelsMargin?: number;
|
|
11
|
+
labelsStyle?: BaseTextStyle;
|
|
12
|
+
size: number;
|
|
13
|
+
autoRotation?: boolean;
|
|
14
|
+
};
|
|
15
|
+
domain: {
|
|
16
|
+
size: number;
|
|
17
|
+
color?: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function axisBottom(args: AxisBottomArgs): (selection: Selection<SVGGElement, unknown, null, undefined>) => void;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { select } from 'd3';
|
|
2
|
+
import { getXAxisItems, getXAxisOffset, getXTickPosition } from '../axis';
|
|
3
|
+
import { hasOverlappingLabels, setEllipsisForOverflowText } from '../text';
|
|
4
|
+
function addDomain(selection, options) {
|
|
5
|
+
const { size, color } = options;
|
|
6
|
+
selection
|
|
7
|
+
.selectAll('.domain')
|
|
8
|
+
.data([null])
|
|
9
|
+
.enter()
|
|
10
|
+
.insert('path', '.tick')
|
|
11
|
+
.attr('class', 'domain')
|
|
12
|
+
.attr('stroke', color || 'currentColor')
|
|
13
|
+
.attr('d', `M0,0V0H${size}`);
|
|
14
|
+
}
|
|
15
|
+
export function axisBottom(args) {
|
|
16
|
+
const { scale, ticks: { labelFormat, labelsPaddings = 0, labelsMargin = 0, labelsStyle, size: tickSize, count: ticksCount, maxTickCount, autoRotation = true, }, domain: { size: domainSize, color: domainColor }, } = args;
|
|
17
|
+
const offset = getXAxisOffset();
|
|
18
|
+
const spacing = Math.max(tickSize, 0) + labelsMargin;
|
|
19
|
+
const position = getXTickPosition({ scale, offset });
|
|
20
|
+
const values = getXAxisItems({ scale, count: ticksCount, maxCount: maxTickCount });
|
|
21
|
+
return function (selection) {
|
|
22
|
+
var _a, _b, _c, _e;
|
|
23
|
+
const x = ((_b = (_a = selection.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.x) || 0;
|
|
24
|
+
const right = x + domainSize;
|
|
25
|
+
selection
|
|
26
|
+
.selectAll('.tick')
|
|
27
|
+
.data(values)
|
|
28
|
+
.order()
|
|
29
|
+
.join((el) => {
|
|
30
|
+
const tick = el.append('g').attr('class', 'tick');
|
|
31
|
+
tick.append('line').attr('stroke', 'currentColor').attr('y2', tickSize);
|
|
32
|
+
tick.append('text')
|
|
33
|
+
.attr('fill', 'currentColor')
|
|
34
|
+
.attr('y', spacing)
|
|
35
|
+
.attr('dy', '0.71em')
|
|
36
|
+
.text(labelFormat);
|
|
37
|
+
return tick;
|
|
38
|
+
})
|
|
39
|
+
.attr('transform', function (d) {
|
|
40
|
+
return `translate(${position(d) + offset},0)`;
|
|
41
|
+
});
|
|
42
|
+
// Remove tick that has the same x coordinate like domain
|
|
43
|
+
selection
|
|
44
|
+
.select('.tick')
|
|
45
|
+
.filter((d) => {
|
|
46
|
+
return position(d) === 0;
|
|
47
|
+
})
|
|
48
|
+
.select('line')
|
|
49
|
+
.remove();
|
|
50
|
+
const labels = selection.selectAll('.tick text');
|
|
51
|
+
const labelNodes = labels.nodes();
|
|
52
|
+
const overlapping = hasOverlappingLabels({
|
|
53
|
+
width: domainSize,
|
|
54
|
+
labels: values.map(labelFormat),
|
|
55
|
+
padding: labelsPaddings,
|
|
56
|
+
style: labelsStyle,
|
|
57
|
+
});
|
|
58
|
+
const rotationAngle = overlapping && autoRotation ? '-45' : undefined;
|
|
59
|
+
if (rotationAngle) {
|
|
60
|
+
const labelHeight = (_e = (_c = labelNodes[0]) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect()) === null || _e === void 0 ? void 0 : _e.height;
|
|
61
|
+
const labelOffset = (labelHeight / 2 + labelsMargin) / 2;
|
|
62
|
+
labels
|
|
63
|
+
.attr('text-anchor', 'end')
|
|
64
|
+
.attr('transform', `rotate(${rotationAngle}) translate(-${labelOffset}, -${labelOffset})`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// remove overlapping labels
|
|
68
|
+
let elementX = 0;
|
|
69
|
+
selection
|
|
70
|
+
.selectAll('.tick')
|
|
71
|
+
.filter(function () {
|
|
72
|
+
const node = this;
|
|
73
|
+
const r = node.getBoundingClientRect();
|
|
74
|
+
if (r.left < elementX) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
elementX = r.right + labelsPaddings;
|
|
78
|
+
return false;
|
|
79
|
+
})
|
|
80
|
+
.remove();
|
|
81
|
+
// add an ellipsis to the labels that go beyond the boundaries of the chart
|
|
82
|
+
labels.each(function (_d, i, nodes) {
|
|
83
|
+
if (i === nodes.length - 1) {
|
|
84
|
+
const currentElement = this;
|
|
85
|
+
const prevElement = nodes[i - 1];
|
|
86
|
+
const text = select(currentElement);
|
|
87
|
+
const currentElementPosition = currentElement.getBoundingClientRect();
|
|
88
|
+
const prevElementPosition = prevElement === null || prevElement === void 0 ? void 0 : prevElement.getBoundingClientRect();
|
|
89
|
+
const lackingSpace = Math.max(0, currentElementPosition.right - right);
|
|
90
|
+
if (lackingSpace) {
|
|
91
|
+
const remainSpace = right - ((prevElementPosition === null || prevElementPosition === void 0 ? void 0 : prevElementPosition.right) || 0) - labelsPaddings;
|
|
92
|
+
const translateX = currentElementPosition.width / 2 - lackingSpace;
|
|
93
|
+
text.attr('text-anchor', 'end').attr('transform', `translate(${translateX},0)`);
|
|
94
|
+
setEllipsisForOverflowText(text, remainSpace);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
selection
|
|
100
|
+
.call(addDomain, { size: domainSize, color: domainColor })
|
|
101
|
+
.attr('text-anchor', 'middle')
|
|
102
|
+
.style('font-size', (labelsStyle === null || labelsStyle === void 0 ? void 0 : labelsStyle.fontSize) || '');
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './bottom';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './bottom';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PreparedAxis } from '../hooks';
|
|
2
|
+
import { AxisDomain, AxisScale, ScaleBand } from 'd3';
|
|
3
|
+
export declare function getTicksCount({ axis, range }: {
|
|
4
|
+
axis: PreparedAxis;
|
|
5
|
+
range: number;
|
|
6
|
+
}): number | undefined;
|
|
7
|
+
export declare function isBandScale(scale: AxisScale<AxisDomain>): scale is ScaleBand<AxisDomain>;
|
|
8
|
+
export declare function getScaleTicks(scale: AxisScale<AxisDomain>, ticksCount?: number): any;
|
|
9
|
+
export declare function getXAxisOffset(): 0 | 0.5;
|
|
10
|
+
export declare function getXTickPosition({ scale, offset }: {
|
|
11
|
+
scale: AxisScale<AxisDomain>;
|
|
12
|
+
offset: number;
|
|
13
|
+
}): (d: AxisDomain) => number;
|
|
14
|
+
export declare function getXAxisItems({ scale, count, maxCount, }: {
|
|
15
|
+
scale: AxisScale<AxisDomain>;
|
|
16
|
+
count?: number;
|
|
17
|
+
maxCount: number;
|
|
18
|
+
}): any;
|
|
19
|
+
export declare function getMaxTickCount({ axis, width }: {
|
|
20
|
+
axis: PreparedAxis;
|
|
21
|
+
width: number;
|
|
22
|
+
}): number;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function getTicksCount({ axis, range }) {
|
|
2
|
+
let ticksCount;
|
|
3
|
+
if (axis.ticks.pixelInterval) {
|
|
4
|
+
ticksCount = Math.ceil(range / axis.ticks.pixelInterval);
|
|
5
|
+
}
|
|
6
|
+
return ticksCount;
|
|
7
|
+
}
|
|
8
|
+
export function isBandScale(scale) {
|
|
9
|
+
return 'bandwidth' in scale && typeof scale.bandwidth === 'function';
|
|
10
|
+
}
|
|
11
|
+
export function getScaleTicks(scale, ticksCount) {
|
|
12
|
+
return 'ticks' in scale && typeof scale.ticks === 'function'
|
|
13
|
+
? scale.ticks(ticksCount)
|
|
14
|
+
: scale.domain();
|
|
15
|
+
}
|
|
16
|
+
export function getXAxisOffset() {
|
|
17
|
+
return typeof window !== 'undefined' && window.devicePixelRatio > 1 ? 0 : 0.5;
|
|
18
|
+
}
|
|
19
|
+
function number(scale) {
|
|
20
|
+
return (d) => Number(scale(d));
|
|
21
|
+
}
|
|
22
|
+
function center(scale, offset) {
|
|
23
|
+
offset = Math.max(0, scale.bandwidth() - offset * 2) / 2;
|
|
24
|
+
if (scale.round()) {
|
|
25
|
+
offset = Math.round(offset);
|
|
26
|
+
}
|
|
27
|
+
return (d) => Number(scale(d)) + offset;
|
|
28
|
+
}
|
|
29
|
+
export function getXTickPosition({ scale, offset }) {
|
|
30
|
+
return isBandScale(scale) ? center(scale.copy(), offset) : number(scale.copy());
|
|
31
|
+
}
|
|
32
|
+
export function getXAxisItems({ scale, count, maxCount, }) {
|
|
33
|
+
let values = getScaleTicks(scale, count);
|
|
34
|
+
if (values.length > maxCount) {
|
|
35
|
+
const step = Math.ceil(values.length / maxCount);
|
|
36
|
+
values = values.filter((_, i) => i % step === 0);
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
}
|
|
40
|
+
export function getMaxTickCount({ axis, width }) {
|
|
41
|
+
const minTickWidth = parseInt(axis.labels.style.fontSize) + axis.labels.padding;
|
|
42
|
+
return Math.floor(width / minTickWidth);
|
|
43
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { AxisDomain } from 'd3';
|
|
2
|
-
import type { BaseTextStyle, ChartKitWidgetSeries, ChartKitWidgetSeriesData
|
|
2
|
+
import type { BaseTextStyle, ChartKitWidgetSeries, ChartKitWidgetSeriesData } from '../../../../types/widget-data';
|
|
3
|
+
import { PreparedAxis } from '../hooks';
|
|
3
4
|
export * from './math';
|
|
4
5
|
export * from './text';
|
|
6
|
+
export * from './time';
|
|
7
|
+
export * from './axis';
|
|
5
8
|
export type AxisDirection = 'x' | 'y';
|
|
6
9
|
type UnknownSeries = {
|
|
7
10
|
type: ChartKitWidgetSeries['type'];
|
|
@@ -43,10 +46,9 @@ export declare const parseTransformStyle: (style: string | null) => {
|
|
|
43
46
|
y?: number | undefined;
|
|
44
47
|
};
|
|
45
48
|
export declare const formatAxisTickLabel: (args: {
|
|
46
|
-
|
|
49
|
+
axis: PreparedAxis;
|
|
47
50
|
value: AxisDomain;
|
|
48
|
-
|
|
49
|
-
numberFormat?: ChartKitWidgetAxisLabels['numberFormat'];
|
|
51
|
+
step?: number;
|
|
50
52
|
}) => string;
|
|
51
53
|
/**
|
|
52
54
|
* Calculates the height of a text element in a horizontal SVG layout.
|
|
@@ -65,3 +67,4 @@ export declare const getDataCategoryValue: (args: {
|
|
|
65
67
|
categories: string[];
|
|
66
68
|
data: ChartKitWidgetSeriesData;
|
|
67
69
|
}) => string;
|
|
70
|
+
export declare function getClosestPointsRange(axis: PreparedAxis, points: AxisDomain[]): number | undefined;
|
|
@@ -4,8 +4,12 @@ import isNil from 'lodash/isNil';
|
|
|
4
4
|
import { dateTime } from '@gravity-ui/date-utils';
|
|
5
5
|
import { formatNumber } from '../../../shared';
|
|
6
6
|
import { DEFAULT_AXIS_LABEL_FONT_SIZE } from '../constants';
|
|
7
|
+
import { getNumberUnitRate } from '../../../shared/format-number/format-number';
|
|
8
|
+
import { getDefaultDateFormat } from './time';
|
|
7
9
|
export * from './math';
|
|
8
10
|
export * from './text';
|
|
11
|
+
export * from './time';
|
|
12
|
+
export * from './axis';
|
|
9
13
|
const CHARTS_WITHOUT_AXIS = ['pie'];
|
|
10
14
|
/**
|
|
11
15
|
* Checks whether the series should be drawn with axes.
|
|
@@ -89,20 +93,20 @@ export const parseTransformStyle = (style) => {
|
|
|
89
93
|
const y = Number.isNaN(Number(yString)) ? undefined : Number(yString);
|
|
90
94
|
return { x, y };
|
|
91
95
|
};
|
|
92
|
-
const defaultFormatNumberOptions = {
|
|
93
|
-
precision: 0,
|
|
94
|
-
};
|
|
95
96
|
export const formatAxisTickLabel = (args) => {
|
|
96
|
-
const {
|
|
97
|
-
switch (
|
|
97
|
+
const { axis, value, step } = args;
|
|
98
|
+
switch (axis.type) {
|
|
98
99
|
case 'category': {
|
|
99
100
|
return value;
|
|
100
101
|
}
|
|
101
102
|
case 'datetime': {
|
|
102
|
-
|
|
103
|
+
const date = value;
|
|
104
|
+
const format = axis.labels.dateFormat || getDefaultDateFormat(step);
|
|
105
|
+
return dateTime({ input: date }).format(format);
|
|
103
106
|
}
|
|
104
107
|
case 'linear':
|
|
105
108
|
default: {
|
|
109
|
+
const numberFormat = Object.assign({ unitRate: value && step ? getNumberUnitRate(step) : undefined }, axis.labels.numberFormat);
|
|
106
110
|
return formatNumber(value, numberFormat);
|
|
107
111
|
}
|
|
108
112
|
}
|
|
@@ -153,3 +157,9 @@ export const getDataCategoryValue = (args) => {
|
|
|
153
157
|
const categoryValue = extractCategoryValue({ axisDirection, categories, data });
|
|
154
158
|
return categoryValue;
|
|
155
159
|
};
|
|
160
|
+
export function getClosestPointsRange(axis, points) {
|
|
161
|
+
if (axis.type === 'category') {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
return points[1] - points[0];
|
|
165
|
+
}
|
|
@@ -1,2 +1,20 @@
|
|
|
1
|
-
import { Selection } from 'd3
|
|
2
|
-
|
|
1
|
+
import type { Selection } from 'd3';
|
|
2
|
+
import { BaseTextStyle } from '../../../../types';
|
|
3
|
+
export declare function setEllipsisForOverflowText(selection: Selection<SVGTextElement, unknown, null, unknown>, maxWidth: number): void;
|
|
4
|
+
export declare function setEllipsisForOverflowTexts(selection: Selection<SVGTextElement, string, any, unknown>, maxWidth: number): void;
|
|
5
|
+
export declare function hasOverlappingLabels({ width, labels, padding, style, }: {
|
|
6
|
+
width: number;
|
|
7
|
+
labels: string[];
|
|
8
|
+
style?: BaseTextStyle;
|
|
9
|
+
padding?: number;
|
|
10
|
+
}): boolean;
|
|
11
|
+
export declare function getLabelsMaxWidth({ labels, style, transform, }: {
|
|
12
|
+
labels: string[];
|
|
13
|
+
style?: BaseTextStyle;
|
|
14
|
+
transform?: string;
|
|
15
|
+
}): number;
|
|
16
|
+
export declare function getLabelsMaxHeight({ labels, style, transform, }: {
|
|
17
|
+
labels: string[];
|
|
18
|
+
style?: BaseTextStyle;
|
|
19
|
+
transform?: string;
|
|
20
|
+
}): number;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { select } from 'd3';
|
|
1
2
|
export function setEllipsisForOverflowText(selection, maxWidth) {
|
|
2
3
|
var _a, _b;
|
|
3
4
|
let text = selection.text();
|
|
4
|
-
selection.text(null).
|
|
5
|
+
selection.text(null).append('title').text(text);
|
|
5
6
|
const tSpan = selection.append('tspan').text(text);
|
|
6
7
|
let textLength = ((_a = tSpan.node()) === null || _a === void 0 ? void 0 : _a.getComputedTextLength()) || 0;
|
|
7
8
|
while (textLength > maxWidth && text.length > 1) {
|
|
@@ -10,3 +11,52 @@ export function setEllipsisForOverflowText(selection, maxWidth) {
|
|
|
10
11
|
textLength = ((_b = tSpan.node()) === null || _b === void 0 ? void 0 : _b.getComputedTextLength()) || 0;
|
|
11
12
|
}
|
|
12
13
|
}
|
|
14
|
+
export function setEllipsisForOverflowTexts(selection, maxWidth) {
|
|
15
|
+
selection.each(function () {
|
|
16
|
+
setEllipsisForOverflowText(select(this), maxWidth);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function hasOverlappingLabels({ width, labels, padding = 0, style, }) {
|
|
20
|
+
const maxWidth = (width - padding * (labels.length - 1)) / labels.length;
|
|
21
|
+
const textElement = select(document.body)
|
|
22
|
+
.append('text')
|
|
23
|
+
.style('font-size', (style === null || style === void 0 ? void 0 : style.fontSize) || '');
|
|
24
|
+
const result = labels.some((label) => {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
const textWidth = ((_b = (_a = textElement.text(label).node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0;
|
|
27
|
+
return textWidth > maxWidth;
|
|
28
|
+
});
|
|
29
|
+
textElement.remove();
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function renderLabels(selection, { labels, style, transform, }) {
|
|
33
|
+
const text = selection
|
|
34
|
+
.append('g')
|
|
35
|
+
.append('text')
|
|
36
|
+
.attr('transform', transform || '')
|
|
37
|
+
.style('font-size', (style === null || style === void 0 ? void 0 : style.fontSize) || '');
|
|
38
|
+
text.selectAll('tspan')
|
|
39
|
+
.data(labels)
|
|
40
|
+
.enter()
|
|
41
|
+
.append('tspan')
|
|
42
|
+
.attr('x', 0)
|
|
43
|
+
.attr('dy', 0)
|
|
44
|
+
.text((d) => d);
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
export function getLabelsMaxWidth({ labels, style, transform, }) {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
const svg = select(document.body).append('svg');
|
|
50
|
+
svg.call(renderLabels, { labels, style, transform });
|
|
51
|
+
const maxWidth = ((_b = (_a = svg.select('g').node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) || 0;
|
|
52
|
+
svg.remove();
|
|
53
|
+
return maxWidth;
|
|
54
|
+
}
|
|
55
|
+
export function getLabelsMaxHeight({ labels, style, transform, }) {
|
|
56
|
+
var _a, _b;
|
|
57
|
+
const svg = select(document.body).append('svg');
|
|
58
|
+
svg.call(renderLabels, { labels, style, transform });
|
|
59
|
+
const maxHeight = ((_b = (_a = svg.select('g').node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.height) || 0;
|
|
60
|
+
svg.remove();
|
|
61
|
+
return maxHeight;
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const TIME_UNITS = {
|
|
2
|
+
millisecond: 1,
|
|
3
|
+
second: 1000,
|
|
4
|
+
minute: 60000,
|
|
5
|
+
hour: 3600000,
|
|
6
|
+
day: 24 * 3600000,
|
|
7
|
+
week: 7 * 24 * 3600000,
|
|
8
|
+
month: 28 * 24 * 3600000,
|
|
9
|
+
year: 364 * 24 * 3600000,
|
|
10
|
+
};
|
|
11
|
+
export const DATETIME_LABEL_FORMATS = {
|
|
12
|
+
millisecond: 'DD.MM.YY HH:mm:ss.SSS',
|
|
13
|
+
second: 'DD.MM.YY HH:mm:ss',
|
|
14
|
+
minute: 'DD.MM.YY HH:mm',
|
|
15
|
+
hour: 'DD.MM.YY HH:mm',
|
|
16
|
+
day: 'DD.MM.YY',
|
|
17
|
+
week: 'DD.MM.YY',
|
|
18
|
+
month: "MMM 'YY",
|
|
19
|
+
year: 'YYYY',
|
|
20
|
+
};
|
|
21
|
+
function getTimeUnit(range) {
|
|
22
|
+
const units = Object.keys(TIME_UNITS);
|
|
23
|
+
const index = units.findIndex((unit) => range < TIME_UNITS[unit]);
|
|
24
|
+
return index === -1 ? 'year' : units[index - 1];
|
|
25
|
+
}
|
|
26
|
+
export function getDefaultDateFormat(range) {
|
|
27
|
+
if (range) {
|
|
28
|
+
const unit = getTimeUnit(range);
|
|
29
|
+
if (unit in DATETIME_LABEL_FORMATS) {
|
|
30
|
+
return DATETIME_LABEL_FORMATS[unit];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return DATETIME_LABEL_FORMATS.day;
|
|
34
|
+
}
|