wip: initial graph setup

This commit is contained in:
Wlad Meixner
2023-07-04 21:55:40 +02:00
parent 5462b16090
commit 930951439a
2 changed files with 481 additions and 33 deletions
+336
View File
@@ -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
View File
@@ -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>