flikkui 0.2.0-beta.8 → 0.2.0-beta.9
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/charts/AreaChart/AreaChart.js +15 -107
- package/dist/components/charts/BarChart/BarChart.js +63 -32
- package/dist/components/charts/ComboChart/ComboChart.js +1 -1
- package/dist/components/charts/LineChart/LineChart.js +14 -60
- package/dist/components/charts/StackedBarChart/StackedBarChart.js +1 -1
- package/dist/components/charts/hooks/useChartScales.d.ts +0 -4
- package/dist/components/charts/hooks/useChartScales.js +1 -28
- package/dist/components/charts/index.d.ts +1 -1
- package/dist/components/charts/index.js +1 -1
- package/dist/components/charts/shared/BarRenderer/BarRenderer.js +34 -10
- package/dist/components/charts/shared/ChartAxis/YAxis.js +6 -44
- package/dist/components/charts/shared/ChartContainer/ChartContainer.js +2 -1
- package/dist/components/charts/shared/ChartTooltip/ChartTooltip.js +4 -6
- package/dist/components/charts/shared/LineRenderer/LineRenderer.js +1 -51
- package/dist/components/charts/utils/chart-validation.d.ts +20 -1
- package/dist/components/charts/utils/chart-validation.js +92 -12
- package/dist/components/charts/utils/index.d.ts +2 -1
- package/dist/components/charts/utils/path-utils.d.ts +35 -0
- package/dist/components/charts/utils/path-utils.js +156 -0
- package/dist/components/core/ContextMenu/ContextMenuContent.js +3 -1
- package/dist/components/core/ContextMenu/ContextMenuSubContent.js +3 -1
- package/dist/components/core/Dropdown/Dropdown.theme.js +1 -1
- package/dist/components/core/Dropdown/DropdownMenu.js +3 -1
- package/dist/components/core/Metric/Metric.theme.js +1 -1
- package/dist/components/core/Popover/PopoverContent.js +3 -1
- package/dist/components/core/Table/TableActions.js +6 -4
- package/dist/components/core/Table/TableDeclarative.js +4 -1
- package/dist/components/core/Toast/ToastProvider.js +3 -1
- package/dist/components/core/Tooltip/Tooltip.js +3 -1
- package/dist/components/effects/Overlay/Overlay.js +5 -3
- package/dist/components/forms/ColorPicker/ColorPickerContent.js +3 -1
- package/dist/components/forms/Combobox/Combobox.js +3 -1
- package/dist/components/forms/DatePicker/DatePickerContent.js +3 -1
- package/dist/components/forms/Input/Input.js +11 -9
- package/dist/components/forms/Input/Input.theme.js +27 -6
- package/dist/components/forms/Input/Input.types.d.ts +2 -0
- package/dist/components/forms/InputAddress/InputAddress.js +8 -16
- package/dist/components/forms/InputAddress/InputAddress.types.d.ts +4 -0
- package/dist/components/forms/InputTag/InputTag.js +3 -1
- package/dist/components/forms/RichTextEditor/RichTextEditor.js +4 -2
- package/dist/components/forms/Select/Select.js +3 -1
- package/dist/components/forms/TimePicker/TimePickerContent.js +3 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useIsClient.d.ts +7 -0
- package/dist/hooks/useIsClient.js +17 -0
- package/dist/index.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +15 -1
|
@@ -9,102 +9,11 @@ import { HorizontalGrid } from '../shared/ChartGrid/HorizontalGrid.js';
|
|
|
9
9
|
import { ChartTooltip } from '../shared/ChartTooltip/ChartTooltip.js';
|
|
10
10
|
import { ChartMarker } from '../shared/ChartMarker/ChartMarker.js';
|
|
11
11
|
import { ChartCrosshair } from '../shared/ChartCrosshair/ChartCrosshair.js';
|
|
12
|
+
import { generateNiceTicks } from '../utils/chart-validation.js';
|
|
13
|
+
import { generateCurvePath } from '../utils/path-utils.js';
|
|
12
14
|
import { AREA_CHART_DEFAULTS } from './AreaChart.types.js';
|
|
13
15
|
import { areaChartTheme, getSeriesFillColor, getSeriesColorClass } from './AreaChart.theme.js';
|
|
14
16
|
|
|
15
|
-
// Simplified curve generation
|
|
16
|
-
const generateMonotonePath = (points) => {
|
|
17
|
-
if (points.length === 0)
|
|
18
|
-
return "";
|
|
19
|
-
if (points.length === 1)
|
|
20
|
-
return `M ${points[0].x} ${points[0].y}`;
|
|
21
|
-
let path = `M ${points[0].x} ${points[0].y}`;
|
|
22
|
-
for (let i = 1; i < points.length; i++) {
|
|
23
|
-
const prev = points[i - 1];
|
|
24
|
-
const curr = points[i];
|
|
25
|
-
// Calculate control points for smooth curve
|
|
26
|
-
const dx = curr.x - prev.x;
|
|
27
|
-
const controlDistance = dx * 0.3;
|
|
28
|
-
const cp1x = prev.x + controlDistance;
|
|
29
|
-
const cp1y = prev.y;
|
|
30
|
-
const cp2x = curr.x - controlDistance;
|
|
31
|
-
const cp2y = curr.y;
|
|
32
|
-
path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${curr.x} ${curr.y}`;
|
|
33
|
-
}
|
|
34
|
-
return path;
|
|
35
|
-
};
|
|
36
|
-
const generateLinearPath = (points) => {
|
|
37
|
-
if (points.length === 0)
|
|
38
|
-
return "";
|
|
39
|
-
let path = `M ${points[0].x} ${points[0].y}`;
|
|
40
|
-
for (let i = 1; i < points.length; i++) {
|
|
41
|
-
path += ` L ${points[i].x} ${points[i].y}`;
|
|
42
|
-
}
|
|
43
|
-
return path;
|
|
44
|
-
};
|
|
45
|
-
const generateStepPath = (points) => {
|
|
46
|
-
if (points.length === 0)
|
|
47
|
-
return "";
|
|
48
|
-
if (points.length === 1)
|
|
49
|
-
return `M ${points[0].x} ${points[0].y}`;
|
|
50
|
-
let path = `M ${points[0].x} ${points[0].y}`;
|
|
51
|
-
for (let i = 1; i < points.length; i++) {
|
|
52
|
-
const prev = points[i - 1];
|
|
53
|
-
const curr = points[i];
|
|
54
|
-
const midX = prev.x + (curr.x - prev.x) / 2;
|
|
55
|
-
// Step: horizontal to midpoint, vertical to current y, horizontal to current x
|
|
56
|
-
path += ` L ${midX} ${prev.y} L ${midX} ${curr.y} L ${curr.x} ${curr.y}`;
|
|
57
|
-
}
|
|
58
|
-
return path;
|
|
59
|
-
};
|
|
60
|
-
const generateStepBeforePath = (points) => {
|
|
61
|
-
if (points.length === 0)
|
|
62
|
-
return "";
|
|
63
|
-
if (points.length === 1)
|
|
64
|
-
return `M ${points[0].x} ${points[0].y}`;
|
|
65
|
-
let path = `M ${points[0].x} ${points[0].y}`;
|
|
66
|
-
for (let i = 1; i < points.length; i++) {
|
|
67
|
-
const prev = points[i - 1];
|
|
68
|
-
const curr = points[i];
|
|
69
|
-
// Step before: vertical first, then horizontal
|
|
70
|
-
path += ` L ${prev.x} ${curr.y} L ${curr.x} ${curr.y}`;
|
|
71
|
-
}
|
|
72
|
-
return path;
|
|
73
|
-
};
|
|
74
|
-
const generateStepAfterPath = (points) => {
|
|
75
|
-
if (points.length === 0)
|
|
76
|
-
return "";
|
|
77
|
-
if (points.length === 1)
|
|
78
|
-
return `M ${points[0].x} ${points[0].y}`;
|
|
79
|
-
let path = `M ${points[0].x} ${points[0].y}`;
|
|
80
|
-
for (let i = 1; i < points.length; i++) {
|
|
81
|
-
const prev = points[i - 1];
|
|
82
|
-
const curr = points[i];
|
|
83
|
-
// Step after: horizontal first, then vertical
|
|
84
|
-
path += ` L ${curr.x} ${prev.y} L ${curr.x} ${curr.y}`;
|
|
85
|
-
}
|
|
86
|
-
return path;
|
|
87
|
-
};
|
|
88
|
-
// Simple curve implementation
|
|
89
|
-
const generateCurvePath = (points, curveType = "monotone") => {
|
|
90
|
-
if (points.length === 0)
|
|
91
|
-
return "";
|
|
92
|
-
if (points.length === 1)
|
|
93
|
-
return `M ${points[0].x} ${points[0].y}`;
|
|
94
|
-
switch (curveType) {
|
|
95
|
-
case "monotone":
|
|
96
|
-
return generateMonotonePath(points);
|
|
97
|
-
case "step":
|
|
98
|
-
return generateStepPath(points);
|
|
99
|
-
case "stepBefore":
|
|
100
|
-
return generateStepBeforePath(points);
|
|
101
|
-
case "stepAfter":
|
|
102
|
-
return generateStepAfterPath(points);
|
|
103
|
-
case "linear":
|
|
104
|
-
default:
|
|
105
|
-
return generateLinearPath(points);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
17
|
const AreaChart = ({ data, config,
|
|
109
18
|
// Standardized display props with defaults
|
|
110
19
|
showGrid = AREA_CHART_DEFAULTS.showGrid, showXAxis = AREA_CHART_DEFAULTS.showXAxis, showYAxis = AREA_CHART_DEFAULTS.showYAxis, minValue: propMinValue, maxValue: propMaxValue, variant = AREA_CHART_DEFAULTS.variant, theme,
|
|
@@ -193,18 +102,16 @@ svgClassName, areaClassName, lineClassName, gridClassName, axisClassName, childr
|
|
|
193
102
|
const value = item[key];
|
|
194
103
|
return typeof value === "number" ? value : 0;
|
|
195
104
|
}))));
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return Math.ceil(rawMaxValue / 1000) * 1000;
|
|
207
|
-
})();
|
|
105
|
+
const rawMinValue = Math.min(...data.map((item) => Math.min(...dataKeys.map((key) => {
|
|
106
|
+
const value = item[key];
|
|
107
|
+
return typeof value === "number" ? value : 0;
|
|
108
|
+
}))));
|
|
109
|
+
// Compute nice axis boundaries using Heckbert's algorithm
|
|
110
|
+
const dataMinWithZero = Math.min(rawMinValue, 0);
|
|
111
|
+
const dataMaxWithZero = Math.max(rawMaxValue, 0);
|
|
112
|
+
const { niceMin, niceMax } = generateNiceTicks(dataMinWithZero, dataMaxWithZero, 5);
|
|
113
|
+
let minValue = propMinValue !== null && propMinValue !== void 0 ? propMinValue : niceMin;
|
|
114
|
+
let maxValue = propMaxValue !== null && propMaxValue !== void 0 ? propMaxValue : niceMax;
|
|
208
115
|
// Calculate point position
|
|
209
116
|
const getPointPosition = (dataIndex, value) => {
|
|
210
117
|
// Spread points evenly across the full width
|
|
@@ -256,7 +163,7 @@ svgClassName, areaClassName, lineClassName, gridClassName, axisClassName, childr
|
|
|
256
163
|
const value = typeof rawValue === "number" ? rawValue : 0;
|
|
257
164
|
let baselineValue = stacked && seriesStackedValues
|
|
258
165
|
? seriesStackedValues[index]
|
|
259
|
-
:
|
|
166
|
+
: 0;
|
|
260
167
|
let topValue = stacked && seriesStackedValues
|
|
261
168
|
? seriesStackedValues[index] + value
|
|
262
169
|
: value;
|
|
@@ -351,10 +258,11 @@ svgClassName, areaClassName, lineClassName, gridClassName, axisClassName, childr
|
|
|
351
258
|
})()
|
|
352
259
|
: undefined, margin: margin, innerWidth: innerWidth, innerHeight: innerHeight, showHorizontal: dataKeys.length === 1 }),
|
|
353
260
|
React__default.createElement(YAxis, { show: showYAxis, min: minValue, max: maxValue, x: margin.left, y: margin.top, height: innerHeight, showGrid: false, gridWidth: innerWidth, className: axisClassName, tickFormatter: (value) => {
|
|
354
|
-
if (value >= 1000)
|
|
261
|
+
if (Math.abs(value) >= 1000)
|
|
355
262
|
return `${(value / 1000).toFixed(0)}k`;
|
|
356
263
|
return value.toString();
|
|
357
264
|
} }),
|
|
265
|
+
minValue < 0 && maxValue > 0 && (React__default.createElement("line", { x1: margin.left, y1: margin.top + innerHeight - ((0 - minValue) / (maxValue - minValue)) * innerHeight, x2: margin.left + innerWidth, y2: margin.top + innerHeight - ((0 - minValue) / (maxValue - minValue)) * innerHeight, className: "stroke-[var(--color-text-secondary)]", strokeWidth: 1, opacity: 0.6 })),
|
|
358
266
|
React__default.createElement("g", { className: cn(areaChartTheme.areaGroupStyle, areaClassName), clipPath: `url(#${clipId})` }, dataKeys.map((key, keyIndex) => {
|
|
359
267
|
const paths = generatePathData(key); // Actual path
|
|
360
268
|
const colorClass = getSeriesColorClass(keyIndex);
|
|
@@ -140,6 +140,19 @@ className, title, description, enableKeyboardNavigation = BAR_CHART_DEFAULTS.ena
|
|
|
140
140
|
Z
|
|
141
141
|
`.trim();
|
|
142
142
|
};
|
|
143
|
+
// Helper function to create a path for bars with only bottom corners rounded (for negative bars)
|
|
144
|
+
const createRoundedBottomBarPath = (x, y, width, height, radius) => {
|
|
145
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
146
|
+
return `
|
|
147
|
+
M ${x},${y}
|
|
148
|
+
L ${x + width},${y}
|
|
149
|
+
L ${x + width},${y + height - r}
|
|
150
|
+
Q ${x + width},${y + height} ${x + width - r},${y + height}
|
|
151
|
+
L ${x + r},${y + height}
|
|
152
|
+
Q ${x},${y + height} ${x},${y + height - r}
|
|
153
|
+
Z
|
|
154
|
+
`.trim();
|
|
155
|
+
};
|
|
143
156
|
// Calculate dimensions with safe math
|
|
144
157
|
const margin = {
|
|
145
158
|
top: 15,
|
|
@@ -193,11 +206,15 @@ className, title, description, enableKeyboardNavigation = BAR_CHART_DEFAULTS.ena
|
|
|
193
206
|
const key = dataKeys[0]; // Show first series for keyboard navigation
|
|
194
207
|
const value = item[key];
|
|
195
208
|
if (typeof value === "number" && isFinite(value)) {
|
|
196
|
-
// Calculate position for focused bar with safe math
|
|
209
|
+
// Calculate position for focused bar with safe math using zero baseline
|
|
197
210
|
const x = margin.left + focusedElementIndex * (barWidth + categoryGap);
|
|
198
211
|
const range = maxValue - minValue;
|
|
199
|
-
const
|
|
200
|
-
const
|
|
212
|
+
const scaledValue = range > 0 ? ((value - minValue) / range) * innerHeight : 0;
|
|
213
|
+
const scaledZero = range > 0 ? ((0 - minValue) / range) * innerHeight : 0;
|
|
214
|
+
const zeroY = margin.top + innerHeight - scaledZero;
|
|
215
|
+
const barHeight = Math.abs(scaledValue - scaledZero);
|
|
216
|
+
const isNeg = value < 0;
|
|
217
|
+
const y = isNeg ? zeroY : zeroY - barHeight;
|
|
201
218
|
// Get the fill color for the first series (index 0)
|
|
202
219
|
const fillColor = getSeriesFillColor(0);
|
|
203
220
|
const content = {
|
|
@@ -269,7 +286,7 @@ className, title, description, enableKeyboardNavigation = BAR_CHART_DEFAULTS.ena
|
|
|
269
286
|
return map;
|
|
270
287
|
}, [sanitizedData.length, dataKeys.join(","), showPlaceholderBars]);
|
|
271
288
|
// Helper function to render individual bar path
|
|
272
|
-
const renderBarPath = (item, index, key, keyIndex, barX, barY, barHeight, shouldAnimate = true) => {
|
|
289
|
+
const renderBarPath = (item, index, key, keyIndex, barX, barY, barHeight, shouldAnimate = true, isNegative = false) => {
|
|
273
290
|
var _a;
|
|
274
291
|
const rawValue = item[key];
|
|
275
292
|
const isNull = rawValue === null ||
|
|
@@ -333,7 +350,9 @@ className, title, description, enableKeyboardNavigation = BAR_CHART_DEFAULTS.ena
|
|
|
333
350
|
if (barHeight <= 0 || individualBarWidth <= 0) {
|
|
334
351
|
return null;
|
|
335
352
|
}
|
|
336
|
-
const pathD =
|
|
353
|
+
const pathD = isNegative
|
|
354
|
+
? createRoundedBottomBarPath(barX, barY, individualBarWidth, barHeight, resolvedBarRadius)
|
|
355
|
+
: createRoundedTopBarPath(barX, barY, individualBarWidth, barHeight, resolvedBarRadius);
|
|
337
356
|
const delay = (index * dataKeys.length + keyIndex) * BAR_STAGGER;
|
|
338
357
|
return (React__default.createElement(motion.path, { key: keyIndex, d: pathD, initial: shouldAnimate && showAnimation && !shouldReduceMotion
|
|
339
358
|
? { scaleY: 0, opacity: 0 }
|
|
@@ -346,8 +365,8 @@ className, title, description, enableKeyboardNavigation = BAR_CHART_DEFAULTS.ena
|
|
|
346
365
|
damping: 20,
|
|
347
366
|
delay,
|
|
348
367
|
}, className: cn("transition-colors duration-200", !isNull && "cursor-pointer", isFocused && "stroke-2"), style: {
|
|
349
|
-
originY: 1,
|
|
350
|
-
transformOrigin: "bottom",
|
|
368
|
+
originY: isNegative ? 0 : 1,
|
|
369
|
+
transformOrigin: isNegative ? "top" : "bottom",
|
|
351
370
|
fill: isNull
|
|
352
371
|
? "var(--color-background-tertiary)"
|
|
353
372
|
: shouldReduceOpacity
|
|
@@ -380,7 +399,7 @@ className, title, description, enableKeyboardNavigation = BAR_CHART_DEFAULTS.ena
|
|
|
380
399
|
return (rawVal != null &&
|
|
381
400
|
typeof rawVal === "number" &&
|
|
382
401
|
isFinite(rawVal) &&
|
|
383
|
-
rawVal
|
|
402
|
+
rawVal !== 0);
|
|
384
403
|
});
|
|
385
404
|
const content = {
|
|
386
405
|
category: item.name || `Category ${index + 1}`,
|
|
@@ -430,33 +449,45 @@ className, title, description, enableKeyboardNavigation = BAR_CHART_DEFAULTS.ena
|
|
|
430
449
|
React__default.createElement("path", { d: "M-2,2 l4,-4\n M0,8 l8,-8\n M6,10 l4,-4", style: { stroke: "black", strokeWidth: 4, opacity: 0.1 } }))),
|
|
431
450
|
React__default.createElement(HorizontalGrid, { show: showGrid, x: margin.left, y: margin.top, width: innerWidth, height: innerHeight, lineCount: 5 }),
|
|
432
451
|
React__default.createElement(YAxis, { show: showYAxis, min: minValue, max: maxValue, x: margin.left, y: margin.top, height: innerHeight, showGrid: false, gridWidth: innerWidth, tickFormatter: (value) => {
|
|
433
|
-
if (value >= 1000)
|
|
452
|
+
if (Math.abs(value) >= 1000)
|
|
434
453
|
return `${(value / 1000).toFixed(0)}k`;
|
|
435
454
|
return value.toString();
|
|
436
455
|
} }),
|
|
437
|
-
React__default.createElement("
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
456
|
+
minValue < 0 && maxValue > 0 && (React__default.createElement("line", { x1: margin.left, y1: margin.top + innerHeight - ((0 - minValue) / (maxValue - minValue)) * innerHeight, x2: margin.left + innerWidth, y2: margin.top + innerHeight - ((0 - minValue) / (maxValue - minValue)) * innerHeight, className: "stroke-[var(--color-text-secondary)]", strokeWidth: 1, opacity: 0.6 })),
|
|
457
|
+
React__default.createElement("g", { className: "chart-bars" }, (() => {
|
|
458
|
+
// Calculate the zero baseline position (where value=0 maps to in pixel space)
|
|
459
|
+
const range = maxValue - minValue;
|
|
460
|
+
const scaledZero = range > 0
|
|
461
|
+
? safeMath.scale(0, minValue, maxValue, 0, innerHeight)
|
|
462
|
+
: 0;
|
|
463
|
+
const zeroY = margin.top + innerHeight - scaledZero;
|
|
464
|
+
return sanitizedData.map((item, index) => {
|
|
465
|
+
const x = margin.left + index * (barWidth + categoryGap);
|
|
466
|
+
return (React__default.createElement("g", { key: index }, dataKeys.map((key, keyIndex) => {
|
|
467
|
+
const rawValue = item[key];
|
|
468
|
+
const value = typeof rawValue === "number" && isFinite(rawValue)
|
|
469
|
+
? rawValue
|
|
470
|
+
: 0;
|
|
471
|
+
const isNull = rawValue === null ||
|
|
472
|
+
rawValue === undefined ||
|
|
473
|
+
(typeof rawValue === "number" && !isFinite(rawValue));
|
|
474
|
+
// For null values, use the average of non-null values
|
|
475
|
+
const actualValue = isNull
|
|
476
|
+
? calculateAverageForKey(key)
|
|
477
|
+
: value;
|
|
478
|
+
const scaledValue = range > 0
|
|
479
|
+
? safeMath.scale(actualValue, minValue, maxValue, 0, innerHeight)
|
|
480
|
+
: 0;
|
|
481
|
+
const barHeight = Math.abs(scaledValue - scaledZero);
|
|
482
|
+
const isNeg = actualValue < 0;
|
|
483
|
+
const barX = x + keyIndex * (individualBarWidth + gap);
|
|
484
|
+
// Positive: bar extends upward from zero baseline
|
|
485
|
+
// Negative: bar extends downward from zero baseline
|
|
486
|
+
const barY = isNeg ? zeroY : zeroY - barHeight;
|
|
487
|
+
return renderBarPath(item, index, key, keyIndex, barX, barY, barHeight, true, isNeg);
|
|
488
|
+
})));
|
|
489
|
+
});
|
|
490
|
+
})()),
|
|
460
491
|
React__default.createElement(XAxis, { show: showXAxis, data: sanitizedData, x: margin.left, y: margin.top + innerHeight, width: innerWidth, categoryWidth: barWidth, categoryGap: categoryGap })),
|
|
461
492
|
React__default.createElement(ChartTooltip, { tooltipRef: tooltipRef, content: tooltipData === null || tooltipData === void 0 ? void 0 : tooltipData.content, active: !!tooltipData, position: tooltipData
|
|
462
493
|
? { x: tooltipData.x, y: tooltipData.y }
|
|
@@ -59,7 +59,7 @@ const ComboChartRenderers = ({ barKeys, lineKeys, stacked, barRadius, dotRadius,
|
|
|
59
59
|
}).filter(series => {
|
|
60
60
|
// Only show series that have valid non-zero values
|
|
61
61
|
const rawVal = dataItem[series.key];
|
|
62
|
-
return rawVal != null && typeof rawVal === 'number' && isFinite(rawVal) && rawVal
|
|
62
|
+
return rawVal != null && typeof rawVal === 'number' && isFinite(rawVal) && rawVal !== 0;
|
|
63
63
|
});
|
|
64
64
|
const enhancedContent = {
|
|
65
65
|
category: content.category,
|
|
@@ -9,6 +9,8 @@ import { HorizontalGrid } from '../shared/ChartGrid/HorizontalGrid.js';
|
|
|
9
9
|
import { ChartTooltip } from '../shared/ChartTooltip/ChartTooltip.js';
|
|
10
10
|
import { ChartMarker } from '../shared/ChartMarker/ChartMarker.js';
|
|
11
11
|
import { ChartCrosshair } from '../shared/ChartCrosshair/ChartCrosshair.js';
|
|
12
|
+
import { generateNiceTicks } from '../utils/chart-validation.js';
|
|
13
|
+
import { generateCurvePath } from '../utils/path-utils.js';
|
|
12
14
|
import { LINE_CHART_DEFAULTS } from './LineChart.types.js';
|
|
13
15
|
|
|
14
16
|
// Production-ready color system with CORRECT CSS variable syntax
|
|
@@ -31,53 +33,6 @@ const getSeriesFillColor = (index) => {
|
|
|
31
33
|
];
|
|
32
34
|
return colorVars[index % colorVars.length];
|
|
33
35
|
};
|
|
34
|
-
// Monotone cubic interpolation implementation based on D3's curveMonotoneX
|
|
35
|
-
const generateMonotonePath = (points) => {
|
|
36
|
-
if (points.length === 0)
|
|
37
|
-
return "";
|
|
38
|
-
if (points.length === 1)
|
|
39
|
-
return `M ${points[0].x} ${points[0].y}`;
|
|
40
|
-
let path = `M ${points[0].x} ${points[0].y}`;
|
|
41
|
-
for (let i = 1; i < points.length; i++) {
|
|
42
|
-
const prev = points[i - 1];
|
|
43
|
-
const curr = points[i];
|
|
44
|
-
// Calculate control points for smooth curve
|
|
45
|
-
const dx = curr.x - prev.x;
|
|
46
|
-
curr.y - prev.y;
|
|
47
|
-
// Control point distance (adjust for smoothness)
|
|
48
|
-
const controlDistance = dx * 0.3;
|
|
49
|
-
const cp1x = prev.x + controlDistance;
|
|
50
|
-
const cp1y = prev.y;
|
|
51
|
-
const cp2x = curr.x - controlDistance;
|
|
52
|
-
const cp2y = curr.y;
|
|
53
|
-
path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${curr.x} ${curr.y}`;
|
|
54
|
-
}
|
|
55
|
-
return path;
|
|
56
|
-
};
|
|
57
|
-
const generateLinearPath = (points) => {
|
|
58
|
-
if (points.length === 0)
|
|
59
|
-
return "";
|
|
60
|
-
let path = `M ${points[0].x} ${points[0].y}`;
|
|
61
|
-
for (let i = 1; i < points.length; i++) {
|
|
62
|
-
path += ` L ${points[i].x} ${points[i].y}`;
|
|
63
|
-
}
|
|
64
|
-
return path;
|
|
65
|
-
};
|
|
66
|
-
// Simple curve implementation
|
|
67
|
-
const generateCurvePath = (points, curveType = "monotone") => {
|
|
68
|
-
if (points.length === 0)
|
|
69
|
-
return "";
|
|
70
|
-
if (points.length === 1)
|
|
71
|
-
return `M ${points[0].x} ${points[0].y}`;
|
|
72
|
-
switch (curveType) {
|
|
73
|
-
case "monotone":
|
|
74
|
-
return generateMonotonePath(points);
|
|
75
|
-
case "linear":
|
|
76
|
-
return generateLinearPath(points);
|
|
77
|
-
default:
|
|
78
|
-
return generateLinearPath(points);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
36
|
const LineChart = ({ data, config,
|
|
82
37
|
// Standardized display props with defaults
|
|
83
38
|
showGrid = LINE_CHART_DEFAULTS.showGrid, showXAxis = LINE_CHART_DEFAULTS.showXAxis, showYAxis = LINE_CHART_DEFAULTS.showYAxis, minValue: propMinValue, maxValue: propMaxValue,
|
|
@@ -160,18 +115,16 @@ className, title, description, enableKeyboardNavigation = LINE_CHART_DEFAULTS.en
|
|
|
160
115
|
const value = item[key];
|
|
161
116
|
return typeof value === "number" ? value : 0;
|
|
162
117
|
}))));
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return Math.ceil(rawMaxValue / 1000) * 1000;
|
|
174
|
-
})();
|
|
118
|
+
const rawMinValue = Math.min(...data.map((item) => Math.min(...dataKeys.map((key) => {
|
|
119
|
+
const value = item[key];
|
|
120
|
+
return typeof value === "number" ? value : 0;
|
|
121
|
+
}))));
|
|
122
|
+
// Compute nice axis boundaries using Heckbert's algorithm
|
|
123
|
+
const dataMinWithZero = Math.min(rawMinValue, 0);
|
|
124
|
+
const dataMaxWithZero = Math.max(rawMaxValue, 0);
|
|
125
|
+
const { niceMin, niceMax } = generateNiceTicks(dataMinWithZero, dataMaxWithZero, 5);
|
|
126
|
+
let minValue = propMinValue !== null && propMinValue !== void 0 ? propMinValue : niceMin;
|
|
127
|
+
let maxValue = propMaxValue !== null && propMaxValue !== void 0 ? propMaxValue : niceMax;
|
|
175
128
|
// Calculate point position
|
|
176
129
|
const getPointPosition = (dataIndex, value) => {
|
|
177
130
|
// Spread points evenly across the full width
|
|
@@ -316,10 +269,11 @@ className, title, description, enableKeyboardNavigation = LINE_CHART_DEFAULTS.en
|
|
|
316
269
|
})()
|
|
317
270
|
: undefined, margin: margin, innerWidth: innerWidth, innerHeight: innerHeight, showHorizontal: dataKeys.length === 1 }),
|
|
318
271
|
React__default.createElement(YAxis, { show: showYAxis, min: minValue, max: maxValue, x: margin.left, y: margin.top, height: innerHeight, showGrid: false, gridWidth: innerWidth, tickFormatter: (value) => {
|
|
319
|
-
if (value >= 1000)
|
|
272
|
+
if (Math.abs(value) >= 1000)
|
|
320
273
|
return `${(value / 1000).toFixed(0)}k`;
|
|
321
274
|
return value.toString();
|
|
322
275
|
} }),
|
|
276
|
+
minValue < 0 && maxValue > 0 && (React__default.createElement("line", { x1: margin.left, y1: margin.top + innerHeight - ((0 - minValue) / (maxValue - minValue)) * innerHeight, x2: margin.left + innerWidth, y2: margin.top + innerHeight - ((0 - minValue) / (maxValue - minValue)) * innerHeight, className: "stroke-[var(--color-text-secondary)]", strokeWidth: 1, opacity: 0.6 })),
|
|
323
277
|
React__default.createElement("g", { className: "chart-lines" }, dataKeys.map((key, keyIndex) => {
|
|
324
278
|
const targetPath = generatePath(key);
|
|
325
279
|
const colorClass = getSeriesColorClass(keyIndex);
|
|
@@ -300,7 +300,7 @@ className, title, description, enableKeyboardNavigation = STACKED_BAR_CHART_DEFA
|
|
|
300
300
|
React__default.createElement(YAxis, { show: showYAxis, min: minValue, max: maxValue, x: margin.left, y: margin.top, height: innerHeight, showGrid: false, gridWidth: innerWidth, tickFormatter: (value) => {
|
|
301
301
|
if (stackOffset === "expand")
|
|
302
302
|
return `${value}%`;
|
|
303
|
-
if (value >= 1000)
|
|
303
|
+
if (Math.abs(value) >= 1000)
|
|
304
304
|
return `${(value / 1000).toFixed(0)}k`;
|
|
305
305
|
return value.toString();
|
|
306
306
|
} }),
|
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import { useChartValidation } from './useChartValidation.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Smart rounding for line chart Y-axis (matches LineChart logic)
|
|
6
|
-
*/
|
|
7
|
-
function smartRoundMaxValue(rawMax) {
|
|
8
|
-
if (rawMax <= 10)
|
|
9
|
-
return Math.ceil(rawMax);
|
|
10
|
-
if (rawMax <= 100)
|
|
11
|
-
return Math.ceil(rawMax / 5) * 5;
|
|
12
|
-
if (rawMax <= 1000)
|
|
13
|
-
return Math.ceil(rawMax / 50) * 50;
|
|
14
|
-
if (rawMax <= 10000)
|
|
15
|
-
return Math.ceil(rawMax / 500) * 500;
|
|
16
|
-
return Math.ceil(rawMax / 1000) * 1000;
|
|
17
|
-
}
|
|
18
4
|
/**
|
|
19
5
|
* Unified hook for chart scale calculations
|
|
20
6
|
* Handles both bar and line charts with consistent logic
|
|
21
7
|
*/
|
|
22
8
|
function useChartScales(options) {
|
|
23
|
-
const { data, config, chartType = 'bar', stacked = false, minValue: propMinValue, maxValue: propMaxValue, includeZero = true, innerWidth = 600, categoryGap = 8,
|
|
9
|
+
const { data, config, chartType = 'bar', stacked = false, minValue: propMinValue, maxValue: propMaxValue, includeZero = true, innerWidth = 600, categoryGap = 8, } = options;
|
|
24
10
|
// Use validation hook for data sanitization and safe calculations
|
|
25
11
|
const { validation, sanitizedData, isValid, hasWarnings, safeScale, safeMath, } = useChartValidation(data, config, {
|
|
26
12
|
autoSanitize: true,
|
|
@@ -96,18 +82,6 @@ function useChartScales(options) {
|
|
|
96
82
|
includeZero,
|
|
97
83
|
});
|
|
98
84
|
}
|
|
99
|
-
// Apply smart rounding for line charts if requested
|
|
100
|
-
if (useSmartRounding && chartType === 'line' && propMaxValue === undefined) {
|
|
101
|
-
const rawMaxValue = Math.max(...sanitizedData.map((item) => Math.max(...dataKeys.map((key) => {
|
|
102
|
-
const value = item[key];
|
|
103
|
-
return typeof value === "number" ? value : 0;
|
|
104
|
-
}))));
|
|
105
|
-
return {
|
|
106
|
-
min: propMinValue !== null && propMinValue !== void 0 ? propMinValue : 0,
|
|
107
|
-
max: smartRoundMaxValue(rawMaxValue),
|
|
108
|
-
hasValidData: scaleRange.hasValidData,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
85
|
return scaleRange;
|
|
112
86
|
}, [
|
|
113
87
|
sanitizedData,
|
|
@@ -119,7 +93,6 @@ function useChartScales(options) {
|
|
|
119
93
|
propMaxValue,
|
|
120
94
|
includeZero,
|
|
121
95
|
safeScale,
|
|
122
|
-
useSmartRounding,
|
|
123
96
|
]);
|
|
124
97
|
// Calculate X-axis scale
|
|
125
98
|
const xScale = useMemo(() => {
|
|
@@ -44,7 +44,7 @@ export type { ComboChartProps, ComboChartConfig, SeriesType, } from "./ComboChar
|
|
|
44
44
|
export type { ActivityRingsProps, RingData, ActivityRingsThemeOverrides, } from "./ActivityRings/ActivityRings.types";
|
|
45
45
|
export { chartTheme } from "./theme/chart.theme";
|
|
46
46
|
export { extractFillClass, extractStrokeClass, colorClassToVariable, combineClasses, createColorClass, generateSeriesColors, DEFAULT_CHART_COLORS, EXTENDED_CHART_COLORS, MINIMAL_CHART_COLORS, } from "./utils/color-utils";
|
|
47
|
-
export { validateChart, validateChartData, validateChartConfig, sanitizeChartData, calculateSafeScaleRange, SafeMath, } from "./utils/chart-validation";
|
|
47
|
+
export { validateChart, validateChartData, validateChartConfig, sanitizeChartData, calculateSafeScaleRange, niceNum, generateNiceTicks, SafeMath, } from "./utils/chart-validation";
|
|
48
48
|
export { BAR_CHART_DEFAULTS } from "./BarChart/BarChart.types";
|
|
49
49
|
export { STACKED_BAR_CHART_DEFAULTS } from "./StackedBarChart/StackedBarChart.types";
|
|
50
50
|
export { LINE_CHART_DEFAULTS } from "./LineChart/LineChart.types";
|
|
@@ -31,7 +31,7 @@ export { useChartAccessibility } from './hooks/useChartAccessibility.js';
|
|
|
31
31
|
export { useChartValidation } from './hooks/useChartValidation.js';
|
|
32
32
|
export { DEFAULT_CHART_COLORS, EXTENDED_CHART_COLORS, MINIMAL_CHART_COLORS, colorClassToVariable, combineClasses, createColorClass, extractFillClass, extractStrokeClass, generateColorPalette, generateSeriesColors, getContrastColor } from './utils/color-utils.js';
|
|
33
33
|
export { chartTheme } from './theme/chart.theme.js';
|
|
34
|
-
export { SafeMath, calculateSafeScaleRange, sanitizeChartData, validateChart, validateChartConfig, validateChartData } from './utils/chart-validation.js';
|
|
34
|
+
export { SafeMath, calculateSafeScaleRange, generateNiceTicks, niceNum, sanitizeChartData, validateChart, validateChartConfig, validateChartData } from './utils/chart-validation.js';
|
|
35
35
|
export { BAR_CHART_DEFAULTS } from './BarChart/BarChart.types.js';
|
|
36
36
|
export { LINE_CHART_DEFAULTS } from './LineChart/LineChart.types.js';
|
|
37
37
|
export { AREA_CHART_DEFAULTS } from './AreaChart/AreaChart.types.js';
|
|
@@ -33,6 +33,21 @@ const createRoundedTopBarPath = (x, y, width, height, radius) => {
|
|
|
33
33
|
Z
|
|
34
34
|
`.trim();
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Helper function to create a path for bars with only bottom corners rounded (for negative bars)
|
|
38
|
+
*/
|
|
39
|
+
const createRoundedBottomBarPath = (x, y, width, height, radius) => {
|
|
40
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
41
|
+
return `
|
|
42
|
+
M ${x},${y}
|
|
43
|
+
L ${x + width},${y}
|
|
44
|
+
L ${x + width},${y + height - r}
|
|
45
|
+
Q ${x + width},${y + height} ${x + width - r},${y + height}
|
|
46
|
+
L ${x + r},${y + height}
|
|
47
|
+
Q ${x},${y + height} ${x},${y + height - r}
|
|
48
|
+
Z
|
|
49
|
+
`.trim();
|
|
50
|
+
};
|
|
36
51
|
/**
|
|
37
52
|
* BarRenderer Component
|
|
38
53
|
*
|
|
@@ -84,7 +99,7 @@ const BarRenderer = ({ data, config, scales, dimensions, margin, stacked = false
|
|
|
84
99
|
return safeDivide(sum, nonNullValues.length);
|
|
85
100
|
}, [data]);
|
|
86
101
|
// Render individual bar path
|
|
87
|
-
const renderBarPath = useCallback((item, index, key, keyIndex, barX, barY, barHeight, applyRadius, isStacked, stackOrderIndex) => {
|
|
102
|
+
const renderBarPath = useCallback((item, index, key, keyIndex, barX, barY, barHeight, applyRadius, isStacked, stackOrderIndex, isNegative) => {
|
|
88
103
|
var _a;
|
|
89
104
|
const rawValue = item[key];
|
|
90
105
|
const isNull = rawValue === null ||
|
|
@@ -104,7 +119,9 @@ const BarRenderer = ({ data, config, scales, dimensions, margin, stacked = false
|
|
|
104
119
|
if (barHeight <= 0 || individualBarWidth <= 0) {
|
|
105
120
|
return null;
|
|
106
121
|
}
|
|
107
|
-
const pathD =
|
|
122
|
+
const pathD = isNegative
|
|
123
|
+
? createRoundedBottomBarPath(barX, barY, individualBarWidth, barHeight, applyRadius ? radius : 0)
|
|
124
|
+
: createRoundedTopBarPath(barX, barY, individualBarWidth, barHeight, applyRadius ? radius : 0);
|
|
108
125
|
const stackedDelayOrder = typeof stackOrderIndex === "number" ? stackOrderIndex : keyIndex;
|
|
109
126
|
const delay = isStacked
|
|
110
127
|
? index * STACKED_CATEGORY_DELAY +
|
|
@@ -127,8 +144,8 @@ const BarRenderer = ({ data, config, scales, dimensions, margin, stacked = false
|
|
|
127
144
|
damping: 20,
|
|
128
145
|
delay,
|
|
129
146
|
}, className: cn("transition-colors duration-200", !isNull && "cursor-pointer", isFocused && "stroke-2"), style: {
|
|
130
|
-
originY: 1,
|
|
131
|
-
transformOrigin: "bottom",
|
|
147
|
+
originY: isNegative ? 0 : 1,
|
|
148
|
+
transformOrigin: isNegative ? "top" : "bottom",
|
|
132
149
|
fill: isNull
|
|
133
150
|
? "var(--color-background-tertiary)"
|
|
134
151
|
: shouldReduceOpacity
|
|
@@ -163,7 +180,7 @@ const BarRenderer = ({ data, config, scales, dimensions, margin, stacked = false
|
|
|
163
180
|
return (rawVal != null &&
|
|
164
181
|
typeof rawVal === "number" &&
|
|
165
182
|
isFinite(rawVal) &&
|
|
166
|
-
rawVal
|
|
183
|
+
rawVal !== 0);
|
|
167
184
|
});
|
|
168
185
|
const content = {
|
|
169
186
|
category: item.name || `Category ${index + 1}`,
|
|
@@ -220,7 +237,7 @@ const BarRenderer = ({ data, config, scales, dimensions, margin, stacked = false
|
|
|
220
237
|
return data.map((item, index) => {
|
|
221
238
|
const x = margin.left + index * (barWidth + categoryGap);
|
|
222
239
|
if (stacked) {
|
|
223
|
-
const visibleBarCount = dataKeys.filter((key) => typeof item[key] === 'number' && item[key]
|
|
240
|
+
const visibleBarCount = dataKeys.filter((key) => typeof item[key] === 'number' && item[key] !== 0).length;
|
|
224
241
|
let stackedY = margin.top + innerHeight;
|
|
225
242
|
let visibleStackIndex = 0;
|
|
226
243
|
// Find the index of the topmost visible (non-zero) bar to apply radius
|
|
@@ -228,7 +245,7 @@ const BarRenderer = ({ data, config, scales, dimensions, margin, stacked = false
|
|
|
228
245
|
const rawVal = item[key];
|
|
229
246
|
const isVisible = typeof rawVal === "number" &&
|
|
230
247
|
isFinite(rawVal) &&
|
|
231
|
-
rawVal
|
|
248
|
+
rawVal !== 0;
|
|
232
249
|
return isVisible ? idx : topIndex;
|
|
233
250
|
}, -1);
|
|
234
251
|
return (React__default.createElement("g", { key: index }, dataKeys.map((key, keyIndex) => {
|
|
@@ -256,6 +273,9 @@ const BarRenderer = ({ data, config, scales, dimensions, margin, stacked = false
|
|
|
256
273
|
})));
|
|
257
274
|
}
|
|
258
275
|
else {
|
|
276
|
+
// Calculate the zero baseline position (where value=0 maps to in pixel space)
|
|
277
|
+
const scaledZero = safeScale(0, minValue, maxValue, 0, innerHeight);
|
|
278
|
+
const zeroY = margin.top + innerHeight - scaledZero;
|
|
259
279
|
return (React__default.createElement("g", { key: index }, dataKeys.map((key, keyIndex) => {
|
|
260
280
|
const rawValue = item[key];
|
|
261
281
|
const value = typeof rawValue === "number" && isFinite(rawValue)
|
|
@@ -269,12 +289,16 @@ const BarRenderer = ({ data, config, scales, dimensions, margin, stacked = false
|
|
|
269
289
|
? calculateAverageForKey(key)
|
|
270
290
|
: value;
|
|
271
291
|
const range = maxValue - minValue;
|
|
272
|
-
const
|
|
292
|
+
const scaledValue = range > 0
|
|
273
293
|
? safeScale(actualValue, minValue, maxValue, 0, innerHeight)
|
|
274
294
|
: 0;
|
|
295
|
+
const barHeight = Math.abs(scaledValue - scaledZero);
|
|
296
|
+
const isNeg = actualValue < 0;
|
|
275
297
|
const barX = x + keyIndex * (individualBarWidth + gap);
|
|
276
|
-
|
|
277
|
-
|
|
298
|
+
// Positive: bar extends upward from zero baseline
|
|
299
|
+
// Negative: bar extends downward from zero baseline
|
|
300
|
+
const barY = isNeg ? zeroY : zeroY - barHeight;
|
|
301
|
+
return renderBarPath(item, index, key, keyIndex, barX, barY, barHeight, true, false, undefined, isNeg);
|
|
278
302
|
})));
|
|
279
303
|
}
|
|
280
304
|
});
|