import LayerCake from 'layercake';
import { scaleBand } from 'd3-scale';
import groups from './data/groups.js';
import Column from './components/Column.html';
import AxisX from './components/AxisXScaleBand.html';
import AxisY from './components/AxisY.html';
import Annotations from './components/Annotations.html';
import Arrows from './components/Arrows.html';
import DefArrowhead from './components/DefArrowhead.html';
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%'
}
}]
}
];
const myCake = new LayerCake({
padding: { top: 0, right: 0, bottom: 20, left: 20 },
x: 'year',
y: 'value',
xScale: scaleBand().paddingInner([0.02]).round(true),
xDomain: ['1979', '1980', '1981', '1982', '1983'],
yDomain: [0, null],
data: groups,
target: document.getElementById('my-chart')
})
.svgLayers([
{ component: AxisX, opts: { gridlines: false } },
{ component: AxisY, opts: { gridlines: false } },
{ component: Column, opts: {} }
])
.htmlLayers([
{ component: Annotations, opts: { annotations } }
])
.svgLayers([
{ component: Arrows, opts: { annotations } }
], { defs: DefArrowhead });
myCake.render();
{#each $data as d, i}
<rect class='group-rect' data-id="{i}" x="{$xGet(d)}" y="{$yGet(d)}" {width} height="{height(d)}"
fill="{opts.fill || '#00e047'}" stroke="{opts.stroke || ''}" stroke-width="{opts['stroke-width'] || 0}"></rect>
{/each}
<script>
export default {
namespace: 'svg',
computed: {
width: ({ $xScale }) => {
return $xScale.bandwidth();
},
height: ({ $height, $yGet }) => {
return (d) => {
return $height - $yGet(d);
};
}
}
};
</script>
<g class='axis x-axis'>
{#each $xScale.domain() as tick}
<g class='tick tick-{ tick }' transform='translate({$xScale(tick)},{$yScale.range()[0]})'>
{#if opts.gridlines !== false}
<line y1='{$height * -1}' y2='0' x1='0' x2='0'></line>
{/if}
<text y='16' x="{$xScale.bandwidth() / 2}">{opts.formatTick ? opts.formatTick(tick) : tick}</text>
</g>
{/each}
</g>
<style>
.tick {
font-size: .725em;
font-weight: 200;
}
.tick line {
stroke: #aaa;
stroke-dasharray: 2;
}
.tick text {
fill: #666;
text-anchor: start;
}
.tick.tick-0 line {
stroke-dasharray: 0;
}
.x-axis .tick text {
text-anchor: middle;
}
</style>
<script>
export default {
namespace: 'svg'
};
</script>
<g class='axis y-axis' transform='translate(-{$padding.left}, 0)'>
{#each $yScale.ticks(opts.ticks || opts.tickNumber || 5) as tick, i}
<g class='tick tick-{tick}' transform='translate(0, {$yScale(tick)})'>
{#if opts.gridlines !== false}
<line x2='100%'></line>
{/if}
<text y='-4'>{opts.formatTick ? opts.formatTick(tick) : tick}</text>
</g>
{/each}
</g>
<style>
.tick {
font-size: .725em;
font-weight: 200;
}
.tick line {
stroke: #aaa;
stroke-dasharray: 2;
}
.tick text {
fill: #666;
text-anchor: start;
}
.tick.tick-0 line {
stroke-dasharray: 0;
}
</style>
<script>
export default {
namespace: 'svg'
};
</script>
{#each opts.annotations as d, i}
<div class="layercake-annotation" data-id="{i}" style="{fillStyle(d)}">{d.text}</div>
{/each}
<style>
.layercake-annotation {
position: absolute;
}
</style>
<script>
const vals = ['top', 'right', 'bottom', 'left'];
export default {
onupdate () {
this.store.set({ annotationEls: document.querySelectorAll('.layercake-annotation') });
},
computed: {
fillStyle: ({ $width, $height }) => {
return d => {
let style = '';
vals.forEach(val => {
if (d[val]) {
style += `${val}:${d[val]};`;
}
});
return style;
};
}
}
};
</script>
{#if opts.annotations}
<g class="swoops">
{#each opts.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}
<style>
.swoops {
position: absolute;
max-width: 200px;
line-height: 14px;
}
.swoops path {
fill: none;
stroke: #000;
stroke-width: 1;
}
</style>
<script>
import swoopyArrow from '../modules/swoopyArrow.js';
import getElPosition from '../modules/getElPosition.js';
import parseCssValue from '../modules/parseCssValue.js';
const lookups = [
{ dimension: 'width', css: 'left', position: 'x' },
{ dimension: 'height', css: 'top', position: 'y' }
];
export default {
namespace: 'svg',
computed: {
d: ({ $width, $height, opts, $annotationEls }) => {
return (anno, i, arrow) => {
if ($annotationEls) {
const el = $annotationEls[i];
const arrowSource = getElPosition(el);
const sourceCoords = arrow.source.anchor.split('-').map((d, i) => {
const point = d === 'middle' ? arrowSource[lookups[i].css] + (arrowSource[lookups[i].dimension] / 2) : arrowSource[d];
return point + (parseCssValue(arrow.source[`d${lookups[i].position}`], i, arrowSource.width, arrowSource.height));
});
const clockwise = typeof arrow.clockwise === 'undefined' ? true : arrow.clockwise;
const targetCoords = [arrow.target.x, arrow.target.y].map((d, i) => {
return parseCssValue(d, i, $width, $height);
});
const d = swoopyArrow()
.angle(Math.PI / 2)
.clockwise(clockwise)
.x(d => d[0])
.y(d => d[1])([sourceCoords, targetCoords]);
return d;
}
return '';
};
}
}
};
</script>
<defs>
<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="{opts.fill || '#000'}"/>
</marker>
</defs>
<script>
export default {
namespace: 'svg'
};
</script>
export default [
{
year: '1979',
value: 2
},
{
year: '1980',
value: 3
},
{
year: '1981',
value: 5
},
{
year: '1982',
value: 8
},
{
year: '1983',
value: 18
}
];
export default function swoopyArrow () {
let angle = Math.PI;
let clockwise = true;
let xValue = function(d) { return d[0]; };
let yValue = function(d) { return d[1]; };
function render(data) {
data = data.map(function(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;
}
function hypotenuse(a, b) {
return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
render.angle = function(_) {
if (!arguments.length) return angle;
angle = Math.min(Math.max(_, 1e-6), Math.PI - 1e-6);
return render;
};
render.clockwise = function(_) {
if (!arguments.length) return clockwise;
clockwise = !!_;
return render;
};
render.x = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return render;
};
render.y = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return render;
};
return render;
}
export default 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 default 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', '');
}