universal-adaptive-bars 0.0.6 → 0.1.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "universal-adaptive-bars",
3
3
  "private": false,
4
- "version": "0.0.6",
4
+ "version": "0.1.0",
5
5
  "description": "A highly customizable smart bar chart for React and React Native",
6
6
  "author": "Sammed Doshi <Sammeddoshi03.sd@gmail.com>",
7
7
  "repository": {
@@ -10,12 +10,17 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
10
10
  data,
11
11
  view = 'month',
12
12
  variant = 'default',
13
+ layout = 'vertical',
14
+ missingDataStrategy = 'skip',
15
+ valueFormatter = (val) => String(val),
16
+ annotations = [],
13
17
  dataKeys: providedDataKeys = { date: 'date', value: 'value', label: 'label' },
14
18
 
15
19
  geminiConfig,
16
20
  colors,
17
21
  axisLabels,
18
22
  onViewChange,
23
+ renderTooltip,
19
24
  height = 400,
20
25
  width = '100%',
21
26
  className = '',
@@ -61,7 +66,7 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
61
66
  return Array.from(years).sort((a, b) => b - a);
62
67
  }, [data, dataKeys.date]);
63
68
 
64
- const fullChartData = useChartData({ data, view, dataKeys, colors });
69
+ const fullChartData = useChartData({ data, view, dataKeys, colors, missingDataStrategy });
65
70
  const [activeItem, setActiveItem] = useState<DataPoint | null>(null);
66
71
  const [predictions, setPredictions] = useState<DataPoint[]>([]);
67
72
  const [isPredicting, setIsPredicting] = useState(false);
@@ -208,19 +213,22 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
208
213
  const chartHeight = (typeof height === 'number' ? height : 400) - margin.top - margin.bottom;
209
214
 
210
215
  // Scales
211
- const xScale = useMemo(() => {
216
+ const isHorizontal = layout === 'horizontal';
217
+
218
+ // Scales - abstracted to domain vs numerical
219
+ const domainScale = useMemo(() => {
212
220
  return scaleBand()
213
221
  .domain(visibleData.map(d => d.id))
214
- .range([0, chartWidth])
222
+ .range(isHorizontal ? [0, chartHeight] : [0, chartWidth])
215
223
  .padding(0.3);
216
- }, [visibleData, chartWidth]);
224
+ }, [visibleData, chartWidth, chartHeight, isHorizontal]);
217
225
 
218
- const yScale = useMemo(() => {
226
+ const valueScale = useMemo(() => {
219
227
  const maxVal = Math.max(...visibleData.map(d => d.value), 0);
220
228
  return scaleLinear()
221
229
  .domain([0, maxVal * 1.1])
222
- .range([chartHeight, 0]);
223
- }, [visibleData, chartHeight]);
230
+ .range(isHorizontal ? [0, chartWidth] : [chartHeight, 0]);
231
+ }, [visibleData, chartWidth, chartHeight, isHorizontal]);
224
232
 
225
233
  const handlePredict = async () => {
226
234
  if (!geminiConfig?.apiKey) return;
@@ -464,15 +472,54 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
464
472
  aria-label="Bar chart showing data over time"
465
473
  >
466
474
  <g transform={`translate(${margin.left},${margin.top})`}>
467
- {/* Gridlines & Y-Axis */}
468
- {gridVisible && yScale.ticks(5).map(tickValue => (
469
- <g key={`y-tick-${tickValue}`} transform={`translate(0, ${yScale(tickValue)})`}>
470
- <line x1={0} x2={chartWidth} stroke={gridStroke} strokeDasharray={gridDash} />
471
- <text x={-10} y={4} textAnchor="end" fontSize={axisSize} fill={tickColor}>
472
- {tickValue}
473
- </text>
474
- </g>
475
- ))}
475
+ {/* Gridlines & Value Axis */}
476
+ {gridVisible && valueScale.ticks(5).map(tickValue => {
477
+ const valPos = valueScale(tickValue);
478
+ return (
479
+ <g key={`val-tick-${tickValue}`} transform={isHorizontal ? `translate(${valPos}, 0)` : `translate(0, ${valPos})`}>
480
+ <line
481
+ x1={0} y1={0}
482
+ x2={isHorizontal ? 0 : chartWidth}
483
+ y2={isHorizontal ? chartHeight : 0}
484
+ stroke={gridStroke} strokeDasharray={gridDash}
485
+ />
486
+ <text
487
+ x={isHorizontal ? 0 : -10}
488
+ y={isHorizontal ? chartHeight + 15 : 4}
489
+ textAnchor={isHorizontal ? "middle" : "end"}
490
+ fontSize={axisSize} fill={tickColor}
491
+ >
492
+ {valueFormatter(tickValue)}
493
+ </text>
494
+ </g>
495
+ )
496
+ })}
497
+
498
+ {/* Annotations */}
499
+ {annotations.map((ann, i) => {
500
+ const valPos = valueScale(ann.value);
501
+ if (isNaN(valPos)) return null;
502
+ return (
503
+ <g key={`annotation-${i}`} transform={isHorizontal ? `translate(${valPos}, 0)` : `translate(0, ${valPos})`}>
504
+ <line
505
+ x1={0} y1={0}
506
+ x2={isHorizontal ? 0 : chartWidth}
507
+ y2={isHorizontal ? chartHeight : 0}
508
+ stroke={ann.color || '#ef4444'} strokeDasharray={ann.strokeDasharray || '4 4'} strokeWidth={2}
509
+ />
510
+ {ann.label && (
511
+ <text
512
+ x={isHorizontal ? 0 : chartWidth}
513
+ y={isHorizontal ? -5 : -5}
514
+ textAnchor={isHorizontal ? "middle" : "end"}
515
+ fill={ann.color || '#ef4444'} fontSize={10} fontWeight="bold"
516
+ >
517
+ {ann.label}
518
+ </text>
519
+ )}
520
+ </g>
521
+ );
522
+ })}
476
523
 
477
524
  {/* Axis Lines */}
478
525
  <line x1={0} y1={chartHeight} x2={chartWidth} y2={chartHeight} stroke={gridStroke} />
@@ -505,22 +552,30 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
505
552
 
506
553
  {/* Bars */}
507
554
  {visibleData.map((d) => {
508
- const xBand = xScale(d.id);
509
- const bandwidth = xScale.bandwidth();
510
- const barWidth = Math.min(bandwidth, maxBarWidth);
511
- const x = xBand! + (bandwidth - barWidth) / 2; // Center the bar
512
-
513
- const yTotal = yScale(d.value);
514
- const barHeightTotal = chartHeight - yTotal;
555
+ const bandPos = domainScale(d.id);
556
+ const bandwidth = domainScale.bandwidth();
557
+ const barThickness = Math.min(bandwidth, maxBarWidth);
558
+ const orthogonalOffset = bandPos! + (bandwidth - barThickness) / 2; // Center the bar
559
+
560
+ const valEndPos = valueScale(d.value);
561
+ const valZeroPos = valueScale(0);
515
562
 
516
- if (xBand === undefined) return null;
563
+ if (bandPos === undefined) return null;
517
564
 
518
565
  const isSelected = activeItem?.id === d.id;
519
566
  const isDimmed = activeItem !== null && !isSelected;
520
567
 
521
- // Stacked Rendering
522
- if (variant === 'stacked' && d.stackedValues) {
523
- let currentY = chartHeight;
568
+ const barLengthTotal = Math.abs(valZeroPos - valEndPos);
569
+ const xTotal = isHorizontal ? valZeroPos : orthogonalOffset;
570
+ const yTotal = isHorizontal ? orthogonalOffset : valEndPos;
571
+ const wTotal = isHorizontal ? barLengthTotal : barThickness;
572
+ const hTotal = isHorizontal ? barThickness : barLengthTotal;
573
+
574
+ // Stacked or Grouped Rendering
575
+ if ((variant === 'stacked' || variant === 'grouped') && d.stackedValues) {
576
+ let currentStart = valZeroPos; // valZeroPos is chartHeight for vertical, 0 for horizontal
577
+ const groupThickness = barThickness / d.stackedValues.length;
578
+
524
579
  return (
525
580
  <g key={d.id}
526
581
  onMouseEnter={() => setActiveItem(d)}
@@ -529,25 +584,51 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
529
584
  style={{ opacity: isDimmed ? 0.3 : 1, transition: 'opacity 0.3s' }}
530
585
  >
531
586
  {d.stackedValues.map((stack, i) => {
532
- const segmentHeight = Math.abs(yScale(stack.value) - yScale(0));
533
- const segmentY = currentY - segmentHeight;
534
-
535
- // Determine corners
536
- const isTop = i === d.stackedValues!.length - 1;
537
- const isBottom = i === 0;
538
-
539
- // Only top-most segment gets top radius.
540
- // Only bottom-most segment gets bottom radius.
541
- const corners = {
542
- tl: isTop,
543
- tr: isTop,
544
- bl: isBottom,
545
- br: isBottom
546
- };
547
-
548
- const pathD = getRoundedPath(x, segmentY, barWidth, segmentHeight, barRadius, corners);
549
-
550
- const rect = (
587
+ const segLength = Math.abs(valueScale(stack.value) - valueScale(0));
588
+
589
+ let segmentX, segmentY, segmentW, segmentH;
590
+ let corners = { tl: false, tr: false, bl: false, br: false };
591
+
592
+ if (variant === 'stacked') {
593
+ if (isHorizontal) {
594
+ segmentX = currentStart;
595
+ segmentY = orthogonalOffset;
596
+ segmentW = segLength;
597
+ segmentH = barThickness;
598
+
599
+ const isFirst = i === 0;
600
+ const isLast = i === d.stackedValues!.length - 1;
601
+ corners = { tl: isFirst, bl: isFirst, tr: isLast, br: isLast };
602
+ currentStart += segLength;
603
+ } else {
604
+ segmentH = segLength;
605
+ segmentY = currentStart - segLength;
606
+ segmentX = orthogonalOffset;
607
+ segmentW = barThickness;
608
+
609
+ const isTop = i === d.stackedValues!.length - 1;
610
+ const isBottom = i === 0;
611
+ corners = { tl: isTop, tr: isTop, bl: isBottom, br: isBottom };
612
+ currentStart = segmentY;
613
+ }
614
+ } else { // grouped
615
+ if (isHorizontal) {
616
+ segmentX = valZeroPos;
617
+ segmentY = orthogonalOffset + i * groupThickness;
618
+ segmentW = segLength;
619
+ segmentH = groupThickness;
620
+ } else {
621
+ segmentX = orthogonalOffset + i * groupThickness;
622
+ segmentY = valueScale(stack.value);
623
+ segmentW = groupThickness;
624
+ segmentH = segLength;
625
+ }
626
+ corners = { tl: !isHorizontal, tr: true, bl: isHorizontal, br: isHorizontal };
627
+ }
628
+
629
+ const pathD = getRoundedPath(segmentX, segmentY, segmentW, segmentH, barRadius, corners);
630
+
631
+ return (
551
632
  <path
552
633
  key={`${d.id}-${i}`}
553
634
  d={pathD}
@@ -557,11 +638,9 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
557
638
  strokeWidth={isSelected ? 1 : 0}
558
639
  />
559
640
  );
560
- currentY = segmentY;
561
- return rect;
562
641
  })}
563
- {/* Transparent overlay */}
564
- <rect x={x} y={yTotal} width={barWidth} height={barHeightTotal} fill="transparent" />
642
+ {/* Transparent overlay for mouse events */}
643
+ <rect x={xTotal} y={yTotal} width={wTotal} height={hTotal} fill="transparent" />
565
644
  </g>
566
645
  )
567
646
  }
@@ -570,10 +649,10 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
570
649
  return (
571
650
  <g key={d.id}>
572
651
  <Bar
573
- x={x}
652
+ x={xTotal}
574
653
  y={yTotal}
575
- width={barWidth}
576
- height={barHeightTotal}
654
+ width={wTotal}
655
+ height={hTotal}
577
656
  data={d}
578
657
  isActive={isSelected}
579
658
  isDimmed={isDimmed}
@@ -587,31 +666,31 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
587
666
  />
588
667
  {isSelected && (
589
668
  <text
590
- x={x + barWidth / 2}
591
- y={yTotal - 5}
592
- textAnchor="middle"
669
+ x={isHorizontal ? xTotal + wTotal + 10 : xTotal + wTotal / 2}
670
+ y={isHorizontal ? yTotal + hTotal / 2 + 4 : yTotal - 5}
671
+ textAnchor={isHorizontal ? "start" : "middle"}
593
672
  fill="#333"
594
673
  fontSize={12}
595
674
  fontWeight="bold"
596
675
  pointerEvents="none"
597
676
  >
598
- {d.value}
677
+ {valueFormatter(d.value)}
599
678
  </text>
600
679
  )}
601
680
  </g>
602
681
  );
603
682
  })}
604
683
 
605
- {/* X Axis Labels */}
684
+ {/* Domain Axis Labels */}
606
685
  {visibleData.map((d) => {
607
- const x = xScale(d.id);
608
- if (x === undefined) return null;
686
+ const bandPos = domainScale(d.id);
687
+ if (bandPos === undefined) return null;
609
688
  return (
610
689
  <text
611
690
  key={`label-${d.id}`}
612
- x={x + xScale.bandwidth() / 2}
613
- y={chartHeight + 15}
614
- textAnchor="middle"
691
+ x={isHorizontal ? -10 : bandPos + domainScale.bandwidth() / 2}
692
+ y={isHorizontal ? bandPos + domainScale.bandwidth() / 2 + 4 : chartHeight + 15}
693
+ textAnchor={isHorizontal ? "end" : "middle"}
615
694
  fill={tickColor}
616
695
  fontSize={axisSize}
617
696
  >
@@ -621,6 +700,19 @@ export const SmartBarChart: React.FC<SmartBarChartProps> = ({
621
700
  })}
622
701
  </g>
623
702
  </svg>
703
+
704
+ {/* Custom HTML Tooltip Portal */}
705
+ {renderTooltip && activeItem && (
706
+ <div style={{
707
+ position: 'absolute',
708
+ top: margin.top,
709
+ right: margin.right + 20,
710
+ pointerEvents: 'none',
711
+ zIndex: 10,
712
+ }}>
713
+ {renderTooltip(activeItem)}
714
+ </div>
715
+ )}
624
716
  </div>
625
717
 
626
718
  {/* Right Button */}
@@ -1,6 +1,6 @@
1
1
  import React, { useMemo, useState, useEffect } from 'react';
2
2
  import { View, Text, TouchableOpacity, StyleSheet, Dimensions, Modal, ScrollView } from 'react-native';
3
- import Svg, { G, Text as SvgText, Line, Rect } from 'react-native-svg';
3
+ import Svg, { G, Text as SvgText, Line, Rect, Path } from 'react-native-svg';
4
4
  import { scaleBand, scaleLinear } from 'd3-scale';
5
5
  import { getYear, getMonth, format, endOfWeek, eachWeekOfInterval, endOfMonth, isWithinInterval } from 'date-fns';
6
6
  import { useChartData } from './hooks/useChartData';
@@ -12,14 +12,37 @@ export const SmartBarChartNative: React.FC<SmartBarChartProps> = ({
12
12
  data,
13
13
  view = 'month',
14
14
  variant = 'default',
15
+ layout = 'vertical',
16
+ missingDataStrategy = 'skip',
17
+ valueFormatter = (val) => String(val),
18
+ annotations = [],
15
19
  dataKeys,
16
20
  geminiConfig,
17
21
  colors,
18
22
  axisLabels,
19
23
  onViewChange,
20
24
  height = 400,
21
- width = '100%',
25
+ width = '100%'
22
26
  }) => {
27
+ const getRoundedPath = (x: number, y: number, w: number, h: number, r: number, corners: { tl: boolean, tr: boolean, bl: boolean, br: boolean }) => {
28
+ const tl = corners.tl ? r : 0;
29
+ const tr = corners.tr ? r : 0;
30
+ const bl = corners.bl ? r : 0;
31
+ const br = corners.br ? r : 0;
32
+
33
+ return `
34
+ M ${x + tl} ${y}
35
+ L ${x + w - tr} ${y}
36
+ Q ${x + w} ${y} ${x + w} ${y + tr}
37
+ L ${x + w} ${y + h - br}
38
+ Q ${x + w} ${y + h} ${x + w - br} ${y + h}
39
+ L ${x + bl} ${y + h}
40
+ Q ${x} ${y + h} ${x} ${y + h - bl}
41
+ L ${x} ${y + tl}
42
+ Q ${x} ${y} ${x + tl} ${y}
43
+ Z
44
+ `;
45
+ };
23
46
  // Calendar / Filter State
24
47
  const [filterDate, setFilterDate] = useState<{ year: number | null, month: number | null, weekStartDate: Date | null }>({ year: null, month: null, weekStartDate: null });
25
48
  const [isPickerVisible, setIsPickerVisible] = useState(false);
@@ -50,7 +73,7 @@ export const SmartBarChartNative: React.FC<SmartBarChartProps> = ({
50
73
  return Array.from(years).sort((a, b) => b - a);
51
74
  }, [data, dataKeys.date]);
52
75
 
53
- const fullChartData = useChartData({ data: filteredRawData, view, dataKeys, colors });
76
+ const fullChartData = useChartData({ data: filteredRawData, view, dataKeys, colors, missingDataStrategy });
54
77
  const [activeItem, setActiveItem] = useState<DataPoint | null>(null);
55
78
  const [predictions, setPredictions] = useState<DataPoint[]>([]);
56
79
  const [isPredicting, setIsPredicting] = useState(false);
@@ -131,19 +154,21 @@ export const SmartBarChartNative: React.FC<SmartBarChartProps> = ({
131
154
  // Simplifying: we'll just overlay specific buttons outside margin.
132
155
 
133
156
  // Scales
134
- const xScale = useMemo(() => {
157
+ const isHorizontal = layout === 'horizontal';
158
+
159
+ const domainScale = useMemo(() => {
135
160
  return scaleBand()
136
161
  .domain(visibleData.map(d => d.id))
137
- .range([0, chartWidth])
162
+ .range(isHorizontal ? [0, chartHeight] : [0, chartWidth])
138
163
  .padding(0.3);
139
- }, [visibleData, chartWidth]);
164
+ }, [visibleData, chartWidth, chartHeight, isHorizontal]);
140
165
 
141
- const yScale = useMemo(() => {
166
+ const valueScale = useMemo(() => {
142
167
  const maxVal = Math.max(...visibleData.map(d => d.value), 0);
143
168
  return scaleLinear()
144
169
  .domain([0, maxVal * 1.1])
145
- .range([chartHeight, 0]);
146
- }, [visibleData, chartHeight]);
170
+ .range(isHorizontal ? [0, chartWidth] : [chartHeight, 0]);
171
+ }, [visibleData, chartWidth, chartHeight, isHorizontal]);
147
172
 
148
173
  const handlePredict = async () => {
149
174
  if (!geminiConfig?.apiKey) return;
@@ -339,15 +364,44 @@ export const SmartBarChartNative: React.FC<SmartBarChartProps> = ({
339
364
  >
340
365
  <Svg width="100%" height="100%">
341
366
  <G transform={`translate(${margin.left},${margin.top})`}>
342
- {/* Gridlines & Y-Axis */}
343
- {yScale.ticks(5).map(tickValue => (
344
- <G key={`y-tick-${tickValue}`} transform={`translate(0, ${yScale(tickValue)})`}>
345
- <Line x1={0} x2={chartWidth} stroke="#eee" strokeDasharray="4 4" />
346
- <SvgText x={-10} y={4} textAnchor="end" fontSize={10} fill="#999">
347
- {tickValue}
348
- </SvgText>
349
- </G>
350
- ))}
367
+ {/* Gridlines & Value Axis */}
368
+ {valueScale.ticks(5).map(tickValue => {
369
+ const valPos = valueScale(tickValue);
370
+ return (
371
+ <G key={`y-tick-${tickValue}`} transform={isHorizontal ? `translate(${valPos}, 0)` : `translate(0, ${valPos})`}>
372
+ <Line x1={0} y1={0} x2={isHorizontal ? 0 : chartWidth} y2={isHorizontal ? chartHeight : 0} stroke="#eee" strokeDasharray="4 4" />
373
+ <SvgText x={isHorizontal ? 0 : -10} y={isHorizontal ? chartHeight + 15 : 4} textAnchor={isHorizontal ? "middle" : "end"} fontSize={10} fill="#999">
374
+ {valueFormatter(tickValue)}
375
+ </SvgText>
376
+ </G>
377
+ )
378
+ })}
379
+
380
+ {/* Annotations */}
381
+ {annotations.map((ann, i) => {
382
+ const valPos = valueScale(ann.value);
383
+ if (isNaN(valPos)) return null;
384
+ return (
385
+ <G key={`annotation-${i}`} transform={isHorizontal ? `translate(${valPos}, 0)` : `translate(0, ${valPos})`}>
386
+ <Line
387
+ x1={0} y1={0}
388
+ x2={isHorizontal ? 0 : chartWidth}
389
+ y2={isHorizontal ? chartHeight : 0}
390
+ stroke={ann.color || '#ef4444'} strokeDasharray={ann.strokeDasharray || '4 4'} strokeWidth={2}
391
+ />
392
+ {ann.label && (
393
+ <SvgText
394
+ x={isHorizontal ? 0 : chartWidth}
395
+ y={isHorizontal ? -5 : -5}
396
+ textAnchor={isHorizontal ? "middle" : "end"}
397
+ fill={ann.color || '#ef4444'} fontSize={10} fontWeight="bold"
398
+ >
399
+ {ann.label}
400
+ </SvgText>
401
+ )}
402
+ </G>
403
+ );
404
+ })}
351
405
 
352
406
  {/* Axis Lines */}
353
407
  <Line x1={0} y1={chartHeight} x2={chartWidth} y2={chartHeight} stroke="#ccc" />
@@ -381,48 +435,93 @@ export const SmartBarChartNative: React.FC<SmartBarChartProps> = ({
381
435
 
382
436
  {/* Bars */}
383
437
  {visibleData.map((d) => {
384
- const xBand = xScale(d.id);
385
- const bandwidth = xScale.bandwidth();
438
+ const bandPos = domainScale(d.id);
439
+ const bandwidth = domainScale.bandwidth();
386
440
  const maxBarWidth = 60;
387
- const barWidth = Math.min(bandwidth, maxBarWidth);
388
- const x = xBand! + (bandwidth - barWidth) / 2;
389
-
390
- const yTotal = yScale(d.value);
391
- const barHeightTotal = chartHeight - yTotal;
441
+ const barThickness = Math.min(bandwidth, maxBarWidth);
442
+ const orthogonalOffset = bandPos! + (bandwidth - barThickness) / 2;
443
+
444
+ const valEndPos = valueScale(d.value);
445
+ const valZeroPos = valueScale(0);
392
446
 
393
- if (xBand === undefined) return null;
447
+ if (bandPos === undefined) return null;
394
448
 
395
449
  const isSelected = activeItem?.id === d.id;
396
450
  const isDimmed = activeItem !== null && !isSelected;
397
451
 
398
- // Stacked Rendering
399
- if (variant === 'stacked' && d.stackedValues) {
400
- let currentY = chartHeight;
452
+ const barLengthTotal = Math.abs(valZeroPos - valEndPos);
453
+ const xTotal = isHorizontal ? valZeroPos : orthogonalOffset;
454
+ const yTotal = isHorizontal ? orthogonalOffset : valEndPos;
455
+ const wTotal = isHorizontal ? barLengthTotal : barThickness;
456
+ const hTotal = isHorizontal ? barThickness : barLengthTotal;
457
+
458
+ // Stacked or Grouped Rendering
459
+ if ((variant === 'stacked' || variant === 'grouped') && d.stackedValues) {
460
+ let currentStart = valZeroPos;
461
+ const groupThickness = barThickness / d.stackedValues.length;
462
+
401
463
  return (
402
464
  <G key={d.id}
403
465
  onPress={() => setActiveItem(activeItem === d ? null : d)}
404
466
  opacity={isDimmed ? 0.3 : 1}
405
467
  >
406
468
  {d.stackedValues.map((stack, i) => {
407
- const segmentHeight = Math.abs(yScale(stack.value) - yScale(0));
408
- const segmentY = currentY - segmentHeight;
409
- const rect = (
410
- <Rect
469
+ const segLength = Math.abs(valueScale(stack.value) - valueScale(0));
470
+
471
+ let segmentX, segmentY, segmentW, segmentH;
472
+ let corners = { tl: false, tr: false, bl: false, br: false };
473
+
474
+ if (variant === 'stacked') {
475
+ if (isHorizontal) {
476
+ segmentX = currentStart;
477
+ segmentY = orthogonalOffset;
478
+ segmentW = segLength;
479
+ segmentH = barThickness;
480
+
481
+ const isFirst = i === 0;
482
+ const isLast = i === d.stackedValues!.length - 1;
483
+ corners = { tl: isFirst, bl: isFirst, tr: isLast, br: isLast };
484
+ currentStart += segLength;
485
+ } else {
486
+ segmentH = segLength;
487
+ segmentY = currentStart - segLength;
488
+ segmentX = orthogonalOffset;
489
+ segmentW = barThickness;
490
+
491
+ const isTop = i === d.stackedValues!.length - 1;
492
+ const isBottom = i === 0;
493
+ corners = { tl: isTop, tr: isTop, bl: isBottom, br: isBottom };
494
+ currentStart = segmentY;
495
+ }
496
+ } else { // grouped
497
+ if (isHorizontal) {
498
+ segmentX = valZeroPos;
499
+ segmentY = orthogonalOffset + i * groupThickness;
500
+ segmentW = segLength;
501
+ segmentH = groupThickness;
502
+ } else {
503
+ segmentX = orthogonalOffset + i * groupThickness;
504
+ segmentY = valueScale(stack.value);
505
+ segmentW = groupThickness;
506
+ segmentH = segLength;
507
+ }
508
+ corners = { tl: !isHorizontal, tr: true, bl: isHorizontal, br: isHorizontal };
509
+ }
510
+
511
+ const pathD = getRoundedPath(segmentX, segmentY, segmentW, segmentH, 4, corners);
512
+
513
+ return (
514
+ <Path
411
515
  key={`${d.id}-${i}`}
412
- x={x}
413
- y={segmentY}
414
- width={barWidth}
415
- height={segmentHeight}
516
+ d={pathD}
416
517
  fill={stack.color}
417
518
  stroke={isSelected ? '#fff' : 'none'}
418
519
  strokeWidth={isSelected ? 1 : 0}
419
520
  />
420
521
  );
421
- currentY = segmentY;
422
- return rect;
423
522
  })}
424
523
  {/* Hitbox */}
425
- <Rect x={x} y={yTotal} width={barWidth} height={barHeightTotal} fill="transparent" onPress={() => setActiveItem(activeItem === d ? null : d)} />
524
+ <Rect x={xTotal} y={yTotal} width={wTotal} height={hTotal} fill="transparent" onPress={() => setActiveItem(activeItem === d ? null : d)} />
426
525
  </G>
427
526
  )
428
527
  }
@@ -430,10 +529,10 @@ export const SmartBarChartNative: React.FC<SmartBarChartProps> = ({
430
529
  return (
431
530
  <G key={d.id}>
432
531
  <BarNative
433
- x={x}
532
+ x={xTotal}
434
533
  y={yTotal}
435
- width={barWidth}
436
- height={barHeightTotal}
534
+ width={wTotal}
535
+ height={hTotal}
437
536
  data={d}
438
537
  isActive={isSelected}
439
538
  isDimmed={isDimmed}
@@ -445,30 +544,30 @@ export const SmartBarChartNative: React.FC<SmartBarChartProps> = ({
445
544
  />
446
545
  {isSelected && (
447
546
  <SvgText
448
- x={x + barWidth / 2}
449
- y={yTotal - 5}
450
- textAnchor="middle"
547
+ x={isHorizontal ? xTotal + wTotal + 10 : xTotal + wTotal / 2}
548
+ y={isHorizontal ? yTotal + hTotal / 2 + 4 : yTotal - 5}
549
+ textAnchor={isHorizontal ? "start" : "middle"}
451
550
  fill="#333"
452
551
  fontSize={10}
453
552
  fontWeight="bold"
454
553
  >
455
- {d.value}
554
+ {valueFormatter(d.value)}
456
555
  </SvgText>
457
556
  )}
458
557
  </G>
459
558
  );
460
559
  })}
461
560
 
462
- {/* X Axis Labels */}
561
+ {/* Domain Axis Labels */}
463
562
  {visibleData.map((d) => {
464
- const x = xScale(d.id);
465
- if (x === undefined) return null;
563
+ const bandPos = domainScale(d.id);
564
+ if (bandPos === undefined) return null;
466
565
  return (
467
566
  <SvgText
468
567
  key={`label-${d.id}`}
469
- x={x + xScale.bandwidth() / 2}
470
- y={chartHeight + 15}
471
- textAnchor="middle"
568
+ x={isHorizontal ? -10 : bandPos + domainScale.bandwidth() / 2}
569
+ y={isHorizontal ? bandPos + domainScale.bandwidth() / 2 + 4 : chartHeight + 15}
570
+ textAnchor={isHorizontal ? "end" : "middle"}
472
571
  fill="#666"
473
572
  fontSize={10}
474
573
  >