@scality/core-ui 0.186.0 → 0.188.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/components/{linetemporalchart/MetricTimespanProvider.d.ts → charts/MetricsTimeSpanProvider.d.ts} +1 -1
- package/dist/components/charts/MetricsTimeSpanProvider.d.ts.map +1 -0
- package/dist/components/{barchartv2/Barchart.component.d.ts → charts/barchart/Barchart.d.ts} +2 -33
- package/dist/components/charts/barchart/Barchart.d.ts.map +1 -0
- package/dist/components/charts/barchart/Barchart.js +56 -0
- package/dist/components/{barchartv2/utils.d.ts → charts/barchart/Barchart.utils.d.ts} +8 -35
- package/dist/components/charts/barchart/Barchart.utils.d.ts.map +1 -0
- package/dist/components/{barchartv2/utils.js → charts/barchart/Barchart.utils.js} +7 -88
- package/dist/components/{barchartv2 → charts/barchart}/BarchartTooltip.d.ts +2 -1
- package/dist/components/charts/barchart/BarchartTooltip.d.ts.map +1 -0
- package/dist/components/{barchartv2 → charts/barchart}/BarchartTooltip.js +3 -3
- package/dist/components/{charttooltip → charts/common}/ChartTooltip.d.ts +9 -2
- package/dist/components/charts/common/ChartTooltip.d.ts.map +1 -0
- package/dist/components/{charttooltip → charts/common}/ChartTooltip.js +11 -15
- package/dist/components/charts/common/SharedComponents.d.ts +47 -0
- package/dist/components/charts/common/SharedComponents.d.ts.map +1 -0
- package/dist/components/charts/common/SharedComponents.js +83 -0
- package/dist/components/charts/common/chartUtils.d.ts +91 -0
- package/dist/components/charts/common/chartUtils.d.ts.map +1 -0
- package/dist/components/charts/common/chartUtils.js +243 -0
- package/dist/components/{globalhealthbar/GlobalHealthBarRecharts.component.d.ts → charts/globalhealthbar/GlobalHealthBar.d.ts} +2 -2
- package/dist/components/charts/globalhealthbar/GlobalHealthBar.d.ts.map +1 -0
- package/dist/components/{globalhealthbar/useHealthBarData.d.ts → charts/globalhealthbar/GlobalHealthBar.hooks.d.ts} +1 -1
- package/dist/components/charts/globalhealthbar/GlobalHealthBar.hooks.d.ts.map +1 -0
- package/dist/components/{globalhealthbar/GlobalHealthBarRecharts.component.js → charts/globalhealthbar/GlobalHealthBar.js} +4 -4
- package/dist/components/{globalhealthbar/healthBarUtils.d.ts → charts/globalhealthbar/GlobalHealthBar.utils.d.ts} +1 -1
- package/dist/components/charts/globalhealthbar/GlobalHealthBar.utils.d.ts.map +1 -0
- package/dist/components/{globalhealthbar/healthBarUtils.js → charts/globalhealthbar/GlobalHealthBar.utils.js} +1 -1
- package/dist/components/{globalhealthbar/components → charts/globalhealthbar}/GlobalHealthBarTooltip.d.ts +1 -1
- package/dist/components/charts/globalhealthbar/GlobalHealthBarTooltip.d.ts.map +1 -0
- package/dist/components/{globalhealthbar/components → charts/globalhealthbar}/GlobalHealthBarTooltip.js +7 -3
- package/dist/components/charts/globalhealthbar/HealthBarXAxis.d.ts.map +1 -0
- package/dist/components/{globalhealthbar/components → charts/globalhealthbar}/HealthBarXAxis.js +1 -1
- package/dist/components/charts/index.d.ts +16 -0
- package/dist/components/charts/index.d.ts.map +1 -0
- package/dist/components/charts/index.js +15 -0
- package/dist/components/{chartlegend → charts/legend}/ChartLegend.d.ts +1 -1
- package/dist/components/charts/legend/ChartLegend.d.ts.map +1 -0
- package/dist/components/{chartlegend → charts/legend}/ChartLegend.js +2 -2
- package/dist/components/{chartlegend → charts/legend}/ChartLegendWrapper.d.ts +1 -1
- package/dist/components/charts/legend/ChartLegendWrapper.d.ts.map +1 -0
- package/dist/components/{linetimeseriechart/linetimeseriechart.component.d.ts → charts/linetimeseries/LineTimeSerieChart.d.ts} +12 -2
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.d.ts.map +1 -0
- package/dist/components/{linetimeseriechart/linetimeseriechart.component.js → charts/linetimeseries/LineTimeSerieChart.js} +34 -35
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.utils.d.ts +14 -0
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.utils.d.ts.map +1 -0
- package/dist/components/{linetimeseriechart/utils.js → charts/linetimeseries/LineTimeSerieChart.utils.js} +4 -6
- package/dist/components/charts/sparkline/Sparkline.d.ts +23 -0
- package/dist/components/charts/sparkline/Sparkline.d.ts.map +1 -0
- package/dist/components/{sparkline/sparkline.component.js → charts/sparkline/Sparkline.js} +12 -6
- package/dist/components/charts/types.d.ts +34 -0
- package/dist/components/charts/types.d.ts.map +1 -0
- package/dist/components/charts/types.js +4 -0
- package/dist/components/icon/Icon.component.d.ts +1 -0
- package/dist/components/icon/Icon.component.d.ts.map +1 -1
- package/dist/components/icon/Icon.component.js +2 -2
- package/dist/components/textbadge/TextBadge.component.d.ts +1 -1
- package/dist/components/textbadge/TextBadge.component.d.ts.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/next.d.ts +3 -11
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +4 -11
- package/package.json +2 -6
- package/src/lib/components/{barchartv2/Barchart.component.test.tsx → charts/barchart/Barchart.test.tsx} +35 -10
- package/src/lib/components/{barchartv2/Barchart.component.tsx → charts/barchart/Barchart.tsx} +42 -207
- package/src/lib/components/{barchartv2/utils.test.ts → charts/barchart/Barchart.utils.test.ts} +2 -141
- package/src/lib/components/{barchartv2/utils.ts → charts/barchart/Barchart.utils.ts} +17 -143
- package/src/lib/components/{barchartv2 → charts/barchart}/BarchartTooltip.tsx +5 -9
- package/src/lib/components/{charttooltip → charts/common}/ChartTooltip.tsx +13 -20
- package/src/lib/components/charts/common/SharedComponents.tsx +193 -0
- package/src/lib/components/charts/common/chartUtils.test.ts +402 -0
- package/src/lib/components/charts/common/chartUtils.ts +334 -0
- package/src/lib/components/{globalhealthbar/useHealthBarData.spec.tsx → charts/globalhealthbar/GlobalHealthBar.hooks.test.tsx} +1 -1
- package/src/lib/components/{globalhealthbar/GlobalHealthBarRecharts.component.tsx → charts/globalhealthbar/GlobalHealthBar.tsx} +4 -4
- package/src/lib/components/{globalhealthbar/healthBarUtils.spec.ts → charts/globalhealthbar/GlobalHealthBar.utils.test.ts} +1 -1
- package/src/lib/components/{globalhealthbar/healthBarUtils.ts → charts/globalhealthbar/GlobalHealthBar.utils.ts} +1 -1
- package/src/lib/components/{globalhealthbar/components → charts/globalhealthbar}/GlobalHealthBarTooltip.tsx +8 -4
- package/src/lib/components/{globalhealthbar/components → charts/globalhealthbar}/HealthBarXAxis.tsx +1 -1
- package/src/lib/components/charts/index.ts +59 -0
- package/src/lib/components/{chartlegend → charts/legend}/ChartLegend.tsx +2 -2
- package/src/lib/components/{chartlegend → charts/legend}/ChartLegendWrapper.tsx +1 -1
- package/src/lib/components/{linetimeseriechart/linetimeseriechart.test.tsx → charts/linetimeseries/LineTimeSerieChart.test.tsx} +3 -6
- package/src/lib/components/{linetimeseriechart/linetimeseriechart.component.tsx → charts/linetimeseries/LineTimeSerieChart.tsx} +48 -44
- package/src/lib/components/{linetimeseriechart/utils.test.ts → charts/linetimeseries/LineTimeSerieChart.utils.test.ts} +1 -1
- package/src/lib/components/{linetimeseriechart/utils.ts → charts/linetimeseries/LineTimeSerieChart.utils.ts} +4 -6
- package/src/lib/components/charts/sparkline/Sparkline.tsx +80 -0
- package/src/lib/components/charts/types.ts +36 -0
- package/src/lib/components/icon/Icon.component.tsx +12 -8
- package/src/lib/components/textbadge/TextBadge.component.tsx +1 -1
- package/src/lib/index.ts +4 -2
- package/src/lib/next.ts +26 -13
- package/stories/BarChart/barchart.stories.tsx +10 -9
- package/stories/GlobalHealthBar/{globalhealthbarRecharts.stories.tsx → globalhealthbar.stories.tsx} +3 -21
- package/stories/GlobalHealthBar/globalheathbarrecharts.guideline.mdx +2 -2
- package/stories/guideline/chart-guideline.mdx +1 -38
- package/stories/linetimeseriechart.stories.tsx +4 -6
- package/stories/sparkline.stories.tsx +13 -11
- package/stories/textbadge.stories.tsx +15 -0
- package/dist/components/barchart/BarChart.component.d.ts +0 -17
- package/dist/components/barchart/BarChart.component.d.ts.map +0 -1
- package/dist/components/barchart/BarChart.component.js +0 -27
- package/dist/components/barchartv2/Barchart.component.d.ts.map +0 -1
- package/dist/components/barchartv2/Barchart.component.js +0 -122
- package/dist/components/barchartv2/BarchartTooltip.d.ts.map +0 -1
- package/dist/components/barchartv2/utils.d.ts.map +0 -1
- package/dist/components/chartlegend/ChartLegend.d.ts.map +0 -1
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +0 -1
- package/dist/components/charttooltip/ChartTooltip.d.ts.map +0 -1
- package/dist/components/globalhealthbar/GlobalHealthBar.component.d.ts +0 -23
- package/dist/components/globalhealthbar/GlobalHealthBar.component.d.ts.map +0 -1
- package/dist/components/globalhealthbar/GlobalHealthBar.component.js +0 -173
- package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts.map +0 -1
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts.map +0 -1
- package/dist/components/globalhealthbar/components/HealthBarXAxis.d.ts.map +0 -1
- package/dist/components/globalhealthbar/healthBarUtils.d.ts.map +0 -1
- package/dist/components/globalhealthbar/healthBarUtils.spec.d.ts +0 -2
- package/dist/components/globalhealthbar/healthBarUtils.spec.d.ts.map +0 -1
- package/dist/components/globalhealthbar/healthBarUtils.spec.js +0 -391
- package/dist/components/globalhealthbar/tooltip/index.d.ts +0 -8
- package/dist/components/globalhealthbar/tooltip/index.d.ts.map +0 -1
- package/dist/components/globalhealthbar/tooltip/index.js +0 -55
- package/dist/components/globalhealthbar/useHealthBarData.d.ts.map +0 -1
- package/dist/components/globalhealthbar/useHealthBarData.spec.d.ts +0 -2
- package/dist/components/globalhealthbar/useHealthBarData.spec.d.ts.map +0 -1
- package/dist/components/globalhealthbar/useHealthBarData.spec.js +0 -209
- package/dist/components/linetemporalchart/ChartUtil.d.ts +0 -41
- package/dist/components/linetemporalchart/ChartUtil.d.ts.map +0 -1
- package/dist/components/linetemporalchart/ChartUtil.js +0 -148
- package/dist/components/linetemporalchart/LineTemporalChart.component.d.ts +0 -46
- package/dist/components/linetemporalchart/LineTemporalChart.component.d.ts.map +0 -1
- package/dist/components/linetemporalchart/LineTemporalChart.component.js +0 -579
- package/dist/components/linetemporalchart/MetricTimespanProvider.d.ts.map +0 -1
- package/dist/components/linetemporalchart/tooltip/index.d.ts +0 -30
- package/dist/components/linetemporalchart/tooltip/index.d.ts.map +0 -1
- package/dist/components/linetemporalchart/tooltip/index.js +0 -104
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +0 -1
- package/dist/components/linetimeseriechart/utils.d.ts +0 -16
- package/dist/components/linetimeseriechart/utils.d.ts.map +0 -1
- package/dist/components/sparkline/sparkline.component.d.ts +0 -17
- package/dist/components/sparkline/sparkline.component.d.ts.map +0 -1
- package/dist/components/vegachartv2/SyncedCursorCharts.d.ts +0 -12
- package/dist/components/vegachartv2/SyncedCursorCharts.d.ts.map +0 -1
- package/dist/components/vegachartv2/SyncedCursorCharts.js +0 -15
- package/dist/components/vegachartv2/VegaChartV2.component.d.ts +0 -15
- package/dist/components/vegachartv2/VegaChartV2.component.d.ts.map +0 -1
- package/dist/components/vegachartv2/VegaChartV2.component.js +0 -218
- package/src/lib/components/barchart/BarChart.component.tsx +0 -51
- package/src/lib/components/globalhealthbar/GlobalHealthBar.component.tsx +0 -204
- package/src/lib/components/globalhealthbar/tooltip/index.ts +0 -72
- package/src/lib/components/linetemporalchart/ChartUtil.test.ts +0 -250
- package/src/lib/components/linetemporalchart/ChartUtil.ts +0 -225
- package/src/lib/components/linetemporalchart/LineTemporalChart.component.tsx +0 -800
- package/src/lib/components/linetemporalchart/tooltip/index.ts +0 -178
- package/src/lib/components/sparkline/sparkline.component.tsx +0 -56
- package/src/lib/components/vegachartv2/SyncedCursorCharts.tsx +0 -28
- package/src/lib/components/vegachartv2/VegaChartV2.component.tsx +0 -276
- package/stories/barchart.stories.tsx +0 -142
- package/stories/globalhealthbar.stories.tsx +0 -191
- package/stories/guideline/mdxExampleComponents.tsx +0 -57
- package/stories/linecharttemporal.stories.tsx +0 -88
- /package/dist/components/{linetemporalchart/MetricTimespanProvider.js → charts/MetricsTimeSpanProvider.js} +0 -0
- /package/dist/components/{globalhealthbar/useHealthBarData.js → charts/globalhealthbar/GlobalHealthBar.hooks.js} +0 -0
- /package/dist/components/{globalhealthbar/components → charts/globalhealthbar}/HealthBarXAxis.d.ts +0 -0
- /package/dist/components/{chartlegend → charts/legend}/ChartLegendWrapper.js +0 -0
- /package/src/lib/components/{linetemporalchart/MetricTimespanProvider.tsx → charts/MetricsTimeSpanProvider.tsx} +0 -0
- /package/src/lib/components/{barchartv2 → charts/barchart}/BarchartTooltip.test.tsx +0 -0
- /package/src/lib/components/{globalhealthbar/useHealthBarData.ts → charts/globalhealthbar/GlobalHealthBar.hooks.ts} +0 -0
- /package/src/lib/components/{chartlegend → charts/legend}/ChartLegend.test.tsx +0 -0
- /package/src/lib/components/{chartlegend → charts/legend}/ChartLegendWrapper.test.tsx +0 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { NAN_STRING } from '../../constants';
|
|
2
|
+
import { TooltipDateFormat } from './ChartTooltip';
|
|
3
|
+
import { UnitRange } from '../types';
|
|
4
|
+
|
|
5
|
+
/* -------------------------------------------------------------------------- */
|
|
6
|
+
/* constants */
|
|
7
|
+
/* -------------------------------------------------------------------------- */
|
|
8
|
+
|
|
9
|
+
export const maxWidthTooltip = { maxWidth: '20rem' };
|
|
10
|
+
|
|
11
|
+
/* -------------------------------------------------------------------------- */
|
|
12
|
+
/* utils functions */
|
|
13
|
+
/* -------------------------------------------------------------------------- */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Round a value to a nice number for chart display
|
|
17
|
+
* Used by Barchart and LineTimeSerieChart for Y-axis scaling
|
|
18
|
+
*/
|
|
19
|
+
export const getRoundReferenceValue = (value: number): number => {
|
|
20
|
+
if (value <= 0) return 1; // Default for zero or negative values
|
|
21
|
+
|
|
22
|
+
// Buffer the value by 10% to avoid being too close to the edge of the chart
|
|
23
|
+
const bufferedValue = value * 1.1;
|
|
24
|
+
|
|
25
|
+
if (value >= 10) {
|
|
26
|
+
const remainder = value % 10;
|
|
27
|
+
const incremented = value + (10 - remainder);
|
|
28
|
+
|
|
29
|
+
// If the remainder is less than 5, round down to the nearest 10
|
|
30
|
+
if (remainder < 5) {
|
|
31
|
+
return value - remainder;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If incrementing would exceed the buffered max, also round down
|
|
35
|
+
if (incremented > bufferedValue) {
|
|
36
|
+
return value - remainder;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Otherwise, round up to the next 10
|
|
40
|
+
return incremented;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const magnitude = Math.pow(10, Math.floor(Math.log10(value)));
|
|
44
|
+
|
|
45
|
+
const remainder = bufferedValue % magnitude;
|
|
46
|
+
|
|
47
|
+
// Round to nice numbers based on normalized value
|
|
48
|
+
// appearance for small values
|
|
49
|
+
return remainder === 0 ? bufferedValue : bufferedValue - remainder;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate tick values for Y-axis
|
|
54
|
+
* Used by Barchart and LineTimeSerieChart
|
|
55
|
+
*/
|
|
56
|
+
export const getTicks = (topValue: number, isSymmetrical: boolean) => {
|
|
57
|
+
const possibleTickNumbers = [4, 3];
|
|
58
|
+
const numberOfTicks =
|
|
59
|
+
possibleTickNumbers.find((number) => topValue % (number - 1) === 0) || 3; // Default to 3 ticks if no match
|
|
60
|
+
|
|
61
|
+
const tickInterval = topValue / (numberOfTicks - 1);
|
|
62
|
+
const ticks = Array.from(
|
|
63
|
+
{ length: numberOfTicks },
|
|
64
|
+
(_, index) => index * tickInterval,
|
|
65
|
+
);
|
|
66
|
+
if (isSymmetrical) {
|
|
67
|
+
// Create negative ticks in order without 0
|
|
68
|
+
const negativeTicks = Array.from(
|
|
69
|
+
{ length: numberOfTicks - 1 },
|
|
70
|
+
(_, index) => (index - numberOfTicks + 1) * tickInterval,
|
|
71
|
+
);
|
|
72
|
+
ticks.unshift(...negativeTicks);
|
|
73
|
+
}
|
|
74
|
+
return ticks;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Return the unit label based on the current dataset, and the valueBase which is used to convert the data
|
|
79
|
+
* Used by LineTimeSerieChart
|
|
80
|
+
* @param unitRange - Array of threshold and label pairs
|
|
81
|
+
* @param maxValue - The maximum value among the data set
|
|
82
|
+
* @returns Object with valueBase and unitLabel
|
|
83
|
+
*/
|
|
84
|
+
export function getUnitLabel(
|
|
85
|
+
unitRange: {
|
|
86
|
+
threshold: number;
|
|
87
|
+
label: string;
|
|
88
|
+
}[],
|
|
89
|
+
maxValue: number,
|
|
90
|
+
): {
|
|
91
|
+
valueBase: number;
|
|
92
|
+
unitLabel: string | undefined;
|
|
93
|
+
} {
|
|
94
|
+
if (!unitRange || unitRange.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
valueBase: 1,
|
|
97
|
+
unitLabel: undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// first sort the unitRange
|
|
101
|
+
unitRange.sort(
|
|
102
|
+
(
|
|
103
|
+
unitA: {
|
|
104
|
+
threshold: number;
|
|
105
|
+
label: string;
|
|
106
|
+
},
|
|
107
|
+
unitB: {
|
|
108
|
+
threshold: number;
|
|
109
|
+
label: string;
|
|
110
|
+
},
|
|
111
|
+
) => {
|
|
112
|
+
return unitA.threshold - unitB.threshold;
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
let index = unitRange.findIndex((range) => range.threshold > maxValue);
|
|
116
|
+
|
|
117
|
+
// last unit
|
|
118
|
+
if (index === -1) {
|
|
119
|
+
index = unitRange.length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (index === 0) {
|
|
123
|
+
return {
|
|
124
|
+
valueBase: unitRange[index].threshold || 1,
|
|
125
|
+
unitLabel: unitRange[index].label,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
// if the threshold is 0, we use 1 as the value base to avoid division by 0
|
|
131
|
+
valueBase: unitRange[index - 1].threshold || 1,
|
|
132
|
+
unitLabel: unitRange[index - 1].label,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Computes unit label and normalizes chart data based on unit range.
|
|
138
|
+
* This is shared logic used by both Barchart and LineTimeSerieChart.
|
|
139
|
+
*
|
|
140
|
+
* @param data - Chart data to normalize
|
|
141
|
+
* @param maxValue - Maximum value in the dataset
|
|
142
|
+
* @param unitRange - Optional unit range configuration for automatic scaling
|
|
143
|
+
* @param excludeKey - Key to exclude from normalization (e.g., 'category' for Barchart, 'timestamp' for LineTimeSerieChart)
|
|
144
|
+
* @returns Object containing unit label, top value for Y-axis, and normalized data
|
|
145
|
+
*/
|
|
146
|
+
export const normalizeChartDataWithUnits = <T extends Record<string, any>>(
|
|
147
|
+
data: T[],
|
|
148
|
+
maxValue: number,
|
|
149
|
+
unitRange: UnitRange | undefined,
|
|
150
|
+
excludeKey: string,
|
|
151
|
+
): {
|
|
152
|
+
unitLabel: string | undefined;
|
|
153
|
+
topValue: number;
|
|
154
|
+
rechartsData: T[];
|
|
155
|
+
topDomain: number;
|
|
156
|
+
} => {
|
|
157
|
+
// If no unit range provided, just calculate top value without unit conversion
|
|
158
|
+
if (!unitRange || unitRange.length === 0) {
|
|
159
|
+
const topValue = getRoundReferenceValue(maxValue);
|
|
160
|
+
return {
|
|
161
|
+
unitLabel: undefined,
|
|
162
|
+
topValue,
|
|
163
|
+
rechartsData: data,
|
|
164
|
+
topDomain: maxValue * 1.1,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get appropriate unit and value base for normalization
|
|
169
|
+
const { valueBase, unitLabel } = getUnitLabel(unitRange, maxValue);
|
|
170
|
+
const basedValue = maxValue / valueBase;
|
|
171
|
+
const topValue = getRoundReferenceValue(basedValue);
|
|
172
|
+
const topDomain = basedValue * 1.1;
|
|
173
|
+
// Normalize all numeric values by dividing by valueBase
|
|
174
|
+
const rechartsData = data.map((dataPoint) => {
|
|
175
|
+
const normalizedDataPoint: Record<string, number | string> = {
|
|
176
|
+
...dataPoint,
|
|
177
|
+
};
|
|
178
|
+
Object.entries(dataPoint).forEach(([key, value]) => {
|
|
179
|
+
if (key !== excludeKey && typeof value === 'number') {
|
|
180
|
+
normalizedDataPoint[key] = value / valueBase;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return normalizedDataPoint as T;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return { unitLabel, topValue, rechartsData, topDomain };
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* This function manually adds the missing data points with `null` value caused by downtime of the VMs
|
|
191
|
+
* Missing data points are only added when the gap between consecutive data points is bigger than 2 intervals
|
|
192
|
+
* Used by LineTimeSerieChart and Sparkline
|
|
193
|
+
*
|
|
194
|
+
* @param orginalValues - The array of the data points are already sorted according to the time series
|
|
195
|
+
* @param startingTimeStamp - The starting timestamp in seconds
|
|
196
|
+
* @param sampleDuration - The time span value in seconds
|
|
197
|
+
* @param sampleInterval - The time difference between two data points in seconds
|
|
198
|
+
*/
|
|
199
|
+
export function addMissingDataPoint(
|
|
200
|
+
originalValues: [number, number | string | null][],
|
|
201
|
+
startingTimeStamp?: number,
|
|
202
|
+
sampleDuration?: number,
|
|
203
|
+
sampleInterval?: number,
|
|
204
|
+
): [number, number | string | null][] {
|
|
205
|
+
if (
|
|
206
|
+
!originalValues ||
|
|
207
|
+
startingTimeStamp === undefined ||
|
|
208
|
+
!sampleDuration ||
|
|
209
|
+
!sampleInterval ||
|
|
210
|
+
startingTimeStamp < 0 ||
|
|
211
|
+
sampleDuration <= 0 ||
|
|
212
|
+
sampleInterval <= 0
|
|
213
|
+
) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// If there are no original values, return empty array
|
|
218
|
+
if (originalValues.length === 0) {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const newValues: [number, number | string | null][] = [];
|
|
223
|
+
|
|
224
|
+
// add missing data points for the starting time
|
|
225
|
+
for (
|
|
226
|
+
let i = startingTimeStamp;
|
|
227
|
+
i < originalValues[0][0];
|
|
228
|
+
i += sampleInterval
|
|
229
|
+
) {
|
|
230
|
+
newValues.push([i, NAN_STRING]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Process all but the last element
|
|
234
|
+
for (let i = 0; i < originalValues.length - 1; i++) {
|
|
235
|
+
if (
|
|
236
|
+
originalValues[i][0] < startingTimeStamp ||
|
|
237
|
+
originalValues[i][0] > startingTimeStamp + sampleDuration
|
|
238
|
+
) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Always add the current data point
|
|
243
|
+
newValues.push(originalValues[i]);
|
|
244
|
+
|
|
245
|
+
const currentTimestamp = originalValues[i][0];
|
|
246
|
+
const nextTimestamp = originalValues[i + 1][0];
|
|
247
|
+
const gap = nextTimestamp - currentTimestamp;
|
|
248
|
+
|
|
249
|
+
// Calculate how many missing points to add
|
|
250
|
+
const missingIntervals = Math.floor(gap / sampleInterval) - 1;
|
|
251
|
+
|
|
252
|
+
// Add missing data points with NAN_STRING (only executes if missingIntervals > 0)
|
|
253
|
+
for (let j = 1; j <= missingIntervals; j++) {
|
|
254
|
+
const missingTimestamp = currentTimestamp + j * sampleInterval;
|
|
255
|
+
newValues.push([missingTimestamp, NAN_STRING]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Add the last element
|
|
260
|
+
newValues.push(originalValues[originalValues.length - 1]);
|
|
261
|
+
|
|
262
|
+
// add missing data points for the ending time
|
|
263
|
+
for (
|
|
264
|
+
let i = originalValues[originalValues.length - 1][0] + sampleInterval;
|
|
265
|
+
i < startingTimeStamp + sampleDuration;
|
|
266
|
+
i += sampleInterval
|
|
267
|
+
) {
|
|
268
|
+
newValues.push([i, NAN_STRING]);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return newValues;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Date Format Reference Table
|
|
275
|
+
* ============================
|
|
276
|
+
*
|
|
277
|
+
* This table documents the date formatting logic used across charts:
|
|
278
|
+
* - X-Axis Format: Used for chart axis labels (formatXAxisDate + LineTimeSerieChart's formatXAxisLabel)
|
|
279
|
+
* - Tooltip Format: Used for tooltip headers (getTooltipDateFormat)
|
|
280
|
+
*
|
|
281
|
+
* ┌─────────────────┬──────────────┬────────────────────────┬──────────────────┬──────────────────────────────────────────┬───────────────────────────┐
|
|
282
|
+
* │ Interval │ Duration (s) │ X-axis format │ Example (X-axis) │ Tooltip format │ Example (Tooltip) │
|
|
283
|
+
* ├─────────────────┼──────────────┼────────────────────────┼──────────────────┼──────────────────────────────────────────┼───────────────────────────┤
|
|
284
|
+
* │ Last hour │ ≤ 3,600 │ HH:MM │ 14:05 │ DD MMM HH:MM:SS │ 01 Oct 00:15:00 │
|
|
285
|
+
* │ Last 24 hours │ ≤ 86,400 │ HH:MM │ 23:00 │ DD MMM HH:MM │ 01 Oct 00:15 │
|
|
286
|
+
* │ Last 7 days │ ≤ 604,800 │ DD MMM HH:MM │ 27 Sep 10:12 │ DD MMM HH:MM │ 01 Oct 00:15 │
|
|
287
|
+
* │ Long term │ > 604,800 │ DDMMMYY │ 15Sep25 │ DD MMM YYYY HH:MM │ 01 Oct 2025 00:15 │
|
|
288
|
+
* └─────────────────┴──────────────┴────────────────────────┴──────────────────┴──────────────────────────────────────────┴───────────────────────────┘
|
|
289
|
+
*
|
|
290
|
+
* Note: Duration is in seconds. Some intervals share the same format, which is why both functions only have 3 cases.
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get the format of the date based on the duration
|
|
295
|
+
* Used by Barchart CustomTick component
|
|
296
|
+
* @param duration - Duration in seconds
|
|
297
|
+
* @returns Formatted string type
|
|
298
|
+
*/
|
|
299
|
+
export const formatXAxisDate = (
|
|
300
|
+
duration: number,
|
|
301
|
+
): 'time' | 'day-month-abbreviated' | 'chart-long-term-date' => {
|
|
302
|
+
if (duration <= 24 * 60 * 60) {
|
|
303
|
+
return 'time';
|
|
304
|
+
} else if (duration <= 7 * 24 * 60 * 60) {
|
|
305
|
+
return 'day-month-abbreviated';
|
|
306
|
+
} else {
|
|
307
|
+
return 'chart-long-term-date';
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get the format of the date based on the duration
|
|
313
|
+
* Used by TooltipHeader component
|
|
314
|
+
* @param duration - Duration in seconds
|
|
315
|
+
* @returns Formatted string type
|
|
316
|
+
*/
|
|
317
|
+
export const getTooltipDateFormat: (duration: number) => TooltipDateFormat = (
|
|
318
|
+
duration: number,
|
|
319
|
+
) => {
|
|
320
|
+
if (duration <= 60 * 60) {
|
|
321
|
+
return 'day-month-abbreviated-hour-minute-second';
|
|
322
|
+
} else if (duration <= 7 * 24 * 60 * 60) {
|
|
323
|
+
return 'day-month-abbreviated-hour-minute';
|
|
324
|
+
} else {
|
|
325
|
+
return 'day-month-abbreviated-year-hour-minute';
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const formatToISONumber = (value: number): string => {
|
|
330
|
+
const formattedValue = new Intl.NumberFormat('fr-FR')
|
|
331
|
+
.format(value)
|
|
332
|
+
.replace(',', '.');
|
|
333
|
+
return formattedValue;
|
|
334
|
+
};
|
|
@@ -8,14 +8,14 @@ import {
|
|
|
8
8
|
YAxis,
|
|
9
9
|
} from 'recharts';
|
|
10
10
|
import styled, { useTheme } from 'styled-components';
|
|
11
|
-
import { GlobalHealthBarTooltip } from './
|
|
12
|
-
import { HealthBarXAxis } from './
|
|
11
|
+
import { GlobalHealthBarTooltip } from './GlobalHealthBarTooltip';
|
|
12
|
+
import { HealthBarXAxis } from './HealthBarXAxis';
|
|
13
13
|
import {
|
|
14
14
|
CHART_CONFIG,
|
|
15
15
|
getNavigationAction,
|
|
16
16
|
getNavigationStateUpdate,
|
|
17
|
-
} from './
|
|
18
|
-
import { Alert, useHealthBarData } from './
|
|
17
|
+
} from './GlobalHealthBar.utils';
|
|
18
|
+
import { Alert, useHealthBarData } from './GlobalHealthBar.hooks';
|
|
19
19
|
|
|
20
20
|
export interface GlobalHealthProps {
|
|
21
21
|
id: string;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import styled, { css, useTheme } from 'styled-components';
|
|
3
|
-
import { FormattedDateTime, Stack, Text, Wrap, spacing } from '../../../index';
|
|
4
|
-
import { Alert } from '../GlobalHealthBarRecharts.component';
|
|
5
3
|
import { TooltipContentProps } from 'recharts';
|
|
4
|
+
import { FormattedDateTime } from '../../date/FormattedDateTime';
|
|
5
|
+
import { Stack } from '../../../spacing';
|
|
6
|
+
import { Text } from '../../text/Text.component';
|
|
7
|
+
import { Wrap } from '../../../spacing';
|
|
8
|
+
import { spacing } from '../../../spacing';
|
|
9
|
+
import { Alert } from './GlobalHealthBar.hooks';
|
|
6
10
|
import { zIndex } from '../../../style/theme';
|
|
7
|
-
import { CHART_CONFIG, getTooltipPosition } from '
|
|
8
|
-
import { ChartTooltipPortal } from '
|
|
11
|
+
import { CHART_CONFIG, getTooltipPosition } from './GlobalHealthBar.utils';
|
|
12
|
+
import { ChartTooltipPortal } from '../common/ChartTooltip';
|
|
9
13
|
|
|
10
14
|
interface GlobalHealthBarTooltipProps {
|
|
11
15
|
tooltipData: Alert | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { Barchart } from './barchart/Barchart';
|
|
3
|
+
export type {
|
|
4
|
+
BarchartProps,
|
|
5
|
+
BarchartBars,
|
|
6
|
+
BarchartTooltipFn,
|
|
7
|
+
BarchartSortFn,
|
|
8
|
+
Point,
|
|
9
|
+
} from './barchart/Barchart';
|
|
10
|
+
|
|
11
|
+
export { LineTimeSerieChart } from './linetimeseries/LineTimeSerieChart';
|
|
12
|
+
export type {
|
|
13
|
+
LineChartProps,
|
|
14
|
+
Serie,
|
|
15
|
+
} from './linetimeseries/LineTimeSerieChart';
|
|
16
|
+
|
|
17
|
+
export { GlobalHealthBar } from './globalhealthbar/GlobalHealthBar';
|
|
18
|
+
export type { GlobalHealthProps } from './globalhealthbar/GlobalHealthBar';
|
|
19
|
+
export type { Alert } from './globalhealthbar/GlobalHealthBar.hooks';
|
|
20
|
+
|
|
21
|
+
export { Sparkline } from './sparkline/Sparkline';
|
|
22
|
+
|
|
23
|
+
// Legend
|
|
24
|
+
export { ChartLegend } from './legend/ChartLegend';
|
|
25
|
+
export {
|
|
26
|
+
ChartLegendWrapper,
|
|
27
|
+
useChartId,
|
|
28
|
+
useChartLegend,
|
|
29
|
+
} from './legend/ChartLegendWrapper';
|
|
30
|
+
|
|
31
|
+
// Tooltips (for advanced usage)
|
|
32
|
+
export { BarchartTooltip } from './barchart/BarchartTooltip';
|
|
33
|
+
export {
|
|
34
|
+
ChartTooltipContainer,
|
|
35
|
+
ChartTooltipItem,
|
|
36
|
+
ChartTooltipHeader,
|
|
37
|
+
ChartTooltipItemsContainer,
|
|
38
|
+
ChartTooltipPortal,
|
|
39
|
+
} from './common/ChartTooltip';
|
|
40
|
+
|
|
41
|
+
// Shared utilities (for advanced usage)
|
|
42
|
+
export {
|
|
43
|
+
getRoundReferenceValue,
|
|
44
|
+
getTicks,
|
|
45
|
+
getUnitLabel,
|
|
46
|
+
addMissingDataPoint,
|
|
47
|
+
formatXAxisDate,
|
|
48
|
+
getTooltipDateFormat,
|
|
49
|
+
normalizeChartDataWithUnits,
|
|
50
|
+
} from './common/chartUtils';
|
|
51
|
+
|
|
52
|
+
// Context Providers (for backward compatibility)
|
|
53
|
+
export {
|
|
54
|
+
MetricsTimeSpanProvider,
|
|
55
|
+
useMetricsTimeSpan,
|
|
56
|
+
} from './MetricsTimeSpanProvider';
|
|
57
|
+
|
|
58
|
+
// Types
|
|
59
|
+
export type { UnitRange, TimeType, CategoryType } from './types';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
import { useChartLegend } from './ChartLegendWrapper';
|
|
3
|
-
import { Text, TextVariant } from '
|
|
4
|
-
import { chartColors } from '
|
|
3
|
+
import { Text, TextVariant } from '../../text/Text.component';
|
|
4
|
+
import { chartColors } from '../../../style/theme';
|
|
5
5
|
import { useCallback } from 'react';
|
|
6
6
|
|
|
7
7
|
type ChartLegendProps = {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useRef,
|
|
10
10
|
} from 'react';
|
|
11
11
|
import { v4 as uuidv4 } from 'uuid';
|
|
12
|
-
import { ChartColors } from '
|
|
12
|
+
import { ChartColors } from '../../../style/theme';
|
|
13
13
|
|
|
14
14
|
export const useChartId = (): string => {
|
|
15
15
|
const idRef = useRef<string | null>(null);
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { render } from '@testing-library/react';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
LineTimeSerieChart,
|
|
6
|
-
} from './linetimeseriechart.component';
|
|
7
|
-
import { ChartLegendWrapper } from '../chartlegend/ChartLegendWrapper';
|
|
3
|
+
import { LineChartProps, LineTimeSerieChart } from './LineTimeSerieChart';
|
|
4
|
+
import { ChartLegendWrapper } from '../legend/ChartLegendWrapper';
|
|
8
5
|
import { ThemeProvider } from 'styled-components';
|
|
9
|
-
import { coreUIAvailableThemes } from '
|
|
6
|
+
import { coreUIAvailableThemes } from '../../../style/theme';
|
|
10
7
|
|
|
11
8
|
const TestSeries = [
|
|
12
9
|
{
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
1
2
|
import {
|
|
2
3
|
CartesianGrid,
|
|
3
4
|
Line,
|
|
@@ -8,32 +9,31 @@ import {
|
|
|
8
9
|
XAxis,
|
|
9
10
|
YAxis,
|
|
10
11
|
} from 'recharts';
|
|
11
|
-
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
12
12
|
import styled, { useTheme } from 'styled-components';
|
|
13
|
-
import { Stack } from '
|
|
14
|
-
import { fontSize } from '
|
|
15
|
-
import {
|
|
13
|
+
import { Stack } from '../../../spacing';
|
|
14
|
+
import { fontSize } from '../../../style/theme';
|
|
15
|
+
import { IconHelp } from '../../iconhelper/IconHelper';
|
|
16
|
+
import { Loader } from '../../loader/Loader.component';
|
|
17
|
+
import { ChartTitleText } from '../../text/Text.component';
|
|
18
|
+
import { LegendShape } from '../legend/ChartLegend';
|
|
19
|
+
import { useChartLegend } from '../legend/ChartLegendWrapper';
|
|
20
|
+
import { StyledResponsiveContainer } from '../common/SharedComponents';
|
|
16
21
|
import {
|
|
17
|
-
addMissingDataPoint,
|
|
18
|
-
getUnitLabel,
|
|
19
|
-
} from '../linetemporalchart/ChartUtil';
|
|
20
|
-
import { Loader } from '../loader/Loader.component';
|
|
21
|
-
import { ChartTitleText } from '../text/Text.component';
|
|
22
|
-
import { formatXAxisLabel } from './utils';
|
|
23
|
-
import {
|
|
24
|
-
ChartTooltipPortal,
|
|
25
|
-
ChartTooltipItem,
|
|
26
22
|
ChartTooltipHeader,
|
|
23
|
+
ChartTooltipItem,
|
|
27
24
|
ChartTooltipItemsContainer,
|
|
25
|
+
ChartTooltipPortal,
|
|
28
26
|
ChartTooltipSeparator,
|
|
29
27
|
TooltipHeader,
|
|
30
|
-
} from '../
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
} from '../common/ChartTooltip';
|
|
29
|
+
import {
|
|
30
|
+
addMissingDataPoint,
|
|
31
|
+
formatToISONumber,
|
|
32
|
+
getTicks,
|
|
33
|
+
maxWidthTooltip,
|
|
34
|
+
normalizeChartDataWithUnits,
|
|
35
|
+
} from '../common/chartUtils';
|
|
36
|
+
import { formatXAxisLabel } from './LineTimeSerieChart.utils';
|
|
37
37
|
|
|
38
38
|
const LineTemporalChartWrapper = styled.div`
|
|
39
39
|
display: flex;
|
|
@@ -211,6 +211,16 @@ const isSymmetricalSeries = (
|
|
|
211
211
|
return 'above' in series && 'below' in series;
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Props for LineTimeSerieChart component
|
|
216
|
+
* @param series - The data series to display
|
|
217
|
+
* @param title - The title of the chart
|
|
218
|
+
* @param height - The height of the chart in pixels
|
|
219
|
+
* @param startingTimeStamp - Starting timestamp in seconds
|
|
220
|
+
* @param interval - Interval between data points in seconds
|
|
221
|
+
* @param duration - Total duration of the chart in seconds
|
|
222
|
+
*
|
|
223
|
+
*/
|
|
214
224
|
export function LineTimeSerieChart({
|
|
215
225
|
series,
|
|
216
226
|
title,
|
|
@@ -361,7 +371,6 @@ export function LineTimeSerieChart({
|
|
|
361
371
|
|
|
362
372
|
// 3. Transform the data base on the valuebase
|
|
363
373
|
const { topValue, unitLabel, rechartsData, topDomain } = useMemo(() => {
|
|
364
|
-
|
|
365
374
|
const values = chartData.flatMap((dataPoint) =>
|
|
366
375
|
Object.entries(dataPoint)
|
|
367
376
|
.filter(([key]) => key !== 'timestamp')
|
|
@@ -386,22 +395,21 @@ export function LineTimeSerieChart({
|
|
|
386
395
|
const top = Math.abs(Math.max(...values));
|
|
387
396
|
const bottom = Math.abs(Math.min(...values));
|
|
388
397
|
const maxValue = Math.max(top, bottom);
|
|
389
|
-
const { valueBase, unitLabel } = yAxisType === 'percentage' ? { valueBase: 1, unitLabel: '%' } : getUnitLabel(unitRange ?? [], maxValue);
|
|
390
|
-
// Use round reference value to add extra padding to the top value
|
|
391
|
-
const basedValue = maxValue / valueBase
|
|
392
|
-
const topDomain = basedValue * 1.1;
|
|
393
|
-
const topValue = getRoundReferenceValue(basedValue);
|
|
394
|
-
const rechartsData = chartData.map((dataPoint) => {
|
|
395
|
-
const normalizedDataPoint = { ...dataPoint };
|
|
396
|
-
Object.entries(dataPoint).forEach(([key, value]) => {
|
|
397
|
-
if (key !== 'timestamp' && typeof value === 'number') {
|
|
398
|
-
normalizedDataPoint[key] = value / valueBase;
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
return normalizedDataPoint;
|
|
402
|
-
});
|
|
403
398
|
|
|
404
|
-
|
|
399
|
+
// Use shared normalization function
|
|
400
|
+
const result = normalizeChartDataWithUnits(
|
|
401
|
+
chartData,
|
|
402
|
+
maxValue,
|
|
403
|
+
unitRange,
|
|
404
|
+
'timestamp', // LineTimeSerieChart uses 'timestamp' as the key to exclude
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
topValue: result.topValue,
|
|
409
|
+
unitLabel: result.unitLabel,
|
|
410
|
+
rechartsData: result.rechartsData,
|
|
411
|
+
topDomain: result.topDomain,
|
|
412
|
+
};
|
|
405
413
|
}, [chartData, yAxisType, unitRange]);
|
|
406
414
|
|
|
407
415
|
// Group series by resource and create color mapping
|
|
@@ -516,20 +524,16 @@ export function LineTimeSerieChart({
|
|
|
516
524
|
},
|
|
517
525
|
}}
|
|
518
526
|
domain={
|
|
519
|
-
yAxisType === '
|
|
520
|
-
? [
|
|
521
|
-
:
|
|
522
|
-
? [-topDomain, topDomain]
|
|
523
|
-
: [0, topDomain]
|
|
527
|
+
yAxisType === 'symmetrical'
|
|
528
|
+
? [-topDomain, topDomain]
|
|
529
|
+
: [0, topDomain]
|
|
524
530
|
}
|
|
525
531
|
axisLine={{ stroke: theme.border }}
|
|
526
532
|
tick={{
|
|
527
533
|
fill: theme.textSecondary,
|
|
528
534
|
fontSize: fontSize.smaller,
|
|
529
535
|
}}
|
|
530
|
-
tickFormatter={(value) =>
|
|
531
|
-
new Intl.NumberFormat('fr-FR').format(value)
|
|
532
|
-
}
|
|
536
|
+
tickFormatter={(value) => formatToISONumber(value)}
|
|
533
537
|
ticks={getTicks(topValue, yAxisType === 'symmetrical')}
|
|
534
538
|
interval={0}
|
|
535
539
|
/>
|