<script>
import { LayerCake, Svg, WebGL, Html } from 'layercake';
import ScatterWebGL from './_components/Scatter.webgl.svelte';
import AxisX from './_components/AxisX.svelte';
import AxisY from './_components/AxisY.svelte';
import QuadTree from './_components/QuadTree.html.svelte';
import data from './_data/points.csv';
const xKey = 'myX';
const yKey = 'myY';
const r = 3;
const xyPadding = 6;
data.forEach(d => {
d[yKey] = +d[yKey];
});
</script>
<div class="chart-container">
<LayerCake
padding={{ top: 5, right: 5, bottom: 20, left: 25 }}
x={xKey}
y={yKey}
xPadding={[xyPadding, xyPadding]}
yPadding={[xyPadding, xyPadding]}
{data}
>
<Svg>
<AxisX />
<AxisY tickMarks={false} ticks={5} />
</Svg>
<WebGL>
<ScatterWebGL {r} />
</WebGL>
<Html>
<QuadTree let:x let:y let:visible>
<div
class="circle"
style="top:{y}px;left:{x}px;display: {visible ? 'block' : 'none'};"
></div>
</QuadTree>
</Html>
</LayerCake>
</div>
<style>
.chart-container {
width: 100%;
height: 250px;
}
.circle {
position: absolute;
border-radius: 50%;
background-color: rgba(171, 0, 214);
transform: translate(-50%, -50%);
pointer-events: none;
width: 10px;
height: 10px;
}
</style>
<script>
import reglWrapper from 'regl';
import { getContext } from 'svelte';
const { data, xGet, yGet, width, height } = getContext('LayerCake');
export let r = 5;
export let fill = '#0cf';
export let stroke = '#000';
function hexToRgbPercent(hex) {
let str = hex.replace('#', '');
if (str.length === 3) {
str = str[0] + str[0] + str[1] + str[1] + str[2] + str[2];
}
return str.match(/.{1,2}/g).map(d => parseInt(d, 16) / 255);
}
const { gl } = getContext('gl');
function resize() {
if ($gl) {
const canvas = $gl.canvas;
const displayWidth = canvas.clientWidth;
const displayHeight = canvas.clientHeight;
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
}
$gl.viewport(0, 0, canvas.width, canvas.height);
}
}
let regl;
function render() {
if ($gl) {
regl = reglWrapper({
gl: $gl,
extensions: ['oes_standard_derivatives']
});
regl.clear({
color: [0, 0, 0, 0],
depth: 1
});
const draw = regl({
frag: `
#extension GL_OES_standard_derivatives : enable
precision mediump float;
uniform vec3 fill_color;
uniform vec3 stroke_color;
varying float s_s;
void main () {
vec2 cxy = 2.0 * gl_PointCoord - 1.0;
float dist = dot(cxy, cxy);
float delta = fwidth(dist);
float alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, dist);
float outer_edge_center = 1.0 - s_s;
float stroke = 1.0 - smoothstep(outer_edge_center - delta, outer_edge_center + delta, dist);
// gl_FragColor = vec4(fill_color,1.0) * alpha;
gl_FragColor = vec4( mix(stroke_color, fill_color, stroke), 1.0 ) * alpha;
gl_FragColor.rgb *= gl_FragColor.a;
}`,
vert: `
precision mediump float;
attribute vec2 position;
attribute float r;
attribute float stroke_size;
varying float s_s;
uniform float stage_width;
uniform float stage_height;
// http://peterbeshai.com/beautifully-animate-points-with-webgl-and-regl.html
vec2 normalizeCoords(vec2 position) {
// read in the positions into x and y vars
float x = position[0];
float y = position[1];
return vec2(
2.0 * ((x / stage_width) - 0.5),
// invert y to treat [0,0] as bottom left in pixel space
-(2.0 * ((y / stage_height) - 0.5))
);
}
void main () {
s_s = stroke_size;
gl_PointSize = r;
gl_Position = vec4(normalizeCoords(position), 0.0, 1.0);
}`,
attributes: {
position: (context, props) => {
return props.points.map(point => {
return [$xGet(point), $yGet(point)];
});
},
r: (context, props) => {
return props.points.map(p => props.pointWidth);
},
stroke_size: (context, props) => {
return props.points.map(p => 0);
}
},
uniforms: {
fill_color: hexToRgbPercent(fill),
stroke_color: hexToRgbPercent(stroke),
stage_width: regl.context('drawingBufferWidth'),
stage_height: regl.context('drawingBufferHeight')
},
count: (context, props) => {
return props.points.length;
},
primitive: 'points',
blend: {
enable: true,
func: {
srcRGB: 'src alpha',
srcAlpha: 'src alpha',
dstRGB: 'one minus src alpha',
dstAlpha: 'one minus src alpha'
}
},
depth: { enable: false }
});
draw({
pointWidth: r * 2,
points: $data
});
}
}
$: $width, $height, $gl, resize(), render();
</script>
<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 { getContext } from 'svelte';
import { quadtree } from 'd3-quadtree';
const { data, xGet, yGet, width, height } = getContext('LayerCake');
let visible = false;
let found = {};
let e = {};
export let x = 'x';
export let y = 'y';
export let searchRadius = undefined;
export let dataset = undefined;
$: xGetter = x === 'x' ? $xGet : $yGet;
$: yGetter = y === 'y' ? $yGet : $xGet;
function findItem(evt) {
e = evt;
const xLayerKey = `layer${x.toUpperCase()}`;
const yLayerKey = `layer${y.toUpperCase()}`;
found = finder.find(evt[xLayerKey], evt[yLayerKey], searchRadius) || {};
visible = Object.keys(found).length > 0;
}
$: finder = quadtree()
.extent([
[-1, -1],
[$width + 1, $height + 1]
])
.x(xGetter)
.y(yGetter)
.addAll(dataset || $data);
</script>
<div
class="bg"
on:mousemove={findItem}
on:mouseout={() => (visible = false)}
on:blur={() => (visible = false)}
role="tooltip"
></div>
<slot x={xGetter(found) || 0} y={yGetter(found) || 0} {found} {visible} {e}></slot>
<style>
.bg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
</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