Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/extension/snippets/emd.code-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@
"body": ["<Histogram data={${1:query_name}} x=${2:column}/>\n\n"],
"description": "Insert Histogram"
},
"Vega-Lite Chart": {
"prefix": "/VegaLiteChart",
"body": [
"<VegaLiteChart\n\tdata={${1:query_name}}\n\tspec={${2:{\n\t\ttitle: 'My Vega-Lite Chart',\n\t\tmark: 'bar',\n\t\tencoding: {\n\t\t\tx: { field: '${3:x_column}', type: 'nominal' },\n\t\t\ty: { field: '${4:y_column}', type: 'quantitative' }\n\t\t}\n\t}}}\n/>\n\n"
],
"description": "Insert Vega-Lite Chart"
},
"Data Table": {
"prefix": "/DataTable",
"body": ["<DataTable data={${1:query_name}}/>\n\n"],
Expand Down
58 changes: 58 additions & 0 deletions packages/extension/src/props_list.json
Original file line number Diff line number Diff line change
Expand Up @@ -8454,6 +8454,64 @@
}
]
},
"VegaLiteChart": {
"props": [
{
"rank": 0,
"name": "data",
"description": "Query name, wrapped in curly braces",
"required": true,
"type": "string",
"options": "query name",
"defaultValue": null
},
{
"rank": 1,
"name": "spec",
"description": "Vega-Lite specification object literal",
"required": true,
"type": "string",
"options": "Vega-Lite spec",
"defaultValue": null
},
{
"rank": 2,
"name": "height",
"description": "Chart height in pixels",
"required": false,
"type": "string",
"options": "number",
"defaultValue": "291"
},
{
"rank": 3,
"name": "width",
"description": "Chart width (number or CSS size)",
"required": false,
"type": "string",
"options": "number | string",
"defaultValue": "\"100%\""
},
{
"rank": 4,
"name": "actions",
"description": "Toggle the Vega-Embed action menu (download PNG/SVG/JSON)",
"required": false,
"type": "boolean",
"options": "{['true','false']}",
"defaultValue": "true"
},
{
"rank": 5,
"name": "renderer",
"description": "Vega renderer to use (canvas or svg)",
"required": false,
"type": "string",
"options": "{['canvas','svg']}",
"defaultValue": null
}
]
},
"Value": {
"props": [
{
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/core-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
"ssf": "0.11.2",
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.1.20",
"vega": "^5.30.0",
"vega-embed": "^6.26.0",
"vega-lite": "^5.22.1",
"thememirror": "^2.0.1",
"tua-body-scroll-lock": "^1.5.0",
"yaml": "^2.4.2",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/core-components/src/lib/unsorted/viz/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ export { default as USMap } from './map/USMap.svelte';
export { default as Value } from './core/Value.svelte';
export { default as ValueError } from './core/ValueError.svelte';
export { default as VennDiagram } from './venn/VennDiagram.svelte';
export { default as VegaLiteChart } from './vega/VegaLiteChart.svelte';
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<script context="module">
export const evidenceInclude = true;
</script>

<script>
import { Query } from '@evidence-dev/sdk/usql';
import { QueryLoad } from '../../../atoms/query-load';

import EmptyChart from '../core/EmptyChart.svelte';
import ErrorChart from '../core/ErrorChart.svelte';
import VegaLiteRenderer from './VegaLiteRenderer.svelte';

export let data = undefined;
export let spec = undefined;

/** @type {number | string | undefined} */
export let height = 291;

/** @type {number | string | undefined} */
export let width = '100%';

export let actions = true;
export let renderer = undefined;
export let tooltip = undefined;
export let description = undefined;
export let embedOptions = undefined;

export let emptySet = undefined;
export let emptyMessage = undefined;

export let title = undefined;
export let subtitle = undefined;

export let chartType = 'Vega-Lite Chart';
export let skeletonClass = undefined;

const toRows = (value) => (Query.isQuery(value) ? Array.from(value) : value);

const skeletonHeight = () => (typeof height === 'number' ? height : 231);
</script>

<QueryLoad
{data}
height={skeletonHeight()}
skeletonClass={skeletonClass}
let:loaded
>
<EmptyChart
slot="empty"
{emptyMessage}
{emptySet}
chartType={chartType}
/>
<ErrorChart
slot="error"
title={chartType}
error={loaded?.error?.message ?? 'Unable to load Vega-Lite chart data'}
/>
{@const rows = toRows(loaded)}
<VegaLiteRenderer
data={rows}
{spec}
{height}
{width}
{actions}
{renderer}
{tooltip}
{description}
{embedOptions}
chartType={chartType}
{title}
{subtitle}
/>
<slot />
</QueryLoad>
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<script>
import { afterUpdate, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import vegaEmbed from 'vega-embed';

import ChartLoading from '../../ui/ChartLoading.svelte';
import ErrorChart from '../core/ErrorChart.svelte';

export let data = undefined;
export let spec = undefined;

/** @type {number | string | undefined} */
export let height = 291;

/** @type {number | string | undefined} */
export let width = '100%';

export let actions = true;
export let renderer = undefined;
export let tooltip = undefined;
export let description = undefined;
export let embedOptions = undefined;
export let chartType = 'Vega-Lite Chart';
export let title = undefined;
export let subtitle = undefined;

let container;
let embedResult;
let renderError;
let pendingRender = false;
let renderToken = 0;

const heightCss = () =>
height === undefined ? '291px' : typeof height === 'number' ? `${height}px` : height;

const widthCss = () =>
width === undefined ? '100%' : typeof width === 'number' ? `${width}px` : width;

const cloneSpec = (input) => {
if (!input) return undefined;
if (typeof structuredClone === 'function') {
return structuredClone(input);
}
return JSON.parse(JSON.stringify(input));
};

async function renderChart() {
if (!browser || !container || !spec) return;
const specCopy = cloneSpec(spec);
renderError = undefined;

if (specCopy) {
if (title && specCopy.title === undefined) {
specCopy.title = title;
}

if (subtitle && specCopy.subtitle === undefined) {
specCopy.subtitle = subtitle;
}

if (!specCopy.data && data !== undefined) {
specCopy.data = { values: data };
}

if (specCopy.height === undefined && typeof height === 'number') {
specCopy.height = height;
}

if (specCopy.width === undefined && typeof width === 'number') {
specCopy.width = width;
}
}

const options = {
actions,
renderer,
tooltip,
description,
...(embedOptions ?? {})
};

const token = ++renderToken;
try {
embedResult?.view?.finalize();
const result = await vegaEmbed(container, specCopy, options);
if (token !== renderToken) {
result.view.finalize();
return;
}
embedResult = result;
} catch (err) {
console.error('Failed to render VegaLite chart', err);
renderError = err;
} finally {
pendingRender = false;
}
}

function scheduleRender() {
if (!browser || !container || !spec) return;
if (pendingRender) return;
pendingRender = true;
}

$: spec, scheduleRender();
$: data, scheduleRender();
$: actions, scheduleRender();
$: renderer, scheduleRender();
$: tooltip, scheduleRender();
$: description, scheduleRender();
$: embedOptions, scheduleRender();
$: height, scheduleRender();
$: width, scheduleRender();
$: container, scheduleRender();

afterUpdate(() => {
if (!pendingRender) return;
renderChart();
});

onDestroy(() => {
embedResult?.view?.finalize();
});
</script>

{#if !spec}
<ErrorChart title={chartType} error="VegaLiteChart requires a spec prop" />
{:else}
<div class="chart-container">
{#if !browser}
<ChartLoading height={heightCss()} />
{:else if renderError}
<ErrorChart title={chartType} error={renderError?.message ?? renderError} />
{:else}
<div
class="vega-chart"
bind:this={container}
style={`height:${heightCss()};width:${widthCss()};`}
/>
{/if}
</div>
{/if}

<style>
.chart-container {
width: 100%;
}

.vega-chart {
overflow: visible;
}
</style>
Loading
Loading