<script>
import { LayerCake, Svg, ScaledSvg, Html } from 'layercake';
import { scaleBand } from 'd3-scale';
import Column from './_components/Column.svelte';
import AxisX from './_components/AxisX.html.svelte';
import AxisY from './_components/AxisY.html.svelte';
import Annotations from './_components/Annotations.html.svelte';
import Arrows from './_components/Arrows.svelte';
import ArrowheadMarker from './_components/ArrowheadMarker.svelte';
import data from './_data/groups.csv';
const xKey = 'year';
const yKey = 'value';
const annotations = [
{
text: 'Example text...',
top: '18%',
left: '30%',
arrows: [{
clockwise: false,
source: {
anchor: 'left-bottom',
dx: -2,
dy: -7
},
target: {
x: '28%',
y: '75%'
}
},
{
source: {
anchor: 'right-bottom',
dy: -7,
dx: 5
},
target: {
x: '68%',
y: '48%'
}
}]
}
];
data.forEach(d => {
d[yKey] = +d[yKey];
});
</script>
<style>
.chart-container {
width: 100%;
height: 400px;
position: relative;
}
</style>
<div class="chart-container">
<LayerCake
ssr={true}
percentRange={true}
position='absolute'
padding={{ top: 0, right: 0, bottom: 20, left: 20 }}
x={xKey}
y={yKey}
xScale={scaleBand().paddingInner(0.028).round(true)}
xDomain={['1979', '1980', '1981', '1982', '1983']}
yDomain={[0, null]}
data={data}
>
<ScaledSvg>
<Column/>
</ScaledSvg>
<Html>
<AxisX
gridlines={false}
/>
<AxisY
ticks={4}
gridlines={false}
/>
<Annotations {annotations}/>
</Html>
</LayerCake>
<LayerCake
position='absolute'
padding={{ top: 0, right: 0, bottom: 20, left: 20 }}
x={xKey}
y={yKey}
xScale={scaleBand().paddingInner(0.028).round(true)}
xDomain={['1979', '1980', '1981', '1982', '1983']}
yDomain={[0, null]}
data={data}
>
<Svg>
<svelte:fragment slot="defs">
<ArrowheadMarker/>
</svelte:fragment>
<Arrows {annotations}/>
</Svg>
</LayerCake>
</div>
<script>
import { getContext } from 'svelte';
const { data, xGet, yGet, x, yRange, xScale, y, height, zGet, zScale, z } = getContext('LayerCake');
export let fill = '#00e047';
export let stroke = '#000';
export let strokeWidth = 0;
export let showLabels = false;
$: columnWidth = d => {
const vals = $xGet(d);
return Math.abs(vals[1] - vals[0]);
};
$: columnHeight = d => {
return $yRange[0] - $yGet(d);
};
</script>
<g class="column-group">
{#each $data as d, i}
{@const colHeight = columnHeight(d)}
{@const xGot = $xGet(d)}
{@const xPos = Array.isArray(xGot) ? xGot[0] : xGot}
{@const colWidth = $xScale.bandwidth ? $xScale.bandwidth() : columnWidth(d)}
{@const yValue = $y(d)}
<rect
class='group-rect'
data-id="{i}"
data-range="{$x(d)}"
data-count="{yValue}"
x="{xPos}"
y="{$yGet(d)}"
width="{colWidth}"
height="{colHeight}"
{fill}
{stroke}
stroke-width="{strokeWidth}"
/>
{#if showLabels && yValue}
<text x="{xPos + colWidth / 2}" y="{$height - colHeight - 5}" text-anchor="middle">{yValue}</text>
{/if}
{/each}
</g>
<style>
text {
font-size: 12px;
}
</style>
<script>
import { getContext } from 'svelte';
const { xScale } = getContext('LayerCake');
export let gridlines = true;
export let tickMarks = false;
export let baseline = false;
export let snapTicks = false;
export let formatTick = d => d;
export let ticks = undefined;
export let yTick = 7;
$: isBandwidth = typeof $xScale.bandwidth === 'function';
$: tickVals = Array.isArray(ticks) ? ticks :
isBandwidth ?
$xScale.domain() :
typeof ticks === 'function' ?
ticks($xScale.ticks()) :
$xScale.ticks(ticks);
</script>
<div class='axis x-axis' class:snapTicks>
{#each tickVals as tick, i (tick)}
{#if gridlines !== false}
<div class="gridline" style='left:{$xScale(tick)}%;top: 0px;bottom: 0;'></div>
{/if}
{#if tickMarks === true}
<div class="tick-mark" style='left:{$xScale(tick) + (isBandwidth ? $xScale.bandwidth() / 2 : 0)}%;height:6px;bottom: -6px;'></div>
{/if}
<div
class='tick tick-{ i }'
style='left:{$xScale(tick) + (isBandwidth ? $xScale.bandwidth() / 2 : 0)}%;top:100%;'>
<div
class="text"
style='top:{(yTick)}px;'>{formatTick(tick)}</div>
</div>
{/each}
{#if baseline === true}
<div class="baseline" style='top: 100%;width: 100%;'></div>
{/if}
</div>
<style>
.axis,
.tick,
.tick-mark,
.gridline,
.baseline {
position: absolute;
}
.axis {
width: 100%;
height: 100%;
}
.tick {
font-size: .725em;
font-weight: 200;
}
.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.snapTicks .tick:last-child {
transform: translateX(-40%);
}
.axis.snapTicks .tick.tick-0 {
transform: translateX(40%);
}
</style>
<script>
import { getContext } from 'svelte';
const { padding, xRange, yScale } = getContext('LayerCake');
export let gridlines = true;
export let tickMarks = false;
export let baseline = false;
export let formatTick = d => d;
export let ticks = 4;
export let xTick = -4;
export let yTick = -1;
$: isBandwidth = typeof $yScale.bandwidth === 'function';
$: tickVals = Array.isArray(ticks) ? ticks :
isBandwidth ?
$yScale.domain() :
typeof ticks === 'function' ?
ticks($yScale.ticks()) :
$yScale.ticks(ticks);
</script>
<div class='axis y-axis' style='transform:translate(-{$padding.left}px, 0)'>
{#each tickVals as tick, i (tick)}
<div class='tick tick-{i}' style='top:{$yScale(tick) + (isBandwidth ? $yScale.bandwidth () / 2 : 0)}%;left:{$xRange[0]}%;'>
{#if gridlines !== false}
<div class="gridline" style='top:0;left:{isBandwidth ? $padding.left : 0}px;right:-{$padding.left + $padding.right}px;'></div>
{/if}
{#if baseline !== false && i === 0}
<div class="gridline baseline" style='top:0;left:{isBandwidth ? $padding.left : 0};right:-{$padding.left + $padding.right}px;'></div>
{/if}
{#if tickMarks === true}
<div class="tick-mark" style='top:0;left:{isBandwidth ? $padding.left - 6 : 0}px;width:6px;'></div>
{/if}
<div
class="text"
style='
top:{yTick}px;
left:{isBandwidth ? ($padding.left + xTick - 4) : 0}px;
transform: translate({isBandwidth ? '-100%' : 0}, {isBandwidth ? -50 - Math.floor($yScale.bandwidth() / -2) : '-100'}%);
'
>{formatTick(tick)}</div>
</div>
{/each}
</div>
<style>
.axis,
.tick,
.tick-mark,
.gridline,
.baseline,
.text {
position: absolute;
}
.axis {
width: 100%;
height: 100%;
}
.tick {
font-size: 12px;
width: 100%;
font-weight: 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>
const vals = ['top', 'right', 'bottom', 'left'];
export let annotations = [];
export let getText = d => d.text;
$: fillStyle = d => {
let style = '';
vals.forEach(val => {
if (d[val]) {
style += `${val}:${d[val]};`;
}
});
return style;
};
</script>
<div class="layercake-annotations">
{#each annotations as d, i}
<div
class="layercake-annotation"
data-id="{i}"
style="{fillStyle(d)}"
>{getText(d)}</div>
{/each}
</div>
<style>
.layercake-annotation {
position: absolute;
}
</style>
<script>
import { getContext, onMount } from 'svelte';
import { swoopyArrow, getElPosition, parseCssValue } from '../_modules/arrowUtils.js';
export let annotations = [];
export let containerClass = '.chart-container';
export let annotationClass = '.layercake-annotation';
let container;
const { width, height } = getContext('LayerCake');
const lookups = [
{ dimension: 'width', css: 'left', position: 'x' },
{ dimension: 'height', css: 'top', position: 'y' }
];
let d = (anno, i, arrow) => '';
let annotationEls;
onMount(() => {
annotationEls = Array.from(
container.closest(containerClass)
.querySelectorAll(annotationClass)
);
});
function setPath (w, h) {
return (anno, i, arrow) => {
const el = annotationEls[i];
const arrowSource = getElPosition(el);
const sourceCoords = arrow.source.anchor.split('-').map((q, j) => {
const point = q === 'middle' ? arrowSource[lookups[j].css] + (arrowSource[lookups[j].dimension] / 2) : arrowSource[q];
return point + (parseCssValue(arrow.source[`d${lookups[j].position}`], i, arrowSource.width, arrowSource.height));
});
const clockwise = typeof arrow.clockwise === 'undefined' ? true : arrow.clockwise;
const targetCoords = [arrow.target.x, arrow.target.y].map((q, j) => {
return parseCssValue(q, j, w, h);
});
return swoopyArrow()
.angle(Math.PI / 2)
.clockwise(clockwise)
.x(q => q[0])
.y(q => q[1])([sourceCoords, targetCoords]);
};
}
$: if (annotationEls && annotationEls.length) d = setPath($width, $height);
</script>
<g bind:this={container}>
{#if annotations.length}
<g class="swoops">
{#each annotations as anno, i}
{#if anno.arrows}
{#each anno.arrows as arrow}
<path
marker-end='url(#arrowhead)'
d='{d(anno, i, arrow)}'></path>
{/each}
{/if}
{/each}
</g>
{/if}
</g>
<style>
.swoops {
position: absolute;
max-width: 200px;
line-height: 14px;
}
.swoops path {
fill: none;
stroke: #000;
stroke-width: 1;
}
</style>
<script>
export let fill = '#000';
export let stroke = '#000';
</script>
<marker id="arrowhead" viewBox="-10 -10 20 20" markerWidth="17" markerHeight="17" orient="auto">
<path d="M-6,-6 L 0,0 L -6,6" {fill} {stroke}/>
</marker>
export function parseCssValue(d, i, width, height) {
if (!d) return 0;
if (typeof d === 'number') {
return d;
}
if (d.indexOf('%') > -1) {
return ((+d.replace('%', '')) / 100) * (i ? height : width);
}
return +d.replace('px', '');
}
export function getElPosition(el) {
const annotationBbox = el.getBoundingClientRect();
const parentBbox = el.parentNode.getBoundingClientRect();
const coords = {
top: annotationBbox.top - parentBbox.top,
right: annotationBbox.right - parentBbox.left,
bottom: annotationBbox.bottom - parentBbox.top,
left: annotationBbox.left - parentBbox.left,
width: annotationBbox.width,
height: annotationBbox.height
};
return coords;
}
export function swoopyArrow() {
let angle = Math.PI;
let clockwise = true;
let xValue = d => d[0];
let yValue = d => d[1];
function hypotenuse(a, b) {
return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
function render(data) {
data = data.map((d, i) => {
return [xValue.call(data, d, i), yValue.call(data, d, i)];
});
const h = hypotenuse(data[1][0] - data[0][0], data[1][1] - data[0][1]);
const d = h / (2 * Math.tan(angle / 2));
const r = hypotenuse(d, h / 2);
const path = 'M ' + data[0][0] + ',' + data[0][1] +
' a ' + r + ',' + r +
' 0 0,' + (clockwise ? '1' : '0') + ' ' +
(data[1][0] - data[0][0]) + ',' + (data[1][1] - data[0][1]);
return path;
}
render.angle = function renderAngle (_) {
if (!arguments.length) return angle;
angle = Math.min(Math.max(_, 1e-6), Math.PI - 1e-6);
return render;
};
render.clockwise = function renderClockwise (_) {
if (!arguments.length) return clockwise;
clockwise = !!_;
return render;
};
render.x = function renderX (_) {
if (!arguments.length) return xValue;
xValue = _;
return render;
};
render.y = function renderY (_) {
if (!arguments.length) return yValue;
yValue = _;
return render;
};
return render;
}
year,value
1979,2
1980,3
1981,5
1982,8
1983,18