--- 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 ```typescript import { createStore } from '@composable-svelte/core'; import { Chart, chartReducer, createInitialChartState } from '@composable-svelte/charts'; // Sample data 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' } ]; // Create chart store const chartStore = createStore({ initialState: createInitialChartState({ data }), reducer: chartReducer, dependencies: {} }); // Render scatter plot ``` --- ## CHART COMPONENT **Purpose**: High-level wrapper for creating charts with Observable Plot. ### Props - `store: Store` - 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 ```typescript 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. ```typescript ``` **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. ```typescript ``` **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. ```typescript ``` **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. ```typescript ``` **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. ```typescript ``` **Notes**: - Automatically bins data - Y-axis shows frequency count - Customize bins via state actions --- ## STATE MANAGEMENT ### ChartState Interface ```typescript interface ChartState { // Data data: T[]; // Original data filteredData: T[]; // After filters applied // Visualization config spec: PlotSpec; // Observable Plot spec dimensions: { width: number; height: number; }; // Selection selection: { type: 'none' | 'point' | 'range' | 'brush'; selectedData: T[]; selectedIndices: number[]; brushExtent?: [[number, number], [number, number]]; range?: [number, number]; }; // Zoom/pan transform: { x: number; y: number; k: number; // scale factor }; targetTransform?: ZoomTransform; // For animated zoom // Animation isAnimating: boolean; transitionDuration: number; } ``` ### ChartAction Types ```typescript type ChartAction = // Data | { type: 'setData'; data: T[] } | { type: 'filterData'; predicate: (d: T) => boolean } | { type: 'clearFilters' } // Selection | { 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' } // Zoom/pan | { type: 'zoom'; transform: ZoomTransform } | { type: 'zoomAnimated'; targetTransform: ZoomTransform } | { type: 'zoomProgress'; transform: ZoomTransform } | { type: 'zoomComplete' } | { type: 'resetZoom' } // Dimensions | { type: 'resize'; dimensions: { width: number; height: number } } // Config | { type: 'updateSpec'; spec: Partial }; ``` ### Creating Initial State ```typescript 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**: ```typescript // Zoom in chartStore.dispatch({ type: 'zoom', transform: { x: 0, y: 0, k: 2 } // 2x zoom }); // Reset zoom chartStore.dispatch({ type: 'resetZoom' }); // Animated zoom 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**: ```typescript const selected = $chartStore.selection.selectedData; console.log('Selected points:', selected); ``` **Callback**: ```typescript { console.log('Selected:', selected); // Do something with selected data }} /> ``` ### 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**: ```typescript // Via Plot spec const spec = { marks: [ Plot.dot(data, { x: 'x', y: 'y', title: (d) => `${d.name}: ${d.value}` // Custom tooltip }) ] }; ``` ### Point Selection **Enable**: Click on points when `enableBrush={false}` ```typescript // Listen for point selection $effect(() => { if ($chartStore.selection.type === 'point') { const selected = $chartStore.selection.selectedData[0]; console.log('Selected point:', selected); } }); // Programmatic selection chartStore.dispatch({ type: 'selectPoint', data: myDataPoint, index: 5 }); // Clear selection chartStore.dispatch({ type: 'clearSelection' }); ``` --- ## DATA BINDING ### Static Data ```typescript 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 ```typescript // Update data chartStore.dispatch({ type: 'setData', data: newData }); // Filter data chartStore.dispatch({ type: 'filterData', predicate: (d) => d.value > 10 }); // Clear filters chartStore.dispatch({ type: 'clearFilters' }); ``` ### Real-time Data ```typescript // Append new point const currentData = $chartStore.data; chartStore.dispatch({ type: 'setData', data: [...currentData, newPoint] }); // Update via Effect Effect.run(async (dispatch) => { const newData = await fetchLatestData(); dispatch({ type: 'setData', data: newData }); }); ``` --- ## RESPONSIVE DESIGN ### Auto-sizing Omit `width` and `height` for responsive sizing: ```typescript ``` Chart will: - Use container width (100%) - Default height (400px) - Resize on window resize ### Fixed Dimensions ```typescript ``` ### Container-based Sizing ```svelte ``` ### Responsive Breakpoints ```typescript let chartWidth = $state(800); $effect(() => { const updateWidth = () => { chartWidth = window.innerWidth < 768 ? 400 : 800; }; window.addEventListener('resize', updateWidth); updateWidth(); return () => window.removeEventListener('resize', updateWidth); }); ``` --- ## ACCESSIBILITY ### ARIA Labels Chart component includes: - `role="img"` - Marks as image - `aria-label` - Describes chart content - `aria-describedby` - Links to summary ```svelte ``` ### 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 ```typescript ``` ### Time Series Line Chart ```typescript ``` ### Interactive Bar Chart ```typescript {#if selectedCategory} Selected: {selectedCategory} {/if} ``` ### Real-time Data Visualization ```typescript ``` --- ## COMMON PATTERNS ### Multiple Charts with Shared Selection ```typescript ``` ### Linked Zoom ```typescript ``` ### Dynamic Filtering ```typescript ``` --- ## PERFORMANCE CONSIDERATIONS ### Large Datasets **Problem**: Rendering 10,000+ points can be slow. **Solutions**: 1. **Data aggregation**: Bin/group data before rendering 2. **Sampling**: Show subset of data (e.g., every 10th point) 3. **Level-of-detail**: Show more detail when zoomed in 4. **WebGL rendering**: Use Plot's WebGL marks (future) ```typescript // Example: Downsample data 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**: 1. **Batch updates**: Update every N milliseconds, not every data point 2. **Sliding window**: Keep fixed number of points (e.g., last 100) 3. **Throttle**: Limit update frequency ```typescript // Throttle updates 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); // Update max once per 100ms } } ``` ### Animation Performance Disable animations for large datasets or frequent updates: ```typescript ``` --- ## TESTING ### Basic Chart Testing ```typescript import { TestStore } from '@composable-svelte/core'; import { chartReducer, createInitialChartState } from '@composable-svelte/charts'; const store = new TestStore({ initialState: createInitialChartState({ data: [] }), reducer: chartReducer, dependencies: {} }); // Test data update await store.send({ type: 'setData', data: [{ x: 1, y: 10 }] }, (state) => { expect(state.data.length).toBe(1); expect(state.filteredData.length).toBe(1); }); // Test filtering await store.send({ type: 'filterData', predicate: (d) => d.y > 5 }, (state) => { expect(state.filteredData.length).toBe(1); }); ``` ### Selection Testing ```typescript 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)
Selected: {selectedCategory}