@@ -35,20 +35,100 @@ type BarChartProps = {
3535 barSize ?: number
3636 /** Render vertical grid lines (one per x tick). Default `true`. */
3737 verticalGrid ?: boolean
38+ /** Chart height in pixels. Default 400. */
39+ height ?: number
40+ /**
41+ * Render series side by side instead of stacked. Required for non-additive
42+ * values (rates, percentages) where stacking would be meaningless.
43+ */
44+ grouped ?: boolean
45+ /**
46+ * dataKey → stack id, for part-of-whole bars: series sharing a stack id
47+ * stack together, distinct ids sit side by side (e.g. converted/remainder
48+ * segments stacked per variant, variants grouped). Overrides `grouped`.
49+ */
50+ stackMap ?: Record < string , string >
51+ /**
52+ * dataKey → fill opacity (0–1). Colours are CSS `var()` strings, so
53+ * transparency must come from SVG fill-opacity, not an alpha channel.
54+ */
55+ opacityMap ?: Record < string , number >
56+ /** Left axis overrides, e.g. a `%` tick formatter. */
57+ yAxis ?: {
58+ tickFormatter ?: ( value : number ) => string
59+ domain ?: [ number , number ]
60+ }
61+ /**
62+ * Per-entry tooltip value renderer, threaded to ChartTooltip. Skipped for
63+ * missing or non-numeric values, which render blank.
64+ */
65+ tooltipValueFormatter ?: (
66+ value : number ,
67+ seriesKey : string ,
68+ label : string ,
69+ ) => string
70+ /**
71+ * Hide the tooltip's total row — required when `tooltipValueFormatter`
72+ * renders a non-additive unit such as a percentage.
73+ */
74+ tooltipHideTotal ?: boolean
3875}
3976
77+ type FadedSwatchLegendProps = {
78+ opacityMap : Record < string , number >
79+ seriesLabels ?: Record < string , string >
80+ // Injected by recharts' <Legend content={...}>.
81+ payload ?: { value ?: string | number ; color ?: string } [ ]
82+ }
83+
84+ const FadedSwatchLegend : FC < FadedSwatchLegendProps > = ( {
85+ opacityMap,
86+ payload,
87+ seriesLabels,
88+ } ) => (
89+ < div className = 'd-flex justify-content-center flex-wrap gap-3' >
90+ { payload ?. map ( ( entry ) => {
91+ const key = String ( entry . value )
92+ return (
93+ < span className = 'd-flex align-items-center gap-1' key = { key } >
94+ < span
95+ style = { {
96+ backgroundColor : entry . color ,
97+ display : 'inline-block' ,
98+ height : 10 ,
99+ opacity : opacityMap [ key ] ?? 1 ,
100+ width : 10 ,
101+ } }
102+ />
103+ < span style = { { color : entry . color , fontSize : 12 } } >
104+ { seriesLabels ?. [ key ] ?? key }
105+ </ span >
106+ </ span >
107+ )
108+ } ) }
109+ </ div >
110+ )
111+
40112const BarChart : FC < BarChartProps > = ( {
41113 barSize,
42114 colorMap,
43115 data,
116+ grouped = false ,
117+ height = 400 ,
118+ opacityMap,
44119 series,
45120 seriesLabels,
46121 showLegend = false ,
122+ stackMap,
123+ tooltipHideTotal,
124+ tooltipValueFormatter,
47125 verticalGrid = true ,
48126 xAxisInterval = 0 ,
127+ yAxis,
49128} ) => {
129+ const defaultStackId = grouped ? undefined : 'series'
50130 return (
51- < ResponsiveContainer height = { 400 } width = '100%' >
131+ < ResponsiveContainer height = { height } width = '100%' >
52132 < RawBarChart data = { data } >
53133 < CartesianGrid
54134 strokeDasharray = '3 5'
@@ -69,28 +149,48 @@ const BarChart: FC<BarChartProps> = ({
69149 < YAxis
70150 tick = { { fill : colorTextSecondary , fontSize : 11 } }
71151 axisLine = { { stroke : colorTextSecondary } }
72- tickFormatter = { ( value ) =>
73- value >= 1000 ? `${ ( value / 1000 ) . toFixed ( 0 ) } k` : value
152+ domain = { yAxis ?. domain }
153+ tickFormatter = {
154+ yAxis ?. tickFormatter ??
155+ ( ( value ) =>
156+ value >= 1000 ? `${ ( value / 1000 ) . toFixed ( 0 ) } k` : value )
74157 }
75158 />
76159 < Tooltip
77160 cursor = { { fill : 'transparent' } }
78- content = { < ChartTooltip seriesLabels = { seriesLabels } /> }
161+ content = {
162+ < ChartTooltip
163+ hideTotal = { tooltipHideTotal }
164+ seriesLabels = { seriesLabels }
165+ valueFormatter = { tooltipValueFormatter }
166+ />
167+ }
79168 />
80169 { showLegend && (
81170 < Legend
82171 wrapperStyle = { { paddingTop : 16 } }
83172 formatter = { ( value ) =>
84173 seriesLabels ?. [ String ( value ) ] ?? String ( value )
85174 }
175+ content = {
176+ // The default legend swatch ignores fillOpacity, so faded
177+ // series need their own renderer to match the bars.
178+ opacityMap ? (
179+ < FadedSwatchLegend
180+ opacityMap = { opacityMap }
181+ seriesLabels = { seriesLabels }
182+ />
183+ ) : undefined
184+ }
86185 />
87186 ) }
88187 { series . map ( ( label , index ) => (
89188 < Bar
90189 key = { label }
91190 dataKey = { label }
92- stackId = 'series'
191+ stackId = { stackMap ? stackMap [ label ] : defaultStackId }
93192 fill = { colorMap [ label ] }
193+ fillOpacity = { opacityMap ?. [ label ] }
94194 barSize = { barSize }
95195 animationBegin = { index * 80 }
96196 animationDuration = { 600 }
0 commit comments