Skip to content
Merged
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Imports point one way only:
- One shared worker keeps tables loaded by key across navigations. Don't `new ArrowProcessingWorkerManager()` per page or `terminate()` the shared instance.
- Avoid blocking the main thread with large Arrow transforms.
- Preserve guards like `isLoading` / `firstLoad` around query execution.
- A canvas path holds about 150,000 arcs. Above that the browser drops the fill and reports no error, so the plot draws blank. `fillPointChunks` in `plots/uplot-render.ts` flushes the path every 10,000 arcs, and it also draws faster than one large path. Draw every point set through it. A palette bucket needs the same flush: the buckets divide the Z range, not the row count, so one bucket can hold every row.
- `QueryWorkspace.blocks` gets a new array, with new block objects, on every write to the block collection — including `markBlockRun`/`markBlockRunning` and any draft update. Do not read `workspace.activeBlock` (or a query object derived from it) directly inside an `$effect`. That makes the effect re-fire after its own write, in a loop that never stops. Track primitive values instead (block id, a stringified compiled query) and read the live block/query with `untrack`. See `src/routes/visualisations/table-explorer/+page.svelte` for the pattern.

## Editing Guidance for Agents
Expand Down
8 changes: 8 additions & 0 deletions src/lib/beacon-api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Utils } from '@/utils';
import { addToast } from '@/stores/toasts';
import { BeaconClient as BeaconSdkClient } from '@beacon/client';
import { normalizeUrl, splitNodeUrl } from '@/services/beacon-node-url';
import { PUBLIC_NODE_TABLES } from '@/services/open-nodes';

import {
isAbortError,
Expand Down Expand Up @@ -300,6 +301,13 @@ export class BeaconClient {
async getTables(): Promise<Array<string>> {
const url = this.buildUrl('/api/tables');
const response: Array<string> = await this.fetch(url);

// Temporary fix: a public demo node shows only its curated tables.
const allowedTables = PUBLIC_NODE_TABLES[this.baseUrl];
if (allowedTables) {
return response.filter((table) => allowedTables.includes(table));
Comment thread
Jasper-Maris marked this conversation as resolved.
}

return response;
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/home/HowItWorks.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@

<div class="section-head">
<h2>How it works</h2>
<p class="lead">Three steps from an empty query to a downloaded file</p>
<p class="lead">Three steps from an empty query to your subset.</p>
<Button variant="link" onclick={toggle}>{open ? 'Hide guide' : 'Show guide'}</Button>
</div>

Expand Down Expand Up @@ -115,6 +115,7 @@
flex-direction: row;
align-items: center;
gap: 0.75rem;
padding-bottom: 0.5rem;

h3 {
margin: 0;
Expand Down
8 changes: 4 additions & 4 deletions src/lib/components/home/QuickStartExamples.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
<div class="section-head">
<h2>Quick start examples</h2>
<p class="lead">Three real queries. Open one to review the query in the query builder and plot it on the map.</p>
<span class="pending" title={PENDING_HINT}>
<!-- <span class="pending" title={PENDING_HINT}>
<Button variant="link" disabled>View all examples</Button>
</span>
</span> -->
Comment on lines +31 to +33
</div>

<div class="examples">
Expand Down Expand Up @@ -68,12 +68,12 @@
Check query
</Button>

<span class="pending" title={PENDING_HINT}>
<!-- <span class="pending" title={PENDING_HINT}>
<Button variant="default" disabled>
<MapIcon />
Load map area
</Button>
</span>
</span> -->
</div>
</div>
</Card>
Expand Down
29 changes: 17 additions & 12 deletions src/lib/components/palette/PalettePicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
onSelect,
reverse = false,
showSolids = true,
showGradients = true,
id = undefined
}: {
/** The id of the palette now in use. */
Expand All @@ -33,6 +34,8 @@
/** Preview the palette the way it is drawn, turned around or not. */
reverse?: boolean;
showSolids?: boolean;
/** False offers the single colours alone, for a plot that paints no value. */
showGradients?: boolean;
/** DOM id of the trigger, so a `<label for>` can point at it. */
id?: string;
} = $props();
Expand Down Expand Up @@ -89,18 +92,20 @@
</Select.Trigger>

<Select.Content>
<Select.Group>
<Select.Label>Gradients</Select.Label>

{#each gradientMaps as map (map.id)}
<Select.Item value={map.id} label={map.label}>
<span class="entry" title={map.description}>
<span class="swatch" style="background: {previewFor(map.id)};"></span>
<span class="name">{map.label}</span>
</span>
</Select.Item>
{/each}
</Select.Group>
{#if showGradients && gradientMaps.length > 0}
<Select.Group>
<Select.Label>Gradients</Select.Label>

{#each gradientMaps as map (map.id)}
<Select.Item value={map.id} label={map.label}>
<span class="entry" title={map.description}>
<span class="swatch" style="background: {previewFor(map.id)};"></span>
<span class="name">{map.label}</span>
</span>
</Select.Item>
{/each}
</Select.Group>
{/if}

{#if showSolids && solidMaps.length > 0}
<Select.Group>
Expand Down
37 changes: 32 additions & 5 deletions src/lib/components/plots/PlotConfigPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
type ColorScale
} from '@/plots/plot-config';
import { CROSS_SECTION_AXIS_LABEL, HISTOGRAM_AXIS_LABEL } from '@/plots/plot-data';
import { DEFAULT_SOLID_PALETTE_ID, getColormap } from '@/colors/palettes';
import { Utils } from '@/utils';

let { controller }: { controller: ChartExplorerController } = $props();
Expand Down Expand Up @@ -348,9 +349,26 @@
return summary;
});

const styleSummary = $derived(
`${draft?.style.palette ?? ''} · ${draft?.style.pointRadius ?? 0}px`
);
/**
* The palette the points take with no colour column: the stored one while it
* is a single colour, and solid blue otherwise. A gradient stays stored, so
* binding the colour column again brings it back.
*/
const solidPalette = $derived.by(() => {
const palette = draft?.style.palette;
if (palette && getColormap(palette).solid) return palette;
return DEFAULT_SOLID_PALETTE_ID;
Comment on lines +357 to +360
});

/** Names the palette that the plot paints with, which is solid without a Z column. */
const styleSummary = $derived.by(() => {
if (!draft) return '';

let palette = draft.style.palette;
if (usesZColumn(draft.type) && !draft.z?.column) palette = solidPalette;

return `${palette} · ${draft.style.pointRadius}px`;
});

const advancedAnalysisSummary = $derived.by(() => {
if (draft && !usesZColumn(draft.type)) return 'Not for this plot type';
Expand Down Expand Up @@ -781,8 +799,17 @@
A solid palette colours every bar the same. A range palette colours each bar by
its count.
</p>{/if}
{:else}<p class="hint">
Bind a column to the colour axis in step 2 to pick a palette.
{:else}<div class="field">
<Label for="plotPointColor">Point colour</Label><PalettePicker
id="plotPointColor"
value={solidPalette}
showGradients={false}
onSelect={(id) => patchStyle({ palette: id })}
/>
</div>
<p class="hint">
Every point takes this colour. Bind a column to the colour axis in step 2 to paint
the points by value.
</p>{/if}
{#if draft.z?.column}
<label class="checkbox-field"
Expand Down
35 changes: 33 additions & 2 deletions src/lib/components/query-builder/QueryBuilder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,54 @@
// re-mounts this component with a client.
if (!node) return;

// A share link can swap the active block's node while this call is in
// flight: the workbench first paints the previously active block, then
// its `onMount` opens the link and points the block at the linked node.
// `{#key}` then remounts this component, but the old async call keeps
// running. `node` is read live, so it would see the *new* node while
// `tables` below still holds the *old* one. Compare against the node
// this call started for, and drop a stale answer instead of reporting
// a mismatch against the wrong node.
//
// Compare the URL, not the object. The node list re-emits on an edit or
// a health check, which gives the same node a new object. That is not a
// new node, and this call must survive it.
const requestedUrl = node.url;

loadError = null;
loaded = false;
client = BeaconClient.new(node);

let tables: string[];
let default_table: string;

try {
tables = await client.getCachedTables();
default_table = await client.getCachedDefaultTable();
} catch (error) {
if (node?.url !== requestedUrl) return;
console.error('Could not read the tables of the Beacon node.', error);
loadError = (error as Error)?.message || 'The Beacon node did not answer.';
loaded = true;
return;
}

if (node?.url !== requestedUrl) return;

// A node can have no default table configured, so this is an offer, not a
// requirement. Fall back to the first table when it fails or is unusable.
let default_table: string | undefined;

try {
default_table = await client.getCachedDefaultTable();
} catch (error) {
console.warn('Could not read the default table of the Beacon node.', error);
}

if (node?.url !== requestedUrl) return;

if (!default_table || !tables.includes(default_table)) {
default_table = tables[0];
}

Comment on lines +134 to +151
// By default, select the first table, or restore the table from the draft/seed.
const seedTable = typeof pendingSeed?.from === 'string' ? pendingSeed.from : null;
const draftTable = initialDraft?.tableName || null;
Expand Down
10 changes: 6 additions & 4 deletions src/lib/components/visualisation/DataTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -159,24 +159,26 @@
flex-direction: column;
flex-grow: 1;
min-height: 0;
min-width: 0;
overflow: hidden;



.table-wrapper {
overflow: auto;
flex-grow: 1;
min-height: 0;
min-width: 0;

table.dataset-table {
width: 100%;
min-width: 100%;
width: max-content;
border-collapse: collapse;

thead th,
thead th,
tbody td {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border-bottom: 1px solid #ddd;
white-space: nowrap;
}

&.small {
Expand Down
7 changes: 5 additions & 2 deletions src/lib/plots/plot-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,11 @@ export const DEFAULT_STYLE: PlotStyleConfig = {
textColor: '#1f2937'
};

/** The colour of a point when the plot has no Z axis. */
export const DEFAULT_POINT_COLOR = '#2563eb';
/**
* Solid blue, the colour of a point when the plot has no Z axis. It matches the
* `solid_blue` palette, and it stands in for it before the colormaps load.
*/
export const DEFAULT_POINT_COLOR = '#4678c8';

export function makeAxisConfig(overrides: Partial<PlotAxisConfig> = {}): PlotAxisConfig {
return {
Expand Down
Loading