Svelte x D3

A collection of data visualisations built with Svelte and D3

Github repo

Scatterplot

alpha
beta

Code

Scatter

<script lang="ts">
	import { DEFAULT_MARGIN } from '../util';
	import { scaleLinear, scaleOrdinal } from 'd3-scale';
	import { extent } from 'd3-array';
	import Chart from '../primatives/Chart.svelte';
	import Circle from '../primatives/Circle.svelte';
	import Axis from '../helpers/Axis.svelte';
	import Tooltip from '../helpers/Tooltip.svelte';
	import LegendOrdinal from '../helpers/LegendOrdinal.svelte';
	import { Delaunay } from 'd3-delaunay';
	import { colorPalette } from '../util';
	import { format } from 'd3-format';
	import Panel from '@visualisations/helpers/Panel.svelte';

	export let margins = DEFAULT_MARGIN;
	export let width = 0;
	export let height = 0;

	export let x: string;
	export let y: string;
	export let color: string;

	export let data: any[];
	export let xFormat = (x: number) => format(',')(x);
	export let yFormat = (y: number) => format(',')(y);
	export let colors = colorPalette;

	// dimensions
	$: dimensions = {
		width,
		height,
		margins,
		innerHeight: Math.max(height - margins.top - margins.bottom, 0),
		innerWidth: Math.max(width - margins.left - margins.right, 0)
	};

	//_ Accessors
	$: getY = (d: any) => d[y];
	$: getX = (d: any) => d[x];
	$: getColor = (d: any) => d[color];

	//_ scales
	$: xScale = scaleLinear().domain(extent(data, getX)).range([0, dimensions.innerWidth]).nice();
	$: yScale = scaleLinear().domain(extent(data, getY)).range([dimensions.innerHeight, 0]).nice();
	$: colorScale = scaleOrdinal()
		.domain([...new Set(data.map(getColor))])
		.range(colors);

	//_ data
	$: getXScaled = (d: any) => xScale(getX(d));
	$: getYScaled = (d: any) => yScale(getY(d));
	$: getColorScaled = (d: any) => colorScale(getColor(d)) as string;

	//_ tooltip
	$: voronoi = Delaunay.from(data.map((d) => [getXScaled(d), getYScaled(d)]));

	$: tooltipLeft = 0;
	$: tooltipTop = 0;
	$: tooltipData = null;

	$: handleMouseOver = (event: MouseEvent) => {
		const x = event.offsetX - margins.left;
		const y = event.offsetY - margins.top;

		const index = voronoi.find(x, y);
		const tooltipPoint = data[index];

		if (tooltipPoint) {
			tooltipData = tooltipPoint;
			tooltipLeft = getXScaled(tooltipPoint) + margins.left;
			tooltipTop = getYScaled(tooltipPoint) + margins.bottom + 10;
		}
	};
</script>

<div class="w-full h-full flex flex-col">
	<div>
		<LegendOrdinal scale={colorScale} />
	</div>
	<div class="w-full h-full relative" bind:clientWidth={width} bind:clientHeight={height}>
		{#if width > 100}
			<Chart {dimensions}>
				<Panel width={dimensions.innerWidth} height={dimensions.innerHeight} {xScale} {yScale} />
				<Axis scale={xScale} orientation="x" formatTick={xFormat} />
				<Axis scale={yScale} orientation="y" formatTick={yFormat} />
				{#each data as datum, i}
					<Circle
						x={getXScaled(datum)}
						y={getYScaled(datum)}
						radius={10}
						fill={getColorScaled(datum)}
						stroke={tooltipData && tooltipData === datum ? 'white' : 'transparent'}
					/>
				{/each}
				<!-- svelte-ignore a11y-no-static-element-interactions -->
				<rect
					width={dimensions.innerWidth}
					height={dimensions.innerHeight}
					on:mousemove={handleMouseOver}
					on:mouseleave={() => (tooltipData = null)}
					fill="transparent"
				/>
			</Chart>
			{#if tooltipData}
				<Tooltip left={tooltipLeft} top={tooltipTop}>
					<span class="px-2 text-slate-900">{getColor(tooltipData)}</span>
				</Tooltip>
			{/if}
		{/if}
	</div>
</div>