wip: refactoring tiled query for multi table

This commit is contained in:
Wlad Meixner 2023-09-18 17:42:39 +02:00
parent 6bdeae7114
commit d0f89bdefd
38 changed files with 6884 additions and 681 deletions

BIN
bun.lockb Executable file

Binary file not shown.

View File

@ -1,57 +1,60 @@
{
"name": "visualization",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "playwright test",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"deploy": "pnpm build && pnpx gh-pages -d build -t true",
"test:unit": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@playwright/test": "^1.35.1",
"@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/kit": "^1.20.5",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0",
"gh-pages": "^6.0.0",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^3.59.2",
"svelte-check": "^3.4.4",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"vite": "^4.3.9",
"vitest": "^0.25.8"
},
"type": "module",
"dependencies": {
"@duckdb/duckdb-wasm": "^1.27.0",
"@sveltejs/adapter-static": "^2.0.2",
"@types/papaparse": "^5.3.7",
"@types/three": "^0.152.1",
"apache-arrow": "^12.0.1",
"autoprefixer": "^10.4.14",
"feather-icons": "^4.29.0",
"monaco-editor": "^0.40.0",
"monaco-sql-languages": "0.12.0-beta.1",
"papaparse": "^5.4.1",
"postcss": "^8.4.24",
"sass": "^1.63.6",
"stats.js": "^0.17.0",
"svelte-feather-icons": "^4.0.1",
"tailwindcss": "^3.3.2",
"three": "^0.154.0",
"three.meshline": "^1.4.0",
"web-worker": "^1.2.0"
}
"name": "visualization",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "playwright test",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"deploy": "pnpm build && pnpx gh-pages -d build -t true",
"test:unit": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@playwright/test": "^1.35.1",
"@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/kit": "^1.20.5",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0",
"gh-pages": "^6.0.0",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^3.59.2",
"svelte-check": "^3.4.4",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"vite": "^4.3.9",
"vitest": "^0.25.8"
},
"type": "module",
"dependencies": {
"@duckdb/duckdb-wasm": "^1.27.0",
"@fontsource/inter": "^5.0.8",
"@sveltejs/adapter-static": "^2.0.2",
"@tweenjs/tween.js": "^21.0.0",
"@types/papaparse": "^5.3.7",
"@types/three": "^0.152.1",
"apache-arrow": "^12.0.1",
"autoprefixer": "^10.4.14",
"feather-icons": "^4.29.0",
"monaco-editor": "^0.40.0",
"monaco-sql-languages": "0.12.0-beta.1",
"papaparse": "^5.4.1",
"postcss": "^8.4.24",
"sass": "^1.63.6",
"stats.js": "^0.17.0",
"svelte-feather-icons": "^4.0.1",
"svelte-icons-pack": "^2.1.0",
"tailwindcss": "^3.3.2",
"three": "^0.154.0",
"three.meshline": "^1.4.0",
"web-worker": "^1.2.0"
}
}

View File

@ -8,6 +8,9 @@ dependencies:
'@duckdb/duckdb-wasm':
specifier: ^1.27.0
version: 1.27.0
'@fontsource/inter':
specifier: ^5.0.8
version: 5.0.8
'@sveltejs/adapter-static':
specifier: ^2.0.2
version: 2.0.2(@sveltejs/kit@1.20.5)
@ -339,6 +342,10 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/@fontsource/inter@5.0.8:
resolution: {integrity: sha512-28knWH1BfOiRalfLs90U4sge5mpQ8ZH6FS0PTT+IZMKrZ7wNHDHRuKa1kQJg+uHcc6axBppnxll+HXM4c7zo/Q==}
dev: false
/@humanwhocodes/config-array@0.11.10:
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
engines: {node: '>=10.10.0'}

View File

@ -1,6 +1,7 @@
<script lang="ts" generics="Data extends unknown">
import type { GraphRenderLoopCallback, GraphService } from './graph/types';
import * as TWEEN from '@tweenjs/tween.js';
import { defineService } from '$lib/contextService';
import Card from './Card.svelte';
@ -108,7 +109,11 @@
setupScene();
renderer = new THREE.WebGLRenderer({ alpha: false });
renderer = new THREE.WebGLRenderer({
alpha: false,
antialias: true,
powerPreference: 'high-performance'
});
renderer.setPixelRatio(window.devicePixelRatio || 1);
renderer.setClearColor(0x000000, 0);
renderer.setSize(containerElement.clientWidth, containerElement.clientHeight);
@ -137,12 +142,12 @@
setupControls();
// Animation loop
const animate = () => {
const animate = (time: number) => {
// call all before subscribers
for (const subscriber of beforeSubscribers) {
subscriber();
}
TWEEN.update(time);
controls.update();
// if (mousePosition) {
@ -173,7 +178,7 @@
isSetupComplete = true;
// Set scene and camera to context
animate();
animate(0);
});
const beforeSubscribers: Set<GraphRenderLoopCallback> = new Set();
@ -251,7 +256,7 @@
}
</script>
<div class="relative w-screen h-screen">
<div class="relative w-screen h-[80vh]">
<div
bind:this={containerElement}
on:mousemove={handleHover}

View File

@ -1,56 +0,0 @@
<script lang="ts">
import type { Action } from '@sveltejs/kit';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let disabled: boolean = false;
export let color: 'primary' | 'secondary' = 'secondary';
let className: string | undefined = undefined;
export { className as class };
function classForSize() {
switch (size) {
case 'sm':
return 'px-2.5 py-1.5 text-xs shadow-sm rounded-md';
case 'md':
return 'px-4 py-2 text-sm shadow-mg rounded-lg';
case 'lg':
return 'px-4 py-2 text-base shadow-lg rounded-xl';
}
}
function classForColors(): string {
switch (color) {
case 'primary':
return 'text-white bg-primary-600 hover:bg-primary-700 border-primary-700 hover:border-primary-700';
case 'secondary':
return 'text-secondary-600 dark:text-background-50 bg-white border-secondary-300 dark:border-background-950 dark:bg-background-900 hover:bg-gray-50 dark:hover:bg-background-800 dark:hover:border-background-900';
}
}
function classForDisabledState(): string {
if (disabled) {
return 'cursor-not-allowed opacity-50 pointer-events-none';
}
return '';
}
</script>
<button
{disabled}
on:click
class:disabled
class="inline-flex justify-between items-center border {classForColors()} {classForSize()} font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 {className ??
''}"
>
<slot name="leading" />
<slot />
<slot name="trailing" />
</button>
<style>
.disabled {
@apply cursor-not-allowed opacity-30 pointer-events-none;
}
</style>

View File

@ -3,8 +3,9 @@
import { onMount, onDestroy } from 'svelte';
import { fade, fly } from 'svelte/transition';
export let large = false;
type DialogSize = 'small' | 'medium' | 'large';
export let size: DialogSize = 'medium';
export let dialogOpen = false;
let className: string | undefined = undefined;
export { className as class };
@ -48,8 +49,7 @@
>
<div
transition:fly={{ y: -120, delay: 25, duration: 150 }}
class="modal rounded-3xl shadow-xl bg-background-50 dark:bg-background-800"
class:large
class="modal rounded-3xl shadow-xl bg-background-50 dark:bg-background-800 {size}"
>
{#if $$slots.title}<div class="pb-4 mb-2 border-b">
<h2 class="font-bold text-xl"><slot name="title" /></h2>
@ -82,5 +82,13 @@
max-width: calc(95vw - 40px);
max-height: calc(95vh - 40px) !important;
}
&.small {
max-width: 500px;
}
@media (max-width: 768px) {
width: 90%;
}
}
</style>

View File

@ -1,8 +1,9 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import Button from './Button.svelte';
import Button from './button/Button.svelte';
import { ChevronDownIcon, ChevronUpIcon } from 'svelte-feather-icons';
import { ButtonSize } from './button/type';
export let isOpen: boolean = false;
export let disabled: boolean = false;
@ -51,24 +52,55 @@
});
</script>
<div class="relative {className}" bind:this={popoverElement}>
<Button class="flex items-center justify-between gap-2 {buttonClass}" on:click={toggleDropdown}>
<div
class="relative mb-2 ring-offset-2 rounded-md ring-offset-background-50 dark:ring-offset-background-800 {className}"
class:ring-4={isOpen}
bind:this={popoverElement}
>
<Button
size={ButtonSize.MD}
class="flex items-center justify-between gap-2 {buttonClass}"
on:click={toggleDropdown}
>
<slot name="button" />
{#if isOpen}
<ChevronDownIcon />
{:else}
<ChevronUpIcon />
{/if}
<svelte:fragment slot="trailing">
{#if isOpen}
<ChevronUpIcon size="18" />
{:else}
<ChevronDownIcon size="18" />
{/if}
</svelte:fragment>
</Button>
{#if isOpen}
<div
transition:fadeSlide={{ duration: 100 }}
class="z-10 overflow-hidden origin-top-left absolute left-0 mt-2 w-56 rounded-xl shadow-2xl shadow-background-700 dark:shadow-background-950 bg-background-50 dark:bg-background-800 max-h-[250px] overflow-y-auto ring-1 ring-background-900/5 dark:ring-background-950/5"
class="z-10 origin-top-left absolute left-0 mt-2 w-64 overflow-hidden rounded-xl shadow-2xl shadow-background-700 dark:shadow-background-950 bg-background-50 dark:bg-background-800 ring-background-200/5 dark:ring-background-950/5 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
<div role="none">
<slot name="content" />
<div class="dropdown-content w-full h-full overflow-y-auto max-h-[350px]">
<div role="none">
<slot name="content" />
</div>
</div>
</div>
{/if}
</div>
<style lang="scss">
.dropdown-content {
// Reset scroll bar styles
&::-webkit-scrollbar {
width: 0.4em;
}
&::-webkit-scrollbar-track {
@apply dark:bg-slate-900 bg-slate-300;
opacity: 0.4;
}
&::-webkit-scrollbar-thumb {
@apply bg-slate-300 dark:bg-slate-600;
border-radius: 20px;
}
}
</style>

View File

@ -1,9 +1,10 @@
<script lang="ts">
import Label from './base/Label.svelte';
import { beforeUpdate } from 'svelte';
import { CheckCircleIcon, CheckIcon, MailIcon, PhoneIcon } from 'svelte-feather-icons';
import Dropdown from './Dropdown.svelte';
import Button from './Button.svelte';
import Button from './button/Button.svelte';
export let isOpen = false;
export let singular = false;
@ -17,30 +18,46 @@
type M = $$Generic;
type Option = { label: string; value: T; id?: number; initiallySelected?: boolean };
type OptionConstructor = (value: R, index: number, meta: unknown) => Option;
export let selection = new Set<T>();
export let optionOrderer: ((a: Option, b: Option) => number) | undefined = undefined;
export let options: Option[] = [];
export let meta: M | undefined = undefined;
export let values: R[] | undefined = undefined;
export let optionConstructor: ((value: R, index: number, meta: unknown) => Option) | undefined =
undefined;
export let optionConstructor: OptionConstructor | undefined = undefined;
let dummy = 0;
// $: {
// selectionLabel = labelForSelection(options.filter((o) => selection.has(o.value)));
// }
$: {
selectionLabel = labelForSelection(options.filter((o) => selection.has(o.value)));
}
$: {
if (values && optionConstructor) {
options = values.map((v, i) => optionConstructor!(v, i, meta));
generateOptions(values, optionConstructor);
}
}
// Set all items that were initially selected
selection = new Set<T>(options.filter((o) => o.initiallySelected).map((o) => o.value));
const selectedOptions = options.filter((o) => selection.has(o.value));
const newLabel = labelForSelection(selectedOptions);
if (newLabel !== selectionLabel) {
selectionLabel = newLabel;
}
$: {
if (optionOrderer) {
options = options.sort(optionOrderer);
}
}
function generateOptions(values: R[], optionConstructor: OptionConstructor) {
options = values.map((v, i) => optionConstructor!(v, i, meta));
if (optionOrderer) {
options = options.sort(optionOrderer);
}
// Set all items that were initially selected
selection = new Set<T>(options.filter((o) => o.initiallySelected).map((o) => o.value));
const selectedOptions = options.filter((o) => selection.has(o.value));
const newLabel = labelForSelection(selectedOptions);
if (newLabel !== selectionLabel) {
selectionLabel = newLabel;
}
}
@ -54,7 +71,6 @@
if (singular) {
selection = new Set<T>([option.value]);
onSelect?.([option], meta);
return;
}
@ -66,9 +82,8 @@
selection = new Set<T>(selection);
const selectedOptions = options.filter((o) => selection.has(o.value));
selectionLabel = labelForSelection(selectedOptions);
onSelect?.(selectedOptions);
onSelect?.(selectedOptions, meta);
}
function clearAll() {
@ -100,17 +115,17 @@
</script>
<div class="flex flex-col">
{#if label !== undefined}<div
class="font-bold text-sm px-4 pb-1 text-secondary-500 dark:text-secondary-400"
>
{#if label !== undefined}
<Label>
{label}
</div>{/if}
</Label>
{/if}
<Dropdown buttonClass="w-full" {isOpen} disabled={!(options && options.length > 0) || disabled}>
<span slot="button">
{#if $$slots.default}
<slot />
{:else}
<span class="text-sm">{selectionLabel}</span>
<span class="text-sm" class:opacity-30={selection.size === 0}>{selectionLabel}</span>
{/if}
</span>
@ -118,25 +133,26 @@
<ul>
{#each options as option, i}
{@const selected = selection.has(option.value)}
<li class="border-spacing-1 border-b dark:border-background-800 last:border-b-0">
<li class="border-spacing-1 border-b dark:border-background-900 last:border-b-0">
<button
on:click={() => internalOnSelect(option)}
class="p-4 hover:bg-secondary-200 dark:hover:bg-secondary-700 w-full text-left flex gap-4"
class="p-2 hover:bg-primary-100 dark:hover:bg-secondary-700 w-full text-left flex gap-2"
>
<div class="w-6">
<div class="w-6 pt pb">
{#if singular}
<div
class="rounded-full op w-6 h-6 border-2 flex items-center justify-center border-secondary-900 {selected
? 'opacity-100'
: 'opacity-20'}"
class:border-foreground-500={!selected}
class:border-primary-500={selected}
class="rounded-full op w-6 h-6 border-2 flex items-center justify-center"
>
<div hidden={!selected} class="rounded-full w-4 h-4 bg-secondary-900" />
<div hidden={!selected} class="rounded-full w-3 h-3 bg-primary-500" />
</div>
{:else}
<i hidden={!selected}><CheckIcon /></i>
{/if}
</div>
<span class={selected ? 'font-bold' : ''}>{option.label}</span></button
<span class:font-bold={selected} class:opacity-60={!selected}>{option.label}</span
></button
>
</li>
{/each}

View File

@ -2,93 +2,132 @@
import { dataStore } from '$lib/store/dataStore/DataStore';
import filterStore, { type IFilterStoreGraphOptions } from '$lib/store/filterStore/FilterStore';
import { onMount } from 'svelte';
import Button from './Button.svelte';
import Button from './button/Button.svelte';
import Card from './Card.svelte';
import DropdownSelect from './DropdownSelect.svelte';
import { get } from 'svelte/store';
import Slider from './Slider.svelte';
import { GraphType } from '$lib/store/filterStore/types';
import { GraphOptions, GraphType } from '$lib/store/filterStore/types';
import OptionRenderer from './OptionRenderer.svelte';
import Divider from './base/Divider.svelte';
import {
LayersIcon,
PlusIcon,
RefreshCcwIcon,
SettingsIcon,
Trash2Icon,
XIcon
} from 'svelte-feather-icons';
import { ButtonColor, ButtonSize, ButtonVariant } from './button/type';
import Dialog from './Dialog.svelte';
import DropZone from './DropZone.svelte';
import TableSelection from './TableSelection.svelte';
let filterOptions: Record<string, unknown> = {};
const sliderDisplay = (filterName: string) => {
switch (filterName) {
case 'size':
return (value: number) => `${(value / 1024 / 1024).toFixed(3)} MB`;
default:
return (value: number) => `${value}`;
}
};
let optionsStore: GraphOptions['optionsStore'] | undefined;
let isFilterBarOpen: boolean = true;
const applyFilter = () => {
console.log('Applying filter', filterOptions);
// TODO: validate filter options
filterStore.setGraphOptions(filterOptions);
// filterStore.setGraphOptions(filterOptions);
};
onMount(async () => {
// load initial options from store
const values = get(filterStore);
console.log('filterStore', values);
// If present load initial values
if (values.graphOptions) {
filterOptions = values.graphOptions.getCurrentOptions();
}
// if (values.graphOptions) {
// filterOptions = values.graphOptions.getCurrentOptions();
// }
filterStore.subscribe((value) => {
if (value.graphOptions) {
filterOptions = { ...value.graphOptions.getCurrentOptions() };
}
});
// filterStore.subscribe((value) => {
// if (value.graphOptions) {
// filterOptions = { ...value.graphOptions.getCurrentOptions() };
// }
// });
});
const onInput = (value: number, label?: string) => {
if (label) {
filterOptions[label] = value;
}
};
$: if ($filterStore.graphOptions) {
optionsStore = $filterStore.graphOptions.optionsStore;
}
const optionConstructor = (value: string, index: number, meta: unknown) => ({
label: value,
value: value,
id: index,
initiallySelected: filterOptions[meta as string] === value
});
function fadeSlide(node: HTMLElement, options?: { duration?: number }) {
return {
duration: options?.duration || 100,
css: (t: number) => `
transform: translateY(${(1 - t) * -20}px) scale(${0.9 + t * 0.1});
opacity: ${t};
`
};
}
const onOptionSelected = (selected: { label: string; value: string }[], meta?: unknown) => {
const key = meta as string;
if (selected.length > 0) {
filterOptions[key] = selected[0].value;
} else {
delete filterOptions[key];
}
};
function _toggleFilterBar() {
isFilterBarOpen = !isFilterBarOpen;
}
</script>
<div class="absolute right-4 pt-4 t-0 bottom-0 w-96 min-h-full overflow-y-auto">
<!-- <Card>Available preloaded tables {$filterStore.preloadedTables.length}</Card> -->
{#if Object.keys($dataStore.tables).length > 0}
<Card title="Filter Family">
<p class="mb-4">
Loaded tables: {Object.entries($dataStore.tables).reduce(
(acc, [name, value]) => acc + ` ${name}`,
''
)}
</p>
<Button color="secondary" on:click={filterStore.reset}>Reset</Button>
</Card>
<Card title="Graph Type"
><Button
on:click={() => {
filterStore.selectGraphType(GraphType.PLANE);
}}>Plane mode</Button
></Card
<div class="mb-4 flex justify-end mr-1">
<Button
size={ButtonSize.SM}
color={isFilterBarOpen ? ButtonColor.PRIMARY : ButtonColor.SECONDARY}
on:click={_toggleFilterBar}
>
{#if $filterStore.graphOptions}
<Card title="Visualization options">
<div class="flex flex-col gap-2">
<!-- {#if typeof $dataStore.combinedSchema['mode'] !== 'undefined'}
<div class="py-1">
<SettingsIcon />
</div>
</Button>
</div>
{#if isFilterBarOpen}
<div transition:fadeSlide={{ duration: 100 }}>
<!-- <Card>Available preloaded tables {$filterStore.preloadedTables.length}</Card> -->
<Card>
<div class="flex justify-between items-center">
<h3 class="font-semibold text-lg">Loaded table</h3>
<Button size={ButtonSize.SM} on:click={filterStore.reset}>
<svelte:fragment slot="trailing">
<RefreshCcwIcon size="12" />
</svelte:fragment>
Reset</Button
>
</div>
<ul>
{#each Object.entries($dataStore.tables) as [tableName, table]}
<li class="flex py-1 justify-between items-center">
<div>{tableName}</div>
<Button variant={ButtonVariant.LINK} size={ButtonSize.SM}><XIcon size="15" /></Button>
</li>
{/each}
</ul>
<Dialog size="small">
<Button slot="trigger" size={ButtonSize.SM}>
<svelte:fragment slot="trailing">
<PlusIcon size="12" />
</svelte:fragment>
Load More</Button
>
<TableSelection />
</Dialog>
{#if Object.keys($dataStore.tables).length > 0}
<Divider />
<h3 class="font-semibold text-lg mb-2">Graph Type</h3>
{#each Object.values(GraphType) as graphType}
<Button
color={graphType === $filterStore.graphOptions?.getType()
? ButtonColor.PRIMARY
: ButtonColor.SECONDARY}
on:click={() => filterStore.selectGraphType(graphType)}
>
<div class="flex gap-2 flex-col items-center">
<LayersIcon />
<p class="text-sm">{graphType}</p>
</div>
</Button>
{/each}
{#if optionsStore && $filterStore.graphOptions}
<Divider />
<h3 class="font-semibold text-lg">Visualization options</h3>
<div class="flex flex-col gap-2">
<!-- {#if typeof $dataStore.combinedSchema['mode'] !== 'undefined'}
<DropdownSelect
label="Groupings"
singular
@ -101,87 +140,23 @@
}))}
/>
{/if} -->
{#each Object.entries($filterStore.graphOptions.filterOptions) as [key, value]}
{#if value?.type === 'string'}
<DropdownSelect
label={value.label || key}
singular
onSelect={onOptionSelected}
meta={key}
values={value.options}
{optionConstructor}
/>
{:else if value?.type === 'number'}
<Slider
label={key}
initialValue={filterOptions[key]}
value={filterOptions[key]}
min={Math.min(...value.options)}
max={Math.max(...value.options)}
diplayFunction={sliderDisplay(key)}
{onInput}
/>
{/if}
{/each}
<!--
<DropdownSelect
label={`${axis} Axis`}
singular
selected={$filterStore.graphOptions?.options[axis] !== undefined
? [$filterStore.graphOptions?.options[axis]]
: undefined}
onSelect={(selected) => {
setAxisOptions(axis, selected.length > 0 ? selected[0] : undefined);
}}
options={Object.entries($dataStore.combinedSchema)
.filter(([_, value]) => value === 'number')
.map(([key]) => ({
label: key,
value: key
}))}
/>
{/each} -->
<Button color="primary" size="lg" on:click={applyFilter}>Visualize</Button>
</div>
</Card>
{/if}
{#if $dataStore.combinedSchema}
<!-- <Card title="Filters">
<div class="flex flex-col gap-2 h-[300px] overflow-y-scroll">
{#each Object.entries($dataStore.commonFilterOptions) as [filterName, filter], idx}
<div>
{#if filter.type === 'string'}
<DropdownSelect
label={filterName}
onSelect={(selected) => {
selectedFilterOptions[filterName] = {
options: selected,
type: filter.type
};
}}
options={filter.options.map((entry) => ({
label: entry,
value: entry
}))}
/>
{:else if filter.type === 'number'}
<Slider
label={filterName}
min={Math.min(...filter.options)}
max={Math.max(...filter.options)}
diplayFunction={sliderDisplay(filterName)}
onInput={(value) => {
selectedFilterOptions[filterName] = {
options: [value],
type: filter.type
};
}}
/>
{/if}
<div class="mb-4">
{#each Object.entries($filterStore.graphOptions.filterOptions ?? {}) as [key, value]}
{#if typeof value !== 'undefined'}
<OptionRenderer
onValueChange={$filterStore.graphOptions.setFilterOption}
option={value}
state={$optionsStore}
{key}
/>
{/if}
{/each}
</div>
<Button color="secondary" on:click={applyFilter}>Reset</Button>
</div>
{/each}
</div>
</Card> -->
{/if}
{/if}
{/if}
</Card>
</div>
{/if}
</div>

View File

@ -0,0 +1,86 @@
<script lang="ts">
import Slider from './Slider.svelte';
import DropdownSelect from './DropdownSelect.svelte';
import type { GraphFilterOption } from '$lib/store/filterStore/types';
type T = $$Generic;
export let key: keyof T;
export let option: GraphFilterOption<T>;
export let state: T | undefined = undefined;
export let style: string | undefined = undefined;
export let onValueChange: (key: keyof T, value?: T[keyof T]) => void;
const keyAsString = key.toString();
const onInput = (value: number, label?: string) => {
onValueChange(key, value as T[keyof T]);
};
const optionConstructor = (value: string, index: number, meta: unknown) => ({
label: value,
value: value,
id: index,
initiallySelected: state?.[meta as keyof T] === value
});
const onOptionSelected = (selected: { label: string; value: string }[], meta?: unknown) => {
const key = meta as keyof T;
if (selected.length > 0) {
onValueChange(key, selected[0].value as T[keyof T]);
} else {
onValueChange(key, undefined as T[keyof T]);
}
};
const sliderDisplay = (value: number) => {
switch (key) {
case 'size':
return `${(value / 1024 / 1024).toFixed(3)} MB`;
default:
return `${value}`;
}
};
</script>
<div {style}>
{#if option.type === 'string'}
<DropdownSelect
label={option.label || keyAsString}
singular
onSelect={onOptionSelected}
meta={key}
values={option.options}
{optionConstructor}
optionOrderer={(a, b) => a.label.localeCompare(b.label)}
/>
{:else if option.type === 'number'}
{@const min = Math.min(...option.options)}
{@const max = Math.max(...option.options)}
{@const initialValue = state?.[key]}
<Slider
label={option.label}
{initialValue}
{min}
{max}
displayFunction={sliderDisplay}
onChange={onInput}
/>
{:else if option.type === 'row'}
<div class="flex justify-stretch gap-2">
{#each option.items as item, index}
<svelte:self
{state}
style="flex-shrink: 0; flex-grow: {option.grow?.[index] ?? 1};"
{onValueChange}
option={item}
key={option.keys[index]}
/>
{/each}
</div>
{/if}
</div>

View File

@ -4,7 +4,7 @@
import CodeEditor from './CodeEditor.svelte';
import type { AsyncDuckDBConnection } from '@duckdb/duckdb-wasm';
import type { editor } from 'monaco-editor';
import Button from './Button.svelte';
import Button from './button/Button.svelte';
import { dataStore } from '$lib/store/dataStore/DataStore';
import Card from './Card.svelte';

View File

@ -1,39 +1,49 @@
<script lang="ts">
import Label from './base/Label.svelte';
import Tag from './base/Tag.svelte';
type DisplayFunction = (v: number) => string;
export let min: number = 0;
export let max: number = 100;
export let label: string | undefined = undefined;
export let diplayFunction: DisplayFunction = (v: number) => v + '';
export let displayFunction: DisplayFunction = (v: number) => v + '';
export let initialValue: number | undefined = undefined;
export let value: number = initialValue ?? 0;
export let onInput: (value: number, label?: string) => void | undefined;
export let onChange: (value: number, label?: string) => void | undefined;
// Execute in separate call to prevent infinite loop when label is set
const _onInput = (newValue: number) => {
console.log('Slider update', newValue, label);
onInput?.(newValue, label);
};
$: _onInput(value);
const _onChange = (e: Event) => {
onChange?.(value, label);
};
// $: _onInput(value);
$: value = initialValue ?? 0;
$: value = Math.max(min, Math.min(max, value)); // Ensure value stays within the min-max range
</script>
<div class="slider flex flex-col items-center">
{#if label !== undefined}<div class="font-bold text-sm px-4 pb-1 text-secondary-500">
{label}
</div>{/if}
<div class="slider mb-4 mt-2">
<div class="flex justify-between align-center">
<Label
>{#if label !== undefined}{label}:
{/if}
</Label>
<Tag>{displayFunction(value)}</Tag>
</div>
<input
type="range"
class="slider-input appearance-none w-full h-2 rounded border border-slate-300 bg-slate-200 transition-opacity"
class="slider-input appearance-none w-full h-2 rounded border border-slate-300 bg-slate-200 dark:border-background-600 dark:bg-background-800 transition-opacity"
bind:value
{min}
{max}
on:change={_onChange}
step="1"
/>
<div class="slider-value mt-2">{diplayFunction(value)}</div>
</div>

View File

@ -0,0 +1,29 @@
<script lang="ts">
import { dataStore } from '$lib/store/dataStore/DataStore';
import filterStore from '$lib/store/filterStore/FilterStore';
import type { FilterEntry } from '../../routes/graph/proxy+page.server';
import DropZone from './DropZone.svelte';
import DropdownSelect from './DropdownSelect.svelte';
function onSelectTable(selectionOptions: { label: string; value: FilterEntry }[]) {
const selectedTables = $filterStore.preloadedTables.filter(
(option) => option.value === selectionOptions[0].value
);
filterStore.selectBuildInTables(selectedTables.map((option) => option.value));
}
function filesDropped(files: FileList) {
dataStore.loadEntriesFromFileList(files);
}
</script>
<h2 class="text-2xl font-bold mb-5">Please select filter family</h2>
<p class="mb-2">from filter data provided by us</p>
<DropdownSelect onSelect={onSelectTable} options={$filterStore.preloadedTables} />
<div class="flex mt-5 mb-5 items-center justify-center">
<div class="border-t dark:border-background-700 w-full" />
<div class="mx-4 opacity-50">OR</div>
<div class="border-t w-full dark:border-background-700" />
</div>
<p class="mb-2">your own dataset in CSV format</p>
<DropZone onFileDropped={filesDropped} />

View File

@ -0,0 +1 @@
<hr class="mt-4 mb-2 dark:border-background-800" />

View File

@ -0,0 +1,3 @@
<label class="font-bold text-sm px-3 pb text-secondary-500 dark:text-secondary-400">
<slot />
</label>

View File

@ -0,0 +1,5 @@
<span
class="px-2 py-1 dark:border-background-600 dark:bg-background-800 border rounded-lg text-center"
>
<slot />
</span>

View File

@ -0,0 +1,82 @@
<script lang="ts">
import { ButtonSize, ButtonColor, ButtonVariant } from './type';
export let size: ButtonSize = ButtonSize.MD;
export let disabled: boolean = false;
export let color: ButtonColor = ButtonColor.SECONDARY;
export let variant: ButtonVariant = ButtonVariant.DEFAULT;
let className: string | undefined = undefined;
export { className as class };
let sizeClasses = classForColors(color);
let colorClasses = classForSize(size);
// Check if we have leading or trailing slots
$: hasLeadingSlot = !!$$slots.leading;
$: hasTrailingSlot = !!$$slots.trailing;
$: sizeClasses = classForSize(size);
$: colorClasses = classForVariant(variant, color);
function classForSize(size: ButtonSize): string {
switch (size) {
case ButtonSize.SM:
return 'px-2 py-1 text-xs shadow-sm rounded-md';
case ButtonSize.MD:
return 'px-4 py-2 text-sm shadow-mg rounded-lg';
case ButtonSize.LG:
return 'px-4 py-2 text-base shadow-lg rounded-xl';
}
}
function classForColors(color: ButtonColor): string {
switch (color) {
case ButtonColor.PRIMARY:
return 'text-white bg-primary-600 hover:bg-primary-700 border-[3px] border-primary-700 hover:border-primary-700';
case ButtonColor.SECONDARY:
return 'text-secondary-600 dark:text-background-50 bg-white border-secondary-300 dark:border-background-950 dark:bg-background-900 hover:bg-gray-50 dark:hover:bg-background-800 dark:hover:border-background-900';
}
}
function classForVariant(variant: ButtonVariant, color: ButtonColor) {
switch (variant) {
case ButtonVariant.OUTLINE:
switch (color) {
case ButtonColor.PRIMARY:
return 'text-primary-600 border-[3px] border-primary-600 hover:bg-primary-600 hover:text-white dark:hover:text-white dark:border-primary-600 dark:hover:bg-primary-600 dark:hover:border-primary-600';
case ButtonColor.SECONDARY:
return 'text-secondary-600 border-secondary-600 hover:bg-secondary-600 hover:text-white dark:hover:text-white dark:border-secondary-800 dark:hover:bg-secondary-600 dark:hover:border-secondary-600';
}
case ButtonVariant.DEFAULT:
return classForColors(color);
case ButtonVariant.LINK:
switch (color) {
case ButtonColor.PRIMARY:
return 'text-primary-600 hover:text-primary-700 dark:text-primary-100 dark:hover:text-primary-300 border-0';
case ButtonColor.SECONDARY:
return 'text-secondary-600 hover:text-secondary-700 dark:text-secondary-100 dark:hover:text-secondary-300 border-0';
}
}
}
</script>
<button
{disabled}
on:click
class:disabled
class:gap-2={hasLeadingSlot || hasTrailingSlot}
class:justify-center={!hasLeadingSlot && !hasTrailingSlot}
class:justify-between={hasLeadingSlot || hasTrailingSlot}
class="inline-flex items-center border {colorClasses} {sizeClasses} font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 {className ??
''}"
>
<slot name="leading" />
<slot />
<slot name="trailing" />
</button>
<style>
.disabled {
@apply cursor-not-allowed opacity-30 pointer-events-none;
}
</style>

View File

@ -0,0 +1,16 @@
export enum ButtonSize {
SM = 'sm',
MD = 'md',
LG = 'lg'
}
export enum ButtonColor {
PRIMARY = 'primary',
SECONDARY = 'secondary'
}
export enum ButtonVariant {
DEFAULT = 'default',
OUTLINE = 'outline',
LINK = 'link'
}

View File

@ -6,7 +6,8 @@
import Card from '../Card.svelte';
import type { PlaneGraphOptions } from '$lib/store/filterStore/graphs/plane';
import type { Unsubscriber } from 'svelte/store';
import Button from '../Button.svelte';
import Button from '../button/Button.svelte';
import type { Axis } from '$lib/rendering/AxisRenderer';
export let options: PlaneGraphOptions;
@ -25,6 +26,7 @@
const updateWithData = (data?: IPlaneRendererData) => {
if (!data || !dataRenderer) return;
dataRenderer.setAxisLabelRenderer(labelForAxis);
dataRenderer.updateWithData(data);
layerVisibility = dataRenderer.getLayerVisibility();
};
@ -40,6 +42,11 @@
unsubscriber?.();
});
const labelForAxis = (axis: Axis, segment: number) => {
$dataStore?.layers
return segment.toFixed(2) *;
};
const toggleLayerVisibility = (index: number) => {
dataRenderer.toggleLayerVisibility(index);
layerVisibility = dataRenderer.getLayerVisibility();

View File

@ -14,6 +14,8 @@ export interface AxisOptions {
lineWidth: number;
lineColor: THREE.ColorRepresentation;
label: AxisLabelOptions;
segments?: number;
labelForSegment?: (segment: number) => string;
}
export interface AxisRendererOptions {
@ -26,6 +28,12 @@ export interface AxisRendererOptions {
z: AxisOptions;
}
export enum Axis {
X = 'x',
Y = 'y',
Z = 'z'
}
export const defaultAxisLabelOptions = {
color: 0xcccccc,
font: 'Courier New',
@ -50,7 +58,8 @@ const defaultAxisRendererOptions: AxisRendererOptions = {
label: {
...defaultAxisLabelOptions,
text: 'x'
}
},
segments: 10
},
y: {
...defaultAxisOptions,
@ -68,6 +77,59 @@ const defaultAxisRendererOptions: AxisRendererOptions = {
}
};
class TextTexture extends THREE.CanvasTexture {
private canvas?: HTMLCanvasElement;
private context?: CanvasRenderingContext2D;
constructor(text: string, options: AxisLabelOptions) {
// Create a canvas element
const canvas = document.createElement('canvas');
canvas.width = text.length * options.fontSize;
canvas.height = options.fontSize * options.fontLineHeight;
// Get the 2D rendering context of the canvas
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Failed to create canvas context');
}
// Set the font properties
context.font = `${options.fontSize}px ${options.font}`;
// Set the text color
context.fillStyle = new THREE.Color(options.color).getStyle();
// Set the text alignment and baseline
context.textAlign = 'center';
context.textBaseline = 'middle';
// Calculate the text position in the center of the canvas
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const textX = canvasWidth / 2;
const textY = canvasHeight / 2;
// Render the text on the canvas
context.fillText(text, textX, textY);
// Create a texture from the canvas
super(canvas);
this.canvas = canvas;
this.context = context;
// TODO: maybe reuse canvas if we update the labels frequently
// Remove the canvas from the DOM
// document.removeChild(textCanvas);
}
dispose(): void {
this.canvas?.remove();
super.dispose();
}
}
export class AxisRenderer extends THREE.Object3D {
private options: AxisRendererOptions;
@ -143,10 +205,73 @@ export class AxisRenderer extends THREE.Object3D {
line.scale.set(scaleFactor.x, scaleFactor.y, scaleFactor.z);
axis.add(line);
// Draw line segments
console.log('!!!Drawing segments', options.segments, scaleFactor);
if (options.segments) {
const segmentDirection = direction
.clone()
.applyAxisAngle(new THREE.Vector3(1, 0, 1), Math.PI / 2);
console.log('Segment direction');
const segmentGap = 1 / (options.segments ?? 1);
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
// Get 90 deg angle to direction vector
new THREE.Vector3(100, 0, 0)
]);
for (let i = 0; i <= options.segments; i++) {
const material = new MeshLineMaterial({
color: options.lineColor,
lineWidth: options.lineWidth
});
const segmentLine = new THREE.Mesh(geometry, material);
segmentLine.position.set(0, 0, 0);
segmentLine.scale.set(scaleFactor.x, scaleFactor.y, scaleFactor.z);
axis.add(segmentLine);
const labelText =
options.labelForSegment?.(i) ?? (i / options.segments).toPrecision(2).toString();
const textWidth = labelText.length * 0.75;
const label = new THREE.Sprite(
new THREE.SpriteMaterial({
transparent: true,
depthWrite: false,
map: new TextTexture(labelText, {
...options.label,
fontSize: options.label.fontSize * 0.5
})
})
);
const labelOffset = direction
.clone()
.multiply(this.options.size.clone().multiplyScalar(i / options.segments));
const sizeScale = 4 / options.segments;
const nonMainAxisOffset = 0.2 + 0.1 * sizeScale;
label.position.set(
labelOffset.x === 0 ? -nonMainAxisOffset * this.options.labelScale : labelOffset.x,
labelOffset.y === 0 ? -nonMainAxisOffset * this.options.labelScale : labelOffset.y,
labelOffset.z === 0 ? -nonMainAxisOffset * this.options.labelScale : labelOffset.z
);
label.scale
.set(
this.options.labelScale * textWidth,
this.options.labelScale,
this.options.labelScale
)
.multiplyScalar(sizeScale);
axis.add(label);
}
}
const label = new THREE.Sprite(
new THREE.SpriteMaterial({
transparent: true,
map: this.createTextTexture(options.label)
depthWrite: false,
map: new TextTexture(options.label.text, options.label)
})
);
@ -167,51 +292,4 @@ export class AxisRenderer extends THREE.Object3D {
return axis;
};
private createTextTexture(options: AxisLabelOptions): THREE.Texture | null {
let textCanvas: HTMLCanvasElement | undefined = undefined;
let textContext: CanvasRenderingContext2D | undefined = undefined;
// Create a canvas element
const canvas = document.createElement('canvas');
canvas.width = options.text.length * options.fontSize;
canvas.height = options.fontSize * options.fontLineHeight;
// Get the 2D rendering context of the canvas
const context = canvas.getContext('2d');
if (!context) {
return null;
}
textCanvas = canvas;
textContext = context;
// Set the font properties
textContext.font = `${options.fontSize}px ${options.font}`;
// Set the text color
textContext.fillStyle = new THREE.Color(options.color).getStyle();
// Set the text alignment and baseline
textContext.textAlign = 'center';
textContext.textBaseline = 'middle';
// Calculate the text position in the center of the canvas
const canvasWidth = textCanvas.width;
const canvasHeight = textCanvas.height;
const textX = canvasWidth / 2;
const textY = canvasHeight / 2;
// Render the text on the canvas
textContext.fillText(options.text, textX, textY);
// Create a texture from the canvas
const texture = new THREE.CanvasTexture(textCanvas);
// TODO: maybe reuse canvas if we update the labels frequently
// Remove the canvas from the DOM
// document.removeChild(textCanvas);
return texture;
}
}

View File

@ -28,6 +28,7 @@ import { AxisRenderer } from './AxisRenderer';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { Easing, Tween } from '@tweenjs/tween.js';
const grayColorList = [
'#2B2B2B', // Charcoal Gray
@ -394,9 +395,25 @@ export class Minimap {
console.log('Looking at', lookDirection, this.trackedCamera);
const cameraPosition = lookDirection.multiplyScalar(300);
this.camera.position.copy(cameraPosition);
this.camera.lookAt(lookDirection);
const initialLookAt = this.camera.position.clone();
const cameraTarget = lookDirection.multiplyScalar(300);
// Compute distance between current camera position and target to compute duration
const distance = initialLookAt.distanceTo(cameraTarget);
const duration = Math.min(200, distance * 2);
new Tween(initialLookAt)
.to(cameraTarget, duration) // 2000 milliseconds
.easing(Easing.Cubic.In) // Easing type
.onUpdate(() => {
this.camera.position.set(initialLookAt.x, initialLookAt.y, initialLookAt.z);
// Called during the update of the tween. Useful if you need to perform actions during the animation.
})
.start();
// const cameraPosition = lookDirection.multiplyScalar(300);
// this.camera.position.copy(cameraPosition);
// this.camera.lookAt(lookDirection);
// Clear face selection to avoid issues
this.clearFaceSelection();

View File

@ -3,7 +3,7 @@ import { GraphRenderer } from './GraphRenderer';
import { DataPlaneShapeMaterial } from './materials/DataPlaneMaterial';
import { DataPlaneShapeGeometry } from './geometry/DataPlaneGeometry';
import { graphColors } from './colors';
import { AxisRenderer, defaultAxisLabelOptions } from './AxisRenderer';
import { Axis, AxisRenderer, defaultAxisLabelOptions } from './AxisRenderer';
export interface IPlaneRendererData {
// A list of ordered planes (e.g. bottom to top)
@ -29,13 +29,15 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
public data?: IPlaneRendererData;
private gridHelper?: THREE.GridHelper;
private group?: THREE.Group;
private depth = 0;
private width = 0;
private dataDepth = 0;
private dataWidth = 0;
private layers: THREE.Group[] = [];
private scale = 2;
private min = 0;
private max = 0;
private axisLabelRenderer?: (axis: Axis, segment: number) => string;
// Dots displayed on top of each data layer
private selectedInstanceId: number | undefined;
private selectedLayerIndex: number | undefined;
@ -62,6 +64,10 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
}
}
setAxisLabelRenderer(renderer?: (axis: Axis, segment: number) => string): void {
this.axisLabelRenderer = renderer;
}
setup(renderContainer: HTMLElement, scene: THREE.Scene, camera: THREE.Camera): void {
super.setup(renderContainer, scene, camera);
@ -70,10 +76,9 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
}
setScale(scale: THREE.Vector3): void {
console.log('Setting scale', scale);
this.size = scale;
this.group?.scale.copy(scale).multiplyScalar(0.25);
// this.group?.scale.copy(scale).multiply(dataScale).multiplyScalar(0.25);
this.group?.position.setY(-0.1 * scale.y);
this.group?.scale.copy(scale).multiplyScalar(1 / (this.scale * 2));
}
getIntersections(raycaster: THREE.Raycaster): THREE.Intersection[] {
@ -118,9 +123,9 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
if (this.onDataPointSelected) {
const point = new THREE.Vector3(
this.selectedInstanceId % this.width,
this.selectedInstanceId % this.dataDepth,
this.selectedLayerIndex,
Math.floor(this.selectedInstanceId / this.width)
Math.floor(this.selectedInstanceId / this.dataWidth)
);
const value = this.data?.layers[meshIndex].points[point.z][point.x];
@ -197,11 +202,11 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
const sphereGeo = new THREE.SphereGeometry(0.008);
this.data = data;
const dataWidth = data.layers[0].points[0].length;
let globalMin = Infinity;
let globalMax = -Infinity;
this.layers = data.layers.map((layer, index) => {
console.log('Layer:', layer.name, 'Min:', layer.min, 'Max:', layer.max);
globalMax = Math.max(globalMax, layer.max);
globalMin = Math.min(globalMin, layer.min);
@ -213,7 +218,7 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
const mat = new THREE.MeshLambertMaterial({
color: color,
opacity: 1,
transparent: true,
depthWrite: true,
// clipIntersection: true,
// clipShadows: true,
side: THREE.DoubleSide
@ -225,8 +230,8 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
// Add metadata to mesh
mesh.userData = { index, name: layer.name, meta: layer.meta };
this.depth = geo.planeDims.depth;
this.width = geo.planeDims.width;
this.dataDepth = geo.planeDims.depth;
this.dataWidth = geo.planeDims.width;
group.add(layerGroup);
return layerGroup;
@ -249,7 +254,7 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
continue;
}
const sphereMat = new THREE.MeshBasicMaterial({ color: 0xeeeeff });
const sphereMat = new THREE.MeshBasicMaterial({ color: 0xeeeeff, depthWrite: false });
const dotMesh = new THREE.InstancedMesh(sphereGeo, sphereMat, geo.pointsPerPlane);
dotMesh.userData = { index };
// dotMesh.renderOrder = 10;
@ -277,11 +282,16 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
layerGroup.children[0].scale.y = dataScaleFactor;
layerGroup.add(dotMesh);
// Move layer group by tine amount to avoid z-fighting
// layerGroup.position.y = index * 0.00001;
}
this.min = globalMin;
this.max = globalMax;
console.log('Min:', this.min, 'Max:', this.max);
// const geometry = new DataPlaneShapeGeometry(highestValues, undefined);
// // Create a material with the custom fragment shader
@ -291,37 +301,82 @@ export class PlaneRenderer extends GraphRenderer<IPlaneRendererData> {
// group.add(mesh);
this.group = group;
this.setupGridHelper();
this.setupAxisRenderer();
this.setScale(this.size);
this.scene?.add(group);
}
private setupAxisRenderer() {
this.axisRenderer = new AxisRenderer({
// labelScale: 10,
size: new THREE.Vector3(2, 2, 2),
labelScale: 0.15,
x: {
label: { text: data.labels?.x ?? 'x' }
label: { text: this.data?.labels?.x ?? 'x' },
segments: this.dataWidth - 1,
labelForSegment: this.axisLabelRenderer
? (segment: number) => this.axisLabelRenderer?.(Axis.Y, segment)
: undefined
},
y: {
label: { text: data.labels?.y ?? 'y' }
label: { text: this.data?.labels?.y ?? 'y' },
segments: 100,
labelForSegment: this.axisLabelRenderer
? (segment: number) => this.axisLabelRenderer?.(Axis.Y, segment)
: undefined
},
z: {
label: { text: data.labels?.z ?? 'z' }
label: { text: this.data?.labels?.z ?? 'z' },
segments: this.dataDepth - 1,
labelForSegment: this.axisLabelRenderer
? (segment: number) => this.axisLabelRenderer?.(Axis.Z, segment)
: undefined
}
});
this.axisRenderer.position.x = -1;
this.axisRenderer.position.z = -1;
this.group.add(this.axisRenderer);
this.gridHelper = new THREE.GridHelper(2 * 4, (dataWidth - 1) * 4, 0x888888, 0x888888);
// Move grid helper by half a section to align with rendering
this.gridHelper.position.x += 1 / (dataWidth - 1);
this.gridHelper.position.z += 1 / (dataWidth - 1);
this.group?.add(this.axisRenderer);
}
this.group?.add(this.gridHelper);
private setupGridHelper() {
const baseScale = 2;
const overlapFactor = 1;
this.setScale(this.size);
const numWidthTiles = this.dataWidth - 1;
const numDepthTiles = this.dataDepth - 1;
const isWidthSmaller = numWidthTiles < numDepthTiles;
const largerSide = isWidthSmaller ? numDepthTiles : numWidthTiles;
this.gridHelper = new THREE.GridHelper(
baseScale * overlapFactor,
largerSide * overlapFactor,
0x888888,
0x888888
);
// Offset grid by half of the size
// gridHelper.position.x = -size / 2;
// gridHelper.position.z = -size / 2;
// this.gridHelper.position.x = 1 / numWidthTiles;
// this.gridHelper.position.z = -1 / numDepthTiles
// FIXME: random missalignment with some X/Z proportions
// Scale other axis to match
if (isWidthSmaller) {
const zSegmentSize = baseScale / largerSide / 2;
const xSegmentSize = zSegmentSize * (numDepthTiles / numWidthTiles);
this.gridHelper.scale.x = numDepthTiles / numWidthTiles;
// this.gridHelper.position.x = -xSegmentSize;
// this.gridHelper.position.z = -zSegmentSize;
} else {
const xSegmentSize = baseScale / largerSide;
const zSegmentSize = xSegmentSize * (numWidthTiles / numDepthTiles);
this.gridHelper.scale.z = numWidthTiles / numDepthTiles;
// this.gridHelper.position.z = -zSegmentSize;
// this.gridHelper.position.x = -xSegmentSize;
}
this.scene?.add(group);
this.group?.add(this.gridHelper);
}
}

View File

@ -32,7 +32,7 @@ export class DataPlaneShapeGeometry extends THREE.BufferGeometry {
data: Data,
previousData: Data | undefined = undefined,
normalized = false,
interpolateZeroes = true,
interpolateZeroes = false,
private drawsSideWalls = false,
private drawsBottom = false
) {
@ -67,8 +67,8 @@ export class DataPlaneShapeGeometry extends THREE.BufferGeometry {
}
}
this.width = this.normalizedData[0].length;
this.depth = this.normalizedData.length;
this.depth = this.normalizedData[0].length;
this.width = this.normalizedData.length;
if (previousData) {
if (previousData.length !== this.depth || previousData[0].length !== this.width) {
@ -77,8 +77,8 @@ export class DataPlaneShapeGeometry extends THREE.BufferGeometry {
}
if (interpolateZeroes) {
for (let z = 0; z < this.normalizedData.length; z++) {
for (let x = 0; x < this.normalizedData[z].length; x++) {
for (let x = 0; x < this.normalizedData.length; x++) {
for (let z = 0; z < this.normalizedData[x].length; z++) {
// Only interpolate within surface
if (
z < 1 ||
@ -89,7 +89,7 @@ export class DataPlaneShapeGeometry extends THREE.BufferGeometry {
continue;
}
const value = this.normalizedData[z][x];
const value = this.normalizedData[x][z];
if (value === 0) {
const interpolatedValue = this.interpolate(this.normalizedData, z, x);
if (interpolatedValue !== null) {
@ -202,7 +202,7 @@ export class DataPlaneShapeGeometry extends THREE.BufferGeometry {
// Add top plane coordinates
vertices[vertexIdx] = (x / (width - 1)) * 2.0 - 1.0; //x
vertices[vertexIdx + 1] = normalizedData[z][x]; //y
vertices[vertexIdx + 1] = normalizedData[x][z]; //y
vertices[vertexIdx + 2] = (z / (depth - 1)) * 2.0 - 1.0; //z
if (this.drawsBottom) {

View File

@ -1,6 +1,6 @@
import type { BaseStoreType } from './DataStore';
import { DataScaling, type FilterOptions } from './types';
import { get, type Writable } from 'svelte/store';
import { DataScaling, type FilterOptions, type IDataStore } from './types';
import { type Writable } from 'svelte/store';
interface ITiledDataRow {
mode: string;
@ -10,8 +10,20 @@ interface ITiledDataRow {
name: string;
}
export type ITiledDataOptions = {
xColumnName: string;
yColumnName: string;
zColumnName: string;
xTileCount?: number;
zTileCount?: number;
tileCount: number;
scaleY: DataScaling;
scaleX: DataScaling;
scaleZ: DataScaling;
};
// Store extension containing actions to load data, transform & drop data
export const dataStoreFilterExtension = (store: BaseStoreType, dataStore: Writable<IDataStore>) => {
export const dataStoreFilterExtension = (store: BaseStoreType) => {
const getFiltersOptions = async (
tableName: string,
fields: string[] = []
@ -34,71 +46,124 @@ export const dataStoreFilterExtension = (store: BaseStoreType, dataStore: Writab
return options;
};
const getSqlScaleWrapper = (scale: DataScaling, inner: string) => {
const getSqlScaleWrapper = (scale: DataScaling, inner: string, wrap = '') => {
switch (scale) {
case DataScaling.LINEAR:
return inner;
return `${wrap}${inner}${wrap}`;
case DataScaling.LOG:
return `LOG2(${inner})`;
return `LOG2(${wrap}${inner}${wrap})`;
}
};
const defaultTiledDataOptions = {
xColumnName: 'iterations',
yColumnName: 'fpr',
zColumnName: 'cpu_time',
tileCount: 20,
scaleY: DataScaling.LINEAR,
scaleX: DataScaling.LINEAR,
scaleZ: DataScaling.LINEAR
};
const getMinMax = async (
tableName: string,
columnName: string,
scale: DataScaling
): Promise<[number, number]> => {
const query = `SELECT MIN(${getSqlScaleWrapper(
scale,
columnName,
'"'
)}) AS min, MAX(${getSqlScaleWrapper(scale, columnName, '"')}) AS max FROM ${tableName}`;
const resp = await store.executeQuery(query);
if (!resp) {
throw new Error('Failed to get min/max');
}
const rows = resp.toArray();
if (rows.length !== 1) {
throw new Error('Invalid number of rows');
}
return [rows[0].min, rows[0].max];
};
const getMultiTableColumnRange = async (
tableNames: string[],
columnName: string,
)
const getTiledRows = async (
tableName: string,
mode: string,
options: {
xColumnName: string;
yColumnName: string;
zColumnName: string;
xTileCount: number;
zTileCount: number;
scale: DataScaling;
}
options: ITiledDataOptions,
tileAggregationMode: 'min' | 'max' | 'avg' | 'sum' = 'min',
groupBy?: string,
xRange?: [number, number],
yRange?: [number, number],
zRange?: [number, number]
): Promise<ITiledDataRow[]> => {
const xTileCount = options.xTileCount ?? options.tileCount;
const zTileCount = options.zTileCount ?? options.tileCount;
// Compute ranges for each axis
const [xMin, xMax] = xRange ?? (await getMinMax(tableName, options.xColumnName, options.scaleX));
const [yMin, yMax] = yRange ?? (await getMinMax(tableName, options.yColumnName, options.scaleY));
const [zMin, zMax] = zRange ?? (await getMinMax(tableName, options.zColumnName, options.scaleZ));
// FIXME: this currently incorrectly pairs up x and z values within a mode/group
const query = `WITH
${options.xColumnName}_min_max AS (
SELECT MIN(${options.xColumnName}) AS min_${options.xColumnName}, MAX(${
"${options.xColumnName}_min_max_x" AS (
SELECT MIN(${getSqlScaleWrapper(options.scaleX, options.xColumnName, '"')}) AS "min_${
options.xColumnName
}) AS max_${options.xColumnName}
}_x", MAX(${getSqlScaleWrapper(options.scaleX, options.xColumnName, '"')}) AS "max_${
options.xColumnName
}_x"
FROM "${tableName}"
),
${options.zColumnName}_min_max AS (
SELECT MIN(${options.zColumnName}) AS min_${options.zColumnName}, MAX(${
"${options.zColumnName}_min_max_z" AS (
SELECT MIN(${getSqlScaleWrapper(options.scaleZ, options.zColumnName, '"')}) AS "min_${
options.zColumnName
}) AS max_${options.zColumnName}
}_z", MAX(${getSqlScaleWrapper(options.scaleZ, options.zColumnName, '"')}) AS "max_${
options.zColumnName
}_z"
FROM "${tableName}"
),
${options.xColumnName}_bucket_sizes AS (
SELECT (max_${options.xColumnName} - min_${options.xColumnName}) / ${options.xTileCount} AS ${
"${options.xColumnName}_bucket_sizes_x" AS (
SELECT ("max_${options.xColumnName}_x" - "min_${options.xColumnName}_x") / ${xTileCount} AS "${
options.xColumnName
}_bucket_size
FROM ${options.xColumnName}_min_max
}_bucket_size_x"
FROM "${options.xColumnName}_min_max_x"
),
${options.zColumnName}_bucket_sizes AS (
SELECT (max_${options.zColumnName} - min_${options.zColumnName}) / ${options.zTileCount} AS ${
"${options.zColumnName}_bucket_sizes_z" AS (
SELECT ("max_${options.zColumnName}_z" - "min_${options.zColumnName}_z") / ${zTileCount} AS "${
options.zColumnName
}_bucket_size
FROM ${options.zColumnName}_min_max
}_bucket_size_z"
FROM "${options.zColumnName}_min_max_z"
)
SELECT mode,
${getSqlScaleWrapper(options.scale, `MIN(${options.yColumnName})`)} AS y,
FLOOR((${options.xColumnName} - min_${options.xColumnName}) / ${
SELECT ${groupBy ? 'mode,' : ''}
"${options.zColumnName}", "${options.xColumnName}",
${getSqlScaleWrapper(options.scaleY, `${tileAggregationMode}("${options.yColumnName}")`)} AS y,
FLOOR((${getSqlScaleWrapper(options.scaleX, options.xColumnName, '"')} - "min_${
options.xColumnName
}_bucket_size) AS x,
FLOOR((${options.zColumnName} - min_${options.zColumnName}) / ${
}_x") / "${options.xColumnName}_bucket_size_x") AS x,
FLOOR((${getSqlScaleWrapper(options.scaleZ, options.zColumnName, '"')} - "min_${
options.zColumnName
}_bucket_size) AS z,
MIN(${options.xColumnName}) as min_${options.xColumnName},
MIN(${options.zColumnName}) as min_${options.zColumnName}
}_z") / "${options.zColumnName}_bucket_size_z") AS z,
MIN(${getSqlScaleWrapper(options.scaleX, options.xColumnName, '"')}) as "min_${
options.xColumnName
}_x",
MIN(${getSqlScaleWrapper(options.scaleZ, options.zColumnName, '"')}) as "min_${
options.zColumnName
}_z"
FROM "${tableName}"
CROSS JOIN ${options.xColumnName}_min_max
CROSS JOIN ${options.xColumnName}_bucket_sizes
CROSS JOIN ${options.zColumnName}_min_max
CROSS JOIN ${options.zColumnName}_bucket_sizes
WHERE mode = '${mode}'
GROUP BY mode, x, z, name
ORDER BY x ASC, z ASC`;
CROSS JOIN "${options.xColumnName}_min_max_x"
CROSS JOIN "${options.xColumnName}_bucket_sizes_x"
CROSS JOIN "${options.zColumnName}_min_max_z"
CROSS JOIN "${options.zColumnName}_bucket_sizes_z"
${groupBy ? `WHERE mode = '${groupBy}'` : ''}
GROUP BY ${groupBy ? 'mode,' : ''} z, x, name, "${options.zColumnName}", "${options.xColumnName}"
ORDER BY z ASC, x ASC`;
try {
const resp = await store.executeQuery(query);
@ -115,28 +180,29 @@ export const dataStoreFilterExtension = (store: BaseStoreType, dataStore: Writab
const getTiledData = async (
tableName: string,
mode: string,
options: {
xColumnName: string;
yColumnName: string;
zColumnName: string;
xTileCount: number;
zTileCount: number;
scale: DataScaling;
}
groupBy?: string,
_options: Partial<ITiledDataOptions> = {},
xRange?: [number, number],
yRange?: [number, number],
zRange?: [number, number]
): Promise<{
data: Float32Array[];
min: number;
max: number;
queryResult?: ITiledDataRow[];
}> => {
const options = {
...defaultTiledDataOptions,
..._options
};
try {
const rows = await getTiledRows(tableName, mode, options);
const rows = await getTiledRows(tableName, options, 'min', groupBy, xRange, yRange, zRange);
// Transform rows into a 2D array for display
const data = Array.from(
{ length: options.xTileCount },
() => new Float32Array(options.zTileCount)
{ length: (options.xTileCount ?? options.tileCount) + 1 },
() => new Float32Array((options.xTileCount ?? options.tileCount) + 1)
);
let min = Number.MAX_VALUE;
@ -145,9 +211,7 @@ export const dataStoreFilterExtension = (store: BaseStoreType, dataStore: Writab
rows.forEach((r) => {
min = Math.min(min, r.y);
max = Math.max(max, r.y);
const x = Math.min(r.x, options.xTileCount - 1);
const z = Math.min(r.z, options.zTileCount - 1);
data[x][z] = r.y;
data[r.x][r.z] = r.y;
});
return {
@ -168,6 +232,7 @@ export const dataStoreFilterExtension = (store: BaseStoreType, dataStore: Writab
return {
getFiltersOptions,
getTiledData
getTiledData,
getMinMax
};
};

View File

@ -3,6 +3,7 @@ import type { BaseStoreType } from './DataStore';
import type { IDataStore, ITableEntry, TableSchema } from './types';
import { DuckDBDataProtocol } from '@duckdb/duckdb-wasm';
import { TableSource, type ITableReference } from '../filterStore/types';
import notificationStore from '../notificationStore';
// Store extension containing actions to load data, transform & drop data
export const dataStoreLoadExtension = (store: BaseStoreType, dataStore: Writable<IDataStore>) => {
@ -88,7 +89,14 @@ export const dataStoreLoadExtension = (store: BaseStoreType, dataStore: Writable
await db.registerFileHandle(tableName, file, DuckDBDataProtocol.BROWSER_FILEREADER, true);
console.log('Registered file handle:', tableName);
} catch (e) {
console.error(`Failed to load table ${tableName} from file ${file.name}:`, e);
const msg = `Failed to load table ${tableName} from file ${file.name}`;
console.error(msg, e);
notificationStore.addNotification({
id: Date.now(),
message: msg,
description: (e as Error)?.message,
type: 'error'
});
throw e;
} finally {
store.setIsLoading(false);
@ -139,7 +147,14 @@ export const dataStoreLoadExtension = (store: BaseStoreType, dataStore: Writable
return tableEntry;
} catch (e) {
console.error(`Failed to load table ${tableName} at ${path}:`, e);
const msg = `Failed to load table ${tableName} from path ${path}`;
console.error(msg, e);
notificationStore.addNotification({
id: Date.now(),
message: msg,
description: (e as Error)?.message,
type: 'error'
});
throw e;
} finally {
if (shouldSetLoading) {
@ -322,10 +337,6 @@ export const dataStoreLoadExtension = (store: BaseStoreType, dataStore: Writable
return tableDefinitions;
} catch (e) {
console.error('Failed to load CSVs:', e);
dataStore.update((store) => {
store.tables = {};
return store;
});
}
});

View File

@ -43,13 +43,11 @@ const initialStore: IFilterStore = {
const urlEncoder: UrlEncoder = (key, type, value) => {
if (key === 'graphOptions') {
const graphOptions = value as GraphOptions;
const state = graphOptions.getCurrentOptions();
return urlEncodeObject({
type: graphOptions.getType(),
state: state
});
console.log('Encoding graph options', value);
if (!value) {
return undefined;
}
return (value as GraphOptions).getType();
}
const encodedValue = defaultUrlEncoder(key, type, value);
// console.log('Encoding', key, encodedValue, value, JSON.stringify(value));
@ -63,18 +61,10 @@ let _graphOptions: GraphOptions | null = null;
const urlDecoder: UrlDecoder = (key, type, value) => {
if (key === 'graphOptions') {
const val = urlDecodeObject(value) as { type: GraphType; state: unknown } | null;
if (!val) {
return null;
}
const { type, state } = val;
switch (type) {
const graphType = value as GraphType;
switch (graphType) {
case GraphType.PLANE: {
console.debug('Decoded plane graph options', state);
_graphOptions = new PlaneGraphOptions(state as Partial<PlaneGraphState>);
_graphOptions = new PlaneGraphOptions();
return undefined;
}
}
@ -162,9 +152,9 @@ const _filterStore = () => {
try {
await selectTables(selectedTables);
if (_graphOptions) {
if (_graphOptions && _graphOptions !== null) {
console.log('Applying graph options', _graphOptions);
_graphOptions.updateFilterOptions();
_graphOptions.reloadFilterOptions();
update((store) => {
store.graphOptions = _graphOptions;
return store;
@ -214,7 +204,7 @@ const _filterStore = () => {
update((store) => {
Object.entries(options).forEach(([key, value]) => {
graphOptions.setStateValue(key, value);
// graphOptions.setStateValue(key, value);
});
return store;
});

View File

@ -1,96 +1,188 @@
import type { IPlaneRendererData } from '$lib/rendering/PlaneRenderer';
import { dataStore } from '$lib/store/dataStore/DataStore';
import { get, readonly, writable, type Writable } from 'svelte/store';
import { GraphOptions, type Paths, setObjectValue, type PathValue, GraphType } from '../types';
import { get, readonly, writable, type Readable, type Writable } from 'svelte/store';
import { GraphOptions, GraphType } from '../types';
import { DataScaling } from '$lib/store/dataStore/types';
import { graphColors, graphColors2 } from '$lib/rendering/colors';
import { graphColors } from '$lib/rendering/colors';
import type { ITiledDataOptions } from '$lib/store/dataStore/filterActions';
import { urlDecodeObject, urlEncodeObject, withSingleKeyUrlStorage } from '$lib/store/urlStorage';
import { withLogMiddleware } from '$lib/store/logMiddleware';
export interface IPlaneGraphState {
// Y Axis display scaling
axisRanges: Partial<{
x: [number, number];
y: [number, number];
z: [number, number];
}>;
xTileCount: number;
zTileCount: number;
type RequiredOptions = ITiledDataOptions & {
groupBy: string;
};
xColumnName: string;
yColumnName: string;
zColumnName: string;
// Data scaling
yScale: DataScaling;
xScale: DataScaling;
zScale: DataScaling;
normalized: string;
}
export type IPlaneGraphState = (
| ({
isValid: true;
} & RequiredOptions)
| ({
isValid: false;
} & Partial<RequiredOptions>)
) & { isRendered: boolean };
export class PlaneGraphOptions extends GraphOptions<
Partial<IPlaneGraphState>,
Partial<RequiredOptions>,
IPlaneRendererData | undefined
> {
private state: Partial<IPlaneGraphState>;
private _dataStore: Writable<IPlaneRendererData | undefined> = writable(undefined);
private _optionsStore: Writable<Partial<IPlaneGraphState>> = writable({});
private _dataStore: Writable<IPlaneRendererData | undefined>;
private _optionsStore: Writable<IPlaneGraphState>;
public dataStore = readonly(this._dataStore);
public optionsStore = readonly(this._optionsStore);
public dataStore: Readable<IPlaneRendererData | undefined>;
public optionsStore: Readable<Partial<RequiredOptions>>;
constructor(initialState: Partial<IPlaneGraphState> = {}) {
constructor(initialState: Partial<RequiredOptions> = {}) {
super({});
this._optionsStore.set(initialState);
this._dataStore = writable(undefined);
this.dataStore = readonly(this._dataStore);
this.updateFilterOptions();
// Check if options are initially valid
const initialOptions = {
isRendered: false,
isValid: this.isValid(initialState),
...initialState
} as IPlaneGraphState;
this._optionsStore = withLogMiddleware(
withSingleKeyUrlStorage(
writable(initialOptions),
'filterStore',
(state) => {
return urlEncodeObject(state);
},
(value) => {
if (!value || value === 'undefined') {
return initialOptions;
}
const state = urlDecodeObject(value);
return {
isRendered: false,
isValid: this.isValid(state),
...state
} as IPlaneGraphState;
}
),
'PlaneGraphOptions',
{
color: 'orange'
}
);
this.optionsStore = readonly(this._optionsStore);
this.reloadFilterOptions();
this.state = initialState;
console.log('Created plane graph options', this.state);
this.applyOptionsIfValid();
}
public updateFilterOptions() {
public setFilterOption = <K extends keyof RequiredOptions>(key: K, value: RequiredOptions[K]) => {
this._optionsStore.update((store) => {
(store as Partial<RequiredOptions>)[key] = value;
store.isValid = this.isValid(store);
return store;
});
this.applyOptionsIfValid();
};
public toString(): string {
const state = get(this._optionsStore);
console.log('Encoding', state);
return urlEncodeObject({
type: this.getType(),
state
});
}
private isValid(state: Partial<RequiredOptions> | undefined): boolean {
if (!state) {
return false;
}
const { xColumnName, yColumnName, zColumnName, tileCount, groupBy } = state;
return (
typeof groupBy === 'string' &&
typeof xColumnName === 'string' &&
typeof yColumnName === 'string' &&
typeof zColumnName === 'string' &&
typeof tileCount === 'number'
);
}
public reloadFilterOptions() {
const data = get(dataStore);
const stringTableColumns = Object.entries(data.combinedSchema)
.filter(([, type]) => type === 'string')
.map(([column]) => column);
const numberTableColumns = Object.entries(data.combinedSchema)
.filter(([, type]) => type === 'number')
.map(([column]) => column);
this.filterOptions = {
xColumnName: {
groupBy: {
type: 'string',
options: numberTableColumns,
label: 'X Axis'
options: stringTableColumns,
label: 'Group By'
},
zColumnName: {
type: 'string',
options: numberTableColumns,
label: 'Z Axis'
xColumnName: {
type: 'row',
keys: ['xColumnName', 'scaleX'],
grow: [0.7, 0.3],
items: [
{
type: 'string',
options: numberTableColumns,
label: 'X Axis'
},
{
type: 'string',
options: [DataScaling.LINEAR, DataScaling.LOG],
label: 'X Scale'
}
]
},
yColumnName: {
type: 'string',
options: numberTableColumns,
label: 'Y Axis'
type: 'row',
keys: ['yColumnName', 'scaleY'],
grow: [0.7, 0.3],
items: [
{
type: 'string',
options: numberTableColumns,
label: 'Y Axis'
},
{
type: 'string',
options: [DataScaling.LINEAR, DataScaling.LOG],
label: 'Y Scale'
}
]
},
xTileCount: {
zColumnName: {
type: 'row',
keys: ['zColumnName', 'scaleZ'],
grow: [0.7, 0.3],
items: [
{
type: 'string',
options: numberTableColumns,
label: 'Z Axis'
},
{
type: 'string',
options: [DataScaling.LINEAR, DataScaling.LOG],
label: 'Z Scale'
}
]
},
tileCount: {
type: 'number',
options: [1, 2, 4, 8, 16, 32, 64],
label: 'X Tile Count'
},
zTileCount: {
type: 'number',
options: [1, 2, 4, 8, 16, 32, 64],
label: 'Z Tile Count'
},
yScale: {
type: 'string',
options: [DataScaling.LINEAR, DataScaling.LOG],
label: 'Y Scale'
},
normalized: {
type: 'string',
label: 'Normalized',
options: ['true', 'false']
options: [2, 128],
label: 'Tile Count'
}
// zTileCount: {
// type: 'number',
// options: [2, 128],
// label: 'Z Tile Count'
// }
};
}
@ -99,61 +191,69 @@ export class PlaneGraphOptions extends GraphOptions<
}
public getCurrentOptions() {
return this.state;
}
public isValid(): boolean {
const requiredFields: (keyof IPlaneGraphState)[] = [
'xTileCount',
'zTileCount',
'xColumnName',
'yColumnName',
'zColumnName'
];
return requiredFields.every((field) => this.state[field] !== undefined);
}
public setStateValue<P extends Paths<Partial<IPlaneGraphState>>>(
path: P,
value: PathValue<Partial<IPlaneGraphState>, P>
) {
(setObjectValue as any)(this.state, path, value as unknown);
return get(this._optionsStore);
}
public async applyOptionsIfValid() {
const isValid = this.isValid();
if (!isValid) {
console.error('Invalid graph options', this.state);
const state = get(this._optionsStore);
if (state.isValid !== true) {
return;
}
// Query data required for this graph
const { xColumnName, yColumnName, zColumnName, xTileCount, zTileCount, yScale } = this
.state as IPlaneGraphState;
console.log('Querying tiled data for plane graph', this.state);
// Get available tables
const data = get(dataStore);
// Get all layers
try {
const options = await dataStore.getDistinctValues('bloom', 'mode');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// const options = await dataStore.getDistinctValues('bloom', state.groupBy);
const tables = Object.keys(data.tables);
const xAxisMinMax = await Promise.all(
tables.map((table) => dataStore.getMinMax(table, state.xColumnName, state.scaleX))
);
const yAxisMinMax = await Promise.all(
tables.map((table) => dataStore.getMinMax(table, state.yColumnName, state.scaleY))
);
const zAxisMinMax = await Promise.all(
tables.map((table) => dataStore.getMinMax(table, state.zColumnName, state.scaleZ))
);
const xAxisRange = xAxisMinMax.reduce(
(acc, [min, max]) => [Math.min(acc[0], min), Math.max(acc[1], max)],
[Infinity, -Infinity]
);
const yAxisRange = yAxisMinMax.reduce(
(acc, [min, max]) => [Math.min(acc[0], min), Math.max(acc[1], max)],
[Infinity, -Infinity]
);
const zAxisRange = zAxisMinMax.reduce(
(acc, [min, max]) => [Math.min(acc[0], min), Math.max(acc[1], max)],
[Infinity, -Infinity]
);
console.log('Z axis min/max', zAxisRange);
console.log('X axis min/max', xAxisRange);
console.log('Y axis min/max', yAxisRange);
const promise = await Promise.all(
options.map((mode) =>
dataStore.getTiledData('bloom', mode as string, {
xColumnName,
yColumnName,
zColumnName,
xTileCount,
zTileCount,
scale: yScale
})
tables.map((table) =>
dataStore.getTiledData(
table,
undefined,
state as RequiredOptions,
xAxisRange,
yAxisRange,
zAxisRange
)
)
);
const layers = promise.map((data, index) => ({
points: data.data,
min: data.min,
max: data.max,
name: options[index] as string,
name: tables[index] as string,
color: graphColors[index % graphColors.length],
meta: {
rows: data.queryResult
@ -163,40 +263,13 @@ export class PlaneGraphOptions extends GraphOptions<
this._dataStore.set({
layers,
labels: {
x: xColumnName,
y: yColumnName,
z: zColumnName
x: state.xColumnName,
y: state.yColumnName,
z: state.zColumnName
},
normalized: this.state.normalized === 'true',
normalized: false,
scaleY: 10
});
// this.renderer.updateWithData({
// layers,
// labels: {
// x: xColumnName,
// y: yColumnName,
// z: zColumnName
// },
// normalized: this.state.normalized === 'true',
// scaleY: 10
// });
// // FIXME: restructure this somewhere else
// this.renderer.onDataPointSelected = (point, meta) => {
// FilterStore.update((store) => {
// if (!point) {
// store.selectedPoint = undefined;
// return store;
// }
// store.selectedPoint = {
// dataPosition: point,
// instanceId: 0,
// meta: meta
// };
// return store;
// });
// };
} catch (e) {
console.error('Failed to load tiled data:', e);
return;

View File

@ -50,29 +50,38 @@ export function setObjectValue<T extends object, P extends Paths<T>>(
current[keys[keys.length - 1]] = value;
}
export type GraphFilterOptions<T> = Partial<
Record<
keyof T,
| {
type: 'string';
options: string[];
label: string;
}
| {
type: 'number';
options: number[];
label: string;
}
| {
type: 'boolean';
label: string;
}
>
>;
export type SimpleGraphFilterOption =
| {
type: 'string';
required?: boolean;
options: string[];
label: string;
}
| {
type: 'number';
options: number[];
label: string;
}
| {
type: 'boolean';
label: string;
};
export type GraphFilterOption<T> =
| SimpleGraphFilterOption
| {
type: 'row';
keys: (keyof T)[];
grow?: number[]; // Flex grow factor default 1 for all
items: SimpleGraphFilterOption[];
};
export type GraphFilterOptions<T> = Partial<Record<keyof T, GraphFilterOption<T>>>;
export abstract class GraphOptions<
Options extends Record<string, unknown> = Record<string, unknown>,
Data = unknown
Data = unknown,
K extends keyof Options = keyof Options
> {
public active = false;
public filterOptions: GraphFilterOptions<Options>;
@ -81,17 +90,18 @@ export abstract class GraphOptions<
this.filterOptions = filterOptions;
}
public abstract isValid(): boolean;
public abstract getType(): GraphType;
public abstract setStateValue<P extends Paths<Options>>(
path: P,
value: PathValue<Options, P>
): void;
public abstract applyOptionsIfValid(): Promise<void>;
public abstract updateFilterOptions(): void;
public abstract reloadFilterOptions(): void;
public abstract setFilterOption(key: K, value: Options[K]): void;
public abstract dataStore: Readable<Data | undefined>;
public abstract optionsStore: Readable<Options | undefined>;
public abstract toString(): string;
public static fromString(str: string): GraphOptions | null {
return null;
}
}
export interface IFilterStore {

View File

@ -0,0 +1,36 @@
import { writable } from 'svelte/store';
import { withLogMiddleware } from './logMiddleware';
export interface INotification {
id: number;
message: string;
description?: string;
callback?: () => void;
dismissDuration?: number;
type: 'success' | 'error' | 'info';
}
const notificationStore = () => {
const store = withLogMiddleware(writable<INotification[]>([]), 'notificationStore');
const addNotification = (notification: INotification) => {
store.update((notifications) => {
notifications.push(notification);
return notifications;
});
};
const removeNotification = (id: number) => {
store.update((notifications) => {
return notifications.filter((notification) => notification.id !== id);
});
};
return {
subscribe: store.subscribe,
addNotification,
removeNotification
};
};
export default notificationStore();

View File

@ -54,6 +54,59 @@ export const defaultUrlEncoder = (
export type UrlDecoder = typeof defaultUrlDecoder;
export type UrlEncoder = typeof defaultUrlEncoder;
export const withSingleKeyUrlStorage = <S>(
store: Writable<S>,
key: string,
encoder: (state: S) => string | null,
decoder: (value?: string | null) => S
) => {
// Restore state from storage
const params = new URLSearchParams(location.search);
// Set initial store state
store.set(decoder(params.get(key)));
const encodeValues = (store: S) => {
const params = new URLSearchParams(location.search);
const encodedValue = encoder(store);
if (encodedValue === null || encodedValue === undefined) {
params.delete(key);
return params;
}
params.set(key, encodedValue);
return params;
};
// Wrap update with storage functionality
const oldUpdate = store.update;
store.update = (updater: Updater<S>) => {
oldUpdate((state) => {
const newState = updater(state);
const params = encodeValues(newState);
// Check if anything has changed
if (params.toString() === location.search.slice(1)) {
return newState;
}
history.replaceState(null, '', `${location.pathname}?${params.toString()}`);
return newState;
});
};
const oldSet = store.set;
store.set = (state: S) => {
oldSet(state);
const newState = get(store);
const params = encodeValues(newState);
history.replaceState(null, '', `${location.pathname}?${params.toString()}`);
};
return store;
};
export const withUrlStorage = <S extends object, T extends EncodableTypes = EncodableTypes>(
store: Writable<S>,
storeKeys: Partial<Record<keyof S, T>>,
@ -96,7 +149,7 @@ export const withUrlStorage = <S extends object, T extends EncodableTypes = Enco
}
const encodedValue = encoder(key.toString(), type, value);
if (encodedValue === null) {
if (encodedValue === null || encodedValue === undefined) {
params.delete(key.toString());
return;
}

View File

@ -1,7 +1,12 @@
<script lang="ts">
import { onMount } from 'svelte';
import '@fontsource/inter';
import '../app.css';
import { Theme, settingsStore } from '$lib/store/SettingsStore';
import notificationStore from '$lib/store/notificationStore';
import Button from '$lib/components/button/Button.svelte';
import { XIcon } from 'svelte-feather-icons';
import { ButtonColor, ButtonSize, ButtonVariant } from '$lib/components/button/type';
function setDarkMode(enabled: boolean) {
if (enabled) {
@ -30,9 +35,29 @@
</script>
<div
class="min-h-screen relative isolate max-h-screen max-w-full bg-slate-100 dark:bg-background-950"
class="min-h-screen relative isolate max-h-screen max-w-full bg-slate-100 dark:bg-background-950 dark:text-slate-200"
>
<main class="dark:text-slate-200">
<div
class="absolute bottom-5 flex flex-col gap-2 max-h-96 max-w-[400px] overflow-hidden right-5 z-50"
>
{#each $notificationStore as notification}
<div class="px-3 py-2 bg-purple-600 rounded-lg max-w-full break-words">
<div class="flex gap-1">
<h3 class="font-bold line-clamp-1 break-all">{notification.message}</h3>
<Button
variant={ButtonVariant.LINK}
on:click={() => notificationStore.removeNotification(notification.id)}
size={ButtonSize.SM}
color={ButtonColor.SECONDARY}><XIcon size={'16'} /></Button
>
</div>
{#if notification.description} <p class="line-clamp-2">{notification.description}</p> {/if}
{#if notification.callback} <button on:click={notification.callback}>More</button> {/if}
</div>
{/each}
</div>
<main>
<slot />
</main>
</div>

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { base } from '$app/paths';
import Button from '$lib/components/Button.svelte';
import Button from '$lib/components/button/Button.svelte';
import { ButtonColor, ButtonSize } from '$lib/components/button/type';
import DropZone from '$lib/components/DropZone.svelte';
import GridBackground from '$lib/components/GridBackground.svelte';
import MessageCard from '$lib/components/MessageCard.svelte';
@ -23,8 +24,8 @@
ut et quos animi eveniet.
</p>
<div class="flex gap-2 mt-6 h-min">
<Button size="lg">Upload CSV</Button><a href="{base}/graph"
><Button color="primary" size="lg">View Existing</Button></a
<Button size={ButtonSize.LG}>Upload CSV</Button><a href="{base}/graph"
><Button size={ButtonSize.LG} color={ButtonColor.SECONDARY}>View Existing</Button></a
>
</div>
<DropZone onFileDropped={onCsvDropped} />

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { PageServerData } from './$types';
import Button from '$lib/components/Button.svelte';
import Button from '$lib/components/button/Button.svelte';
import DropdownSelect from '$lib/components/DropdownSelect.svelte';
import BasicGraph from '$lib/components/BasicGraph.svelte';
import LoadingOverlay from '$lib/components/LoadingOverlay.svelte';
@ -21,6 +21,8 @@
import { GraphOptions } from '$lib/store/filterStore/types';
import { PlaneGraphOptions } from '$lib/store/filterStore/graphs/plane';
import type { FilterEntry } from './proxy+page.server';
import TableSelection from '$lib/components/TableSelection.svelte';
import { ButtonColor, ButtonSize } from '$lib/components/button/type';
export let data: PageServerData;
@ -33,20 +35,9 @@
let hoverPosition: Vector2 | undefined = undefined;
function filesDropped(files: FileList) {
dataStore.loadEntriesFromFileList(files);
}
function onHover(position: Vector2, object?: THREE.Object3D) {
hoverPosition = position;
}
function onSelectTable(selectionOptions: { label: string; value: FilterEntry }[]) {
const selectedTables = $filterStore.preloadedTables.filter(
(option) => option.value === selectionOptions[0].value
);
filterStore.selectBuildInTables(selectedTables.map((option) => option.value));
}
</script>
<div>
@ -74,24 +65,17 @@
{#if $filterStore.selectedTables.length === 0}
<div class="h-full w-full flex flex-col gap-10 justify-center items-center">
<MessageCard>
<h2 class="text-2xl font-bold mb-5">Please select filter family</h2>
<p class="mb-2">from filter data provided by us</p>
<DropdownSelect onSelect={onSelectTable} options={$filterStore.preloadedTables} />
<div class="flex mt-5 mb-5 items-center justify-center">
<div class="border-t dark:border-background-700 w-full" />
<div class="mx-4 opacity-50">OR</div>
<div class="border-t w-full dark:border-background-700" />
</div>
<p class="mb-2">your own dataset in CSV format</p>
<DropZone onFileDropped={filesDropped} />
<TableSelection />
</MessageCard>
</div>
{/if}
</div>
<FilterSidebar />
{#if $filterStore.selectedTables.length !== 0}
<FilterSidebar />
{/if}
<div class="fixed bottom-5 left-5">
<Dialog large>
<Button slot="trigger" color="secondary" size="lg">SQL Editor</Button>
<Dialog size={'large'}>
<Button slot="trigger" color={ButtonColor.PRIMARY} size={ButtonSize.LG}>SQL Editor</Button>
<svelte:fragment slot="title">SQL Query Editor</svelte:fragment>
<QueryEditor />
</Dialog>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "Xor Construct Multi-Threaded 100M (part)",
"iterations": 5,
"preprocess": "preprocess_s",
"fixture": "Construct",
"generator": "Random",
"visualization": {
"enable": false
},
"parameter": {
"k": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 ],
"s": [ 400, 2600, 100, 100, 2600 ],
"n_partitions": [ 32, 64, 128, 256, 512, 1024, 2048, 4096 ],
"n_threads": [ 10 ],
"n_elements": [ 100000000 ]
},
"optimization": {
"Addressing": "Lemire",
"Hashing": "Murmur",
"Partitioning": "Enabled",
"RegisterSize": "_64bit",
"SIMD": "Scalar",
"EarlyStopping": "Disabled",
"MultiThreading": "Enabled"
},
"benchmarks": [
{
"name": "Xor_Part_Scalar",
"filter": {
"type": "Xor",
"variant": "Standard"
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "Xor Construct Multi-Threaded 1M (part)",
"iterations": 5,
"preprocess": "preprocess_s",
"fixture": "Construct",
"generator": "Random",
"visualization": {
"enable": false
},
"parameter": {
"k": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 ],
"s": [ 400, 2600, 100, 100, 2600 ],
"n_partitions": [ 32, 64, 128, 256, 512, 1024, 2048, 4096 ],
"n_threads": [ 10 ],
"n_elements": [ 1000000 ]
},
"optimization": {
"Addressing": "Lemire",
"Hashing": "Murmur",
"Partitioning": "Enabled",
"RegisterSize": "_64bit",
"SIMD": "Scalar",
"EarlyStopping": "Disabled",
"MultiThreading": "Enabled"
},
"benchmarks": [
{
"name": "Xor_Part_Scalar",
"filter": {
"type": "Xor",
"variant": "Standard"
}
}
]
}

View File

@ -7,7 +7,7 @@ export default {
theme: {
fontFamily: {
display: ['Source Serif Pro', 'Georgia', 'serif'],
body: ['Manrope', 'system-ui', 'sans-serif']
body: ['Inter', 'system-ui', 'sans-serif']
},
extend: {