<script>
import { LayerCake, ScaledSvg, Html } from 'layercake';
import Line from './_components/Line.svelte';
import Area from './_components/Area.svelte';
import AxisX from './_components/AxisX.percent-range.html.svelte';
import AxisY from './_components/AxisY.percent-range.html.svelte';
import Brush from './_components/Brush.html.svelte';
import data from './_data/points.csv';
let brushExtents = [null, null];
const xKey = 'myX';
const yKey = 'myY';
data.forEach(d => {
d[yKey] = +d[yKey];
});
let brushedData;
$: {
brushedData = data.slice((brushExtents[0] || 0) * data.length, (brushExtents[1] || 1) * data.length);
if (brushedData.length < 2) {
brushedData = data.slice(brushExtents[0] * data.length, brushExtents[0] * data.length + 2)
}
}
</script>
<style>
.brushed-chart-container {
width: 100%;
height: 80%;
}
.brush-container {
width: 100%;
height: 20%;
}
</style>
<div class="brushed-chart-container">
<LayerCake
ssr
percentRange
padding={{ bottom: 20, left: 25 }}
x={xKey}
y={yKey}
yDomain={[0, null]}
data={brushedData}
>
<Html>
<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={4}
/>
</Html>
<ScaledSvg>
<Line
stroke='#00e047'
/>
<Area
fill='#00e04710'
/>
</ScaledSvg>
</LayerCake>
</div>
<div class="brush-container">
<LayerCake
ssr
percentRange
padding={{ top: 5 }}
x={xKey}
y={yKey}
yDomain={[0, null]}
{data}
>
<ScaledSvg>
<Line
stroke='#00e047'
/>
<Area
fill='#00e04710'
/>
</ScaledSvg>
<Html>
<Brush bind:min={brushExtents[0]} bind:max={brushExtents[1]}/>
</Html>
</LayerCake>
</div>
<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 = 1;
$: 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>
<div class='axis x-axis' class:snapLabels>
{#each tickVals as tick, i (tick)}
{@const tickValPx = $xScale(tick)}
{#if baseline === true}
<div class="baseline" style='top:100%; width:100%;'></div>
{/if}
{#if gridlines === true}
<div
class="gridline"
style:left='{tickValPx}%'
style='top:0; bottom:0;'></div>
{/if}
{#if tickMarks === true}
<div
class="tick-mark"
style:left='{tickValPx + halfBand}%'
style:height='{tickLen}px'
style:bottom='{-tickLen - tickGutter}px'
></div>
{/if}
<div
class='tick tick-{i}'
style:left='{tickValPx + halfBand}%'
style='top:calc(100% + {tickGutter}px);'>
<div
class="text"
style:top='{tickLen}px'
style:transform='translate(calc(-50% + {dx}px), {dy}px)'
>{format(tick)}</div>
</div>
{/each}
</div>
<style>
.axis,
.tick,
.tick-mark,
.gridline,
.baseline {
position: absolute;
}
.axis {
width: 100%;
height: 100%;
}
.tick {
font-size: 11px;
}
.gridline {
border-left: 1px dashed #aaa;
}
.tick-mark {
border-left: 1px solid #aaa;
}
.baseline {
border-top: 1px solid #aaa;
}
.tick .text {
color: #666;
position: relative;
white-space: nowrap;
transform: translateX(-50%);
}
.axis.snapLabels .tick:last-child {
transform: translateX(-40%);
}
.axis.snapLabels .tick.tick-0 {
transform: translateX(40%);
}
</style>
<script>
import { getContext } from 'svelte';
const { xRange, yScale } = 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 = -3;
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);
$: halfBand = isBandwidth ? $yScale.bandwidth() / 2 : 0;
$: maxTickValPerc = Math.max(...tickVals.map($yScale));
</script>
<div class='axis y-axis'>
{#each tickVals as tick, i (tick)}
{@const tickValPerc = $yScale(tick)}
<div class='tick tick-{i}' style='left:{$xRange[0]}%;top:{tickValPerc + halfBand}%;'>
{#if gridlines === true}
<div
class="gridline"
style="top:0;"
style:left='{x1}px'
style:right='0px'
></div>
{/if}
{#if tickMarks === true}
<div
class="tick-mark"
style:top='0'
style:left='{x1}px'
style:width='{tickLen}px'
></div>
{/if}
<div
class="text"
style:top='0'
style:text-align='{labelPosition === 'even' ? 'right' : 'left'}'
style:width='{widestTickLen}px'
style:left='{-widestTickLen - tickGutter - (labelPosition === 'even' ? tickLen : 0)}px'
style:transform='translate({dx + (labelPosition === 'even' ? -3 : 0)}px, calc(-50% + {dy + (labelPosition === 'above' || (snapBaselineLabel === true && tickValPerc === maxTickValPerc) ? -3 : 4)}px))'
>{format(tick)}</div>
</div>
{/each}
</div>
<style>
.axis,
.tick,
.tick-mark,
.gridline,
.baseline,
.text {
position: absolute;
}
.axis {
width: 100%;
height: 100%;
}
.tick {
font-size: 11px;
width: 100%;
}
.gridline {
border-top: 1px dashed #aaa;
}
.tick-mark {
border-top: 1px solid #aaa;
}
.baseline.gridline {
border-top-style: solid;
}
.tick .text {
color: #666;
}
</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}>
{#if min !== null}
<div class="brush-inner" on:mousedown|stopPropagation={move} on:touchstart|stopPropagation={move} style="left: {left}%; right: {right}%"></div>
<div class="brush-handle" on:mousedown|stopPropagation={adjust_min} on:touchstart|stopPropagation={adjust_min} style="left: {left}%"></div>
<div class="brush-handle" on:mousedown|stopPropagation={adjust_max} on:touchstart|stopPropagation={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