Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add TileMap component #565

Draft
wants to merge 10 commits into
base: dev
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/tile-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ldn-viz/charts': minor
---

ADDED: added `TileMap` component
2 changes: 2 additions & 0 deletions packages/charts/src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export { default as Footer } from './chartContainer/Footer.svelte';
export { default as SubTitle } from './chartContainer/SubTitle.svelte';
export { default as Title } from './chartContainer/Title.svelte';

export { default as TileMap } from './tileMap/TileMap.svelte';

export { default as ObservablePlot } from './observablePlot/ObservablePlot.svelte';

export * from './observablePlotFragments/observablePlotFragments';
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
} from '../../data/demoData';

import DemoTooltip from './DemoTooltip.svelte';
import { addEventHandler, registerTooltip } from './ObservablePlot.svelte';
import { addEventHandler, registerTooltip } from './ObservablePlotInner.svelte';
import type { Position } from './types';

$: ({
Expand Down
132 changes: 4 additions & 128 deletions packages/charts/src/lib/observablePlot/ObservablePlot.svelte
Original file line number Diff line number Diff line change
@@ -1,89 +1,9 @@
<script context="module" lang="ts">
/**
* The `ObservablePlot` component allows the rendering of visualisations using the [Observable Plot](https://observablehq.com/plot/) library, wrapped in a [ChartContainer](./?path=/docs/charts-chartcontainer--documentation) wrapper.
* @component
*/

import type {
AddEventHandlerFunction,
AddEventHandlerInnerFunction,
Position,
RegisterTooltipFunction
} from './types.ts';

export const registerTooltip: RegisterTooltipFunction =
(posStore, markShape = 'circle') =>
(index, scales, values, dimensions, context, next) => {
const el = next && next(index, scales, values, dimensions, context);
const marks = el?.querySelectorAll(markShape) || [];

addEventHandlerInner(
'mouseenter',
(ev: MouseEvent, d: any) => {
posStore.set({
...d,
clientX: ev.clientX,
clientY: ev.clientY,
pageX: ev.pageX,
pageY: ev.pageY,
layerX: ev.layerX,
layerY: ev.layerY
}); // can't use the $store syntax here
},
marks,
values,
index
);

addEventHandlerInner(
'mouseleave',
() => {
posStore.set(undefined); // can't use the $store syntax here
},
marks,
values,
index
);

return el ?? null;
};

export const addEventHandler: AddEventHandlerFunction =
(eventName, eventHandler, markShape = 'circle') =>
(index, scales, values, dimensions, context, next) => {
const el = next && next(index, scales, values, dimensions, context);
const marks = el?.querySelectorAll(markShape) || [];

addEventHandlerInner(eventName, eventHandler, marks, values, index);

return el ?? null;
};

const addEventHandlerInner: AddEventHandlerInnerFunction = (
eventName,
eventHandler,
marks,
values,
index
) => {
for (let i = 0; i < marks.length; i++) {
const d = {
index: index[i],
x: values.channels.x.value[i],
y: values.channels.y.value[i]
};

marks[i].addEventListener(eventName, (ev: any) => eventHandler(ev, d));
}
};
</script>

<script lang="ts">
import { afterUpdate, onMount, setContext } from 'svelte';
import { derived, writable } from 'svelte/store';
import type { Position } from './types.ts';

import * as Plot from '@observablehq/plot';
import ChartContainer from '../chartContainer/ChartContainer.svelte';
import ObservablePlotInner from './ObservablePlotInner.svelte';
import { writable } from 'svelte/store';

/**
* The Observable Plot specification for the visualization.
Expand Down Expand Up @@ -161,33 +81,6 @@

/** A y-offset between data points and tooltips (pixels). */
export let tooltipOffset = -16;

const renderPlot = (node: HTMLDivElement) => {
node.appendChild(Plot.plot(spec));
};

let width: number;

onMount(() => {
updateDimensions();
window.addEventListener('resize', updateDimensions);
return () => {
window.removeEventListener('resize', updateDimensions);
};
});

afterUpdate(() => {
updateDimensions();
});

const updateDimensions = () => {
spec.width = width;
};

const tooltipData = derived(tooltipStore, ($tooltipStore) =>
$tooltipStore ? data[$tooltipStore.index] : undefined
);
setContext('tooltipData', tooltipData);
</script>

{#key spec}
Expand All @@ -205,24 +98,7 @@
chartHeight={'h-fit'}
{chartWidth}
>
<div use:renderPlot {...$$restProps} bind:this={domNode} bind:clientWidth={width} />

<!-- IMPORTANT TODO: data prop and exportData prop for buttons - align usage-->
{#if $tooltipStore && $tooltipData}
<div
class="absolute max-w-[200px] text-sm p-2 bg-color-container-level-1 shadow z-50 -translate-x-1/2 -translate-y-full"
style:top={`${$tooltipStore.layerY + tooltipOffset}px`}
style:left={`${$tooltipStore.layerX}px`}
>
<slot name="tooltip">
<pre>{JSON.stringify(data[$tooltipStore.index], null, 2)}</pre>
</slot>

<div
class="absolute bg-color-container-level-1 rotate-45 w-4 h-4 -translate-x-1/2 inset-x-1/2"
/>
</div>
{/if}
<ObservablePlotInner {data} {domNode} {tooltipStore} {tooltipOffset} {spec} />
</ChartContainer>
{/key}

Expand Down
156 changes: 156 additions & 0 deletions packages/charts/src/lib/observablePlot/ObservablePlotInner.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<script context="module" lang="ts">
/**
* The `ObservablePlot` component allows the rendering of visualisations using the [Observable Plot](https://observablehq.com/plot/) library, wrapped in a [ChartContainer](./?path=/docs/charts-chartcontainer--documentation) wrapper.
* @component
*/

import type {
AddEventHandlerFunction,
AddEventHandlerInnerFunction,
Position,
RegisterTooltipFunction
} from './types.ts';

export const registerTooltip: RegisterTooltipFunction =
(posStore, markShape = 'circle') =>
(index, scales, values, dimensions, context, next) => {
const el = next && next(index, scales, values, dimensions, context);
const marks = el?.querySelectorAll(markShape) || [];

addEventHandlerInner(
'mouseenter',
(ev: MouseEvent, d: any) => {
posStore.set({
...d,
clientX: ev.clientX,
clientY: ev.clientY,
pageX: ev.pageX,
pageY: ev.pageY,
layerX: ev.layerX,
layerY: ev.layerY
}); // can't use the $store syntax here
},
marks,
values,
index
);

addEventHandlerInner(
'mouseleave',
() => {
posStore.set(undefined); // can't use the $store syntax here
},
marks,
values,
index
);

return el ?? null;
};

export const addEventHandler: AddEventHandlerFunction =
(eventName, eventHandler, markShape = 'circle') =>
(index, scales, values, dimensions, context, next) => {
const el = next && next(index, scales, values, dimensions, context);
const marks = el?.querySelectorAll(markShape) || [];

addEventHandlerInner(eventName, eventHandler, marks, values, index);

return el ?? null;
};

const addEventHandlerInner: AddEventHandlerInnerFunction = (
eventName,
eventHandler,
marks,
values,
index
) => {
for (let i = 0; i < marks.length; i++) {
const d = {
index: index[i],
x: values.channels.x.value[i],
y: values.channels.y.value[i]
};

marks[i].addEventListener(eventName, (ev: any) => eventHandler(ev, d));
}
};
</script>

<script lang="ts">
import * as Plot from '@observablehq/plot';

import { afterUpdate, onMount, setContext } from 'svelte';
import { derived, writable } from 'svelte/store';

/**
* The Observable Plot specification for the visualization.
*/
export let spec;

/**
* Data being visualized (as an array of objects), to be used by data download button.
*/
export let data: { [key: string]: any }[] = [];

/**
* Provides a way to access the DOM node into which the visualization is rendered.
*/
export let domNode: any = undefined;

/**
* A store that stores details of the moused-over point.
* Used for custom tooltips.
*/
export let tooltipStore = writable<Position>();

/** A y-offset between data points and tooltips (pixels). */
export let tooltipOffset = -16;

const renderPlot = (node: HTMLDivElement) => {
node.appendChild(Plot.plot(spec));
};

let width: number;

onMount(() => {
updateDimensions();
window.addEventListener('resize', updateDimensions);
return () => {
window.removeEventListener('resize', updateDimensions);
};
});

afterUpdate(() => {
updateDimensions();
});

const updateDimensions = () => {
spec.width = width;
};

const tooltipData = derived(tooltipStore, ($tooltipStore) =>
$tooltipStore ? data[$tooltipStore.index] : undefined
);
setContext('tooltipData', tooltipData);
</script>

<div use:renderPlot {...$$restProps} bind:this={domNode} bind:clientWidth={width} />

<!-- IMPORTANT TODO: data prop and exportData prop for buttons - align usage-->
{#if $tooltipStore && $tooltipData}
<div
class="absolute max-w-[200px] text-sm p-2 bg-color-container-level-1 shadow z-50 -translate-x-1/2 -translate-y-full"
style:top={`${$tooltipStore.layerY + tooltipOffset}px`}
style:left={`${$tooltipStore.layerX}px`}
>
<slot name="tooltip">
<pre>{JSON.stringify(data[$tooltipStore.index], null, 2)}</pre>
</slot>

<div
class="absolute bg-color-container-level-1 rotate-45 w-4 h-4 -translate-x-1/2 inset-x-1/2"
/>
</div>
{/if}
33 changes: 33 additions & 0 deletions packages/charts/src/lib/tileMap/Tile.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
import ObservablePlotInner from '../observablePlot/ObservablePlotInner.svelte';

/**
* Data for this specific tile.
*/
export let data;

/**
* Contextual data that can be used as a comparison in each tile.
*/
export let contextData;

/**
* Function which receives the `data` and `contextData` as arguments, and returns an Observable Plot spec.
*/
export let specFn;

/**
* Label to be displayed at the top of each tile.
*/
export let label = '';

/**
* Function that will be called (passing the geography id as an argument) when the user clicks on a tile.
*/
export let onClick = () => undefined;
</script>

<div style="margin-top: auto;" class="overflow-y-auto mb-0 ml-2 mr-2" on:click={onClick}>
<h3 class="text-lg font-bold">{label}</h3>
<ObservablePlotInner {data} spec={specFn(data, contextData)} />
</div>
Loading