<script>
import SyncedBrushWrapper from './_components/SyncedBrushWrapper.svelte';
import pointsOne from './_data/points.csv';
import pointsTwo from './_data/pointsTwo.csv';
import pointsThree from './_data/pointsThree.csv';
import pointsFour from './_data/pointsFour.csv';
let brushExtents = $state([null, null]);
const xKey = 'myX';
const yKey = 'myY';
const datasets = [pointsOne, pointsTwo, pointsThree, pointsFour];
datasets.forEach(dataset => {
dataset.forEach(d => {
d[yKey] = +d[yKey];
});
});
const colors = ['#00e047', '#00bbff', '#ff00cc', '#ffcc00'];
</script>
<div class="chart-container">
{#each datasets as dataset, i}
<SyncedBrushWrapper
data={dataset}
{xKey}
{yKey}
bind:min={brushExtents[0]}
bind:max={brushExtents[1]}
stroke={colors[i]}
/>
{/each}
</div>
<style>
.chart-container {
width: 100%;
height: 250px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-content: space-between;
}
</style>
<script>
import { LayerCake, Svg, Html } from 'layercake';
import Line from './Line.svelte';
import Area from './Area.svelte';
import AxisX from './AxisX.svelte';
import AxisY from './AxisY.svelte';
import Brush from './Brush.html.svelte';
let {
min = $bindable(null),
max = $bindable(null),
xKey = 'x',
yKey = 'y',
data = [],
stroke = '#00e047'
} = $props();
let brushedData = $derived.by(() => {
const start = Math.max(0, Math.floor((min ?? 0) * data.length));
const end = Math.min(data.length, Math.ceil((max ?? 1) * data.length));
let brushed = data.slice(start, end);
if (brushed.length < 2 && data.length >= 2) {
return data.slice(start, start + 2);
}
return brushed;
});
</script>
<div class="chart-wrapper">
<div class="chart-container">
<LayerCake
padding={{ bottom: 20, left: 25 }}
x={xKey}
y={yKey}
yDomain={[0, null]}
data={brushedData}
>
<Svg>
<AxisX
ticks={ticks => {
const filtered = ticks.filter(t => t % 1 === 0);
if (filtered.length > 7) {
return filtered.filter((t, i) => i % 2 === 0);
}
return filtered;
}}
/>
<AxisY ticks={2} />
<Line {stroke} />
<Area fill={`${stroke}10`} />
</Svg>
</LayerCake>
</div>
<div class="brush-container">
<LayerCake padding={{ top: 5 }} x={xKey} y={yKey} yDomain={[0, null]} {data}>
<Svg>
<Line {stroke} />
<Area fill={`${stroke}10`} />
</Svg>
<Html>
<Brush bind:min bind:max />
</Html>
</LayerCake>
</div>
</div>
<style>
.chart-wrapper {
width: 48%;
height: 40%;
}
.chart-container {
width: 100%;
height: 80%;
}
.brush-container {
width: 100%;
height: 20%;
}
</style>
<script>
import { getContext } from 'svelte';
const { data, xGet, yGet } = getContext('LayerCake');
let { stroke = '#ab00d6' } = $props();
let path = $derived(
'M' +
$data
.map(d => {
return $xGet(d) + ',' + $yGet(d);
})
.join('L')
);
</script>
<path class="path-line" d={path} {stroke}></path>
<style>
.path-line {
fill: none;
stroke-linejoin: round;
stroke-linecap: round;
stroke-width: 2;
}
</style>
<script>
import { getContext } from 'svelte';
const { data, xGet, yGet, xScale, yScale, extents } = getContext('LayerCake');
let { fill = '#ab00d610' } = $props();
let path = $derived(
'M' +
$data
.map(( d) => {
return $xGet(d) + ',' + $yGet(d);
})
.join('L')
);
let area = $derived.by(() => {
const yRange = $yScale.range();
return (
path +
('L' +
$xScale($extents.x ? $extents.x[1] : 0) +
',' +
yRange[0] +
'L' +
$xScale($extents.x ? $extents.x[0] : 0) +
',' +
yRange[0] +
'Z')
);
});
</script>
<path class="path-area" d={area} {fill}></path>
<script>
import { getContext } from 'svelte';
const { width, height, xScale, yRange } = getContext('LayerCake');
let {
tickMarks = false,
gridlines = true,
tickMarkLength = 6,
baseline = false,
snapLabels = false,
format = d => d,
ticks = undefined,
tickGutter = 0,
dx = 0,
dy = 12
} = $props();
function textAnchor(i, sl) {
if (sl === true) {
if (i === 0) {
return 'start';
}
if (i === tickVals.length - 1) {
return 'end';
}
}
return 'middle';
}
let tickLen = $derived(tickMarks === true ? (tickMarkLength ?? 6) : 0);
let isBandwidth = $derived(typeof $xScale.bandwidth === 'function');
let tickVals = $derived(
Array.isArray(ticks)
? ticks
: isBandwidth
? $xScale.domain()
: typeof ticks === 'function'
? ticks($xScale.ticks())
: $xScale.ticks(ticks)
);
let halfBand = $derived(isBandwidth ? $xScale.bandwidth() / 2 : 0);
</script>
<g class="axis x-axis" class:snapLabels>
{#each tickVals as tick, i (tick)}
{#if baseline === true}
<line class="baseline" y1={$height} y2={$height} x1="0" x2={$width} />
{/if}
<g class="tick tick-{i}" transform="translate({$xScale(tick)},{Math.max(...$yRange)})">
{#if gridlines === true}
<line class="gridline" x1={halfBand} x2={halfBand} y1={-$height} y2="0" />
{/if}
{#if tickMarks === true}
<line
class="tick-mark"
x1={halfBand}
x2={halfBand}
y1={tickGutter}
y2={tickGutter + tickLen}
/>
{/if}
<text x={halfBand} y={tickGutter + tickLen} {dx} {dy} text-anchor={textAnchor(i, snapLabels)}
>{format(tick)}</text
>
</g>
{/each}
</g>
<style>
.tick {
font-size: 11px;
}
line,
.tick line {
stroke: #aaa;
stroke-dasharray: 2;
}
.tick text {
fill: #666;
}
.tick .tick-mark,
.baseline {
stroke-dasharray: 0;
}
.axis.snapLabels .tick:last-child text {
transform: translateX(3px);
}
.axis.snapLabels .tick.tick-0 text {
transform: translateX(-3px);
}
</style>
<script>
import { getContext } from 'svelte';
const { xRange, yScale, width } = getContext('LayerCake');
let {
tickMarks = false,
labelPosition = 'even',
snapBaselineLabel = false,
gridlines = true,
tickMarkLength = undefined,
format = d => d,
ticks = 4,
tickGutter = 0,
dx = 0,
dy = 0,
charPixelWidth = 7.25
} = $props();
function calcStringLength(sum, val) {
if (val === ',' || val === '.') return sum + charPixelWidth * 0.5;
return sum + charPixelWidth;
}
let isBandwidth = $derived(typeof $yScale.bandwidth === 'function');
let tickVals = $derived(
Array.isArray(ticks)
? ticks
: isBandwidth
? $yScale.domain()
: typeof ticks === 'function'
? ticks($yScale.ticks())
: $yScale.ticks(ticks)
);
let widestTickLen = $derived(
Math.max(
10,
Math.max(...tickVals.map(d => format(d).toString().split('').reduce(calcStringLength, 0)))
)
);
let tickLen = $derived(
tickMarks === true
? labelPosition === 'above'
? (tickMarkLength ?? widestTickLen)
: (tickMarkLength ?? 6)
: 0
);
let x1 = $derived(-tickGutter - (labelPosition === 'above' ? widestTickLen : tickLen));
let y = $derived(isBandwidth ? $yScale.bandwidth() / 2 : 0);
let maxTickValPx = $derived(Math.max(...tickVals.map($yScale)));
</script>
<g class="axis y-axis">
{#each tickVals as tick (tick)}
{@const tickValPx = $yScale(tick)}
<g class="tick tick-{tick}" transform="translate({$xRange[0]}, {tickValPx})">
{#if gridlines === true}
<line class="gridline" {x1} x2={$width} y1={y} y2={y}></line>
{/if}
{#if tickMarks === true}
<line class="tick-mark" {x1} x2={x1 + tickLen} y1={y} y2={y}></line>
{/if}
<text
x={x1}
{y}
dx={dx + (labelPosition === 'even' ? -3 : 0)}
text-anchor={labelPosition === 'above' ? 'start' : 'end'}
dy={dy +
(labelPosition === 'above' || (snapBaselineLabel === true && tickValPx === maxTickValPx)
? -3
: 4)}>{format(tick)}</text
>
</g>
{/each}
</g>
<style>
.tick {
font-size: 11px;
}
.tick line {
stroke: #aaa;
}
.tick .gridline {
stroke-dasharray: 2;
}
.tick text {
fill: #666;
}
.tick.tick-0 line {
stroke-dasharray: 0;
}
</style>
<script>
import { clamp } from 'yootils';
let { min = $bindable(), max = $bindable() } = $props();
let brush = $state();
const p = x => {
const { left, right } = brush.getBoundingClientRect();
return clamp((x - left) / (right - left), 0, 1);
};
const handler = fn => {
return e => {
e.stopPropagation();
e.preventDefault();
if (e.type === 'touchstart') {
if (e.touches.length !== 1) return;
e = e.touches[0];
}
const id = e.identifier;
const start = { min, max, p: p(e.clientX) };
const handle_move = e => {
e.preventDefault();
if (e.type === 'touchmove') {
if (e.changedTouches.length !== 1) return;
e = e.changedTouches[0];
if (e.identifier !== id) return;
}
fn(start, p(e.clientX));
};
const handle_end = e => {
if (e.type === 'touchend') {
if (e.changedTouches.length !== 1) return;
if (e.changedTouches[0].identifier !== id) return;
} else if (e.target === brush) {
clear();
}
window.removeEventListener('mousemove', handle_move);
window.removeEventListener('mouseup', handle_end);
window.removeEventListener('touchmove', handle_move);
window.removeEventListener('touchend', handle_end);
};
window.addEventListener('mousemove', handle_move);
window.addEventListener('mouseup', handle_end);
window.addEventListener('touchmove', handle_move);
window.addEventListener('touchend', handle_end);
};
};
const clear = () => {
min = null;
max = null;
};
const reset = handler((start, p) => {
min = clamp(Math.min(start.p, p), 0, 1);
max = clamp(Math.max(start.p, p), 0, 1);
});
const move = handler((start, p) => {
const d = clamp(p - start.p, -start.min, 1 - start.max);
min = start.min + d;
max = start.max + d;
});
const adjust_min = handler((start, p) => {
min = p > start.max ? start.max : p;
max = p > start.max ? p : start.max;
});
const adjust_max = handler((start, p) => {
min = p < start.min ? p : start.min;
max = p < start.min ? start.min : p;
});
let left = $derived(100 * min);
let right = $derived(100 * (1 - max));
</script>
<div bind:this={brush} class="brush-outer" onmousedown={reset} ontouchstart={reset}>
{#if min !== null}
<div
class="brush-inner"
draggable="false"
onmousedown={move}
ontouchstart={move}
style="left: {left}%; right: {right}%"
></div>
<div
class="brush-handle"
draggable="false"
onmousedown={adjust_min}
ontouchstart={adjust_min}
style="left: {left}%"
></div>
<div
class="brush-handle"
draggable="false"
onmousedown={adjust_max}
ontouchstart={adjust_max}
style="right: {right}%"
></div>
{/if}
</div>
<style>
.brush-outer {
position: relative;
width: 100%;
height: calc(100% + 5px);
top: -5px;
}
.brush-inner {
position: absolute;
height: 100%;
cursor: move;
background-color: #cccccc90;
}
.brush-handle {
position: absolute;
width: 0;
height: 100%;
cursor: ew-resize;
}
.brush-handle::before {
position: absolute;
content: '';
width: 8px;
left: -4px;
height: 100%;
background: transparent;
}
</style>
myX,myY
1979,7.19
1980,7.83
1981,7.24
1982,7.44
1983,7.51
1984,7.1
1985,6.91
1986,7.53
1987,7.47
1988,7.48
1989,7.03
1990,6.23
1991,6.54
1992,7.54
1993,6.5
1994,7.18
1995,6.12
1996,7.87
1997,6.73
1998,6.55
1999,6.23
2000,6.31
2001,6.74
2002,5.95
2003,6.13
2004,6.04
2005,5.56
2006,5.91
2007,4.29
2008,4.72
2009,5.38
2010,4.92
2011,4.61
2012,3.62
2013,5.35
2014,5.28
2015,4.63
2016,4.72
myX,myY
1979,5.03
1980,3.99
1981,10.35
1982,4.06
1983,10.11
1984,11.66
1985,5.95
1986,7.27
1987,2.77
1988,3.43
1989,6.49
1990,8.27
1991,3.85
1992,6.21
1993,0.14
1994,7.65
1995,5.57
1996,15.61
1997,12.42
1998,5.2
1999,9.81
2000,8.64
2001,10.07
2002,1.71
2003,3
2004,11.82
2005,4.74
2006,4.25
2007,0.16
2008,7.97
2009,9.75
2010,3.34
2011,3.46
2012,1.73
2013,5.97
2014,2.17
2015,0.88
2016,8.19
myX,myY
1979,5.43
1980,7.27
1981,8.82
1982,13.14
1983,14.63
1984,5.41
1985,4.76
1986,12.46
1987,10.38
1988,0.06
1989,9.35
1990,5.84
1991,6.94
1992,2.78
1993,9.84
1994,6.48
1995,9.97
1996,4.85
1997,5.35
1998,4.12
1999,1.56
2000,10.11
2001,2.04
2002,4.03
2003,3.85
2004,8.61
2005,1.44
2006,9.41
2007,3.51
2008,4.12
2009,6.76
2010,3.65
2011,6.76
2012,6.46
2013,0.75
2014,9.93
2015,8.11
2016,1.4
myX,myY
1979,10.27
1980,1.99
1981,2.25
1982,3.56
1983,0.32
1984,4.18
1985,1.75
1986,2.35
1987,2.54
1988,6.53
1989,5.02
1990,3.4
1991,4.79
1992,1.29
1993,12.65
1994,5.26
1995,3.11
1996,14.92
1997,13.21
1998,10.34
1999,5.02
2000,9.11
2001,13.24
2002,8.02
2003,1.54
2004,0.25
2005,6.02
2006,5.91
2007,6.83
2008,6.76
2009,3.7
2010,2.3
2011,3.37
2012,4.02
2013,2.15
2014,2.33
2015,5.98
2016,6.27