Accessor Stability
Semiotic's canvas-based rendering pipeline rebuilds the scene graph when chart configuration changes. Every time a parent component re-renders, React creates new prop values — including new function objects for inline accessors likexAccessor={d => d.time}. Without mitigation, each re-render triggers a full scene rebuild even when nothing meaningful changed.
Semiotic handles this automatically via accessor equivalence checking. When updateConfig receives new accessor values, it compares them to the previous values using accessorsEquivalent():
- String accessors — compared by value (
"x" === "x") - Function accessors — compared by reference (
fn === fn) - Different types or references — always treated as changed
Accessor equality is identity-based. A new inline arrow (xAccessor={d => d.time} written fresh in JSX each render) is a new function object, so it does trigger a rebuild on every re-render. This is deliberate: a previous version compared source text with .toString(), but identical source can still close over different values (makeAccessor(1) vs makeAccessor(10)), and treating those as equal let a stale domain or scene survive a real change. Prefer astring accessor (always stable) or memoize the function withuseCallback.
Accessor preference order
From most stable to least:
JSX
// 1. String accessor (best) — always stable <LineChart xAccessor="time" yAccessor="value" /> // 2. Function defined outside the component — stable reference const getX = (d) => d.timestamp / 1000 function MyChart() { return <LineChart xAccessor={getX} yAccessor="value" /> } // 3. useCallback — stable across re-renders function MyChart({ divisor }) { const getX = useCallback((d) => d.timestamp / divisor, [divisor]) return <LineChart xAccessor={getX} yAccessor="value" /> } // 4. Inline arrow (works, but rebuilds every render — new identity each time) <LineChart xAccessor={d => d.timestamp / 1000} yAccessor="value" />
Closures over changing variables
Because equality is identity-based, a function that closes over a changing variable must change its reference when that variable changes — otherwise the store keeps deriving against the old value:
JSX
// WARNING: multiplier changes but the accessor reference does not, // so the store can't tell the behavior changed. function MyChart({ multiplier }) { const getY = useCallback((d) => d.value * multiplier, []) // empty deps — stale! return <LineChart yAccessor={getY} /> } // FIX: put multiplier in the dependency array so the reference changes with it function MyChart({ multiplier }) { const getY = useCallback((d) => d.value * multiplier, [multiplier]) return <LineChart yAccessor={getY} /> }
Escape hatch: accessorRevision (Stream Frames)
For the rare case where you genuinely cannot change the accessor reference — a stable closure reading external mutable state — bump the pipeline config'saccessorRevision to force the store to re-derive domains and rebuild the scene against the current accessor. Prefer changing the reference where you can; reach for this only when you cannot.
diagnoseConfig warning
The diagnoseConfig utility (and npx semiotic-ai --doctor) emits a FUNCTION_ACCESSOR warning when it detects function accessors in your props. This is informational — function accessors work fine, but string accessors are preferred for maximum stability and clarity.
JSX
import { diagnoseConfig } from "semiotic/ai" const result = diagnoseConfig("LineChart", { data: myData, xAccessor: d => d.time, yAccessor: "value", }) // result.diagnoses includes: // { code: "FUNCTION_ACCESSOR", severity: "warning", // message: "Function accessor detected: xAccessor. ..." }
MCP Rasterized Output
The Semiotic MCP server's renderChart tool supports both SVG and PNG output formats. SVG is the default. PNG output requires the optionalsharp package.
JSX
// MCP tool call with format parameter { "component": "LineChart", "props": { "data": [{ "x": 1, "y": 10 }, { "x": 2, "y": 20 }], "xAccessor": "x", "yAccessor": "y" }, "format": "png" // "svg" (default) or "png" }
Installing sharp
PNG output uses sharp for SVG-to-PNG conversion. It's listed as an optional dependency — install it when you need PNG:
If sharp is not installed and format: "png" is requested, the tool returns the SVG output with an installation hint instead of failing.
Output format
PNG output is returned as a Base64 data URI string:data:image/png;base64,.... This can be embedded directly in HTML or saved to a file by decoding the Base64 payload.