# Chart Table Selection

> Bidirectional state synchronization connecting scatter plot points and interactive table rows.

A shadcn/ui block from the [@pacekit](https://ui.pacekit.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/pacekit/tanstack-charts-chart-table-selection).

- Page, with a live preview: https://shadcn.exchange/r/pacekit/tanstack-charts-chart-table-selection
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/pacekit/tanstack-charts-chart-table-selection-light-362194945640be43.png
- Registry item: https://ui.pacekit.dev/r/tanstack-charts-chart-table-selection.json
- Listed: 2026-10-21, source last changed 2026-10-21

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/pacekit/tanstack-charts-chart-table-selection.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@tanstack/charts`, `@tanstack/react-charts`, `d3-scale`
- Registry items: `card`, `button`

## Files

### `components/tanstack-charts/line/chart-table-selection.tsx`

```tsx
import { type ChartPoint, defineChart, dot } from "@tanstack/charts";
import { controlledSignal } from "@tanstack/charts/interaction/signal";
import { motion } from "@tanstack/charts/motion";
import { keyedSelection, whenSelected } from "@tanstack/charts/selection";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo, useState } from "react";

import { scaleLinear } from "d3-scale";
import { Table as TableIcon, X } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";

interface PointDatum {
    id: string;
    label: string;
    flipper_length_mm: number;
    body_mass_g: number;
    island: string;
}

const motionRenderer = motion({
    initial: "always",
    transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});

function generateDemoData(): PointDatum[] {
    const islands = ["Biscoe", "Dream", "Torgersen"];
    const results: PointDatum[] = [];

    for (let i = 0; i < 15; i++) {
        const flipper = 180 + Math.floor(i * 3 + Math.random() * 8);
        const mass = 2800 + Math.floor(i * 200 + Math.random() * 300);
        const island = islands[i % islands.length];
        results.push({
            id: `p-${i + 1}`,
            label: `Observation #${i + 1}`,
            flipper_length_mm: flipper,
            body_mass_g: mass,
            island,
        });
    }

    return results;
}

function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
    if (points.length === 0) return { title: "", rows: [] };
    const d = points[0].datum as PointDatum;
    if (!d) return { title: "", rows: [] };
    return {
        title: `${d.label} (${d.island})`,
        rows: [
            { label: "Flipper Length", value: `${d.flipper_length_mm} mm`, color: "var(--chart-1)" },
            { label: "Body Mass", value: `${d.body_mass_g} g`, color: "var(--chart-2)" },
        ],
    };
}

export function ChartTableSelection() {
    const [selectedId, setSelectedId] = useState<string | null>("p-5");
    const data = useMemo(() => generateDemoData(), []);

    const selection = useMemo(
        () =>
            keyedSelection<PointDatum, string, number, number>({
                selected: controlledSignal<string | null, unknown>(selectedId, (next) => setSelectedId(next)),
                key: (datum) => datum.id,
            }),
        [selectedId],
    );

    const definition = useMemo(
        () =>
            defineChart(
                {
                    renderer: motionRenderer,
                    marks: [
                        dot(data, {
                            id: "observations",
                            x: "flipper_length_mm",
                            y: "body_mass_g",
                            key: "id",
                            r: 4.5,
                            fill: "var(--chart-1)",
                        }),
                        whenSelected(
                            dot(data, {
                                id: "selected-observation",
                                x: "flipper_length_mm",
                                y: "body_mass_g",
                                key: "id",
                                r: 7.5,
                                fill: "var(--chart-2)",
                                stroke: "var(--background)",
                                strokeWidth: 2,
                            }),
                            selection,
                        ),
                    ],
                    scales: {
                        x: { scale: scaleLinear, axis: { label: "Flipper Length (mm)" } },
                        y: {
                            scale: scaleLinear,
                            grid: true,
                            axis: { label: "Body Mass (g)" },
                        },
                    },
                    margin: { top: 24, right: 24, bottom: 32, left: 45 },
                    theme: {
                        foreground: "var(--muted-foreground)",
                        grid: "var(--border)",
                        background: "transparent",
                    },
                    selection,
                },
                {
                    keyboard: true,
                    tooltip: {
                        use: tooltip,
                        className:
                            "font-sans shadow-none rounded-md bg-popover text-popover-foreground border p-2 text-xs",
                        anchor: "pointer",
                        placement: "auto",
                        content: (points) => shadcnTooltipContent(points),
                    },
                },
            ),
        [data, selection],
    );

    const selectedDatum = data.find((d) => d.id === selectedId);

    return (
        <Card className="w-full max-w-xl">
            <CardHeader>
                <CardTitle>Chart & Data Table Bidirectional Selection</CardTitle>
                <CardDescription>
                    2-way state synchronization connecting scatter chart points to interactive table rows
                </CardDescription>
            </CardHeader>
            <CardContent className="space-y-4">
                <div className="h-[200px] w-full">
                    <Chart
                        definition={definition}
                        renderer={motionRenderer}
                        height={200}
                        ariaLabel="Chart & Data Table Bidirectional Selection"
                    />
                </div>

                <div className="text-muted-foreground flex items-center justify-between border-t pt-2 text-xs font-medium">
                    <div>
                        {selectedDatum ? (
                            <span>
                                Selected: <strong className="text-foreground">{selectedDatum.label}</strong> (
                                {selectedDatum.island}, {selectedDatum.body_mass_g}g)
                            </span>
                        ) : (
                            <span>No item selected</span>
                        )}
                    </div>
                    {selectedId && (
                        <Button
                            variant="ghost"
                            size="sm"
                            className="h-7 px-2 text-xs"
                            onClick={() => setSelectedId(null)}>
                            <X className="mr-1 h-3 w-3" /> Clear
                        </Button>
                    )}
                </div>

                <div className="max-h-[220px] overflow-auto rounded-lg border text-xs shadow-xs">
                    <table className="w-full border-collapse text-left">
                        <thead className="bg-muted text-foreground sticky top-0 z-10 border-b font-semibold">
                            <tr>
                                <th className="p-2.5">Label</th>
                                <th className="p-2.5">Island</th>
                                <th className="p-2.5 text-right">Flipper (mm)</th>
                                <th className="p-2.5 text-right">Mass (g)</th>
                            </tr>
                        </thead>
                        <tbody className="divide-y">
                            {data.map((row) => {
                                const isSelected = row.id === selectedId;
                                return (
                                    <tr
                                        key={row.id}
                                        onClick={() => setSelectedId(row.id)}
                                        className={`hover:bg-muted/60 cursor-pointer transition-colors ${
                                            isSelected ? "bg-primary/15 text-primary font-semibold" : ""
                                        }`}>
                                        <td className="p-2.5">{row.label}</td>
                                        <td className="p-2.5">{row.island}</td>
                                        <td className="p-2.5 text-right font-mono">{row.flipper_length_mm}</td>
                                        <td className="p-2.5 text-right font-mono">{row.body_mass_g}</td>
                                    </tr>
                                );
                            })}
                        </tbody>
                    </table>
                </div>
            </CardContent>
            <CardFooter className="flex-col items-start gap-2 text-sm">
                <div className="flex items-center gap-2 leading-none font-medium">
                    Selecting a row in the table highlights the point on chart instantly{" "}
                    <TableIcon className="h-4 w-4 text-emerald-500" />
                </div>
                <div className="text-muted-foreground leading-none">
                    `keyedSelection` provides bidirectional binding between visual marks and tabular HTML elements
                </div>
            </CardFooter>
        </Card>
    );
}

export default ChartTableSelection;
```
