The ScatterPlot
component creates versatile interactive scatter plots.
Basic Plot with Multiple Display Modes
A simple scatter plot showing different display modes (points, lines, or both):
<script>
import { ScatterPlot } from '$lib'
// Basic single series data
const basic_data = {
x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y: [5, 7, 2, 8, 4, 9, 3, 6, 8, 5],
point_style: { fill: 'steelblue', radius: 5 },
label: 'Basic Data'
}
// Multiple series data
const second_series = {
x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y: [2, 4, 6, 3, 7, 5, 8, 4, 6, 9],
point_style: { fill: 'orangered', radius: 4 },
label: 'Second Series'
}
// Currently selected display mode
let display_mode = $state('line+points')
</script>
<div>
<label style="margin-bottom: 1em; display: block;">
Display Mode:
<select bind:value={display_mode}>
{#each [['points', 'Points only'], ['line', 'Lines only'], ['line+points', 'Lines and Points']] as [value, label] (value)}
<option value={value}>{label}</option>
{/each}
</select>
</label>
<ScatterPlot
series={[basic_data, second_series]}
x_label="X Axis"
y_label="Y Value"
markers={display_mode}
style="height: 300px; width: 100%;"
/>
</div>
Custom Point Styling and Tooltips
Demonstrate various point styles, custom tooltips, and hover effects:
<script>
import { ScatterPlot } from '$lib'
// Generate data for demonstration
const point_count = 10
const x_values = Array.from({ length: point_count }, (_, idx) => idx + 1)
// Create series with different point styles
const series_with_styles = [
// Extra large red points with thick border
{
x: x_values,
y: Array(point_count).fill(10),
point_style: {
fill: 'crimson',
radius: 12,
stroke: 'darkred',
stroke_width: 3
},
point_hover: {
scale: 1.3,
stroke: 'gold',
stroke_width: 4
},
point_label: {
text: 'Giant red',
offset_y: -20,
font_size: '12px'
}
},
// Medium green semi-transparent points with dramatic hover effect
{
x: x_values,
y: Array(point_count).fill(8),
point_style: {
fill: 'mediumseagreen',
radius: 8,
fill_opacity: 0.6,
stroke: 'green',
stroke_width: 1
},
point_hover: {
scale: 2.5, // Much larger on hover
stroke: 'lime',
stroke_width: 2
},
point_label: {
text: 'Growing green',
offset_y: -20,
font_size: '12px'
}
},
// Outline-only points (hollow) with color change on hover
{
x: x_values,
y: Array(point_count).fill(6),
point_style: {
fill: 'white',
fill_opacity: 0.1,
radius: 6,
stroke: 'purple',
stroke_width: 2
},
point_hover: {
scale: 1.8,
stroke: 'magenta', // Different color on hover
stroke_width: 3
},
point_label: {
text: 'Color-changing hollow',
offset_y: -20,
font_size: '12px'
}
},
// Tiny points with extreme hover growth
{
x: x_values,
y: Array(point_count).fill(4),
point_style: {
fill: 'orange',
radius: 3
},
point_hover: {
scale: 4, // Extreme growth on hover
stroke: 'red',
stroke_width: 2
},
point_label: {
text: 'Exploding dots',
offset_y: -20,
font_size: '12px'
}
},
// Micro dots with custom glow effect
{
x: x_values,
y: Array(point_count).fill(2),
point_style: {
fill: 'dodgerblue',
radius: 1.5, // Extremely small
stroke: 'transparent',
stroke_width: 0
},
point_hover: {
scale: 6, // Dramatic growth
stroke: 'cyan',
stroke_width: 8 // Creates a glow effect
},
point_label: {
text: 'Glowing microdots',
offset_y: -20,
font_size: '12px'
},
label: 'Glowing microdots'
}
]
// Only show labels for the first point in each series
series_with_styles.forEach((series, series_idx) => {
if (!series.label) {
series.label = series.point_label?.text || `Style ${series_idx + 1}`;
}
// Create a metadata array with empty objects except for the first one
series.metadata = Array(point_count).fill({}).map((_, idx) => {
return idx === 0 ? { firstPoint: true, seriesName: series.point_label.text } : {}
})
// Only show label on the first point of each series
if (series.point_label) {
// ScatterPoint doesn't accept functions for the text property,
// so we'll clear the text for all points and manually handle
// the first point label with metadata
series.point_label.text = ''
}
})
// Selected point tracking for demo
let selected_point = null
</script>
<ScatterPlot
series={series_with_styles}
x_label="X Axis"
y_label="Point Style Examples"
y_lim={[0, 12]}
markers="points"
change={(event) => (selected_point = event)}
style="height: 400px; width: 100%;"
>
{#snippet tooltip({ x, y, metadata })}
<div style="white-space: nowrap;">
{#if metadata?.firstPoint}
<strong>{metadata.seriesName}</strong>
{:else}
Point at ({x}, {y})
{/if}
</div>
{/snippet}
</ScatterPlot>
{#if selected_point}
<div style="margin-top: 1em; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
Selected point: ({selected_point.x}, {selected_point.y})
{#if selected_point.metadata?.firstPoint}
- {selected_point.metadata.seriesName}
{/if}
</div>
{/if}
Per-Point Custom Styling with Marker Symbols
This example demonstrates how to apply different styles to individual points within a single series, including different marker symbols:
<script>
import { ScatterPlot } from '$lib'
import { marker_types } from '$lib/plot'
// Create a dataset with points arranged in a spiral pattern
const point_count = 40
const spiral_data = {
x: [],
y: [],
point_style: [], // Array of styles for each point
metadata: [] // Store angle for each point
}
// Generate points in a spiral pattern
for (let idx = 0; idx < point_count; idx++) {
// Calculate angle and radius for spiral
const angle = idx * 0.5
const radius = 1 + idx * 0.3
// Convert to cartesian coordinates
const x = Math.cos(angle) * radius
const y = Math.sin(angle) * radius
spiral_data.x.push(x)
spiral_data.y.push(y)
// Store angle in metadata
spiral_data.metadata.push({ angle, radius })
// Change color gradually along the spiral
const hue = (idx / point_count) * 360
// Change size dramatically from tiny to huge
const size_factor = 1 + idx / 5; // More aggressive size increase
// Create the point style
spiral_data.point_style.push({
fill: `hsl(${hue}, 80%, 50%)`,
radius: 1 + size_factor * 3, // Much larger variation in size
stroke: 'white',
stroke_width: 1 + idx / 20, // Gradually thicker stroke
marker_type: marker_types[idx % marker_types.length],
marker_size: 20 + size_factor * 25 // More dramatic size progression
})
}
</script>
<ScatterPlot
series={[spiral_data]}
x_label="X Axis"
y_label="Y Axis"
x_lim={[-15, 15]}
y_lim={[-15, 15]}
markers="points"
style="height: 500px; width: 100%;"
>
{#snippet tooltip({ x, y, metadata })}
<div style="white-space: nowrap;">
<strong>Spiral Point</strong><br>
Position: ({x.toFixed(2)}, {y.toFixed(2)})<br>
Angle: {metadata.angle.toFixed(2)} rad<br>
Radius: {metadata.radius.toFixed(2)}
</div>
{/snippet}
</ScatterPlot>
Categorized Data and Custom Axis Tick Intervals
This example shows categorized data with color coding, custom tick intervals, and demonstrates handling negative values:
<script>
import { ScatterPlot } from '$lib'
// Define categories
const categories = ['Category A', 'Category B', 'Category C', 'Category D']
// Define colors for each category
const category_colors = [
'crimson',
'royalblue',
'goldenrod',
'mediumseagreen'
]
// Generate sample data points with categories
const sample_count = 40
const sample_data = Array(sample_count).fill(0).map(() => {
const category_idx = Math.floor(Math.random() * categories.length)
// Generate points across positive and negative coordinate space
return {
x: (Math.random() * 20) - 10, // Range from -10 to 10
y: (Math.random() * 20) - 10, // Range from -10 to 10
category: categories[category_idx],
color: category_colors[category_idx]
}
})
// Group data by category to create series
const series_data = categories.map((category, idx) => {
const points = sample_data.filter(d => d.category === category)
return {
x: points.map(p => p.x),
y: points.map(p => p.y),
point_style: {
fill: category_colors[idx],
radius: 6 - idx, // Size varies by category
stroke: 'black',
stroke_width: 0.5
},
metadata: points.map(p => ({
category: p.category,
color: p.color
})),
label: category
}
})
// Tick interval settings
const ticks = $state({ x: -5, y: -5 })
</script>
<div>
{#each Object.keys(ticks) as axis (axis)}
<label style="display: inline-block; margin: 1em;">
{axis} Tick Interval:
<select bind:value={ticks[axis]}>
{#each [2, 5, 10] as num (num)}
<option value={-num}>{num} units</option>
{/each}
</select>
</label>
{/each}
<ScatterPlot
series={series_data}
x_label="X Value"
y_label="Y Value"
x_lim={[-15, 15]}
y_lim={[-15, 15]}
x_ticks={ticks.x}
y_ticks={ticks.y}
markers="points"
style="height: 400px; width: 100%;"
>
{#snippet tooltip({ x, y, metadata })}
<div style="white-space: nowrap;">
<span style="color: {metadata.color};">●</span>
<strong>{metadata.category}</strong><br>
Position: ({x.toFixed(2)}, {y.toFixed(2)})
</div>
{/snippet}
</ScatterPlot>
<!-- Legend -->
<div style="display: flex; justify-content: center; margin-top: 1em;">
{#each categories as category, idx}
<div style="margin: 0 1em; display: flex; align-items: center;">
<span style="display: inline-block; width: 12px; height: 12px; background: {category_colors[idx]}; border-radius: 50%; margin-right: 0.5em;"></span>
{category}
</div>
{/each}
</div>
</div>
Time-Based Data with Custom Formatting
Using time data on the x-axis with custom formatting:
<script>
import { ScatterPlot } from '$lib'
// Generate dates for the past 30 days
const dates = Array.from({ length: 30 }, (_, idx) => {
const date = new Date()
date.setDate(date.getDate() - (30 - idx))
return date.getTime()
})
// Random data values for multiple series
const values1 = Array.from({ length: 30 }, () => Math.random() * 100)
const values2 = Array.from({ length: 30 }, () => Math.random() * 70 + 30)
const time_series = [
{
x: dates,
y: values1,
point_style: { fill: 'steelblue', radius: 4 },
label: 'Series A',
metadata: Array.from({ length: 30 }, (_, idx) => ({ series: 'Series A', day: idx }))
},
{
x: dates,
y: values2,
point_style: { fill: 'orangered', radius: 4 },
label: 'Series B',
metadata: Array.from({ length: 30 }, (_, idx) => ({ series: 'Series B', day: idx }))
}
]
// Format options
let date_format = '%b %d'
let y_format = '.1f'
</script>
<div>
<div style="margin-bottom: 1em;">
<label>
Date Format:
<select bind:value={date_format}>
{#each [['%b %d', 'Month Day (Jan 01)'], ['%Y-%m-%d', 'YYYY-MM-DD'], ['%d/%m', 'DD/MM']] as [value, label] (value)}
<option value={value}>{label}</option>
{/each}
</select>
</label>
<label style="margin-left: 1em;">
Y-Value Format:
<select bind:value={y_format}>
{#each [['.1f', '1 decimal'], ['.2f', '2 decimals'], ['d', 'Integer']] as [value, label] (value)}
<option value={value}>{label}</option>
{/each}
</select>
</label>
</div>
<ScatterPlot
series={time_series}
markers="line+points"
x_format={date_format}
{y_format}
x_ticks={-7}
y_ticks={5}
x_label="Date"
y_label="Value"
style="height: 350px; width: 100%;"
legend={{ layout: `horizontal`, n_items: 3, wrapper_style: `max-width: none; justify-content: center;` }}
>
{#snippet tooltip({ x, y, x_formatted, y_formatted, metadata })}
<div style="white-space: nowrap;">
<strong>{metadata?.series}</strong><br />
Date: {x_formatted}<br />
Value: {y_formatted}
</div>
{/snippet}
</ScatterPlot>
</div>
Points with Shared Coordinates
This example demonstrates how points with identical coordinates can still be individually identified and interacted with:
<script>
import { ScatterPlot } from '$lib'
// Create points with shared X or Y coordinates
const shared_coords_data = {
// Points with same X values (vertical line)
x: [5, 5, 5, 5, 5,
// Points with same Y values (horizontal line)
1, 2, 3, 4, 5,
// Some random points
7, 8, 9, 7, 9],
y: [1, 2, 3, 4, 5,
3, 3, 3, 3, 3,
1, 2, 3, 4, 5],
point_style: { fill: 'steelblue', radius: 6 },
// Add distinct metadata for each point to identify them
metadata: [
// Vertical line points
{id: 'v1', label: 'V1 (5,1)'}, {id: 'v2', label: 'V2 (5,2)'},
{id: 'v3', label: 'V3 (5,3)'}, {id: 'v4', label: 'V4 (5,4)'},
{id: 'v5', label: 'V5 (5,5)'},
// Horizontal line points
{id: 'h1', label: 'H1 (1,3)'}, {id: 'h2', label: 'H2 (2,3)'},
{id: 'h3', label: 'H3 (3,3)'}, {id: 'h4', label: 'H4 (4,3)'},
{id: 'h5', label: 'H5 (5,3)'},
// Random points
{id: 'r1', label: 'R1 (7,1)'}, {id: 'r2', label: 'R2 (8,2)'},
{id: 'r3', label: 'R3 (9,3)'}, {id: 'r4', label: 'R4 (7,4)'},
{id: 'r5', label: 'R5 (9,5)'}
]
}
let hovered_point = null
</script>
<ScatterPlot
series={[shared_coords_data]}
x_lim={[0, 10]}
y_lim={[0, 6]}
x_ticks={1}
y_ticks={1}
x_label="X Axis"
y_label="Y Axis"
change={(event) => (hovered_point = event)}
style="height: 350px; width: 100%;"
>
{#snippet tooltip({ x, y, metadata })}
{@const { label, id } = metadata}
<div style="white-space: nowrap;">
<strong>{label}</strong><br />
Coordinates: ({x}, {y})<br />
ID: {id}
</div>
{/snippet}
</ScatterPlot>
{#if hovered_point}
<div style="margin-top: 1em; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
<strong>Currently hovering:</strong> {hovered_point.metadata?.label || 'Unknown point'} at ({hovered_point.x}, {hovered_point.y})
</div>
{/if}
Text Annotations for Scatter Points
This example shows how to add permanent text labels to your scatter points:
<script>
import { ScatterPlot } from '$lib'
// Data with text labels
const data = {
x: [1, 3, 5, 7, 9],
y: [2, 5, 3, 7, 4],
point_style: { fill: 'steelblue', radius: 6 },
// Add text labels to each point
point_label: [
{ text: 'Point A', offset_y: -15 },
{ text: 'Point B', offset_y: 15 },
{ text: 'Point C', offset_y: -15 },
{ text: 'Point D', offset_y: -15 },
{ text: 'Point E', offset_y: 15 }
]
}
</script>
<ScatterPlot
series={[data]}
x_label="X Axis"
y_label="Y Axis"
x_lim={[0, 10]}
y_lim={[0, 10]}
markers="points"
style="height: 350px; width: 100%;"
/>
Different Label Positions
You can position labels in different directions relative to each point:
<script>
import { ScatterPlot } from '$lib'
const position_data = {
x: [5, 5, 5, 5, 5],
y: [1, 2, 3, 4, 5],
point_style: { fill: 'goldenrod', radius: 5 },
// Different positions for labels
point_label: [
{ text: 'Above', offset_y: -15, offset_x: 0 },
{ text: 'Right', offset_x: 15, offset_y: 0 },
{ text: 'Below', offset_y: 15, offset_x: 0 },
{ text: 'Left', offset_x: -30, offset_y: 0 },
{ text: 'Diagonal', offset_x: 10, offset_y: -10 }
]
}
</script>
<ScatterPlot
series={[position_data]}
x_label="X Axis"
y_label="Y Axis"
x_lim={[0, 10]}
y_lim={[0, 6]}
markers="points"
style="height: 350px; width: 100%;"
/>
Example Code
Here’s how to add text annotations to your scatter points:
// Data with text labels
const data = {
x: [1, 3, 5, 7, 9],
y: [2, 5, 3, 7, 4],
point_style: {
fill: 'steelblue',
radius: 6,
},
// Add text labels to each point
point_label: [
{ text: 'Point A', offset_y: -15, font_size: '14px' },
{ text: 'Point B', offset_y: -15, font_size: '14px' },
{ text: 'Point C', offset_y: -15, font_size: '14px' },
{ text: 'Point D', offset_y: -15, font_size: '14px' },
{ text: 'Point E', offset_y: -15, font_size: '14px' },
],
}
Interactive Log-Scaled Axes
ScatterPlot supports logarithmic scaling for data that spans multiple orders of magnitude. This example combines multiple datasets and allows you to dynamically switch between linear and logarithmic scales for both the X and Y axes using the checkboxes below. Observe how the appearance of the data changes, particularly for power-law relationships which appear as straight lines on log-log plots.
<script>
import { ScatterPlot } from '$lib'
const point_count = 50;
// Series 1: Exponential Decay
const decay_data = {
x: [],
y: [],
point_style: { fill: 'coral', radius: 4 },
label: 'Exponential Decay',
metadata: []
};
for (let idx = 0; idx < point_count; idx++) {
const x_val = 0.1 + (idx / (point_count - 1)) * 10; // x from 0.1 to 10.1
const y_val = 10000 * Math.exp(-0.5 * x_val);
decay_data.x.push(x_val);
// Ensure y is not exactly 0 for log scale, clamp to a small positive value
decay_data.y.push(Math.max(y_val, 1e-9));
decay_data.metadata.push({ series: 'Exponential Decay' });
}
// Series 2: Logarithmic Sine Wave
const log_sine_data = {
x: [],
y: [],
point_style: { fill: 'deepskyblue', radius: 4 },
label: 'Log Sine Wave',
metadata: []
};
for (let idx = 0; idx < point_count * 2; idx++) { // More points for smoother curve
const x_val = Math.pow(10, -1 + (idx / (point_count * 2 - 1)) * 4); // x from 0.1 to 1000 log-spaced
const y_val = 500 + 400 * Math.sin(Math.log10(x_val) * 5);
log_sine_data.x.push(x_val);
log_sine_data.y.push(Math.max(y_val, 1e-9)); // Clamp potential near-zero y
log_sine_data.metadata.push({ series: 'Log Sine Wave' });
}
// Series 4: Power Law (y = x^2)
const power_law_data = {
x: [],
y: [],
point_style: { fill: 'mediumseagreen', radius: 5 },
label: 'y = x^2',
metadata: []
}
for (let idx = -1; idx <= 3; idx += 0.25) {
const x_val = Math.pow(10, idx)
const y_val = Math.pow(x_val, 2); // y = x^2
power_law_data.x.push(x_val);
power_law_data.y.push(Math.max(y_val, 1e-9)); // Clamp y
power_law_data.metadata.push({ series: 'y = x^2' });
}
// Series 5: Inverse Power Law (y = x^0.5)
const inverse_power_data = {
x: [],
y: [],
point_style: { fill: 'purple', radius: 5 },
label: 'y = x^0.5',
metadata: []
}
for (let idx = -1; idx <= 3; idx += 0.25) {
const x_val = Math.pow(10, idx)
const y_val = Math.pow(x_val, 0.5); // y = √x
inverse_power_data.x.push(x_val);
inverse_power_data.y.push(Math.max(y_val, 1e-9)); // Clamp y
inverse_power_data.metadata.push({ series: 'y = x^0.5' });
}
// Combine all series
const all_series = [decay_data, log_sine_data, power_law_data, inverse_power_data]
// State for controlling scale types
let x_is_log = $state(false)
let y_is_log = $state(false)
// Derived scale types based on state
let x_scale_type = $derived(x_is_log ? `log` : `linear`)
let y_scale_type = $derived(y_is_log ? `log` : `linear`)
// Reactive limits based on scale type to avoid log(0) issues and accommodate data
let x_lim = $derived(x_is_log ? [0.1, 1000] : [null, 1000])
let y_lim = $derived(y_is_log ? [0.1, 10000] : [null, 10000])
</script>
<div>
<div style="display: flex; justify-content: center; gap: 2em; margin-bottom: 1em;">
<label>
<input type="checkbox" bind:checked={x_is_log} />
Log X-Axis
</label>
<label>
<input type="checkbox" bind:checked={y_is_log} />
Log Y-Axis
</label>
</div>
<!-- Use #key to ensure plot redraws correctly when scale types change -->
{#key [x_scale_type, y_scale_type]}
<ScatterPlot
series={all_series}
{x_scale_type}
{y_scale_type}
{x_lim}
{y_lim}
x_label="X Axis ({x_scale_type})"
y_label="Y Axis ({y_scale_type})"
x_format="~s"
y_format="~s"
markers="line+points"
style="height: 400px; width: 100%;"
>
{#snippet tooltip({ x, y, x_formatted, y_formatted, metadata })}
<div style="white-space: nowrap;">
<strong>{metadata.label ?? metadata.series}</strong><br/>
X: {x_formatted || x.toPrecision(3)}<br/>
Y: {y_formatted || y.toPrecision(3)}
</div>
{/snippet}
</ScatterPlot>
{/key}
</div>
Combined Interactive Scatter Plot with Custom Controls
This example combines multiple features including different display modes, custom styling, various marker types, interactive controls for axis customization, and hover styling. It demonstrates the new grid customization options with independent X and Y grid controls and custom grid styling:
Interactive Multi-Series Plot
Random Points with Custom Controls
<script>
import { ScatterPlot } from '$lib'
// Define categories and colors for data points
const categories = ['Group A', 'Group B', 'Group C']
const category_colors = ['crimson', 'royalblue', 'mediumseagreen']
// Generate sample data with categories and different marker types
const marker_types = ['circle', 'diamond', 'star', 'triangle', 'cross', 'wye']
// Create three data series with different styling
const series_data = categories.map((category, cat_idx) => {
const points = 10
const marker_type_for_series = marker_types[cat_idx % marker_types.length];
return {
x: Array.from({ length: points }, (_, idx) => idx + 1),
y: Array.from({ length: points }, () => 3 + cat_idx * 3 + Math.random() * 2),
point_style: {
fill: category_colors[cat_idx],
radius: 6 - cat_idx,
stroke: 'black',
stroke_width: 0.5,
marker_type: marker_type_for_series,
marker_size: 40 + cat_idx * 5
},
metadata: Array.from({ length: points }, (_, idx) => ({
category,
color: category_colors[cat_idx],
marker: marker_type_for_series,
idx
})),
label: category
}
})
// Currently selected display mode
let display_mode = $state('line+points')
// Toggle series visibility
let visible_series = $state({
[categories[0]]: true,
[categories[1]]: true,
[categories[2]]: true
})
// Controls for random data points
let ticks = $state({ x: -5, y: -5 })
// Grid controls
let grid = $state({ x: true, y: true })
let grid_color = $state('gray')
let grid_width = $state(0.4)
let grid_dash = $state('4')
// Custom axis labels
let axis_labels = $state({ x: "X Axis", y: "Y Value" })
// Selected point tracking
let selected_point = $state(null)
// Update series based on visibility toggles
let displayed_series = $derived(series_data.filter((_, idx) => visible_series[categories[idx]]))
// Generate random data points across positive and negative space
const sample_count = 40
const sample_data = {
x: Array(sample_count).fill(0).map(() => (Math.random() * 20) - 10),
y: Array(sample_count).fill(0).map(() => (Math.random() * 20) - 10),
point_style: {
fill: 'goldenrod',
radius: 5,
stroke: 'black',
stroke_width: 0.5
},
point_hover: {
scale: 2,
fill: 'orange',
stroke: 'white',
stroke_width: 2
}
}
</script>
<div>
<h3>Interactive Multi-Series Plot</h3>
<div style="margin-bottom: 1em;">
<label>
Display Mode:
<select bind:value={display_mode}>
<option value="points">Points only</option>
<option value="line">Lines only</option>
<option value="line+points">Lines and Points</option>
</select>
</label>
<!-- Legend with toggles -->
<div style="display: flex; margin-left: 2em;">
{#each categories as category, idx}
<label style="margin-right: 1em; display: flex; align-items: center;">
<input type="checkbox" bind:checked={visible_series[category]} />
<span style="display: inline-block; width: 12px; height: 12px; background: {category_colors[idx]}; border-radius: 50%; margin: 0 0.5em;"></span>
{category}
</label>
{/each}
</div>
</div>
<ScatterPlot
series={displayed_series}
x_label={axis_labels.x}
y_label={axis_labels.y}
markers={display_mode}
change={(event) => (selected_point = event)}
style="height: 400px; width: 100%;"
legend={null}
>
{#snippet tooltip({ x, y, metadata })}
<div style="white-space: nowrap;">
<span style="color: {metadata.color};">●</span>
<strong>{metadata.category}</strong><br>
Point {metadata.idx + 1} ({x}, {y.toFixed(2)})<br>
Marker: {metadata.marker}
</div>
{/snippet}
</ScatterPlot>
{#if selected_point}
<div style="margin-top: 1em; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
Selected point: ({selected_point.x}, {selected_point.y.toFixed(2)}) from {selected_point.metadata.category}
</div>
{/if}
<h3 style="margin-top: 2em;">Random Points with Custom Controls</h3>
<div style="margin-bottom: 1em; display: flex; flex-wrap: wrap; gap: 1em;">
{#each Object.keys(ticks) as axis (axis)}
<label>
{axis} Tick Interval:
<select bind:value={ticks[axis]}>
{#each [2, 5, 10] as num (num)}
<option value={-num}>{num} units</option>
{/each}
</select>
</label>
{/each}
{#each Object.keys(grid) as axis (axis)}
<label>
<input type="checkbox" bind:checked={grid[axis]} />
{axis} Grid
</label>
{/each}
<label>
Grid Color:
<select bind:value={grid_color}>
<option value="gray">Gray</option>
<option value="lightgray">Light Gray</option>
<option value="darkgray">Dark Gray</option>
<option value="#aaaaaa">#aaa</option>
</select>
</label>
{#each Object.keys(axis_labels) as axis (axis)}
<label>
{axis} Label:
<input type="text" bind:value={axis_labels[axis]} style="width: 120px" />
</label>
{/each}
</div>
<ScatterPlot
series={[sample_data]}
x_label={axis_labels.x}
y_label={axis_labels.y}
x_lim={[-15, 15]}
y_lim={[-15, 15]}
x_ticks={ticks.x}
y_ticks={ticks.y}
x_grid={grid.x}
y_grid={grid.y}
markers="points"
style="height: 400px; width: 100%;"
>
{#snippet tooltip({ x, y })}
<div style="white-space: nowrap;">
Position: ({x.toFixed(2)}, {y.toFixed(2)})
</div>
{/snippet}
</ScatterPlot>
</div>
Automatic Color Bar Placement
This example demonstrates how the color bar automatically positions itself in one of the four corners (top-left, top-right, bottom-left, bottom-right) based on where the data points are least dense. Use the sliders to adjust the number of points generated in each quadrant and observe how the color bar moves to avoid overlapping the data.
<script>
import { ScatterPlot } from '$lib'
// State for controlling point density in each quadrant
let density = $state({top_left: 10, top_right: 50, bottom_left: 10, bottom_right: 10})
// Function to generate points within a specific quadrant
const make_quadrant_points = (count, x_range, y_range) => {
const points = []
for (let idx = 0; idx < count; idx++) {
const x_val = x_range[0] + Math.random() * (x_range[1] - x_range[0])
const y_val = y_range[0] + Math.random() * (y_range[1] - y_range[0])
// Assign a color value (e.g., based on distance from origin)
const color_val = Math.sqrt(
Math.pow(x_range[0] + (x_range[1] - x_range[0]) / 2, 2) +
Math.pow(y_range[0] + (y_range[1] - y_range[0]) / 2, 2)
) * Math.random() * 2 // Add some variation
points.push({
x: x_val,
y: y_val,
color_value: color_val,
label: color_val.toFixed(1)
})
}
return points
}
// Reactive generation of plot data based on densities
let plot_series = $derived.by(() => {
const plot_width = 100
const plot_height = 100
const center_x = plot_width / 2
const center_y = plot_height / 2
const tl_points = make_quadrant_points(density.bottom_left, [0, center_x], [0, center_y])
const tr_points = make_quadrant_points(density.bottom_right, [center_x, plot_width], [0, center_y])
const bl_points = make_quadrant_points(density.top_left, [0, center_x], [center_y, plot_height])
const br_points = make_quadrant_points(density.top_right, [center_x, plot_width], [center_y, plot_height])
const all_points = [...tl_points, ...tr_points, ...bl_points, ...br_points]
return [{
x: all_points.map(p => p.x),
y: all_points.map(p => p.y),
color_values: all_points.map(p => p.color_value),
point_label: all_points.map(p => ({ text: p.label, offset_y: -10, font_size: '14px' })),
point_style: {
radius: 5,
stroke: 'white',
stroke_width: 0.5
}
}]
})
</script>
<div>
<div style="display: grid; grid-template-columns: repeat(2, max-content); gap: 1.5em; place-items: center; place-content: center;">
{#each [['top_left', 'Top Left'], ['top_right', 'Top Right'], ['bottom_left', 'Bottom Left'], ['bottom_right', 'Bottom Right']] as [quadrant, label]}
<label>{label}: {density[quadrant]}
<input type="range" min="0" max="100" bind:value={density[quadrant]} style="width: 100px; margin-left: 0.5em;" />
</label>
{/each}
</div>
<ScatterPlot
series={plot_series}
x_label="X Position"
y_label="Y Position"
x_lim={[0, 100]}
y_lim={[0, 100]}
markers="points+text"
color_scheme="turbo"
color_bar={{ label: `Color Bar Title` }}
style="height: 450px; width: 100%;"
>
{#snippet tooltip({ x, y, metadata, color_value })}
<div style="white-space: nowrap; padding: 0.25em; background: rgba(0,0,0,0.7); color: white;">
Point ({x.toFixed(1)}, {y.toFixed(1)})<br />
Color value: {color_value?.toFixed(2)}
</div>
{/snippet}
</ScatterPlot>
</div>
Automatic Label Placement (Repel Mode)
When points are clustered closely together, manually positioning labels can become tedious and result in overlaps. The ScatterPlot
component offers an automatic label placement feature using a force simulation (d3-force
). This feature intelligently positions labels to minimize overlaps while keeping them close to their corresponding data points.
To enable this feature, set auto_placement: true
within the point_label
object for the desired points.
This example demonstrates automatic placement with several clusters of points:
<script>
import { ScatterPlot } from '$lib'
// Function to generate a cluster of points
const generate_cluster = (center_x, center_y, count, radius, label_prefix) => {
const points = {
x: [],
y: [],
point_style: [],
point_label: []
}
for (let idx = 0; idx < count; idx++) {
const angle = Math.random() * 2 * Math.PI
const dist = Math.random() * radius
points.x.push(center_x + Math.cos(angle) * dist)
points.y.push(center_y + Math.sin(angle) * dist)
points.point_style.push({ fill: 'rebeccapurple', radius: 9 })
points.point_label.push({
text: `${label_prefix}-${idx + 1}`,
auto_placement: true, // Enable auto-placement
font_size: '14px' // Increased font size
})
}
return points
}
// Generate multiple clusters
const cluster1 = generate_cluster(20, 80, 8, 5, 'C1')
const cluster2 = generate_cluster(80, 20, 10, 8, 'C2')
const cluster3 = generate_cluster(50, 50, 12, 10, 'C3')
const cluster4 = generate_cluster(15, 15, 6, 4, 'C4')
// Combine into a single series for simplicity in this demo
const combined_series = {
x: [...cluster1.x, ...cluster2.x, ...cluster3.x, ...cluster4.x],
y: [...cluster1.y, ...cluster2.y, ...cluster3.y, ...cluster4.y],
point_style: [...cluster1.point_style, ...cluster2.point_style, ...cluster3.point_style, ...cluster4.point_style],
point_label: [...cluster1.point_label, ...cluster2.point_label, ...cluster3.point_label, ...cluster4.point_label]
}
let auto_place_enabled = true;
</script>
<div>
<label style="margin-bottom: 1em; display: block;">
<input type="checkbox" bind:checked={auto_place_enabled} />
Enable Automatic Label Placement
</label>
<!-- Use #key to force re-render when auto_place_enabled changes -->
{#key auto_place_enabled}
<ScatterPlot
series={[ { ...combined_series, point_label: combined_series.point_label.map(lbl => ({ ...lbl, auto_placement: auto_place_enabled })) } ]}
x_label="X Position"
y_label="Y Position"
x_lim={[0, 100]}
y_lim={[0, 100]}
markers="points"
style="height: 500px; width: 100%;"
/>
{/key}
</div>
Try toggling the checkbox to see the difference between manual (default) offset and automatic placement.
External Vertical Color Bar with Dynamic Controls
This example shows how to place the color bar vertically on the right side of the plot, outside the main plotting area, and make it span the full height available. It also demonstrates how to dynamically change the color scheme and toggle between linear and log color scales.
<script>
import { ScatterPlot } from '$lib'
// Generate data where color value relates to y-value
const point_count = 50
const vertical_color_data = {
x: Array.from({ length: point_count }, (_, idx) => (idx / point_count) * 90 + 5), // Range 5 to 95
y: Array.from({ length: point_count }, () => Math.random() * 90 + 5), // Range 5 to 95
// Color value based on the y-coordinate
color_values: Array.from({ length: point_count }, (_, idx) => idx * 2), // Values from 0 to 98
point_style: {
radius: 6,
stroke: `black`,
stroke_width: 0.5,
},
metadata: Array.from({ length: point_count }, (_, idx) => ({
value: idx * 2,
})),
}
// Adjust right padding to make space for the external color bar
const plot_padding = { t: 20, b: 50, l: 60, r: 70 } // Increased right padding
// --- Color Scaling Controls ---
// Track which color scale type is active
let color_scale_type = $state(`linear`)
// Color scheme options
const color_schemes = [
`viridis`, `inferno`, `plasma`, `magma`, `cividis`,
`turbo`, `warm`, `cool`, `spectral`
]
let selected_scheme = $state(`cool`) // Default matches original example
</script>
<div>
<div style="margin-bottom: 1em; display: flex; gap: 1em; flex-wrap: wrap; align-items: center;">
<div>
<strong>Color Scale Type:</strong>
{#each ['linear', 'log'] as scale_type}
<label style="margin-left: 0.5em;">
<input type="radio" name="scale_type" value={scale_type}
bind:group={color_scale_type} /> {scale_type}
</label>
{/each}
</div>
<div>
<strong>Color Scheme:</strong>
<select bind:value={selected_scheme}>
{#each color_schemes as scheme}
<option value={scheme}>{scheme}</option>
{/each}
</select>
</div>
</div>
The color bar is positioned vertically to the right, outside the plot.
The plot's right padding is increased to prevent overlap. Use the controls above to change the color scheme and scale type.
<ScatterPlot
series={[vertical_color_data]}
x_label="X Position"
y_label="Y Position"
x_lim={[0, 100]}
y_lim={[0, 100]}
markers="points"
color_scheme={selected_scheme}
{color_scale_type}
padding={plot_padding}
color_bar={{
orientation: `vertical`,
label: `Color Bar Title (${color_scale_type})`,
tick_side: `primary`,
wrapper_style: `
position: absolute;
/* Position outside the plot area using padding values */
right: 10px; /* Distance from the container's right edge */
top: ${plot_padding.t}px; /* Align with top padding */
/* Set height directly for the wrapper */
height: calc(100% - ${plot_padding.t + plot_padding.b}px); /* Fill vertical space */
`,
style: `width: 15px; height: 100%;`,
}}
style="height: 400px;"
>
{#snippet tooltip({ x, y, metadata, color_value })}
<div style="white-space: nowrap; padding: 0.25em; background: rgba(0,0,0,0.7); color: white;">
Point ({x.toFixed(1)}, {y.toFixed(1)})<br />
Color value: {color_value?.toFixed(1)}
</div>
{/snippet}
</ScatterPlot>
</div>