Skip to content

Commit a13c435

Browse files
committed
Code cleanup, bug-squashing
1 parent 60eae9d commit a13c435

9 files changed

Lines changed: 168 additions & 47 deletions

File tree

docs/src/App.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@ export default function DocsApp() {
278278
<Route path="features/styling" element={<Navigate to="/theming/styling" replace />} />
279279
<Route path="features/theming" element={<Navigate to="/theming/theme-provider" replace />} />
280280

281+
{/* Redirects for legacy v1/v2 API routes (SEO: prevent stale search results) */}
282+
<Route path="api/xyframe" element={<Navigate to="/frames/xy-frame" replace />} />
283+
<Route path="api/ordinalframe" element={<Navigate to="/frames/ordinal-frame" replace />} />
284+
<Route path="api/networkframe" element={<Navigate to="/frames/network-frame" replace />} />
285+
<Route path="api/mark" element={<Navigate to="/api/charts" replace />} />
286+
281287
{/* Cookbook routes */}
282288
<Route path="cookbook" element={<Outlet />}>
283289
<Route

src/components/charts/network/ForceDirectedGraph.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as React from "react"
33
import { useMemo, useCallback, forwardRef, useRef, useImperativeHandle } from "react"
44
import StreamNetworkFrame from "../../stream/StreamNetworkFrame"
5-
import type { StreamNetworkFrameProps, StreamNetworkFrameHandle } from "../../stream/networkTypes"
5+
import type { StreamNetworkFrameProps, StreamNetworkFrameHandle, EdgePush } from "../../stream/networkTypes"
66
import type { RealtimeFrameHandle } from "../../realtime/types"
77
import { getColor, getSize } from "../shared/colorUtils"
88
import type { BaseChartProps, ChartAccessor } from "../shared/types"
@@ -49,8 +49,8 @@ export interface ForceDirectedGraphProps<TNode extends Record<string, any> = Rec
4949
export const ForceDirectedGraph = forwardRef(function ForceDirectedGraph<TNode extends Record<string, any> = Record<string, any>, TEdge extends Record<string, any> = Record<string, any>>(props: ForceDirectedGraphProps<TNode, TEdge>, ref: React.Ref<RealtimeFrameHandle>) {
5050
const frameRef = useRef<StreamNetworkFrameHandle>(null)
5151
useImperativeHandle(ref, () => ({
52-
push: (point) => frameRef.current?.push(point as any),
53-
pushMany: (points) => frameRef.current?.pushMany(points as any),
52+
push: (point) => frameRef.current?.push(point as EdgePush),
53+
pushMany: (points) => frameRef.current?.pushMany(points as EdgePush[]),
5454
clear: () => frameRef.current?.clear(),
5555
getData: () => frameRef.current?.getTopology()?.nodes?.map((n: any) => n.data) ?? []
5656
}))
@@ -145,7 +145,7 @@ export const ForceDirectedGraph = forwardRef(function ForceDirectedGraph<TNode e
145145
const edgeStyle = useMemo(() => {
146146
return (d: Record<string, any>) => ({
147147
stroke: edgeColor,
148-
strokeWidth: typeof edgeWidth === "number" ? edgeWidth : typeof edgeWidth === "function" ? edgeWidth(d as any) : d[edgeWidth] || 1,
148+
strokeWidth: typeof edgeWidth === "number" ? edgeWidth : typeof edgeWidth === "function" ? edgeWidth(d) : d[edgeWidth] || 1,
149149
opacity: edgeOpacity
150150
})
151151
}, [edgeWidth, edgeColor, edgeOpacity])

src/components/charts/xy/ConnectedScatterplot.tsx

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as React from "react"
33
import { useMemo, forwardRef, useRef, useImperativeHandle } from "react"
44
import StreamXYFrame from "../../stream/StreamXYFrame"
5-
import type { StreamXYFrameProps, StreamXYFrameHandle } from "../../stream/types"
5+
import type { StreamXYFrameProps, StreamXYFrameHandle, SceneNode, StreamScales, StreamLayout } from "../../stream/types"
66
import type { RealtimeFrameHandle } from "../../realtime/types"
77
import type { BaseChartProps, AxisConfig, ChartAccessor } from "../shared/types"
88
import { normalizeTooltip, type TooltipProp } from "../../Tooltip/Tooltip"
@@ -129,20 +129,40 @@ export const ConnectedScatterplot = forwardRef(function ConnectedScatterplot<TDa
129129

130130
const rawData = (data || []) as Record<string, any>[]
131131

132-
// Sort by orderAccessor if provided (bounded data mode only)
132+
// Sort by orderAccessor if provided, then stamp __orderIndex/__orderTotal
133+
// on each datum so pointStyle can read the index directly (order-independent).
133134
const safeData = useMemo(() => {
134-
if (!orderAccessor || rawData.length === 0) return rawData
135-
const getOrder = typeof orderAccessor === "function"
136-
? orderAccessor as (d: any) => number | Date
137-
: (d: any) => d[orderAccessor]
138-
return [...rawData].sort((a, b) => {
139-
const va = getOrder(a)
140-
const vb = getOrder(b)
141-
const na = va instanceof Date ? va.getTime() : +va
142-
const nb = vb instanceof Date ? vb.getTime() : +vb
143-
return na - nb
135+
const xAcc = typeof xAccessor === "function" ? xAccessor : (d: any) => d[xAccessor]
136+
const yAcc = typeof yAccessor === "function" ? yAccessor : (d: any) => d[yAccessor]
137+
let sorted = rawData
138+
if (orderAccessor && rawData.length > 0) {
139+
const getOrder = typeof orderAccessor === "function"
140+
? orderAccessor as (d: any) => number | Date
141+
: (d: any) => d[orderAccessor]
142+
sorted = [...rawData].sort((a, b) => {
143+
const va = getOrder(a)
144+
const vb = getOrder(b)
145+
const na = va instanceof Date ? va.getTime() : +va
146+
const nb = vb instanceof Date ? vb.getTime() : +vb
147+
return na - nb
148+
})
149+
}
150+
// Count renderable points and stamp ordering metadata
151+
const renderable = sorted.filter((p: any) => {
152+
const x = xAcc(p); const y = yAcc(p)
153+
return x != null && y != null && isFinite(x) && isFinite(y)
144154
})
145-
}, [rawData, orderAccessor])
155+
const total = renderable.length
156+
let idx = 0
157+
for (const d of sorted) {
158+
const x = xAcc(d); const y = yAcc(d)
159+
if (x != null && y != null && isFinite(x) && isFinite(y)) {
160+
(d as any).__orderIndex = idx++;
161+
(d as any).__orderTotal = total
162+
}
163+
}
164+
return sorted
165+
}, [rawData, orderAccessor, xAccessor, yAccessor])
146166

147167
// ── Dev-mode warnings ─────────────────────────────────────────────────
148168
warnMissingField("ConnectedScatterplot", safeData, "xAccessor", xAccessor)
@@ -221,35 +241,48 @@ export const ConnectedScatterplot = forwardRef(function ConnectedScatterplot<TDa
221241
[connectingLineRenderer]
222242
)
223243

244+
// ── SVG pre-renderer for SSR (same logic as canvas, produces SVG elements) ──
245+
const connectingLineSVGRenderer = useMemo(() => {
246+
return (nodes: SceneNode[], _scales: StreamScales, _layout: StreamLayout): React.ReactNode => {
247+
const pts = nodes.filter((n) => n.type === "point") as Array<{ x: number; y: number; datum?: any }>
248+
if (pts.length < 2) return null
249+
const count = pts.length
250+
const halo = count < 100
251+
const elements: React.ReactElement[] = []
252+
253+
for (let i = 0; i < count - 1; i++) {
254+
const p0 = pts[i]
255+
const p1 = pts[i + 1]
256+
const color = viridisColor(i, count)
257+
if (halo) {
258+
elements.push(
259+
<line key={`halo-${i}`} x1={p0.x} y1={p0.y} x2={p1.x} y2={p1.y}
260+
stroke="white" strokeWidth={pointRadius + 2} strokeLinecap="round" opacity={0.5} />
261+
)
262+
}
263+
elements.push(
264+
<line key={`seg-${i}`} x1={p0.x} y1={p0.y} x2={p1.x} y2={p1.y}
265+
stroke={color} strokeWidth={pointRadius} strokeLinecap="round" />
266+
)
267+
}
268+
return <>{elements}</>
269+
}
270+
}, [pointRadius])
271+
272+
const svgPreRenderers = useMemo(
273+
() => [connectingLineSVGRenderer],
274+
[connectingLineSVGRenderer]
275+
)
276+
224277
// ── Point style — viridis colored, fixed radius ───────────────────────
225278
//
226-
// pointStyle is called once per datum during scene build, in data order.
227-
// We use a call counter (reset each time the function reference changes)
228-
// so each datum gets the correct viridis position. The total count comes
229-
// from getData() which reflects the pipeline's current windowed data.
230-
231-
const pointCallCounter = useRef({ idx: 0, total: 0 })
279+
// Each datum has __orderIndex and __orderTotal stamped during sorting,
280+
// so pointStyle reads the index directly without relying on call order.
232281

233282
const basePointStyle = useMemo(() => {
234-
const xAcc = typeof xAccessor === "function" ? xAccessor : (d: any) => d[xAccessor]
235-
const yAcc = typeof yAccessor === "function" ? yAccessor : (d: any) => d[yAccessor]
236283
return (d: Record<string, any>) => {
237-
const counter = pointCallCounter.current
238-
// On first call of a batch, snapshot the renderable point count
239-
// (filter out invalid x/y so gradient matches the connecting-line renderer)
240-
if (counter.idx === 0) {
241-
const allData = frameRef.current?.getData() ?? safeData
242-
counter.total = allData.filter((p: any) => {
243-
const x = xAcc(p); const y = yAcc(p)
244-
return x != null && y != null && isFinite(x) && isFinite(y)
245-
}).length
246-
}
247-
const n = counter.total
248-
const i = counter.idx
249-
counter.idx++
250-
// Reset for next scene build cycle
251-
if (counter.idx >= n) counter.idx = 0
252-
284+
const i = (d as any).__orderIndex ?? 0
285+
const n = (d as any).__orderTotal ?? 1
253286
return {
254287
fill: n > 0 ? viridisColor(i, n) : "#6366f1",
255288
stroke: "white",
@@ -258,7 +291,7 @@ export const ConnectedScatterplot = forwardRef(function ConnectedScatterplot<TDa
258291
fillOpacity: 1,
259292
}
260293
}
261-
}, [pointRadius, safeData.length, xAccessor, yAccessor])
294+
}, [pointRadius])
262295

263296
const pointStyle = useMemo(
264297
() => wrapStyleWithSelection(basePointStyle, effectiveSelectionHook, selection),
@@ -321,6 +354,7 @@ export const ConnectedScatterplot = forwardRef(function ConnectedScatterplot<TDa
321354
...((linkedHover || onObservation) && { customHoverBehavior }),
322355
...(pointIdAccessor && { pointIdAccessor }),
323356
canvasPreRenderers,
357+
svgPreRenderers,
324358
...(annotations && annotations.length > 0 && { annotations }),
325359
...frameProps
326360
}

src/components/charts/xy/QuadrantChart.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as React from "react"
33
import { useMemo, forwardRef, useRef, useImperativeHandle } from "react"
44
import StreamXYFrame from "../../stream/StreamXYFrame"
5-
import type { StreamXYFrameProps, StreamXYFrameHandle, CanvasRendererFn, StreamScales, StreamLayout } from "../../stream/types"
5+
import type { StreamXYFrameProps, StreamXYFrameHandle, CanvasRendererFn, SVGPreRendererFn, StreamScales, StreamLayout, SceneNode } from "../../stream/types"
66
import type { RealtimeFrameHandle } from "../../realtime/types"
77
import { getColor, getSize } from "../shared/colorUtils"
88
import type { BaseChartProps, AxisConfig, ChartAccessor } from "../shared/types"
@@ -487,6 +487,65 @@ export const QuadrantChart = forwardRef(function QuadrantChart<TDatum extends Re
487487
return [...fullPreRenderers, ...userRenderers]
488488
}, [fullPreRenderers, frameProps.canvasPreRenderers])
489489

490+
// ── SVG pre-renderer for SSR (quadrant fills + center lines + labels) ──
491+
const svgPreRenderers = useMemo((): SVGPreRendererFn[] => {
492+
const clStyle = {
493+
stroke: centerlineStyle.stroke || "#999",
494+
strokeWidth: centerlineStyle.strokeWidth ?? 1,
495+
dashArray: centerlineStyle.strokeDasharray
496+
? Array.isArray(centerlineStyle.strokeDasharray)
497+
? (centerlineStyle.strokeDasharray as number[]).join(",")
498+
: centerlineStyle.strokeDasharray
499+
: undefined,
500+
}
501+
502+
return [(_nodes: SceneNode[], scales: StreamScales, layout: StreamLayout): React.ReactNode => {
503+
if (!scales?.x || !scales?.y) return null
504+
const w = layout.width
505+
const h = layout.height
506+
const xC = xCenter != null ? scales.x(xCenter) : w / 2
507+
const yC = yCenter != null ? scales.y(yCenter) : h / 2
508+
if (xCenter != null && !isFinite(xC)) return null
509+
if (yCenter != null && !isFinite(yC)) return null
510+
const cx = Math.max(0, Math.min(w, xC))
511+
const cy = Math.max(0, Math.min(h, yC))
512+
513+
const quads = [
514+
{ config: quadrants.topLeft, x: 0, y: 0, w: cx, h: cy },
515+
{ config: quadrants.topRight, x: cx, y: 0, w: w - cx, h: cy },
516+
{ config: quadrants.bottomLeft, x: 0, y: cy, w: cx, h: h - cy },
517+
{ config: quadrants.bottomRight, x: cx, y: cy, w: w - cx, h: h - cy },
518+
]
519+
const padding = 8
520+
return (
521+
<>
522+
{quads.map((q, i) => q.w > 0 && q.h > 0 ? (
523+
<rect key={`qf-${i}`} x={q.x} y={q.y} width={q.w} height={q.h}
524+
fill={q.config.color} opacity={q.config.opacity ?? 0.08} />
525+
) : null)}
526+
<line x1={cx} y1={0} x2={cx} y2={h}
527+
stroke={clStyle.stroke} strokeWidth={clStyle.strokeWidth}
528+
strokeDasharray={clStyle.dashArray} />
529+
<line x1={0} y1={cy} x2={w} y2={cy}
530+
stroke={clStyle.stroke} strokeWidth={clStyle.strokeWidth}
531+
strokeDasharray={clStyle.dashArray} />
532+
{showQuadrantLabels && (
533+
<>
534+
<text x={padding} y={padding + quadrantLabelSize} fill={quadrants.topLeft.color}
535+
fontWeight={600} fontSize={quadrantLabelSize} opacity={0.5}>{quadrants.topLeft.label}</text>
536+
<text x={w - padding} y={padding + quadrantLabelSize} fill={quadrants.topRight.color}
537+
fontWeight={600} fontSize={quadrantLabelSize} opacity={0.5} textAnchor="end">{quadrants.topRight.label}</text>
538+
<text x={padding} y={h - padding} fill={quadrants.bottomLeft.color}
539+
fontWeight={600} fontSize={quadrantLabelSize} opacity={0.5}>{quadrants.bottomLeft.label}</text>
540+
<text x={w - padding} y={h - padding} fill={quadrants.bottomRight.color}
541+
fontWeight={600} fontSize={quadrantLabelSize} opacity={0.5} textAnchor="end">{quadrants.bottomRight.label}</text>
542+
</>
543+
)}
544+
</>
545+
)
546+
}]
547+
}, [xCenter, yCenter, quadrants, centerlineStyle, showQuadrantLabels, quadrantLabelSize])
548+
490549
const streamProps: StreamXYFrameProps = {
491550
chartType: "scatter",
492551
...(data != null && { data: safeData }),
@@ -527,6 +586,7 @@ export const QuadrantChart = forwardRef(function QuadrantChart<TDatum extends Re
527586
...(pointIdAccessor && { pointIdAccessor }),
528587
...(annotations && annotations.length > 0 && { annotations }),
529588
canvasPreRenderers: mergedPreRenderers,
589+
svgPreRenderers,
530590
...frameProps,
531591
// Override canvasPreRenderers after spread so user can't clobber quadrant renderers
532592
...(mergedPreRenderers.length > 0 && { canvasPreRenderers: mergedPreRenderers }),

src/components/stream/OrdinalSVGOverlay.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,13 @@ export function OrdinalSVGUnderlay(props: OrdinalSVGUnderlayProps) {
109109
<svg
110110
width={totalWidth}
111111
height={totalHeight}
112+
overflow="visible"
112113
style={{
113114
position: "absolute",
114115
top: 0,
115116
left: 0,
116-
pointerEvents: "none"
117+
pointerEvents: "none",
118+
overflow: "visible"
117119
}}
118120
>
119121
<g transform={`translate(${margin.left},${margin.top})`}>
@@ -285,11 +287,13 @@ export function OrdinalSVGOverlay(props: OrdinalSVGOverlayProps) {
285287
role="img"
286288
width={totalWidth}
287289
height={totalHeight}
290+
overflow="visible"
288291
style={{
289292
position: "absolute",
290293
top: 0,
291294
left: 0,
292-
pointerEvents: "none"
295+
pointerEvents: "none",
296+
overflow: "visible"
293297
}}
294298
>
295299
<title>{typeof title === "string" ? title : "Ordinal Chart"}</title>

src/components/stream/SVGOverlay.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,11 +429,13 @@ export function SVGOverlay(props: SVGOverlayProps) {
429429
role="img"
430430
width={totalWidth}
431431
height={totalHeight}
432+
overflow="visible"
432433
style={{
433434
position: "absolute",
434435
top: 0,
435436
left: 0,
436-
pointerEvents: "none"
437+
pointerEvents: "none",
438+
overflow: "visible"
437439
}}
438440
>
439441
<title>{typeof title === "string" ? title : "XY Chart"}</title>

src/components/stream/StreamNetworkFrame.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1185,13 +1185,15 @@ const StreamNetworkFrame = forwardRef<
11851185
>
11861186
{backgroundGraphics && (
11871187
<svg
1188+
overflow="visible"
11881189
style={{
11891190
position: "absolute",
11901191
top: 0,
11911192
left: 0,
11921193
width: size[0],
11931194
height: size[1],
1194-
pointerEvents: "none"
1195+
pointerEvents: "none",
1196+
overflow: "visible"
11951197
}}
11961198
>
11971199
<g transform={`translate(${margin.left},${margin.top})`}>

src/components/stream/StreamXYFrame.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ const StreamXYFrame = forwardRef<StreamXYFrameHandle, StreamXYFrameProps>(
414414
backgroundGraphics,
415415
foregroundGraphics,
416416
canvasPreRenderers,
417+
svgPreRenderers,
417418
title,
418419
categoryAccessor,
419420
brush,
@@ -1060,6 +1061,9 @@ const StreamXYFrame = forwardRef<StreamXYFrameHandle, StreamXYFrameProps>(
10601061
{background && (
10611062
<rect x={0} y={0} width={adjustedWidth} height={adjustedHeight} fill={background} />
10621063
)}
1064+
{svgPreRenderers && scales && svgPreRenderers.map((renderer, ri) => (
1065+
<React.Fragment key={`svgpre-${ri}`}>{renderer(scene, scales, { width: adjustedWidth, height: adjustedHeight })}</React.Fragment>
1066+
))}
10631067
{scene.map((node, i) => xySceneNodeToSVG(node, i)).filter(Boolean)}
10641068
</g>
10651069
</svg>

src/components/stream/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,8 @@ export interface StreamXYFrameProps<T = Record<string, any>> {
496496
// ── Custom canvas renderers ───────────────────
497497
/** Canvas renderers executed before the chart-type renderers (e.g. connecting lines under points) */
498498
canvasPreRenderers?: CanvasRendererFn[]
499+
/** SVG pre-renderers for SSR — SVG equivalent of canvasPreRenderers, rendered under data marks */
500+
svgPreRenderers?: SVGPreRendererFn[]
499501

500502
// ── Title ────────────────────────────────────────
501503
title?: string | ReactNode
@@ -556,6 +558,13 @@ export type CanvasRendererFn = (
556558
layout: StreamLayout
557559
) => void
558560

561+
/** SVG equivalent of CanvasRendererFn — returns React elements for SSR/SVG rendering */
562+
export type SVGPreRendererFn = (
563+
nodes: SceneNode[],
564+
scales: StreamScales,
565+
layout: StreamLayout
566+
) => ReactNode
567+
559568
// ── Re-exports for convenience ─────────────────────────────────────────
560569
export type {
561570
ArrowOfTime,

0 commit comments

Comments
 (0)