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
2 changes: 1 addition & 1 deletion packages/backend/start_image.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/bash

uv run embedding-atlas ylecun/mnist --image image --split train --static ../viewer/dist
uv run embedding-atlas ylecun/mnist --image image --split train --static ../viewer/dist "$@"
27 changes: 24 additions & 3 deletions packages/table/src/lib/views/cells/CellContent.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
<script lang="ts">
import type { JSType } from "@uwdata/mosaic-core";

import BigIntContent from "./cell-contents/BigIntContent.svelte";
import CustomCellContents from "./cell-contents/CustomCellContents.svelte";
import ImageContent from "./cell-contents/ImageContent.svelte";
import LinkContent from "./cell-contents/LinkContent.svelte";
import NumberContent from "./cell-contents/NumberContent.svelte";
import TextContent from "./cell-contents/TextContent.svelte";

import { ConfigContext } from "../../context/config.svelte";
import { Context } from "../../context/context.svelte";
import { CustomCellsContext } from "../../context/custom-cells.svelte";
import BigIntContent from "./cell-contents/BigIntContent.svelte";
import CustomCellContents from "./cell-contents/CustomCellContents.svelte";

interface Props {
row: string;
Expand All @@ -34,6 +35,24 @@
const content: string | null = model.getContent({ row, col });
const type: JSType = schema.dataType[col] ?? "string";
const sqlType: string = schema.sqlType[col] ?? "TEXT";

function isLink(value: any): boolean {
return typeof value == "string" && (value.startsWith("http://") || value.startsWith("https://"));
}

function isImage(value: any): boolean {
if (value == null) {
return false;
}
if (typeof value == "string" && value.startsWith("data:image/")) {
return true;
}
if (value.bytes && value.bytes instanceof Uint8Array) {
// TODO: check if the bytes are actually an image.
return true;
}
return false;
}
</script>

<div
Expand All @@ -45,7 +64,7 @@
{#if customCellsConfig[col]}
<CustomCellContents row={row} col={col} customCell={customCellsConfig[col]} bind:height={contentHeight} />
{:else if type === "string"}
{#if content && content.startsWith("http")}
{#if content && isLink(content)}
<LinkContent url={content} bind:height={contentHeight} />
{:else}
<TextContent text={content} bind:height={contentHeight} clamped={clamped} parentHeight={height} />
Expand All @@ -56,6 +75,8 @@
{:else}
<NumberContent number={content as number | null} bind:height={contentHeight} />
{/if}
{:else if isImage(content)}
<ImageContent image={content} bind:height={contentHeight} />
{:else}
<TextContent text={content} bind:height={contentHeight} clamped={clamped} parentHeight={height} />
{/if}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<!-- Copyright (c) 2025 Apple Inc. Licensed under MIT License. -->
<script lang="ts">
import { ConfigContext } from "../../../context/config.svelte";

interface Props {
image: any | null;
height: number;
}

let { image, height = $bindable() }: Props = $props();

const config = ConfigContext.config;

let element: HTMLElement | null = $state(null);

function base64Encode(data: Uint8Array): string {
let binary = "";
for (let i = 0; i < data.length; i++) {
binary += String.fromCharCode(data[i]);
}
return btoa(binary);
}

function base64Decode(base64: string): Uint8Array {
const binaryString = atob(base64);
return new Uint8Array([...binaryString].map((char) => char.charCodeAt(0)));
}

function startsWith(data: Uint8Array, prefix: number[]): boolean {
if (data.length < prefix.length) {
return false;
}
for (let i = 0; i < prefix.length; i++) {
if (data[i] != prefix[i]) {
return false;
}
}
return true;
}

function detectImageType(data: Uint8Array): string {
if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
return "image/png";
} else if (startsWith(data, [0xff, 0xd8, 0xff])) {
return "image/jpeg";
} else if (startsWith(data, [0x49, 0x49, 0x2a, 0x00])) {
return "image/tiff";
} else if (startsWith(data, [0x42, 0x4d])) {
return "image/bmp";
} else if (
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61])
) {
return "image/gif";
}
// Unknown, fallback to generic type
return "application/octet-stream";
}

function imageToDataUrl(img: any): string | null {
if (img == null) {
return null;
}
if (typeof img == "string") {
if (img.startsWith("data:")) {
return img;
} else {
let type = detectImageType(base64Decode(img));
return `data:${type};base64,` + img;
}
} else {
let bytes: Uint8Array | null = null;
if (img.bytes && img.bytes instanceof Uint8Array) {
bytes = img.bytes;
}
if (img instanceof Uint8Array) {
bytes = img;
}
if (bytes != null) {
let type = detectImageType(bytes);
return `data:${type};base64,` + base64Encode(bytes);
}
}
return null;
}
</script>

<img
bind:this={element}
onload={() => {
if (element) {
height = element.scrollHeight;
}
}}
src={imageToDataUrl(image)}
alt=""
/>
2 changes: 1 addition & 1 deletion packages/viewer/src/lib/CategoryLegend.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
});
</script>

<div class="absolute right-0 top-0 p-2 m-2 rounded-md bg-opacity-90 bg-slate-100 dark:bg-slate-800">
<div class="absolute right-0 top-0 p-2 m-2 rounded-md bg-slate-100/75 dark:bg-slate-800/75 backdrop-blur-sm">
<table>
<tbody>
{#each items as item}
Expand Down
42 changes: 42 additions & 0 deletions packages/viewer/src/lib/ColumnStylePicker.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!-- Copyright (c) 2025 Apple Inc. Licensed under MIT License. -->
<script lang="ts">
import ColumnStylePickerRow from "./ColumnStylePickerRow.svelte";

import type { ColumnDesc } from "./database_utils.js";
import { type ColumnStyle } from "./renderers/index.js";

interface Props {
columns: ColumnDesc[];
styles: Record<string, ColumnStyle>;
onStylesChange: (value: Record<string, ColumnStyle>) => void;
}

let { columns, styles, onStylesChange }: Props = $props();
</script>

<div
class="max-h-60 overflow-x-hidden overflow-y-scroll border border-slate-200 dark:border-slate-600 p-2 rounded-md inset-shadow-sm"
>
<table>
<thead>
<tr class="select-none">
<th class="pb-2 text-slate-500 dark:text-slate-400 text-left font-normal text-sm">Column</th>
<th class="pb-2 text-slate-500 dark:text-slate-400 text-left font-normal text-sm">Format</th>
<th class="pb-2 text-slate-500 dark:text-slate-400 text-left font-normal text-sm">Style</th>
</tr>
</thead>
<tbody>
{#each columns as column}
<ColumnStylePickerRow
column={column}
style={styles[column.name] ?? {}}
onChange={(s) => {
let newStyles = { ...styles };
newStyles[column.name] = s;
onStylesChange(newStyles);
}}
/>
{/each}
</tbody>
</table>
</div>
69 changes: 69 additions & 0 deletions packages/viewer/src/lib/ColumnStylePickerRow.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!-- Copyright (c) 2025 Apple Inc. Licensed under MIT License. -->
<script lang="ts">
import Select from "./widgets/Select.svelte";
import Slider from "./widgets/Slider.svelte";

import type { ColumnDesc } from "./database_utils.js";
import { renderersList, type ColumnStyle } from "./renderers/index.js";

interface Props {
column: ColumnDesc;
style: ColumnStyle;
onChange: (value: ColumnStyle) => void;
}

let { column, style, onChange }: Props = $props();

function change(fields: Partial<ColumnStyle>) {
onChange({ ...style, ...fields });
}
</script>

<tr class="leading-10">
<td class="w-full">
<div class="max-w-80 whitespace-nowrap text-ellipsis overflow-x-hidden">
{column.name}
</div>
</td>
<td class="pr-2">
<div class="flex items-center gap-2">
<Select
value={style.renderer ?? null}
onChange={(v) => change({ renderer: v })}
options={[
{ value: null, label: "(default)" },
...renderersList.map((x) => ({ value: x.renderer, label: x.label })),
]}
/>

{#if style.renderer == "image"}
<Slider
bind:value={
() => style.rendererOptions?.size ?? 100,
(v) => {
change({ rendererOptions: { size: v } });
}
}
width={72}
min={16}
max={400}
/>
{/if}
</div>
</td>
<td>
<div class="flex items-center gap-2">
<Select
value={style.display ?? "badge"}
onChange={(v) => {
change({ display: v });
}}
options={[
{ value: "full", label: "Full" },
{ value: "badge", label: "Badge" },
{ value: "hidden", label: "Hidden" },
]}
/>
</div>
</td>
</tr>
Loading