mirror of
https://github.com/gosticks/partition-filter-visualization.git
synced 2025-10-16 11:55:35 +00:00
wip: refactoring tiled query for multi table
This commit is contained in:
parent
6bdeae7114
commit
d0f89bdefd
113
package.json
113
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
86
src/lib/components/OptionRenderer.svelte
Normal file
86
src/lib/components/OptionRenderer.svelte
Normal 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>
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
29
src/lib/components/TableSelection.svelte
Normal file
29
src/lib/components/TableSelection.svelte
Normal 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} />
|
||||
1
src/lib/components/base/Divider.svelte
Normal file
1
src/lib/components/base/Divider.svelte
Normal file
@ -0,0 +1 @@
|
||||
<hr class="mt-4 mb-2 dark:border-background-800" />
|
||||
3
src/lib/components/base/Label.svelte
Normal file
3
src/lib/components/base/Label.svelte
Normal file
@ -0,0 +1,3 @@
|
||||
<label class="font-bold text-sm px-3 pb text-secondary-500 dark:text-secondary-400">
|
||||
<slot />
|
||||
</label>
|
||||
5
src/lib/components/base/Tag.svelte
Normal file
5
src/lib/components/base/Tag.svelte
Normal 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>
|
||||
82
src/lib/components/button/Button.svelte
Normal file
82
src/lib/components/button/Button.svelte
Normal 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>
|
||||
16
src/lib/components/button/type.ts
Normal file
16
src/lib/components/button/type.ts
Normal 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'
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
36
src/lib/store/notificationStore.ts
Normal file
36
src/lib/store/notificationStore.ts
Normal 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();
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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>
|
||||
|
||||
2753
static/xor_construct/xor_construct_mt_part_100m.csv
Executable file
2753
static/xor_construct/xor_construct_mt_part_100m.csv
Executable file
File diff suppressed because it is too large
Load Diff
35
static/xor_construct/xor_construct_mt_part_100m.json
Executable file
35
static/xor_construct/xor_construct_mt_part_100m.json
Executable 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2753
static/xor_construct/xor_construct_mt_part_1m.csv
Executable file
2753
static/xor_construct/xor_construct_mt_part_1m.csv
Executable file
File diff suppressed because it is too large
Load Diff
35
static/xor_construct/xor_construct_mt_part_1m.json
Executable file
35
static/xor_construct/xor_construct_mt_part_1m.json
Executable 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user