<!-- Generated by scripts/generate-ai-task-packets.mjs; edit ai/tasks/update-live-chart.json. -->

# Update a live chart by record ID

Maintain a bounded window of incoming records, apply corrections, and reconcile the display after reconnecting.

Source package: semiotic@3.12.1. Channel: source.
Source revision: sha256:aa7cebe0abe2ebe9c982494ad72e6bf92ddcd6d2babaac4f8142c24075e9a85e. Verified source checkout guidance; npm package, hosted site and MCP deployment identities require separate checks.

## The job

Show incoming observations, keep the UI stable, and correct existing records without duplicates.

Data: Synthetic observations with stable IDs, numeric event time and value; a four-record retained window.

## Fit and constraints

- Use when a React application needs ref-based updates and explicit record correction.
- The host owns subscription lifecycle, replay and reconnection policy.

- Static SVG cannot establish subscription cleanup, correct replay or browser lifecycle behavior.
- This simulated feed is not a production network protocol, durable queue or live service.

## Smallest working path

Component: LineChart from `semiotic/line`. Usage: push. Data prop required in this mode: false.

### Mounted browser lifecycle

React browser only; source: docs/src/pages/tasks/examples/LiveChartExample.tsx.

```tsx
import React, { useEffect, useRef, useState } from "react"
import { LineChart } from "semiotic/line"
import {
  connectLiveChart,
  createLiveSource,
  LIVE_EVENTS,
  liveChartProps,
  type LiveSource,
  type Observation,
} from "./live-chart"
import "./live-chart.css"

/** The optional source lets lifecycle tests inspect subscriptions directly. */
export function LiveChartSession({ source }: { source: LiveSource }) {
  const chartRef = useRef<React.ComponentRef<typeof LineChart>>(null)
  const [connected, setConnected] = useState(true)
  const [playing, setPlaying] = useState(false)
  const [sourceCursor, setSourceCursor] = useState(source.cursor)
  const [display, setDisplay] = useState<{ rows: Observation[]; cursor: number }>({
    rows: [],
    cursor: 0,
  })

  useEffect(() => {
    if (!connected || !chartRef.current) return
    // Effects run after the child ref is available. Cleanup also handles
    // React StrictMode's setup/cleanup/setup cycle without duplicate listeners.
    return connectLiveChart(source, chartRef.current, (rows, cursor) => {
      setDisplay({ rows, cursor })
    })
  }, [source, connected])

  useEffect(() => {
    if (!playing || !connected) return
    const timer = window.setInterval(() => {
      const advanced = source.advance()
      setSourceCursor(source.cursor)
      if (!advanced || source.cursor === LIVE_EVENTS.length) setPlaying(false)
    }, 1_000)
    return () => window.clearInterval(timer)
  }, [source, playing, connected])

  function advance() {
    source.advance()
    setSourceCursor(source.cursor)
  }

  const finished = sourceCursor === LIVE_EVENTS.length
  const lag = sourceCursor - display.cursor
  return (
    <section className="task-live" aria-label="Live chart demonstration">
      <p>
        <strong>Synthetic fixture.</strong> Three readings start at 09:00–09:02. Five events include
        two corrections and three new observations. The line uses observation time; the event
        counter records processing order.
      </p>
      <div className="task-live__controls">
        <button type="button" onClick={advance} disabled={finished || playing}>
          Advance source
        </button>
        <button
          type="button"
          onClick={() => setPlaying((value) => !value)}
          disabled={!connected || finished}
        >
          {playing ? "Pause replay" : "Play replay"}
        </button>
        <button
          type="button"
          onClick={() => {
            setPlaying(false)
            setConnected((value) => !value)
          }}
        >
          {connected ? "Disconnect" : "Reconnect"}
        </button>
      </div>
      <p role="status" aria-live="polite" data-testid="live-chart-status">
        {connected ? "Connected" : "Disconnected"}. Source event {sourceCursor} of{" "}
        {LIVE_EVENTS.length}; display through event {display.cursor}.{" "}
        {lag ? `${lag} unapplied event${lag === 1 ? "" : "s"}.` : "Display is current."}
      </p>
      <p>
        {finished ? "Replay complete." : `Next event: ${LIVE_EVENTS[sourceCursor].label}.`}
        {!connected &&
          " Advance the source while disconnected, then reconnect to replace the frozen display with the source’s current window."}
      </p>
      <div data-testid="live-chart-rendering">
        {/* No data prop: push mode remains active across control re-renders. */}
        <LineChart ref={chartRef} {...liveChartProps} height={320} responsiveWidth />
      </div>
      <div className="task-live__table">
        <table>
          <caption>Records currently retained by the chart</caption>
          <thead>
            <tr>
              <th scope="col">Record ID</th>
              <th scope="col">Observation time</th>
              <th scope="col">Reading (units)</th>
            </tr>
          </thead>
          <tbody>
            {display.rows.map((row) => (
              <tr key={row.id}>
                <th scope="row">{row.id}</th>
                <td>09:{String(row.minute).padStart(2, "0")}</td>
                <td>{row.value}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <p>
        Retention: the four latest observations by event time. Corrections replace a retained record
        by ID; older observations are removed. A reconnect takes the current source window, so it
        can recover a missed correction without retaining a complete event history. At completion,
        expect obs-03 = 17, obs-04 = 20, obs-05 = 16 and obs-06 = 19.
      </p>
    </section>
  )
}

export default function LiveChartExample() {
  const [source, setSource] = useState(createLiveSource)
  const [session, setSession] = useState(0)
  function restart() {
    setSource(createLiveSource())
    setSession((value) => value + 1)
  }
  return (
    <>
      <LiveChartSession key={session} source={source} />
      <button type="button" onClick={restart}>
        Restart fixture
      </button>
    </>
  )
}
```

### Deterministic source and reconnect policy

Host-owned synthetic feed; source: docs/src/pages/tasks/examples/live-chart.ts.

```ts
import type { ComponentRef } from "react"
import type { LineChart } from "semiotic/line"

export interface Observation {
  id: string
  minute: number
  value: number
}

export const RETAINED_RECORDS = 4
export const INITIAL_OBSERVATIONS: Observation[] = [
  { id: "obs-01", minute: 0, value: 12 },
  { id: "obs-02", minute: 1, value: 18 },
  { id: "obs-03", minute: 2, value: 15 },
]

// Event order is processing order. A correction preserves the observation's
// event time and ID; it does not create a new point at its arrival time.
export const LIVE_EVENTS = [
  { row: { id: "obs-02", minute: 1, value: 14 }, label: "Correct obs-02 from 18 to 14" },
  { row: { id: "obs-04", minute: 3, value: 20 }, label: "Add obs-04 at minute 3" },
  { row: { id: "obs-05", minute: 4, value: 16 }, label: "Add obs-05; discard obs-01" },
  { row: { id: "obs-03", minute: 2, value: 17 }, label: "Correct obs-03 from 15 to 17" },
  { row: { id: "obs-06", minute: 5, value: 19 }, label: "Add obs-06; discard obs-02" },
] satisfies Array<{ row: Observation; label: string }>

export const liveChartProps = {
  xAccessor: "minute",
  yAccessor: "value",
  pointIdAccessor: "id",
  xLabel: "Observation time (minutes after 09:00)",
  yLabel: "Synthetic reading (units)",
  title: "The latest four observations",
  description:
    "Synthetic observations by event time. Corrections keep their original time and record ID.",
  summary:
    "Advance the source to inspect corrections and retention. Disconnect to freeze the displayed window, then reconnect to catch up.",
  showPoints: true,
  showGrid: true,
  accessibleTable: true,
  xExtent: [0, 5] as [number, number],
  yExtent: [0, 24] as [number, number],
}

type FeedMessage =
  | { kind: "snapshot"; rows: Observation[]; cursor: number }
  | { kind: "change"; row: Observation; removed: string[]; cursor: number }

/** A deterministic, in-memory source, not a network or ordering protocol. */
export function createLiveSource() {
  let rows = INITIAL_OBSERVATIONS.map((row) => ({ ...row }))
  let cursor = 0
  const listeners = new Set<(message: FeedMessage) => void>()
  return {
    get cursor() {
      return cursor
    },
    get subscriberCount() {
      return listeners.size
    },
    get rows() {
      return rows.map((row) => ({ ...row }))
    },
    subscribe(listener: (message: FeedMessage) => void) {
      listeners.add(listener)
      // Registration and snapshot are synchronous in this fixture. A remote
      // source needs an atomic snapshot/cursor handoff to avoid losing events.
      listener({ kind: "snapshot", rows: rows.map((row) => ({ ...row })), cursor })
      return () => {
        listeners.delete(listener)
      }
    },
    advance() {
      const event = LIVE_EVENTS[cursor]
      if (!event) return false
      const byId = new Map(rows.map((row) => [row.id, row]))
      byId.set(event.row.id, { ...event.row })
      const ordered = [...byId.values()].sort((a, b) => a.minute - b.minute)
      const removed = ordered.slice(0, -RETAINED_RECORDS).map((row) => row.id)
      rows = ordered.slice(-RETAINED_RECORDS)
      cursor += 1
      for (const listener of listeners) {
        listener({ kind: "change", row: { ...event.row }, removed, cursor })
      }
      return true
    },
  }
}

export type LiveSource = ReturnType<typeof createLiveSource>
type LiveChartHandle = ComponentRef<typeof LineChart>

/** Connect only after React mounts the chart and supplies its ref. */
export function connectLiveChart(
  source: LiveSource,
  chart: LiveChartHandle,
  onReadback: (rows: Observation[], cursor: number) => void,
) {
  const retainedIds = new Set<string>()
  return source.subscribe((message) => {
    if (message.kind === "snapshot") {
      chart.clear()
      chart.pushMany(message.rows)
      retainedIds.clear()
      for (const row of message.rows) retainedIds.add(row.id)
    } else {
      for (const id of message.removed) {
        chart.remove(id)
        retainedIds.delete(id)
      }
      if (retainedIds.has(message.row.id)) {
        chart.update(message.row.id, (previous) => ({ ...previous, ...message.row }))
      } else if (!message.removed.includes(message.row.id)) {
        chart.push(message.row)
        retainedIds.add(message.row.id)
      }
    }
    // Inspect the chart's retained store, rather than echoing the source rows.
    const actual = chart.getData().map((row) => ({
      id: String(row.id),
      minute: Number(row.minute),
      value: Number(row.value),
    }))
    onReadback(actual, message.cursor)
  })
}
```

### Example styles

Browser presentation; source: docs/src/pages/tasks/examples/live-chart.css.

```css
.task-live {
  min-width: 0;
}
.task-live__controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}
.task-live__controls button {
  min-height: 40px;
  padding: 0.4rem 0.8rem;
}
.task-live__table {
  overflow-x: auto;
  margin: 1rem 0;
}
.task-live__table table {
  width: 100%;
  border-collapse: collapse;
}
.task-live__table caption {
  text-align: left;
  font-weight: 600;
  margin-bottom: 0.5rem;
}
.task-live__table th,
.task-live__table td {
  text-align: left;
  padding: 0.5rem;
  border-bottom: 1px solid var(--border-color, #aaa);
}
```

## Check the result

Evidence state: stale. Only the content digests and environment recorded here; not a release range or an adoption result.

- Corrections replace records by stable ID without increasing the retained count.
- At most four records remain in the chart after new observations.
- While disconnected, the displayed window remains unchanged; reconnect replaces it with the current source window.
- Unmount and StrictMode cleanup leave no active subscription or playback timer.
- After all five events, retained IDs obs-03 through obs-06 have values 17, 20, 16 and 19.

Run:

```sh
npx vitest run docs/src/pages/tasks/examples/live-chart.test.tsx
npx playwright test --config playwright.docs-examples.config.ts integration-tests/docs-examples-task-live.spec.ts
```

No current execution result is recorded for these source contents.

Not assessed:

- Real transport failures, server retention, clock synchronization and production backpressure.
- Performance on physical devices or under a production event rate.
- Assistive-technology reception and organic adoption.

## Repair and recheck

### Pushed rows disappear when React renders again.

Omit data entirely in push mode. Push the initial window through the mounted ref; do not pass data={[]}.

Recheck: Change host UI state and confirm that the retained records survive.

Contract: streaming.push-mode-data.

### A correction cannot find a record or creates duplicates.

Set pointIdAccessor to id and use update(id, updater). On reconnect, replace the bounded window from the authoritative host snapshot.

Recheck: Inspect the chart handle's data after correction and reconnect, including IDs, values and count.

Contract: streaming.ref-mutations-require-id-accessors.

## Exact API and mode guidance

Selected properties copied from ai/schema.json; retrieve the component resource for the full schema. React push mode follows the included behavior contracts.

Full component resource: semiotic://schema/LineChart. Machine packet: /tasks/update-live-chart.json.

- **accessibility.description-props:** High-level charts expose title for the visible name, description for a concise accessible description, summary for a screen-reader-only takeaway and interaction guidance, and accessibleTable for the data-table fallback. Put title, description, summary, and accessibleTable directly on the chart component when they appear in its schema. If a consumer-owned role=img wraps the chart, use accessibleTable: { portalTarget: "element-id" } and render that target outside the image. For generated L1–L3 description or a navigable chart tree, use ChartContainer with chartConfig plus describe and/or navigable; do not invent frameProps fields.

- **props.data-required-by-usage-mode:** Static usage (`renderChart`, MCP previews, SSR snapshots, and copy/paste examples with immediate data) requires data in props. React push mode selects live ingestion by omitting data and mutating through a ref. Pass usageMode="push" to `semiotic-ai --doctor` when validating ref-based JSX with no data prop. Keep usageMode="static" or omit it for renderChart/MCP/static configs where data must be present.

- **streaming.push-mode-data:** HOC push mode is selected by omitting the data prop entirely. Passing data={[]} is static empty data and can clear/reinitialize the frame on render. For live charts, create a ref, omit data, then call ref.current.push() or pushMany(). For static renderChart/MCP snapshots, provide data because renderChart cannot push later.

- **streaming.ref-mutations-require-id-accessors:** push() and pushMany() can append without IDs, but remove(id) and update(id, updater) require a stable ID accessor: pointIdAccessor for XY/realtime charts, dataIdAccessor for ordinal charts, and nodeIDAccessor/edgeIdAccessor for network operations. When generating code that calls remove() or update(), include the matching ID accessor and make sure pushed rows carry that ID field.

- **rendering.renderchart-static-props:** MCP renderChart and semiotic/server renderChart render a single static SVG/PNG snapshot, never future ref pushes. semiotic/server accepts bounded data for realtime charts; MCP rendering excludes the five live Realtime components but includes TemporalHistogram. Use complete static data for snapshots. For a Realtime component snapshot, use semiotic/server directly; for live behavior, return React code with a ref and do not promise MCP-rendered output.

## Maintenance context

The host needs explicit correction and reconnect behavior over a bounded observation window.

- Cleanup, stable IDs, retention, data-prop omission and reconnect replacement after lifecycle or dependency changes.

A static export records one window. It does not carry an active subscription, prove cleanup or promise future updates.

Read the installed package and lockfile before applying source guidance. A matching version string alone does not establish deployed parity. These notes are removable project context, subordinate to the current task and dependency policy.

## Behavioral update

Status: source-only. Released introduction: not established.

Assumption to revisit: A static render or an empty data array demonstrates a working live chart.

Omit data for push mode. Establish the subscription after the ref mounts, correct records by ID, clean up and reconcile from the authoritative window on reconnect.

- Retained records and actual canvas paint through correction, disconnect, reconnect and restart.
- Subscriptions and replay timers after unmount and StrictMode cleanup.

Production transport reliability and event-rate performance require their own environment-specific checks.
