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/README.md +22 -17
- package/dist/smart-bar-chart.js +2882 -2749
- package/dist/smart-bar-chart.umd.cjs +20 -20
- package/package.json +1 -1
- package/src/lib/SmartBarChart.tsx +155 -63
- package/src/lib/SmartBarChartNative.tsx +152 -53
- package/src/lib/hooks/useChartData.ts +51 -3
- package/src/lib/types.ts +6 -1
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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 &
|
|
468
|
-
{gridVisible &&
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
<
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
|
509
|
-
const bandwidth =
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
const
|
|
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 (
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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={
|
|
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={
|
|
652
|
+
x={xTotal}
|
|
574
653
|
y={yTotal}
|
|
575
|
-
width={
|
|
576
|
-
height={
|
|
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={
|
|
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
|
-
{/*
|
|
684
|
+
{/* Domain Axis Labels */}
|
|
606
685
|
{visibleData.map((d) => {
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
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={
|
|
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
|
|
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
|
|
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 &
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
347
|
-
{
|
|
348
|
-
|
|
349
|
-
|
|
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
|
|
385
|
-
const bandwidth =
|
|
438
|
+
const bandPos = domainScale(d.id);
|
|
439
|
+
const bandwidth = domainScale.bandwidth();
|
|
386
440
|
const maxBarWidth = 60;
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
const
|
|
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 (
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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={
|
|
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={
|
|
532
|
+
x={xTotal}
|
|
434
533
|
y={yTotal}
|
|
435
|
-
width={
|
|
436
|
-
height={
|
|
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={
|
|
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
|
-
{/*
|
|
561
|
+
{/* Domain Axis Labels */}
|
|
463
562
|
{visibleData.map((d) => {
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
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={
|
|
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
|
>
|