mirror of
https://github.com/gosticks/partition-filter-visualization.git
synced 2026-08-12 04:40:23 +00:00
wip: initial graph setup
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import Stats from 'stats.js';
|
||||
import { onMount, afterUpdate, beforeUpdate, onDestroy } from 'svelte';
|
||||
import {
|
||||
Scene,
|
||||
PerspectiveCamera,
|
||||
WebGLRenderer,
|
||||
BoxGeometry,
|
||||
MeshBasicMaterial,
|
||||
Mesh,
|
||||
Color,
|
||||
AmbientLight,
|
||||
MeshLambertMaterial,
|
||||
MeshPhongMaterial,
|
||||
DirectionalLight,
|
||||
Group,
|
||||
Light,
|
||||
PointLight,
|
||||
Vector2,
|
||||
Vector3,
|
||||
OrthographicCamera,
|
||||
Camera,
|
||||
MeshStandardMaterial,
|
||||
Object3D,
|
||||
LineBasicMaterial,
|
||||
BufferGeometry,
|
||||
BufferAttribute,
|
||||
Line,
|
||||
DoubleSide,
|
||||
Raycaster,
|
||||
ShaderMaterial,
|
||||
type Intersection
|
||||
} from 'three';
|
||||
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
|
||||
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
|
||||
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
|
||||
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader';
|
||||
|
||||
export let data: Array<Array<number>> = [[]];
|
||||
|
||||
let containerElement: HTMLDivElement;
|
||||
let statsElement: HTMLDivElement;
|
||||
|
||||
let scene: Scene;
|
||||
let camera: Camera;
|
||||
let renderer: WebGLRenderer;
|
||||
let controls: OrbitControls;
|
||||
let raycaster: Raycaster;
|
||||
let outlinePass: OutlinePass;
|
||||
let composer: EffectComposer;
|
||||
let stats: Stats;
|
||||
|
||||
let barGroup: Group | undefined = undefined;
|
||||
|
||||
function setupControls() {
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.rotateSpeed = 0.4;
|
||||
controls.zoomSpeed = 0.3;
|
||||
controls.panSpeed = 0;
|
||||
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.1;
|
||||
}
|
||||
|
||||
function setupScene() {
|
||||
// Initialize Three.js scene, camera, and renderer
|
||||
scene = new Scene();
|
||||
|
||||
scene.add(new AmbientLight(0xffffff, 0.5));
|
||||
|
||||
camera = new OrthographicCamera(
|
||||
containerElement.clientWidth / -2,
|
||||
containerElement.clientWidth / 2,
|
||||
containerElement.clientHeight / 2,
|
||||
containerElement.clientHeight / -2,
|
||||
-1000,
|
||||
1000
|
||||
);
|
||||
|
||||
camera.position.z = 500;
|
||||
|
||||
// Add directional light pointing from camera
|
||||
const light = new DirectionalLight(0xffffff, 1);
|
||||
// const light = new PointLight(0xffffff, 1, 1000);
|
||||
light.position.set(0, 200, 500);
|
||||
light.lookAt(0, 0, 0);
|
||||
camera.add(light);
|
||||
|
||||
scene.add(camera);
|
||||
}
|
||||
|
||||
let hoveredObjects: Array<Object3D> = [];
|
||||
|
||||
onMount(() => {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
setupScene();
|
||||
|
||||
renderer = new WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio || 1);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
renderer.setSize(containerElement.clientWidth, containerElement.clientHeight);
|
||||
containerElement.appendChild(renderer.domElement);
|
||||
|
||||
composer = new EffectComposer(renderer);
|
||||
|
||||
// Add default render pass
|
||||
const renderPass = new RenderPass(scene, camera);
|
||||
composer.addPass(renderPass);
|
||||
|
||||
// Outline/Hover handling
|
||||
outlinePass = new OutlinePass(
|
||||
new Vector2(containerElement.clientWidth, containerElement.clientHeight),
|
||||
scene,
|
||||
camera
|
||||
);
|
||||
|
||||
outlinePass.visibleEdgeColor.set('#ff0000');
|
||||
outlinePass.hiddenEdgeColor.set('#ff0000');
|
||||
outlinePass.edgeStrength = 100;
|
||||
outlinePass.edgeThickness = 3;
|
||||
outlinePass.pulsePeriod = 2;
|
||||
composer.addPass(outlinePass);
|
||||
|
||||
setupControls();
|
||||
|
||||
raycaster = new Raycaster();
|
||||
|
||||
createBarChart();
|
||||
|
||||
// Add the axis indicator to the scene
|
||||
const axisIndicator = createAxisIndicator(50);
|
||||
axisIndicator.position.set(-25, -20, -25);
|
||||
scene.add(axisIndicator);
|
||||
|
||||
stats = new Stats();
|
||||
stats.showPanel(1);
|
||||
statsElement.appendChild(stats.dom);
|
||||
|
||||
// Animation loop
|
||||
const animate = () => {
|
||||
if (!barGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.update();
|
||||
stats.begin();
|
||||
|
||||
if (mousePosition) {
|
||||
// Handle selection
|
||||
raycaster.setFromCamera(mousePosition, camera);
|
||||
// console.log(mouse);
|
||||
|
||||
const intersections = raycaster.intersectObjects(barGroup.children, true);
|
||||
|
||||
if (intersections.length === 0) {
|
||||
outlinePass.selectedObjects = [];
|
||||
} else {
|
||||
outlinePass.selectedObjects = [intersections[0].object];
|
||||
}
|
||||
}
|
||||
|
||||
composer.render();
|
||||
stats.end();
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
animate();
|
||||
});
|
||||
|
||||
beforeUpdate(() => {
|
||||
console.log('beforeUpdate');
|
||||
// Clear the scene before updating
|
||||
clearScene();
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
console.log('afterUpdate');
|
||||
|
||||
// Re-create the bar chart when data is updated
|
||||
createBarChart();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up Three.js resources on component destroy
|
||||
clearScene();
|
||||
renderer.dispose();
|
||||
});
|
||||
|
||||
let mousePosition: Vector2 | undefined = undefined;
|
||||
|
||||
function onHover(event: MouseEvent) {
|
||||
mousePosition = new Vector2(
|
||||
(event.clientX / renderer.domElement.clientWidth) * 2 - 1,
|
||||
-(event.clientY / renderer.domElement.clientHeight) * 2 + 1.0
|
||||
);
|
||||
}
|
||||
|
||||
function clearScene() {
|
||||
console.log('clearScene');
|
||||
return;
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (scene.children.length > 0) {
|
||||
scene.remove(scene.children[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const barGap = 15;
|
||||
|
||||
function createBarChart() {
|
||||
let maxBarHeight = 0;
|
||||
|
||||
let barWidth = (containerElement.clientWidth * 0.4) / data.length - barGap * 2;
|
||||
let barHeightScale = 10;
|
||||
|
||||
const group = new Group();
|
||||
let positionZ = 0;
|
||||
let positionX = 0;
|
||||
|
||||
// TODO: adjust data for now only mock
|
||||
for (let k = 0; k < data.length; k++) {
|
||||
positionX = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let currentBarHeight = 0;
|
||||
|
||||
for (let j = 0; j < data[i].length; j++) {
|
||||
const barHeight = data[i][j] * barHeightScale;
|
||||
console.log(barHeight);
|
||||
const geometry = new BoxGeometry(barWidth, barHeight, barWidth);
|
||||
const material = new MeshLambertMaterial({
|
||||
color: Math.random() * 0xffffff
|
||||
});
|
||||
const bar = new Mesh(geometry, material);
|
||||
|
||||
bar.userData = {
|
||||
data: data[i][j],
|
||||
x: k,
|
||||
y: i,
|
||||
z: j
|
||||
};
|
||||
|
||||
bar.position.x = positionX;
|
||||
bar.position.y = currentBarHeight + barHeight / 2 + 2;
|
||||
bar.position.z = positionZ;
|
||||
currentBarHeight += barHeight;
|
||||
group.add(bar);
|
||||
}
|
||||
|
||||
maxBarHeight = Math.max(maxBarHeight, currentBarHeight);
|
||||
positionX += barWidth + barGap;
|
||||
}
|
||||
|
||||
positionZ += barWidth + barGap;
|
||||
}
|
||||
// Move group to center
|
||||
group.position.x = -positionX / 2;
|
||||
group.position.y = -maxBarHeight / 2;
|
||||
group.position.z = -positionZ / 2;
|
||||
|
||||
scene.add(group);
|
||||
|
||||
if (barGroup) {
|
||||
scene.remove(barGroup);
|
||||
}
|
||||
|
||||
barGroup = group;
|
||||
}
|
||||
|
||||
// Create the axis indicator
|
||||
function createAxisIndicator(size: number) {
|
||||
const xAxisGeometry = new BufferGeometry().setFromPoints([
|
||||
new Vector3(0, 0, 0),
|
||||
new Vector3(size, 0, 0)
|
||||
]);
|
||||
|
||||
const yAxisGeometry = new BufferGeometry().setFromPoints([
|
||||
new Vector3(0, 0, 0),
|
||||
new Vector3(0, size, 0)
|
||||
]);
|
||||
const zAxisGeometry = new BufferGeometry().setFromPoints([
|
||||
new Vector3(0, 0, 0),
|
||||
new Vector3(0, 0, size)
|
||||
]);
|
||||
|
||||
const xAxisMaterial = new LineBasicMaterial({
|
||||
color: 0xff0000,
|
||||
linewidth: 10,
|
||||
linecap: 'round'
|
||||
});
|
||||
const yAxisMaterial = new LineBasicMaterial({ color: 0x00ff00 });
|
||||
const zAxisMaterial = new LineBasicMaterial({ color: 0x0000ff });
|
||||
|
||||
const xAxis = new Line(xAxisGeometry, xAxisMaterial);
|
||||
const yAxis = new Line(yAxisGeometry, yAxisMaterial);
|
||||
const zAxis = new Line(zAxisGeometry, zAxisMaterial);
|
||||
|
||||
const axisIndicator = new Object3D();
|
||||
axisIndicator.add(xAxis);
|
||||
axisIndicator.add(yAxis);
|
||||
axisIndicator.add(zAxis);
|
||||
|
||||
return axisIndicator;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative w-full">
|
||||
<div
|
||||
bind:this={containerElement}
|
||||
on:mousemove={onHover}
|
||||
class="bar-chart-container w-full aspect-square isolate"
|
||||
/>
|
||||
<div class="stats absolute isolate top-0 left-0" bind:this={statsElement} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.bar-chart-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Override default stats placement */
|
||||
:global(.stats > *) {
|
||||
position: absolute !important;
|
||||
}
|
||||
</style>
|
||||
+145
-33
@@ -1,35 +1,24 @@
|
||||
<script lang="ts">
|
||||
import Papa from 'papaparse';
|
||||
import type { ParseResult } from 'papaparse';
|
||||
import Sidebar from '../lib/Sidebar.svelte';
|
||||
import type { PageServerData } from './$types';
|
||||
import type { DataEntry, EntryDefinition } from './proxy+page.server';
|
||||
import { contenteditable_truthy_values } from 'svelte/internal';
|
||||
import type { DataEntry, EntryDefinition, FilterEntry } from './proxy+page.server';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Dropdown from '$lib/components/Dropdown.svelte';
|
||||
import DropdownSelect from '$lib/components/DropdownSelect.svelte';
|
||||
import BasicGraph from '$lib/components/BasicGraph.svelte';
|
||||
import LoadingOverlay from '$lib/components/LoadingOverlay.svelte';
|
||||
|
||||
export let isLoading = false;
|
||||
|
||||
export let data: PageServerData;
|
||||
let numberOfElementsOptions: string[] = [];
|
||||
|
||||
// TODO: either generate or manually write types for csv data
|
||||
type CsvData = object;
|
||||
|
||||
let selectedItem: {
|
||||
name: string;
|
||||
info: Promise<EntryDefinition>;
|
||||
// FIXME: parse content as csv
|
||||
content: Promise<any>;
|
||||
} | null = null;
|
||||
|
||||
function selectItem(item: DataEntry) {
|
||||
// Load content of csv file and info file
|
||||
let entry: typeof selectedItem = {
|
||||
name: item.name,
|
||||
info: fetch(item.infoUrl).then((r) => r.json()),
|
||||
content: fetchAndParseCSV(item.dataUrl)
|
||||
};
|
||||
|
||||
selectedItem = entry;
|
||||
}
|
||||
let selectedFilters: FilterEntry[] | undefined = [];
|
||||
let selectedNumberOfElements: string | undefined = undefined;
|
||||
// List of possible test sizes (e.g. 10, 100, 1000, 10000)
|
||||
|
||||
async function fetchAndParseCSV(csvUrl: string) {
|
||||
const url = new URL(csvUrl, location.href);
|
||||
@@ -50,37 +39,93 @@
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
let filterPromise: Promise<Papa.ParseResult<object>[]> | undefined = undefined;
|
||||
// TODO: type info values
|
||||
let infoPromise: Promise<any> | undefined = undefined;
|
||||
|
||||
function applyFilters() {
|
||||
isLoading = true;
|
||||
|
||||
// Load all CSV files matching the selected filters
|
||||
if (selectedFilters === undefined || selectedNumberOfElements === undefined) {
|
||||
// FIXME: add error handling
|
||||
alert('Please select a filter and number of elements');
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const csvFiles = selectedFilters.map((f) =>
|
||||
f.entries.find((e) => e.name.includes(selectedNumberOfElements!))
|
||||
);
|
||||
|
||||
console.log(csvFiles);
|
||||
|
||||
filterPromise = Promise.all(csvFiles.map((f) => fetchAndParseCSV(f!.dataUrl)));
|
||||
filterPromise?.finally(() => (isLoading = false));
|
||||
|
||||
// Load info files
|
||||
infoPromise = Promise.all(csvFiles.map((f) => fetch(f!.infoUrl).then((r) => r.json())));
|
||||
}
|
||||
|
||||
function onFilterSelect(selected: FilterEntry[]) {
|
||||
selectedFilters = selected;
|
||||
|
||||
if (selected.length === 0) {
|
||||
numberOfElementsOptions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const regex = /\b(\d+(?:\.\d+)?(?:M|K))\b/g;
|
||||
|
||||
// Construct list of possible element counts based on union between all tests
|
||||
const numberOfElements = selected.map((f) => f.entries.map((e) => e.name.match(regex)?.[0]));
|
||||
|
||||
// FIXME: this is just a placeholder for now
|
||||
// filter out duplicates
|
||||
const uniqueElements = numberOfElements
|
||||
.reduce((acc, val) => acc.concat(val), [])
|
||||
.filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
// Find overlapping elements in all selected filters
|
||||
// const overlappingElements = uniqueElements.reduce((acc, val) =>
|
||||
// acc.filter((v) => val.includes(v))
|
||||
// );
|
||||
|
||||
console.log(uniqueElements);
|
||||
|
||||
numberOfElementsOptions = uniqueElements as string[];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<DropdownSelect
|
||||
label="Filters"
|
||||
onSelect={(selected) => {
|
||||
console.log(selected);
|
||||
//selectItem(selected);
|
||||
}}
|
||||
onSelect={onFilterSelect}
|
||||
options={Object.entries(data.filters).map(([key, value]) => ({
|
||||
label: key,
|
||||
value: value
|
||||
}))}
|
||||
/>
|
||||
<DropdownSelect
|
||||
label="Number of elements"
|
||||
label="Number of elements {numberOfElementsOptions.length}"
|
||||
singular
|
||||
disabled={true}
|
||||
disabled={numberOfElementsOptions.length === 0}
|
||||
onSelect={(selected) => {
|
||||
console.log(selected);
|
||||
//selectItem(selected);
|
||||
selectedNumberOfElements = selected[0];
|
||||
}}
|
||||
options={Object.entries(data.filters).map(([key, value]) => ({
|
||||
label: key,
|
||||
options={numberOfElementsOptions.map((value) => ({
|
||||
label: value.toString(),
|
||||
value: value
|
||||
}))}
|
||||
/>
|
||||
<!-- Add spacer -->
|
||||
<div class="flex-grow" />
|
||||
<Button color="primary" size="lg" on:click={applyFilters}>Update</Button>
|
||||
</div>
|
||||
<div class="border-t-2 mt-6">
|
||||
{#if selectedItem}
|
||||
<!-- {#if selectedItem}
|
||||
<h2>{selectedItem.name}</h2>
|
||||
{#await selectedItem.info}
|
||||
<div>loading...</div>
|
||||
@@ -92,6 +137,73 @@
|
||||
{:then content}
|
||||
<pre>{JSON.stringify(content.data)}</pre>
|
||||
{/await}
|
||||
{/if} -->
|
||||
<div class="lg:flex w-full">
|
||||
<div class="flex-grow flex-shrink">
|
||||
<BasicGraph
|
||||
data={[
|
||||
[2, 3, 4, 5, 6, 7],
|
||||
[20, 2, 3, 4, 5, 6],
|
||||
[20, 2, 3, 4, 5, 6],
|
||||
[3, 4, 5, 6, 7, 8],
|
||||
[20, 2, 3, 4, 5, 6]
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div class="w-96 pt-4 flex-grow-0">
|
||||
<div class="mb-4 p-4 text-left rounded-2xl border-secondary-200 bg-white border shadow-xl">
|
||||
<h2 class="font-bold mb-2">CSV Info</h2>
|
||||
|
||||
{#if filterPromise === undefined}
|
||||
<div>Please select a filter and number of elements</div>
|
||||
{:else}
|
||||
{#await filterPromise}
|
||||
<div>loading...</div>
|
||||
{:then results}
|
||||
<p>Loaded Tables: {results.length}</p>
|
||||
<p>
|
||||
Total Number of parsed rows: {results.reduce(
|
||||
(acc, val) => acc + val.data.length,
|
||||
0
|
||||
)}
|
||||
</p>
|
||||
{:catch error}
|
||||
<pre>{JSON.stringify(error)}</pre>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
{#if filterPromise !== undefined}
|
||||
{#await infoPromise}
|
||||
<div class="mb-4 p-4 rounded-2xl border-secondary-200 bg-white border shadow-xl">
|
||||
<div>loading...</div>
|
||||
</div>
|
||||
{:then results}
|
||||
{#each results as result}
|
||||
<div class="mb-4 p-4 rounded-2xl border-secondary-200 bg-white border shadow-xl">
|
||||
<h2 class="font-bold mb-2">{result.name}</h2>
|
||||
<pre
|
||||
class="w-full bg-secondary-100 p-2 rounded-xl overflow-scroll max-h-60">{JSON.stringify(
|
||||
result,
|
||||
null,
|
||||
2
|
||||
)}</pre>
|
||||
</div>
|
||||
{/each}
|
||||
{:catch error}
|
||||
<pre>{JSON.stringify(error)}</pre>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<LoadingOverlay isLoading={true} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.side {
|
||||
flex: 0 0 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user