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") - Same function reference — identical (
fn === fn) - Inline arrows with identical source — compared via
.toString() - Different types — always treated as changed
This means xAccessor={d => d.time} written inline in JSX willnot trigger a scene rebuild on re-render, as long as the source code is identical each time. However, string accessors remain the most reliable and efficient option.
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 (OK) — caught by .toString() heuristic <LineChart xAccessor={d => d.timestamp / 1000} yAccessor="value" />
When .toString() does not help
The .toString() comparison catches inline arrows with identical source text. It does not help when the function body is the same but it closes over a changing variable:
JSX
// WARNING: multiplier changes but the source text is always "d => d.value * multiplier" // .toString() sees them as equal even though behavior changed function MyChart({ multiplier }) { return <LineChart yAccessor={d => d.value * multiplier} /> } // FIX: use useCallback with multiplier in the dependency array function MyChart({ multiplier }) { const getY = useCallback((d) => d.value * multiplier, [multiplier]) return <LineChart yAccessor={getY} /> }
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.