| name | composable-svelte-charts |
| description | Data visualization and chart components for Composable Svelte. Use when creating charts, graphs, or data visualizations. Covers chart types (scatter, line, bar, area, histogram), data binding, state-driven updates, interactive features (zoom, brush, tooltips), and responsive design from @composable-svelte/charts package built with Observable Plot and D3. |
Composable Svelte Charts
Interactive data visualization components built with Observable Plot and D3.
PACKAGE OVERVIEW
Package: @composable-svelte/charts
Purpose: State-driven interactive charts and data visualizations.
Technology Stack:
- Observable Plot: Declarative visualization grammar from Observable
- D3: Low-level utilities for scales, shapes, and interactions
- Motion One: Smooth transitions and animations
Chart Types:
- Scatter plots
- Line charts
- Bar charts
- Area charts
- Histograms
Interactive Features:
- Zoom & pan
- Brush selection
- Tooltips (automatic)
- Range selection
- Responsive sizing
State Management:
All charts use pure reducers with type-safe actions following Composable Architecture patterns.
QUICK START
import { createStore } from '@composable-svelte/core';
import { Chart, chartReducer, createInitialChartState } from '@composable-svelte/charts';
const data = [
{ x: 1, y: 10, category: 'A' },
{ x: 2, y: 25, category: 'B' },
{ x: 3, y: 15, category: 'A' },
{ x: 4, y: 30, category: 'B' }
];
const chartStore = createStore({
initialState: createInitialChartState({ data }),
reducer: chartReducer,
dependencies: {}
});
<Chart
store={chartStore}
type="scatter"
x="x"
y="y"
color="category"
width={800}
height={400}
enableZoom={true}
enableTooltip={true}
/>
CHART COMPONENT
Purpose: High-level wrapper for creating charts with Observable Plot.
Props
store: Store<ChartState, ChartAction> - Chart store (required)
type: 'scatter' | 'line' | 'bar' | 'area' | 'histogram' - Chart type (default: 'scatter')
width: number - Chart width (optional, responsive if omitted)
height: number - Chart height (optional, defaults to 400px)
x: string | ((d) => any) - X accessor (required)
y: string | ((d) => any) - Y accessor (required)
color: string | ((d) => any) - Color accessor (optional)
size: number - Mark size (optional)
xDomain: [number, number] | 'auto' - X domain (optional)
yDomain: [number, number] | 'auto' - Y domain (optional)
enableZoom: boolean - Enable zoom/pan (default: false)
enableBrush: boolean - Enable brush selection (default: false)
enableTooltip: boolean - Enable tooltips (default: true)
enableAnimations: boolean - Enable transitions (default: true)
onSelectionChange: (selected: any[]) => void - Selection callback (optional)
Usage
<Chart
store={chartStore}
type="scatter"
x="date"
y="value"
color={(d) => d.category}
size={4}
xDomain="auto"
yDomain={[0, 100]}
enableZoom={true}
enableTooltip={true}
onSelectionChange={(selected) => console.log('Selected:', selected)}
/>
CHART TYPES
Scatter Plot
Purpose: Display individual data points in 2D space.
Best for: Correlations, distributions, outliers.
<Chart
store={chartStore}
type="scatter"
x="temperature"
y="sales"
color="region"
size={5}
enableZoom={true}
/>
Accessories:
x: X-axis position
y: Y-axis position
color: Point color (optional)
size: Point size (optional)
Line Chart
Purpose: Show trends over time or continuous data.
Best for: Time series, trends, comparisons.
<Chart
store={chartStore}
type="line"
x="date"
y="price"
color="ticker"
enableZoom={true}
/>
Notes:
- Data should be sorted by X for proper rendering
- Multiple series via
color accessor
- Supports missing data (gaps in line)
Bar Chart
Purpose: Compare categorical data with rectangular bars.
Best for: Category comparisons, rankings, distributions.
<Chart
store={chartStore}
type="bar"
x="category"
y="count"
color="segment"
enableTooltip={true}
/>
Variants:
- Vertical bars (default)
- Grouped bars (via
color)
- Stacked bars (via config)
Area Chart
Purpose: Line chart with filled area below.
Best for: Cumulative data, part-to-whole relationships.
<Chart
store={chartStore}
type="area"
x="date"
y="value"
color="category"
enableZoom={true}
/>
Notes:
- Multiple series stack by default
- Baseline at Y=0 unless configured
Histogram
Purpose: Distribution of numerical data into bins.
Best for: Data distributions, frequency analysis.
<Chart
store={chartStore}
type="histogram"
x="value"
enableTooltip={true}
/>
Notes:
- Automatically bins data
- Y-axis shows frequency count
- Customize bins via state actions
STATE MANAGEMENT
ChartState Interface
interface ChartState<T = unknown> {
data: T[];
filteredData: T[];
spec: PlotSpec;
dimensions: {
width: number;
height: number;
};
selection: {
type: 'none' | 'point' | 'range' | 'brush';
selectedData: T[];
selectedIndices: number[];
brushExtent?: [[number, number], [number, number]];
range?: [number, number];
};
transform: {
x: number;
y: number;
k: number;
};
targetTransform?: ZoomTransform;
isAnimating: boolean;
transitionDuration: number;
}
ChartAction Types
type ChartAction<T = unknown> =
| { type: 'setData'; data: T[] }
| { type: 'filterData'; predicate: (d: T) => boolean }
| { type: 'clearFilters' }
| { type: 'selectPoint'; data: T; index: number }
| { type: 'selectRange'; range: [number, number] }
| { type: 'brushStart'; position: [number, number] }
| { type: 'brushMove'; extent: [[number, number], [number, number]] }
| { type: 'brushEnd' }
| { type: 'clearSelection' }
| { type: 'zoom'; transform: ZoomTransform }
| { type: 'zoomAnimated'; targetTransform: ZoomTransform }
| { type: 'zoomProgress'; transform: ZoomTransform }
| { type: 'zoomComplete' }
| { type: 'resetZoom' }
| { type: 'resize'; dimensions: { width: number; height: number } }
| { type: 'updateSpec'; spec: Partial<PlotSpec> };
Creating Initial State
import { createInitialChartState } from '@composable-svelte/charts';
const initialState = createInitialChartState({
data: myData,
dimensions: { width: 800, height: 400 },
transitionDuration: 300
});
INTERACTIVE FEATURES
Zoom & Pan
Enable: enableZoom={true}
Controls:
- Mouse wheel: Zoom in/out
- Click + drag: Pan
- Double-click: Reset zoom
Programmatic zoom:
chartStore.dispatch({
type: 'zoom',
transform: { x: 0, y: 0, k: 2 }
});
chartStore.dispatch({ type: 'resetZoom' });
chartStore.dispatch({
type: 'zoomAnimated',
targetTransform: { x: 100, y: 50, k: 1.5 }
});
Brush Selection
Enable: enableBrush={true}
Controls:
- Click + drag: Create brush
- Drag corners: Resize brush
- Drag center: Move brush
- Click outside: Clear brush
Access selected data:
const selected = $chartStore.selection.selectedData;
console.log('Selected points:', selected);
Callback:
<Chart
store={chartStore}
enableBrush={true}
onSelectionChange={(selected) => {
console.log('Selected:', selected);
}}
/>
Tooltips
Enable: enableTooltip={true} (default)
Behavior:
- Hover over data points to show tooltip
- Automatically displays data values
- Tooltip content customizable via Observable Plot
Custom tooltips:
const spec = {
marks: [
Plot.dot(data, {
x: 'x',
y: 'y',
title: (d) => `${d.name}: ${d.value}`
})
]
};
Point Selection
Enable: Click on points when enableBrush={false}
$effect(() => {
if ($chartStore.selection.type === 'point') {
const selected = $chartStore.selection.selectedData[0];
console.log('Selected point:', selected);
}
});
chartStore.dispatch({
type: 'selectPoint',
data: myDataPoint,
index: 5
});
chartStore.dispatch({ type: 'clearSelection' });
DATA BINDING
Static Data
const data = [
{ x: 1, y: 10 },
{ x: 2, y: 20 },
{ x: 3, y: 15 }
];
const chartStore = createStore({
initialState: createInitialChartState({ data }),
reducer: chartReducer,
dependencies: {}
});
Dynamic Data Updates
chartStore.dispatch({
type: 'setData',
data: newData
});
chartStore.dispatch({
type: 'filterData',
predicate: (d) => d.value > 10
});
chartStore.dispatch({ type: 'clearFilters' });
Real-time Data
const currentData = $chartStore.data;
chartStore.dispatch({
type: 'setData',
data: [...currentData, newPoint]
});
Effect.run(async (dispatch) => {
const newData = await fetchLatestData();
dispatch({ type: 'setData', data: newData });
});
RESPONSIVE DESIGN
Auto-sizing
Omit width and height for responsive sizing:
<Chart
store={chartStore}
type="scatter"
x="x"
y="y"
/>
Chart will:
- Use container width (100%)
- Default height (400px)
- Resize on window resize
Fixed Dimensions
<Chart
store={chartStore}
type="scatter"
x="x"
y="y"
width={800}
height={600}
/>
Container-based Sizing
<div class="chart-container">
<Chart store={chartStore} ... />
</div>
<style>
.chart-container {
width: 100%;
height: 500px;
}
</style>
Responsive Breakpoints
let chartWidth = $state(800);
$effect(() => {
const updateWidth = () => {
chartWidth = window.innerWidth < 768 ? 400 : 800;
};
window.addEventListener('resize', updateWidth);
updateWidth();
return () => window.removeEventListener('resize', updateWidth);
});
<Chart store={chartStore} width={chartWidth} ... />
ACCESSIBILITY
ARIA Labels
Chart component includes:
role="img" - Marks as image
aria-label - Describes chart content
aria-describedby - Links to summary
<Chart
store={chartStore}
type="scatter"
x="x"
y="y"
aria-label="Scatter plot showing relationship between X and Y"
/>
Screen Reader Summary
Auto-generated summary includes:
- Chart type
- Number of data points
- Selection status
- Filter status
Example output:
"Scatter plot showing 42 data points, 5 selected"
Keyboard Navigation
Tab: Focus chart
Arrow keys: Pan (when zoomed)
+/-: Zoom in/out
0: Reset zoom
Escape: Clear selection
COMPLETE EXAMPLES
Basic Scatter Plot
<script lang="ts">
import { createStore } from '@composable-svelte/core';
import { Chart, chartReducer, createInitialChartState } from '@composable-svelte/charts';
const data = [
{ x: 10, y: 20, category: 'A' },
{ x: 15, y: 35, category: 'B' },
{ x: 20, y: 25, category: 'A' },
{ x: 25, y: 45, category: 'B' }
];
const chartStore = createStore({
initialState: createInitialChartState({ data }),
reducer: chartReducer,
dependencies: {}
});
</script>
<Chart
store={chartStore}
type="scatter"
x="x"
y="y"
color="category"
size={6}
width={800}
height={400}
enableZoom={true}
enableTooltip={true}
/>
Time Series Line Chart
<script lang="ts">
import { createStore } from '@composable-svelte/core';
import { Chart, chartReducer, createInitialChartState } from '@composable-svelte/charts';
interface DataPoint {
date: Date;
value: number;
series: string;
}
const data: DataPoint[] = [
{ date: new Date('2024-01-01'), value: 100, series: 'A' },
{ date: new Date('2024-01-02'), value: 120, series: 'A' },
{ date: new Date('2024-01-03'), value: 115, series: 'A' },
{ date: new Date('2024-01-01'), value: 80, series: 'B' },
{ date: new Date('2024-01-02'), value: 95, series: 'B' },
{ date: new Date('2024-01-03'), value: 105, series: 'B' }
];
const chartStore = createStore({
initialState: createInitialChartState({ data }),
reducer: chartReducer,
dependencies: {}
});
</script>
<Chart
store={chartStore}
type="line"
x="date"
y="value"
color="series"
width={1000}
height={400}
enableZoom={true}
enableTooltip={true}
/>
Interactive Bar Chart
<script lang="ts">
import { createStore } from '@composable-svelte/core';
import { Chart, chartReducer, createInitialChartState } from '@composable-svelte/charts';
const data = [
{ category: 'Q1', revenue: 45000, expenses: 32000 },
{ category: 'Q2', revenue: 52000, expenses: 38000 },
{ category: 'Q3', revenue: 48000, expenses: 35000 },
{ category: 'Q4', revenue: 61000, expenses: 42000 }
];
const chartStore = createStore({
initialState: createInitialChartState({ data }),
reducer: chartReducer,
dependencies: {}
});
let selectedCategory = $state<string | null>(null);
function handleSelection(selected: any[]) {
selectedCategory = selected[0]?.category || null;
}
</script>
<div>
<Chart
store={chartStore}
type="bar"
x="category"
y="revenue"
enableBrush={true}
enableTooltip={true}
onSelectionChange={handleSelection}
/>
{#if selectedCategory}
<p>Selected: {selectedCategory}</p>
{/if}
</div>
Real-time Data Visualization
<script lang="ts">
import { createStore, Effect } from '@composable-svelte/core';
import { Chart, chartReducer, createInitialChartState } from '@composable-svelte/charts';
import { onMount } from 'svelte';
let data = $state<Array<{ time: number; value: number }>>([]);
const chartStore = createStore({
initialState: createInitialChartState({ data }),
reducer: chartReducer,
dependencies: {}
});
let intervalId: number;
onMount(() => {
let time = 0;
intervalId = setInterval(() => {
const newPoint = {
time: time++,
value: Math.random() * 100
};
data = [...data.slice(-50), newPoint];
chartStore.dispatch({
type: 'setData',
data
});
}, 100);
return () => clearInterval(intervalId);
});
</script>
<Chart
store={chartStore}
type="line"
x="time"
y="value"
yDomain={[0, 100]}
enableAnimations={true}
/>
COMMON PATTERNS
Multiple Charts with Shared Selection
<script lang="ts">
const data = [...];
const chartStore1 = createStore({...});
const chartStore2 = createStore({...});
let selectedData = $state<any[]>([]);
function syncSelection(selected: any[]) {
selectedData = selected;
const indices = selected.map(d => data.indexOf(d));
chartStore1.dispatch({ type: 'selectRange', range: [indices[0], indices[indices.length - 1]] });
chartStore2.dispatch({ type: 'selectRange', range: [indices[0], indices[indices.length - 1]] });
}
</script>
<Chart store={chartStore1} ... onSelectionChange={syncSelection} />
<Chart store={chartStore2} ... onSelectionChange={syncSelection} />
Linked Zoom
<script lang="ts">
const masterStore = createStore({...});
const detailStore = createStore({...});
$effect(() => {
const transform = $masterStore.transform;
detailStore.dispatch({ type: 'zoom', transform });
});
</script>
<Chart store={masterStore} enableZoom={true} />
<Chart store={detailStore} /> <!-- Zooms with master -->
Dynamic Filtering
<script lang="ts">
let minValue = $state(0);
let maxValue = $state(100);
$effect(() => {
chartStore.dispatch({
type: 'filterData',
predicate: (d) => d.value >= minValue && d.value <= maxValue
});
});
</script>
<input type="range" bind:value={minValue} min="0" max="100" />
<input type="range" bind:value={maxValue} min="0" max="100" />
<Chart store={chartStore} ... />
PERFORMANCE CONSIDERATIONS
Large Datasets
Problem: Rendering 10,000+ points can be slow.
Solutions:
- Data aggregation: Bin/group data before rendering
- Sampling: Show subset of data (e.g., every 10th point)
- Level-of-detail: Show more detail when zoomed in
- WebGL rendering: Use Plot's WebGL marks (future)
const downsample = (data: any[], factor: number) =>
data.filter((_, i) => i % factor === 0);
const displayData = data.length > 1000
? downsample(data, Math.ceil(data.length / 1000))
: data;
chartStore.dispatch({ type: 'setData', data: displayData });
Frequent Updates
Problem: Real-time data updates cause re-renders.
Solutions:
- Batch updates: Update every N milliseconds, not every data point
- Sliding window: Keep fixed number of points (e.g., last 100)
- Throttle: Limit update frequency
let pendingData: any[] = [];
let updateTimer: number | null = null;
function queueUpdate(newData: any[]) {
pendingData = newData;
if (updateTimer === null) {
updateTimer = setTimeout(() => {
chartStore.dispatch({ type: 'setData', data: pendingData });
updateTimer = null;
}, 100);
}
}
Animation Performance
Disable animations for large datasets or frequent updates:
<Chart
store={chartStore}
enableAnimations={false}
...
/>
TESTING
Basic Chart Testing
import { TestStore } from '@composable-svelte/core';
import { chartReducer, createInitialChartState } from '@composable-svelte/charts';
const store = new TestStore({
initialState: createInitialChartState({ data: [] }),
reducer: chartReducer,
dependencies: {}
});
await store.send({
type: 'setData',
data: [{ x: 1, y: 10 }]
}, (state) => {
expect(state.data.length).toBe(1);
expect(state.filteredData.length).toBe(1);
});
await store.send({
type: 'filterData',
predicate: (d) => d.y > 5
}, (state) => {
expect(state.filteredData.length).toBe(1);
});
Selection Testing
await store.send({
type: 'selectPoint',
data: { x: 1, y: 10 },
index: 0
}, (state) => {
expect(state.selection.type).toBe('point');
expect(state.selection.selectedData.length).toBe(1);
expect(state.selection.selectedIndices).toEqual([0]);
});
await store.send({ type: 'clearSelection' }, (state) => {
expect(state.selection.type).toBe('none');
expect(state.selection.selectedData.length).toBe(0);
});
TROUBLESHOOTING
Chart not rendering:
- Check Observable Plot installed:
npm install @observablehq/plot
- Verify data is non-empty array
- Ensure x/y accessors match data properties
Tooltips not showing:
- Verify
enableTooltip={true}
- Check Observable Plot version (0.6+ required)
- Ensure data points have valid values (not null/undefined)
Zoom not working:
- Verify
enableZoom={true}
- Check chart has fixed dimensions (not 100% width/height)
- Ensure D3-zoom is installed
Poor performance:
- Reduce data points (aggregate, sample, or downsample)
- Disable animations for large datasets
- Use simpler mark types (dots vs complex shapes)
Selection not updating:
- Check
onSelectionChange callback
- Verify
enableBrush={true} or enableSelection={true}
- Ensure store is reactive (
$chartStore.selection)
CROSS-REFERENCES
Related Skills:
- composable-svelte-core: Store, reducer, Effect system
- composable-svelte-components: UI components (Button, Slider, etc.)
- composable-svelte-testing: TestStore for testing chart reducers
When to Use Each Package:
- charts: 2D data visualization, interactive charts
- graphics: 3D scenes, WebGPU/WebGL (see composable-svelte-graphics)
- maps: Geospatial data (see composable-svelte-maps)
- code: Code editors, media players (see composable-svelte-code)
COMPLETE API REFERENCE
All exports from @composable-svelte/charts:
Types
ChartState - Chart state interface
ChartAction - Chart action discriminated union
ChartConfig - Chart configuration options
SelectionState - Selection state (point, range, brush)
ZoomTransform - Zoom/pan transform { x, y, k }
PlotSpec - Observable Plot specification
DataTransform - Data transform function type (data: T[]) => T[]
DataTransforms - Namespace object with all transform functions
Reducers
chartReducer - Pure reducer for chart state management
createInitialChartState(config) - Factory for initial chart state
Components
Chart - High-level chart component (scatter, line, bar, area, histogram)
ChartPrimitive - Low-level chart primitive for custom chart implementations
ChartTooltip - Standalone tooltip component for chart data points
Utility Exports: Plot Builder (plot-builder)
buildScatterPlot(state, config) - Build Observable Plot scatter specification
buildLineChart(state, config) - Build Observable Plot line specification
buildBarChart(state, config) - Build Observable Plot bar specification
buildAreaChart(state, config) - Build Observable Plot area specification
buildHistogram(state, config) - Build Observable Plot histogram specification
buildPlot(state, config) - Build plot specification by chart type
applyZoomToDomain(domain, transform) - Apply zoom transform to axis domain
calculateDomain(data, accessor) - Calculate min/max domain from data
Utility Exports: Data Transforms (data-transforms)
filter(predicate) - Filter data by predicate
sortBy(field, direction) - Sort data by field
groupBy(field) - Group data by field
aggregate(field, fn) - Aggregate grouped data
compose(...transforms) - Compose multiple transforms into a pipeline
binData(field, bins) - Bin numerical data into histogram buckets
rollup(field, fn) - Roll up grouped data with aggregation
topN(n, field) - Take top N items by field value
unique(field) - Deduplicate data by field
sample(n) - Randomly sample N items from data
DataTransforms - Namespace object containing all transform functions
Utility Exports: Responsive (responsive)
createResizeObserver(element, dispatch) - Create ResizeObserver for auto-sizing
calculateDimensions(container) - Calculate dimensions from container element
debounce(fn, delay) - Debounce function for resize throttling