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.
Files changed (48) hide show
  1. package/dist/components/charts/AreaChart/AreaChart.js +15 -107
  2. package/dist/components/charts/BarChart/BarChart.js +63 -32
  3. package/dist/components/charts/ComboChart/ComboChart.js +1 -1
  4. package/dist/components/charts/LineChart/LineChart.js +14 -60
  5. package/dist/components/charts/StackedBarChart/StackedBarChart.js +1 -1
  6. package/dist/components/charts/hooks/useChartScales.d.ts +0 -4
  7. package/dist/components/charts/hooks/useChartScales.js +1 -28
  8. package/dist/components/charts/index.d.ts +1 -1
  9. package/dist/components/charts/index.js +1 -1
  10. package/dist/components/charts/shared/BarRenderer/BarRenderer.js +34 -10
  11. package/dist/components/charts/shared/ChartAxis/YAxis.js +6 -44
  12. package/dist/components/charts/shared/ChartContainer/ChartContainer.js +2 -1
  13. package/dist/components/charts/shared/ChartTooltip/ChartTooltip.js +4 -6
  14. package/dist/components/charts/shared/LineRenderer/LineRenderer.js +1 -51
  15. package/dist/components/charts/utils/chart-validation.d.ts +20 -1
  16. package/dist/components/charts/utils/chart-validation.js +92 -12
  17. package/dist/components/charts/utils/index.d.ts +2 -1
  18. package/dist/components/charts/utils/path-utils.d.ts +35 -0
  19. package/dist/components/charts/utils/path-utils.js +156 -0
  20. package/dist/components/core/ContextMenu/ContextMenuContent.js +3 -1
  21. package/dist/components/core/ContextMenu/ContextMenuSubContent.js +3 -1
  22. package/dist/components/core/Dropdown/Dropdown.theme.js +1 -1
  23. package/dist/components/core/Dropdown/DropdownMenu.js +3 -1
  24. package/dist/components/core/Metric/Metric.theme.js +1 -1
  25. package/dist/components/core/Popover/PopoverContent.js +3 -1
  26. package/dist/components/core/Table/TableActions.js +6 -4
  27. package/dist/components/core/Table/TableDeclarative.js +4 -1
  28. package/dist/components/core/Toast/ToastProvider.js +3 -1
  29. package/dist/components/core/Tooltip/Tooltip.js +3 -1
  30. package/dist/components/effects/Overlay/Overlay.js +5 -3
  31. package/dist/components/forms/ColorPicker/ColorPickerContent.js +3 -1
  32. package/dist/components/forms/Combobox/Combobox.js +3 -1
  33. package/dist/components/forms/DatePicker/DatePickerContent.js +3 -1
  34. package/dist/components/forms/Input/Input.js +11 -9
  35. package/dist/components/forms/Input/Input.theme.js +27 -6
  36. package/dist/components/forms/Input/Input.types.d.ts +2 -0
  37. package/dist/components/forms/InputAddress/InputAddress.js +8 -16
  38. package/dist/components/forms/InputAddress/InputAddress.types.d.ts +4 -0
  39. package/dist/components/forms/InputTag/InputTag.js +3 -1
  40. package/dist/components/forms/RichTextEditor/RichTextEditor.js +4 -2
  41. package/dist/components/forms/Select/Select.js +3 -1
  42. package/dist/components/forms/TimePicker/TimePickerContent.js +3 -1
  43. package/dist/hooks/index.d.ts +1 -0
  44. package/dist/hooks/useIsClient.d.ts +7 -0
  45. package/dist/hooks/useIsClient.js +17 -0
  46. package/dist/index.js +1 -1
  47. package/dist/styles.css +1 -1
  48. 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 minValue = propMinValue !== null && propMinValue !== void 0 ? propMinValue : 0;
197
- const maxValue = propMaxValue !== null && propMaxValue !== void 0 ? propMaxValue : (() => {
198
- if (rawMaxValue <= 10)
199
- return Math.ceil(rawMaxValue);
200
- if (rawMaxValue <= 100)
201
- return Math.ceil(rawMaxValue / 5) * 5;
202
- if (rawMaxValue <= 1000)
203
- return Math.ceil(rawMaxValue / 50) * 50;
204
- if (rawMaxValue <= 10000)
205
- return Math.ceil(rawMaxValue / 500) * 500;
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
- : minValue;
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 barHeight = range > 0 ? ((value - minValue) / range) * innerHeight : 0;
200
- const y = margin.top + innerHeight - barHeight;
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 = createRoundedTopBarPath(barX, barY, individualBarWidth, barHeight, resolvedBarRadius);
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 > 0);
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("g", { className: "chart-bars" }, sanitizedData.map((item, index) => {
438
- const x = margin.left + index * (barWidth + categoryGap);
439
- return (React__default.createElement("g", { key: index }, dataKeys.map((key, keyIndex) => {
440
- const rawValue = item[key];
441
- const value = typeof rawValue === "number" && isFinite(rawValue)
442
- ? rawValue
443
- : 0;
444
- const isNull = rawValue === null ||
445
- rawValue === undefined ||
446
- (typeof rawValue === "number" && !isFinite(rawValue));
447
- // For null values, use the average of non-null values
448
- const actualValue = isNull
449
- ? calculateAverageForKey(key)
450
- : value;
451
- const range = maxValue - minValue;
452
- const barHeight = range > 0
453
- ? safeMath.scale(actualValue, minValue, maxValue, 0, innerHeight)
454
- : 0;
455
- const barX = x + keyIndex * (individualBarWidth + gap);
456
- const barY = margin.top + innerHeight - barHeight;
457
- return renderBarPath(item, index, key, keyIndex, barX, barY, barHeight, true);
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 > 0;
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 minValue = propMinValue !== null && propMinValue !== void 0 ? propMinValue : 0;
164
- const maxValue = propMaxValue !== null && propMaxValue !== void 0 ? propMaxValue : (() => {
165
- if (rawMaxValue <= 10)
166
- return Math.ceil(rawMaxValue);
167
- if (rawMaxValue <= 100)
168
- return Math.ceil(rawMaxValue / 5) * 5;
169
- if (rawMaxValue <= 1000)
170
- return Math.ceil(rawMaxValue / 50) * 50;
171
- if (rawMaxValue <= 10000)
172
- return Math.ceil(rawMaxValue / 500) * 500;
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
  } }),
@@ -31,10 +31,6 @@ export interface UseChartScalesOptions {
31
31
  * Gap between categories
32
32
  */
33
33
  categoryGap?: number;
34
- /**
35
- * Whether to use smart rounding for line charts
36
- */
37
- useSmartRounding?: boolean;
38
34
  }
39
35
  export interface ChartScales {
40
36
  /**
@@ -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, useSmartRounding = false, } = options;
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 = createRoundedTopBarPath(barX, barY, individualBarWidth, barHeight, applyRadius ? radius : 0);
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 > 0);
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] > 0).length;
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 > 0;
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 barHeight = range > 0
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
- const barY = margin.top + innerHeight - barHeight;
277
- return renderBarPath(item, index, key, keyIndex, barX, barY, barHeight, true, false, undefined);
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
  });