Synced brushEdit
1980
1990
2000
2010
0
5
1980
1990
2000
2010
0
10
1980
1990
2000
2010
0
10
1980
1990
2000
2010
0
10
- +page.svelte
- ./_components/SyncedBrushWrapper.percent-range.svelte
- ./_components/Line.svelte
- ./_components/Area.svelte
- ./_components/AxisX.percent-range.html.svelte
- ./_components/AxisY.percent-range.html.svelte
- ./_components/Brush.html.svelte
- ./_data/points.csv
- ./_data/pointsTwo.csv
- ./_data/pointsThree.csv
- ./_data/pointsFour.csv
<script>
import SyncedBrushWrapper from './_components/SyncedBrushWrapper.percent-range.svelte';
// This example loads csv data as json using @rollup/plugin-dsv
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="small-multiple-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>
/*
The wrapper div needs to have an explicit width and height in CSS.
It can also be a flexbox child or CSS grid element.
The point being it needs dimensions since the <LayerCake> element will
expand to fill it.
*/
.small-multiple-container {
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-content: space-between;
}
</style>
<script>
import { LayerCake, ScaledSvg, Html } from 'layercake';
import Line from './Line.svelte';
import Area from './Area.svelte';
import AxisX from './AxisX.percent-range.html.svelte';
import AxisY from './AxisY.percent-range.html.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
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={2} />
</Html>
<ScaledSvg>
<Line {stroke} />
<Area fill={`${stroke}10`} />
</ScaledSvg>
</LayerCake>
</div>
<div class="brush-container">
<LayerCake ssr percentRange padding={{ top: 5 }} x={xKey} y={yKey} yDomain={[0, null]} {data}>
<ScaledSvg>
<Line {stroke} />
<Area fill={`${stroke}10`} />
</ScaledSvg>
<Html>
<Brush bind:min bind:max />
</Html>
</LayerCake>
</div>
</div>
<style>
.chart-wrapper {
width: 48%;
height: 40%;
}
/*
The wrapper div needs to have an explicit width and height in CSS.
It can also be a flexbox child or CSS grid element.
The point being it needs dimensions since the <LayerCake> element will
expand to fill it.
*/
.chart-container {
width: 100%;
height: 80%;
}
.brush-container {
width: 100%;
height: 20%;
}
</style>
<!--
@component
Generates an SVG line shape.
-->
<script>
import { getContext } from 'svelte';
const { data, xGet, yGet } = getContext('LayerCake');
/** @type {String} [stroke='#ab00d6'] - The shape's fill color. This is technically optional because it comes with a default value but you'll likely want to replace it with your own color. */
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>
<!--
@component
Generates an SVG area shape.
-->
<script>
import { getContext } from 'svelte';
const { data, xGet, yGet, xScale, yScale, extents } = getContext('LayerCake');
/** @type {String} [fill='#ab00d610'] The shape's fill color. This is technically optional because it comes with a default value but you'll likely want to replace it with your own color. */
export let fill = '#ab00d610';
$: path =
'M' +
$data
.map(d => {
return $xGet(d) + ',' + $yGet(d);
})
.join('L');
/** @type {String} **/
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>
<!--
@component
Generates an HTML x-axis, useful for server-side rendered charts. This component is also configured to detect if your x-scale is an ordinal scale. If so, it will place the markers in the middle of the bandwidth.
Although this is marked as a percent-range component, you can also use it with a normal scale with no configuration needed. By default, if you have `percentRange={true}` it will use percentages, otherwise it will use pixels. This makes this component compatible with server-side and client-side rendered charts. Set the `units` prop to either `'%'` or `'px'` to override the default behavior.
-->
<script>
import { getContext } from 'svelte';
const { xScale, percentRange } = getContext('LayerCake');
/** @type {boolean} [tickMarks=false] - Show a vertical mark for each tick. */
export let tickMarks = false;
/** @type {boolean} [gridlines=true] - Show gridlines extending into the chart area. */
export let gridlines = true;
/** @type {Number} [tickMarkLength=6] - The length of the tick mark. */
export let tickMarkLength = 6;
/** @type {boolean} [baseline=false] – Show a solid line at the bottom. */
export let baseline = false;
/** @type {boolean} [snapLabels=false] - Instead of centering the text labels on the first and the last items, align them to the edges of the chart. */
export let snapLabels = false;
/** @type {(d: any) => string} [format=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */
export let format = d => d;
/** @type {Number|Array<any>|Function|undefined} [ticks] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. If nothing, it uses the default ticks supplied by the D3 function. */
export let ticks = undefined;
/** @type {Number} [tickGutter=0] - The amount of whitespace between the start of the tick and the chart drawing area (the yRange min). */
export let tickGutter = 0;
/** @type {Number} [dx=0] - Any optional value passed to the `dx` attribute on the text label. */
export let dx = 0;
/** @type {Number} [dy=0] - Any optional value passed to the `dy` attribute on the text label. */
export let dy = 0;
/** @type {String} units - Whether this component should use percentage or pixel values. If `percentRange={true}` it defaults to `'%'`. Options: `'%'` or `'px'`. */
export let units = $percentRange === true ? '%' : 'px';
$: tickLen = tickMarks === true ? tickMarkLength ?? 6 : 0;
$: isBandwidth = typeof $xScale.bandwidth === 'function';
/** @type {Array<any>} */
$: 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 tickValUnits = $xScale(tick)}
{#if baseline === true}
<div class="baseline" style="top:100%; width:100%;"></div>
{/if}
{#if gridlines === true}
<div class="gridline" style:left="{tickValUnits}{units}" style="top:0; bottom:0;"></div>
{/if}
{#if tickMarks === true}
<div
class="tick-mark"
style:left="{tickValUnits + halfBand}{units}"
style:height="{tickLen}px"
style:bottom="{-tickLen - tickGutter}px"
></div>
{/if}
<div
class="tick tick-{i}"
style:left="{tickValUnits + halfBand}{units}"
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%);
}
/* This looks a little better at 40 percent than 50 */
.axis.snapLabels .tick:last-child {
transform: translateX(-40%);
}
.axis.snapLabels .tick.tick-0 {
transform: translateX(40%);
}
</style>
<!--
@component
Generates an HTML y-axis. This component is also configured to detect if your y-scale is an ordinal scale. If so, it will place the tickMarks in the middle of the bandwidth.
Although this is marked as a percent-range component, you can also use it with a normal scale with no configuration needed. By default, if you have `percentRange={true}` it will use percentages, otherwise it will use pixels. This makes this component compatible with server-side and client-side rendered charts. Set the `units` prop to either `'%'` or `'px'` to override the default behavior.
-->
<script>
import { getContext } from 'svelte';
const { xRange, yScale, percentRange } = getContext('LayerCake');
/** @type {boolean} [tickMarks=false] - Show marks next to the tick label. */
export let tickMarks = false;
/** @type {String} [labelPosition='even'] - Whether the label sits even with its value ('even') or sits on top ('above') the tick mark. Default is 'even'. */
export let labelPosition = 'even';
/** @type {boolean} [snapBaselineLabel=false] - When labelPosition='even', adjust the lowest label so that it sits above the tick mark. */
export let snapBaselineLabel = false;
/** @type {boolean} [gridlines=true] - Show gridlines extending into the chart area. */
export let gridlines = true;
/** @type {Number|undefined} [tickMarkLength=undefined] - The length of the tick mark. If not set, becomes the length of the widest tick. */
export let tickMarkLength = undefined;
/** @type {(d: any) => string} [format=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */
export let format = d => d;
/** @type {Number|Array<any>|Function} [ticks=4] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. */
export let ticks = 4;
/** @type {Number} [tickGutter=0] - The amount of whitespace between the start of the tick and the chart drawing area (the xRange min). */
export let tickGutter = 0;
/** @type {Number} [dx=0] - Any optional value passed to the `dx` attribute on the text label. */
export let dx = 0;
/** @type {Number} [dy=-3] - Any optional value passed to the `dy` attribute on the text label. */
export let dy = -3;
/** @type {Number} [charPixelWidth=7.25] - Used to calculate the widest label length to offset labels. Adjust if the automatic tick length doesn't look right because you have a bigger font (or just set `tickMarkLength` to a pixel value). */
export let charPixelWidth = 7.25;
/** @type {String} units - Whether this component should use percentage or pixel values. If `percentRange={true}` it defaults to `'%'`. Options: `'%'` or `'px'`. */
export let units = $percentRange === true ? '%' : 'px';
$: isBandwidth = typeof $yScale.bandwidth === 'function';
/** @type {Array<any>} */
$: tickVals = Array.isArray(ticks)
? ticks
: isBandwidth
? $yScale.domain()
: typeof ticks === 'function'
? ticks($yScale.ticks())
: $yScale.ticks(ticks);
/** @param {Number} sum
* @param {String} val */
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;
$: maxTickValUnits = Math.max(...tickVals.map($yScale));
</script>
<div class="axis y-axis">
{#each tickVals as tick, i (tick)}
{@const tickValUnits = $yScale(tick)}
<div
class="tick tick-{i}"
style="left:{$xRange[0]}{units};top:{tickValUnits + halfBand}{units};"
>
{#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 && tickValUnits === maxTickValUnits)
? -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>
<!--
@component
Adds a brush component to create a range between 0 and 1. Bind to the `min` and `max` props to use them in other components. See the [brushable example](https://layercake.graphcics/example/Brush) for use.
-->
<script>
import { clamp } from 'yootils';
/** @type {Number} min - The brush's min value. Useful to bind to. */
export let min;
/** @type {Number} max - The brush's max value. Useful to bind to. */
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;
/* mix-blend-mode: difference; */
background-color: #cccccc90;
/* border: 1px solid #000; */
}
.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