Skip to content

Commit 46f66a9

Browse files
domoritzdonghaoren
andauthored
feat: move export into dedicated menu action, increase spacing between buttons (#104)
Co-authored-by: Donghao Ren <[email protected]> Co-authored-by: Donghao Ren <[email protected]>
1 parent e41e345 commit 46f66a9

File tree

5 files changed

+83
-41
lines changed

5 files changed

+83
-41
lines changed

packages/backend/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,7 @@ embedding-atlas path_to_dataset.parquet --x projection_x --y projection_y --neig
6565

6666
The `neighbors` column should have values in the following format: `{"ids": [id1, id2, ...], "distances": [d1, d2, ...]}`.
6767
If this column is specified, you'll be able to see nearest neighbors for a selected point in the tool.
68+
69+
## Local Development
70+
71+
Launch Embedding Altas with a wine reviews dataset with `./start.sh` and the MNIST dataset with `./start_image.sh`.

packages/viewer/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ Start development server:
1313
```bash
1414
npm run dev
1515
```
16+
17+
This will serve Embedding Atlas UI at http://localhost:5173. Note that the UI requires a backend server to provide data to it. You can start one via the `./start.sh` mentioned above. Without a backend server, you can still go to http://localhost:5173/#/test to view a test dataset.

packages/viewer/src/EmbeddingAtlas.svelte

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import Spinner from "./widgets/Spinner.svelte";
2121
2222
import {
23+
IconClose,
2324
IconDarkMode,
2425
IconDashboardLayout,
2526
IconDownload,
@@ -370,19 +371,60 @@
370371
{/if}
371372
</div>
372373
<!-- Right side -->
373-
<div class="flex flex-none gap-2 items-center">
374+
<div
375+
class="flex flex-none gap-2 items-center pl-2 rounded-md border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900"
376+
>
374377
<FilteredCount coordinator={coordinator} filter={crossFilter} table={data.table} />
375-
<div class="flex flex-row gap-1 items-center">
378+
<div class="flex flex-row items-center">
376379
<button
377-
class="flex px-2.5 mr-1 select-none items-center justify-center text-slate-500 dark:text-slate-300 rounded-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-600 focus-visible:outline-2 outline-blue-600 -outline-offset-1"
378-
onclick={resetFilter}
379380
title="Clear filters"
381+
onclick={resetFilter}
382+
class="rounded-md flex select-none items-center p-1.5 text-slate-400 dark:text-slate-500 focus-visible:outline-2 outline-blue-600 -outline-offset-1"
380383
>
381-
Clear
384+
<IconClose class="w-5 h-5" />
382385
</button>
386+
387+
{#if onExportSelection}
388+
<PopupButton title="Export Selection">
389+
{#snippet button({ visible, toggle })}
390+
<button
391+
title="Export Selection"
392+
onclick={toggle}
393+
class="rounded-md px-1.5 py-1.5 flex select-none items-center focus-visible:outline-2 outline-blue-600 -outline-offset-1"
394+
class:text-slate-400={!visible}
395+
class:dark:text-slate-500={!visible}
396+
>
397+
<IconExport class="w-5 h-5" />
398+
</button>
399+
{/snippet}
400+
<div class="min-w-[420px] flex flex-col gap-2">
401+
<div class="flex flex-row gap-2">
402+
<ActionButton
403+
icon={IconExport}
404+
label="Export Selection"
405+
title="Export the selected points"
406+
class="w-48"
407+
onClick={() => onExportSelection(currentPredicate(), exportFormat)}
408+
/>
409+
<Select
410+
label="Format"
411+
value={exportFormat}
412+
onChange={(v) => (exportFormat = v)}
413+
options={[
414+
{ value: "parquet", label: "Parquet" },
415+
{ value: "jsonl", label: "JSONL" },
416+
{ value: "json", label: "JSON" },
417+
{ value: "csv", label: "CSV" },
418+
]}
419+
/>
420+
</div>
421+
</div>
422+
</PopupButton>
423+
{/if}
383424
</div>
384425
</div>
385-
<div class="flex flex-none flex-row gap-0.5">
426+
427+
<div class="flex flex-none flex-row gap-2">
386428
<div class="grid grid-cols-1 grid-rows-1 justify-items-end items-center">
387429
{#key layout}
388430
<div transition:scale class="col-start-1 row-start-1">
@@ -429,41 +471,17 @@
429471
}}
430472
/>
431473
{/if}
432-
<!-- Export -->
433-
{#if onExportSelection || onExportApplication}
474+
<!-- Export Application -->
475+
{#if onExportApplication}
434476
<h4 class="text-slate-500 dark:text-slate-400 select-none">Export</h4>
435477
<div class="flex flex-col gap-2">
436-
{#if onExportSelection}
437-
<div class="flex flex-row gap-2">
438-
<ActionButton
439-
icon={IconExport}
440-
label="Export Selection"
441-
title="Export the selected points"
442-
class="w-48"
443-
onClick={() => onExportSelection(currentPredicate(), exportFormat)}
444-
/>
445-
<Select
446-
label="Format"
447-
value={exportFormat}
448-
onChange={(v) => (exportFormat = v)}
449-
options={[
450-
{ value: "parquet", label: "Parquet" },
451-
{ value: "jsonl", label: "JSONL" },
452-
{ value: "json", label: "JSON" },
453-
{ value: "csv", label: "CSV" },
454-
]}
455-
/>
456-
</div>
457-
{/if}
458-
{#if onExportApplication}
459-
<ActionButton
460-
icon={IconDownload}
461-
label="Export Application"
462-
title="Download a self-contained static web application"
463-
class="w-48"
464-
onClick={onExportApplication}
465-
/>
466-
{/if}
478+
<ActionButton
479+
icon={IconDownload}
480+
label="Export Application"
481+
title="Download a self-contained static web application"
482+
class="w-48"
483+
onClick={onExportApplication}
484+
/>
467485
</div>
468486
{/if}
469487
<h4 class="text-slate-500 dark:text-slate-400 select-none">About</h4>

packages/viewer/src/app/Test.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
77
import type { EmbeddingAtlasProps } from "../api.js";
88
import { initializeDatabase } from "../utils/database.js";
9+
import { downloadBuffer } from "../utils/download.js";
10+
import { exportMosaicSelection, type ExportFormat } from "../utils/mosaic_exporter.js";
911
import type { DataSource } from "./data_source.js";
1012
1113
export class TestDataSource implements DataSource {
1214
private count: number;
1315
16+
downloadSelection: ((predicate: string | null, format: ExportFormat) => Promise<void>) | undefined = undefined;
17+
1418
constructor(count: number) {
1519
this.count = count;
1620
}
@@ -55,6 +59,11 @@
5559
UPDATE ${table} SET y = y + 5 * floor(floor(var_uniform * 24 + random()) / 5);
5660
`);
5761
62+
this.downloadSelection = async (predicate, format) => {
63+
let [bytes, name] = await exportMosaicSelection(coordinator, table, predicate, format);
64+
downloadBuffer(bytes, name);
65+
};
66+
5867
return {
5968
data: {
6069
table: table,

packages/viewer/src/widgets/PopupButton.svelte

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@
1111
label?: string | null;
1212
icon?: any | null;
1313
anchor?: "left" | "right";
14+
button?: Snippet<[{ visible: boolean; toggle: () => void }]>;
1415
children?: Snippet;
1516
}
1617
17-
let { title = "", label = null, icon = null, anchor = "right", children }: Props = $props();
18+
let { title = "", label = null, icon = null, anchor = "right", children, button }: Props = $props();
1819
1920
let visible: boolean = $state(false);
21+
22+
function toggle() {
23+
visible = !visible;
24+
}
2025
let container: HTMLDivElement;
2126
2227
function onKeyDown(e: KeyboardEvent) {
@@ -50,7 +55,11 @@
5055

5156
<!-- svelte-ignore a11y_no_static_element_interactions -->
5257
<div class="relative" bind:this={container} onkeydown={onKeyDown}>
53-
<ToggleButton icon={icon} title={title} label={label} bind:checked={visible} />
58+
{#if button}
59+
{@render button({ visible, toggle })}
60+
{:else}
61+
<ToggleButton icon={icon} title={title} label={label} bind:checked={visible} />
62+
{/if}
5463
<div
5564
bind:this={popoverElement}
5665
class="absolute px-3 py-3 rounded-md z-20 bg-slate-100 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 shadow-lg"

0 commit comments

Comments
 (0)