| 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>>(
- path: P,
- value: PathValue, 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;
diff --git a/src/lib/store/filterStore/types.ts b/src/lib/store/filterStore/types.ts
index 6c2224b..7b9f160 100644
--- a/src/lib/store/filterStore/types.ts
+++ b/src/lib/store/filterStore/types.ts
@@ -50,29 +50,38 @@ export function setObjectValue>(
current[keys[keys.length - 1]] = value;
}
-export type GraphFilterOptions = 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 =
+ | SimpleGraphFilterOption
+ | {
+ type: 'row';
+ keys: (keyof T)[];
+ grow?: number[]; // Flex grow factor default 1 for all
+ items: SimpleGraphFilterOption[];
+ };
+
+export type GraphFilterOptions = Partial>>;
export abstract class GraphOptions<
Options extends Record = Record,
- Data = unknown
+ Data = unknown,
+ K extends keyof Options = keyof Options
> {
public active = false;
public filterOptions: GraphFilterOptions;
@@ -81,17 +90,18 @@ export abstract class GraphOptions<
this.filterOptions = filterOptions;
}
- public abstract isValid(): boolean;
public abstract getType(): GraphType;
- public abstract setStateValue>(
- path: P,
- value: PathValue
- ): void;
public abstract applyOptionsIfValid(): Promise;
- public abstract updateFilterOptions(): void;
+ public abstract reloadFilterOptions(): void;
+ public abstract setFilterOption(key: K, value: Options[K]): void;
public abstract dataStore: Readable;
public abstract optionsStore: Readable;
+
+ public abstract toString(): string;
+ public static fromString(str: string): GraphOptions | null {
+ return null;
+ }
}
export interface IFilterStore {
diff --git a/src/lib/store/notificationStore.ts b/src/lib/store/notificationStore.ts
new file mode 100644
index 0000000..73ebd25
--- /dev/null
+++ b/src/lib/store/notificationStore.ts
@@ -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([]), '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();
diff --git a/src/lib/store/urlStorage.ts b/src/lib/store/urlStorage.ts
index 031972e..a04e001 100644
--- a/src/lib/store/urlStorage.ts
+++ b/src/lib/store/urlStorage.ts
@@ -54,6 +54,59 @@ export const defaultUrlEncoder = (
export type UrlDecoder = typeof defaultUrlDecoder;
export type UrlEncoder = typeof defaultUrlEncoder;
+export const withSingleKeyUrlStorage = (
+ store: Writable,
+ 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) => {
+ 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 = (
store: Writable,
storeKeys: Partial>,
@@ -96,7 +149,7 @@ export const withUrlStorage =
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 @@
-
+
+ {#each $notificationStore as notification}
+
+
+
{notification.message}
+
+
+ {#if notification.description}
{notification.description}
{/if}
+ {#if notification.callback}
{/if}
+
+ {/each}
+
+
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index cc25a47..f1650cd 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,6 +1,7 @@
@@ -74,24 +65,17 @@
{#if $filterStore.selectedTables.length === 0}
- Please select filter family
- from filter data provided by us
-
-
- your own dataset in CSV format
-
+
{/if}
-
+ {#if $filterStore.selectedTables.length !== 0}
+
+ {/if}
-