Skip to content

Commit c945047

Browse files
committed
Consolidate functionality after weird charts
Chart file line counts (before → after): - StackedBarChart: 243 → 198 (-45) - GroupedBarChart: 241 → 196 (-45) - PieChart: 226 → 181 (-45) - DonutChart: 232 → 187 (-45) - SwimlaneChart: 280 → 237 (-43) - LikertChart: 669 → 432 (-237) - BoxPlot: 204 → 184 (-20) - ViolinPlot: 230 → 198 (-32) - RidgelinePlot: 203 → 193 (-10, but gained forwardRef + push API) - Histogram: 229 → 230 (+1, gained legendInteraction) New shared files: - useOrdinalStreaming.ts: 110 lines - statsTooltip.tsx: 74 lines - useLikertAggregation.ts: 257 lines Net reduction across ordinal charts: ~567 lines removed, 441 lines added to shared = ~126 fewer total lines, but more importantly the code is now DRY and consistent. Here's the complete summary of all 7 items: #1 — useOrdinalStreaming hook: Extracted streaming legend + push API + margin merge into shared hook. Used by 5 charts (StackedBar, GroupedBar, Pie, Donut, Swimlane). Each lost ~45 lines of boilerplate. #2 — buildStatsTooltip: Extracted shared stats tooltip for distribution charts. BoxPlot, ViolinPlot, RidgelinePlot now all use the same function. ViolinPlot's fallback computation from raw data is also shared via the valueAccessor option. #3 — useLikertAggregation hook: Extracted all aggregation logic (aggregateData, toDivergingValues, orderForDiverging, defaultDivergingScheme, resolveAccessorFn, sentinels) into a dedicated hook. LikertChart dropped from 669 → 432 lines. #4 — Selection hook fix: Changed activeSelectionHook → effectiveSelectionHook in Histogram, ViolinPlot, and RidgelinePlot. #5 — Naming standardization: Renamed actualColorBy → effectiveColorBy across all 6 charts that used it. #6 — legendInteraction consistency: Added to Histogram, ViolinPlot, and RidgelinePlot prop interfaces and wired to useChartSetup. #7 — RidgelinePlot forwardRef: Converted to forwardRef with push API (push, pushMany, clear, getData). Also made data optional for push mode consistency.
1 parent 36f8b88 commit c945047

13 files changed

Lines changed: 586 additions & 656 deletions

src/components/charts/ordinal/BoxPlot.tsx

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { getColor } from "../shared/colorUtils"
77
import { useChartMode, useThemeCategorical, resolveDefaultFill } from "../shared/hooks"
88
import type { LegendInteractionMode, LegendPosition } from "../shared/hooks"
99
import type { BaseChartProps, ChartAccessor } from "../shared/types"
10-
import { normalizeTooltip, defaultTooltipStyle, type TooltipProp } from "../../Tooltip/Tooltip"
10+
import { normalizeTooltip, type TooltipProp } from "../../Tooltip/Tooltip"
1111
import ChartError from "../shared/ChartError"
1212
import { SafeRender } from "../shared/withChartWrapper"
1313
import { validateArrayData } from "../shared/validateChartData"
1414
import { wrapStyleWithSelection } from "../shared/selectionUtils"
1515
import type { RealtimeFrameHandle } from "../../realtime/types"
1616
import { useChartSetup } from "../shared/useChartSetup"
17+
import { buildStatsTooltip } from "../shared/statsTooltip"
1718

1819
export interface BoxPlotProps<TDatum extends Record<string, any> = Record<string, any>> extends BaseChartProps {
1920
data?: TDatum[]
@@ -133,27 +134,7 @@ export const BoxPlot = forwardRef(function BoxPlot<TDatum extends Record<string,
133134
[baseSummaryStyle, setup.effectiveSelectionHook, selection]
134135
)
135136

136-
const defaultTooltipContent = useMemo(() => {
137-
return (d: Record<string, any>) => {
138-
const stats = d.stats || (d.data || d).stats || {}
139-
const category = d.category || (d.data || d).category || ""
140-
return (
141-
<div className="semiotic-tooltip" style={defaultTooltipStyle}>
142-
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>{String(category)}</div>
143-
{stats.median != null && (
144-
<>
145-
{stats.n != null && <div>n = {stats.n}</div>}
146-
<div>Median: {stats.median.toLocaleString()}</div>
147-
<div>Q1: {stats.q1.toLocaleString()}</div>
148-
<div>Q3: {stats.q3.toLocaleString()}</div>
149-
<div>Min: {stats.min.toLocaleString()}</div>
150-
<div>Max: {stats.max.toLocaleString()}</div>
151-
</>
152-
)}
153-
</div>
154-
)
155-
}
156-
}, [])
137+
const defaultTooltipContent = useMemo(() => buildStatsTooltip(), [])
157138

158139
const error = validateArrayData({
159140
componentName: "BoxPlot", data: data,

src/components/charts/ordinal/DonutChart.tsx

Lines changed: 15 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22
import * as React from "react"
3-
import { useMemo, useCallback, forwardRef, useRef, useImperativeHandle } from "react"
3+
import { useMemo, forwardRef, useRef } from "react"
44
import StreamOrdinalFrame from "../../stream/StreamOrdinalFrame"
55
import type { StreamOrdinalFrameProps, StreamOrdinalFrameHandle } from "../../stream/ordinalTypes"
66
import { getColor } from "../shared/colorUtils"
@@ -15,7 +15,7 @@ import { validateArrayData } from "../shared/validateChartData"
1515
import { wrapStyleWithSelection } from "../shared/selectionUtils"
1616
import type { RealtimeFrameHandle } from "../../realtime/types"
1717
import { useChartSetup } from "../shared/useChartSetup"
18-
import { useStreamingLegend } from "../shared/useStreamingLegend"
18+
import { useOrdinalStreaming } from "../shared/useOrdinalStreaming"
1919

2020
export interface DonutChartProps<TDatum extends Record<string, any> = Record<string, any>> extends BaseChartProps {
2121
data?: TDatum[]
@@ -77,46 +77,19 @@ export const DonutChart = forwardRef(function DonutChart<TDatum extends Record<s
7777
const accessibleTable = resolved.accessibleTable
7878

7979
const safeData = data || []
80-
const actualColorBy = colorBy || categoryAccessor
80+
const effectiveColorBy = colorBy || categoryAccessor
8181
const isPushMode = data === undefined
8282

83-
const streaming = useStreamingLegend({
84-
isPushMode,
85-
colorBy: actualColorBy,
86-
colorScheme,
87-
showLegend,
88-
legendPosition: legendPositionProp,
89-
})
90-
91-
const wrappedPush = useCallback(
92-
streaming.wrapPush((d: any) => frameRef.current?.push(d)),
93-
[streaming.wrapPush]
94-
)
95-
const wrappedPushMany = useCallback(
96-
streaming.wrapPushMany((d: any[]) => frameRef.current?.pushMany(d)),
97-
[streaming.wrapPushMany]
98-
)
99-
100-
useImperativeHandle(ref, () => ({
101-
push: wrappedPush,
102-
pushMany: wrappedPushMany,
103-
clear: () => {
104-
streaming.resetCategories()
105-
frameRef.current?.clear()
106-
},
107-
getData: () => frameRef.current?.getData() ?? []
108-
}), [wrappedPush, wrappedPushMany, streaming.resetCategories])
109-
11083
const setup = useChartSetup({
11184
data: safeData,
11285
rawData: data,
113-
colorBy: actualColorBy,
86+
colorBy: effectiveColorBy,
11487
colorScheme,
11588
legendInteraction,
11689
legendPosition: legendPositionProp,
11790
selection,
11891
linkedHover,
119-
fallbackFields: actualColorBy ? [typeof actualColorBy === "string" ? actualColorBy : ""] : [],
92+
fallbackFields: effectiveColorBy ? [typeof effectiveColorBy === "string" ? effectiveColorBy : ""] : [],
12093
unwrapData: true,
12194
onObservation,
12295
chartType: "DonutChart",
@@ -137,13 +110,13 @@ export const DonutChart = forwardRef(function DonutChart<TDatum extends Record<s
137110

138111
const basePieceStyle = useMemo(() => {
139112
return (d: Record<string, any>, category?: string) => {
140-
if (actualColorBy) {
141-
if (setup.colorScale) return { fill: getColor(d, actualColorBy, setup.colorScale) }
113+
if (effectiveColorBy) {
114+
if (setup.colorScale) return { fill: getColor(d, effectiveColorBy, setup.colorScale) }
142115
return {} // Let frame use its own color scheme (push API)
143116
}
144117
return { fill: resolveDefaultFill(color, themeCategorical, colorScheme, category, categoryIndexMap) }
145118
}
146-
}, [actualColorBy, setup.colorScale, color, themeCategorical, colorScheme, categoryIndexMap])
119+
}, [effectiveColorBy, setup.colorScale, color, themeCategorical, colorScheme, categoryIndexMap])
147120

148121
const pieceStyle = useMemo(
149122
() => wrapStyleWithSelection(basePieceStyle, setup.effectiveSelectionHook, selection),
@@ -166,30 +139,13 @@ export const DonutChart = forwardRef(function DonutChart<TDatum extends Record<s
166139
accessors: { categoryAccessor, valueAccessor },
167140
})
168141

169-
// Merge streaming legend into legendBehaviorProps when in push API mode
170-
const effectiveLegendProps = useMemo(() => {
171-
if (streaming.streamingLegend) {
172-
return {
173-
...setup.legendBehaviorProps,
174-
legend: streaming.streamingLegend,
175-
legendPosition: legendPositionProp || setup.legendPosition,
176-
}
177-
}
178-
return setup.legendBehaviorProps
179-
}, [setup.legendBehaviorProps, setup.legendPosition, streaming.streamingLegend, legendPositionProp])
180-
181-
// Adjust margin for streaming legend
182-
const effectiveMargin = useMemo(() => {
183-
if (streaming.streamingMarginAdjust) {
184-
const m = { ...setup.margin }
185-
for (const [key, val] of Object.entries(streaming.streamingMarginAdjust)) {
186-
const k = key as keyof typeof m
187-
if (m[k] < val) m[k] = val
188-
}
189-
return m
190-
}
191-
return setup.margin
192-
}, [setup.margin, streaming.streamingMarginAdjust])
142+
const { effectiveLegendProps, effectiveMargin } = useOrdinalStreaming({
143+
ref, frameRef, isPushMode,
144+
colorBy: effectiveColorBy,
145+
colorScheme, showLegend,
146+
legendPosition: legendPositionProp,
147+
setup,
148+
})
193149

194150
const streamProps: StreamOrdinalFrameProps = {
195151
chartType: "donut",

src/components/charts/ordinal/GroupedBarChart.tsx

Lines changed: 15 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22
import * as React from "react"
3-
import { useMemo, useCallback, forwardRef, useRef, useImperativeHandle } from "react"
3+
import { useMemo, forwardRef, useRef } from "react"
44
import StreamOrdinalFrame from "../../stream/StreamOrdinalFrame"
55
import type { StreamOrdinalFrameProps, StreamOrdinalFrameHandle } from "../../stream/ordinalTypes"
66
import { getColor } from "../shared/colorUtils"
@@ -15,7 +15,7 @@ import { validateArrayData } from "../shared/validateChartData"
1515
import { wrapStyleWithSelection } from "../shared/selectionUtils"
1616
import type { RealtimeFrameHandle } from "../../realtime/types"
1717
import { useChartSetup } from "../shared/useChartSetup"
18-
import { useStreamingLegend } from "../shared/useStreamingLegend"
18+
import { useOrdinalStreaming } from "../shared/useOrdinalStreaming"
1919

2020
export interface GroupedBarChartProps<TDatum extends Record<string, any> = Record<string, any>> extends BaseChartProps {
2121
data?: TDatum[]
@@ -85,46 +85,19 @@ export const GroupedBarChart = forwardRef(function GroupedBarChart<TDatum extend
8585
const valueLabel = resolved.valueLabel
8686

8787
const safeData = data || []
88-
const actualColorBy = colorBy || groupBy
88+
const effectiveColorBy = colorBy || groupBy
8989
const isPushMode = data === undefined
9090

91-
const streaming = useStreamingLegend({
92-
isPushMode,
93-
colorBy: actualColorBy,
94-
colorScheme,
95-
showLegend,
96-
legendPosition: legendPositionProp,
97-
})
98-
99-
const wrappedPush = useCallback(
100-
streaming.wrapPush((d: any) => frameRef.current?.push(d)),
101-
[streaming.wrapPush]
102-
)
103-
const wrappedPushMany = useCallback(
104-
streaming.wrapPushMany((d: any[]) => frameRef.current?.pushMany(d)),
105-
[streaming.wrapPushMany]
106-
)
107-
108-
useImperativeHandle(ref, () => ({
109-
push: wrappedPush,
110-
pushMany: wrappedPushMany,
111-
clear: () => {
112-
streaming.resetCategories()
113-
frameRef.current?.clear()
114-
},
115-
getData: () => frameRef.current?.getData() ?? []
116-
}), [wrappedPush, wrappedPushMany, streaming.resetCategories])
117-
11891
const setup = useChartSetup({
11992
data: safeData,
12093
rawData: data,
121-
colorBy: actualColorBy,
94+
colorBy: effectiveColorBy,
12295
colorScheme,
12396
legendInteraction,
12497
legendPosition: legendPositionProp,
12598
selection,
12699
linkedHover,
127-
fallbackFields: actualColorBy ? [typeof actualColorBy === "string" ? actualColorBy : ""] : [],
100+
fallbackFields: effectiveColorBy ? [typeof effectiveColorBy === "string" ? effectiveColorBy : ""] : [],
128101
unwrapData: true,
129102
onObservation,
130103
chartType: "GroupedBarChart",
@@ -145,13 +118,13 @@ export const GroupedBarChart = forwardRef(function GroupedBarChart<TDatum extend
145118

146119
const basePieceStyle = useMemo(() => {
147120
return (d: Record<string, any>, category?: string) => {
148-
if (actualColorBy) {
149-
if (setup.colorScale) return { fill: getColor(d, actualColorBy, setup.colorScale) }
121+
if (effectiveColorBy) {
122+
if (setup.colorScale) return { fill: getColor(d, effectiveColorBy, setup.colorScale) }
150123
return {} // Let frame use its own color scheme (push API)
151124
}
152125
return { fill: resolveDefaultFill(color, themeCategorical, colorScheme, category, categoryIndexMap) }
153126
}
154-
}, [actualColorBy, setup.colorScale, color, themeCategorical, colorScheme, categoryIndexMap])
127+
}, [effectiveColorBy, setup.colorScale, color, themeCategorical, colorScheme, categoryIndexMap])
155128

156129
const pieceStyle = useMemo(
157130
() => wrapStyleWithSelection(basePieceStyle, setup.effectiveSelectionHook, selection),
@@ -172,30 +145,13 @@ export const GroupedBarChart = forwardRef(function GroupedBarChart<TDatum extend
172145
accessors: { categoryAccessor, valueAccessor }, requiredProps: { groupBy },
173146
})
174147

175-
// Merge streaming legend into legendBehaviorProps when in push API mode
176-
const effectiveLegendProps = useMemo(() => {
177-
if (streaming.streamingLegend) {
178-
return {
179-
...setup.legendBehaviorProps,
180-
legend: streaming.streamingLegend,
181-
legendPosition: legendPositionProp || setup.legendPosition,
182-
}
183-
}
184-
return setup.legendBehaviorProps
185-
}, [setup.legendBehaviorProps, setup.legendPosition, streaming.streamingLegend, legendPositionProp])
186-
187-
// Adjust margin for streaming legend
188-
const effectiveMargin = useMemo(() => {
189-
if (streaming.streamingMarginAdjust) {
190-
const m = { ...setup.margin }
191-
for (const [key, val] of Object.entries(streaming.streamingMarginAdjust)) {
192-
const k = key as keyof typeof m
193-
if (m[k] < val) m[k] = val
194-
}
195-
return m
196-
}
197-
return setup.margin
198-
}, [setup.margin, streaming.streamingMarginAdjust])
148+
const { effectiveLegendProps, effectiveMargin } = useOrdinalStreaming({
149+
ref, frameRef, isPushMode,
150+
colorBy: effectiveColorBy,
151+
colorScheme, showLegend,
152+
legendPosition: legendPositionProp,
153+
setup,
154+
})
199155

200156
const streamProps: StreamOrdinalFrameProps = {
201157
chartType: "clusterbar",

src/components/charts/ordinal/Histogram.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import StreamOrdinalFrame from "../../stream/StreamOrdinalFrame"
55
import type { StreamOrdinalFrameProps, StreamOrdinalFrameHandle } from "../../stream/ordinalTypes"
66
import { getColor } from "../shared/colorUtils"
77
import { useChartMode, useThemeCategorical, resolveDefaultFill } from "../shared/hooks"
8-
import type { LegendPosition } from "../shared/hooks"
8+
import type { LegendInteractionMode, LegendPosition } from "../shared/hooks"
99
import type { BaseChartProps, ChartAccessor } from "../shared/types"
1010
import { normalizeTooltip, defaultTooltipStyle, type TooltipProp } from "../../Tooltip/Tooltip"
1111
import ChartError from "../shared/ChartError"
@@ -32,6 +32,7 @@ export interface HistogramProps<TDatum extends Record<string, any> = Record<stri
3232
showGrid?: boolean
3333
showCategoryTicks?: boolean
3434
showLegend?: boolean
35+
legendInteraction?: LegendInteractionMode
3536
legendPosition?: LegendPosition
3637
tooltip?: TooltipProp
3738
annotations?: Record<string, any>[]
@@ -80,6 +81,7 @@ export const Histogram = forwardRef(function Histogram<TDatum extends Record<str
8081
frameProps = {}, selection, linkedHover,
8182
onObservation, chartId,
8283
loading, emptyContent,
84+
legendInteraction,
8385
legendPosition: legendPositionProp,
8486
color: colorProp,
8587
showCategoryTicks
@@ -104,7 +106,7 @@ export const Histogram = forwardRef(function Histogram<TDatum extends Record<str
104106
rawData: data,
105107
colorBy,
106108
colorScheme,
107-
legendInteraction: undefined,
109+
legendInteraction,
108110
legendPosition: legendPositionProp,
109111
selection,
110112
linkedHover,
@@ -153,8 +155,8 @@ export const Histogram = forwardRef(function Histogram<TDatum extends Record<str
153155
}, [colorBy, setup.colorScale, colorProp, themeCategorical, colorScheme, categoryIndexMap])
154156

155157
const summaryStyle = useMemo(
156-
() => wrapStyleWithSelection(baseSummaryStyle, setup.activeSelectionHook, selection),
157-
[baseSummaryStyle, setup.activeSelectionHook, selection]
158+
() => wrapStyleWithSelection(baseSummaryStyle, setup.effectiveSelectionHook, selection),
159+
[baseSummaryStyle, setup.effectiveSelectionHook, selection]
158160
)
159161

160162
const defaultTooltipContent = useMemo(() => {

0 commit comments

Comments
 (0)