<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 = [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';
export let min = null;
export let max = null;
export let xKey = 'x';
export let yKey = 'y';
export let data = [];
export let stroke = '#00e047';
let brushedData;
$: {
brushedData = data.slice((min || 0) * data.length, (max || 1) * data.length);
if (brushedData.length < 2) {
brushedData = data.slice(min * data.length, min * data.length + 2);
}
}
</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');
export let stroke = '#ab00d6';
$: path =
'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');
export let fill = '#ab00d610';
$: path =
'M' +
$data
.map(d => {
return $xGet(d) + ',' + $yGet(d);
})
.join('L');
let area;
$: {
const yRange = $yScale.range();
area =
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');
export let tickMarks = false;
export let gridlines = true;
export let tickMarkLength = 6;
export let baseline = false;
export let snapLabels = false;
export let format = d => d;
export let ticks = undefined;
export let tickGutter = 0;
export let dx = 0;
export let dy = 12;
function textAnchor(i, sl) {
if (sl === true) {
if (i === 0) {
return 'start';
}
if (i === tickVals.length - 1) {
return 'end';
}
}
return 'middle';
}
$: tickLen = tickMarks === true ? tickMarkLength ?? 6 : 0;
$: isBandwidth = typeof $xScale.bandwidth === 'function';
$: tickVals = Array.isArray(ticks)
? ticks
: isBandwidth
? $xScale.domain()
: typeof ticks === 'function'
? ticks($xScale.ticks())
: $xScale.ticks(ticks);
$: halfBand = 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');
export let tickMarks = false;
export let labelPosition = 'even';
export let snapBaselineLabel = false;
export let gridlines = true;
export let tickMarkLength = undefined;
export let format = d => d;
export let ticks = 4;
export let tickGutter = 0;
export let dx = 0;
export let dy = 0;
export let charPixelWidth = 7.25;
$: isBandwidth = typeof $yScale.bandwidth === 'function';
$: tickVals = Array.isArray(ticks)
? ticks
: isBandwidth
? $yScale.domain()
: typeof ticks === 'function'
? ticks($yScale.ticks())
: $yScale.ticks(ticks);
function calcStringLength(sum, val) {
if (val === ',' || val === '.') return sum + charPixelWidth * 0.5;
return sum + charPixelWidth;
}
$: tickLen =
tickMarks === true
? labelPosition === 'above'
? tickMarkLength ?? widestTickLen
: tickMarkLength ?? 6
: 0;
$: widestTickLen = Math.max(
10,
Math.max(...tickVals.map(d => format(d).toString().split('').reduce(calcStringLength, 0)))
);
$: x1 = -tickGutter - (labelPosition === 'above' ? widestTickLen : tickLen);
$: y = isBandwidth ? $yScale.bandwidth() / 2 : 0;
$: maxTickValPx = 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';
export let min;
export let max;
let brush;
const p = x => {
const { left, right } = brush.getBoundingClientRect();
return clamp((x - left) / (right - left), 0, 1);
};
const handler = fn => {
return e => {
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 => {
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;
});
$: left = 100 * min;
$: right = 100 * (1 - max);
</script>
<div
bind:this={brush}
class="brush-outer"
on:mousedown|stopPropagation={reset}
on:touchstart|stopPropagation={reset}
role="slider"
aria-valuemin={min}
aria-valuemax={max}
aria-valuetext="{min} to {max}"
tabindex="0"
>
{#if min !== null}
<div
class="brush-inner"
on:mousedown|stopPropagation={move}
on:touchstart|stopPropagation={move}
style="left: {left}%; right: {right}%"
role="slider"
aria-valuemin={min}
aria-valuemax={max}
aria-valuetext="{min} to {max}"
tabindex="0"
></div>
<div
class="brush-handle"
on:mousedown|stopPropagation={adjust_min}
on:touchstart|stopPropagation={adjust_min}
style="left: {left}%"
role="slider"
aria-valuemin={min}
aria-valuemax={max}
aria-valuetext="{min} to {max}"
tabindex="0"
></div>
<div
class="brush-handle"
on:mousedown|stopPropagation={adjust_max}
on:touchstart|stopPropagation={adjust_max}
style="right: {right}%"
role="slider"
aria-valuemin={min}
aria-valuemax={max}
aria-valuetext="{min} to {max}"
tabindex="0"
></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