mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-27 05:49:04 +01:00
initial commit for rooflineuplot
This commit is contained in:
parent
69ee19bed0
commit
f5c43d60d3
7
web/frontend/package-lock.json
generated
7
web/frontend/package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"@rollup/plugin-commonjs": "^24.1.0",
|
"@rollup/plugin-commonjs": "^24.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
"@rollup/plugin-terser": "^0.4.1",
|
"@rollup/plugin-terser": "^0.4.1",
|
||||||
|
"@timohausmann/quadtree-js": "^1.2.5",
|
||||||
"rollup": "^3.21.0",
|
"rollup": "^3.21.0",
|
||||||
"rollup-plugin-css-only": "^4.3.0",
|
"rollup-plugin-css-only": "^4.3.0",
|
||||||
"rollup-plugin-svelte": "^7.1.4",
|
"rollup-plugin-svelte": "^7.1.4",
|
||||||
@ -225,6 +226,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@timohausmann/quadtree-js": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-WcH3pouYtpyLjTCRvNP0WuSV4m7mRyYhLzW44egveFryT7pJhpDsdIJASEe37iCFNA0vmEpqTYGoG0siyXEthA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"@rollup/plugin-commonjs": "^24.1.0",
|
"@rollup/plugin-commonjs": "^24.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
"@rollup/plugin-terser": "^0.4.1",
|
"@rollup/plugin-terser": "^0.4.1",
|
||||||
|
"@timohausmann/quadtree-js": "^1.2.5",
|
||||||
"rollup": "^3.21.0",
|
"rollup": "^3.21.0",
|
||||||
"rollup-plugin-css-only": "^4.3.0",
|
"rollup-plugin-css-only": "^4.3.0",
|
||||||
"rollup-plugin-svelte": "^7.1.4",
|
"rollup-plugin-svelte": "^7.1.4",
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import Refresher from "./joblist/Refresher.svelte";
|
import Refresher from "./joblist/Refresher.svelte";
|
||||||
import Roofline, { transformPerNodeData } from "./plots/Roofline.svelte";
|
import Roofline, { transformPerNodeData } from "./plots/Roofline.svelte";
|
||||||
|
import Rooflineuplot from "./plots/Rooflineuplot.svelte";
|
||||||
import Pie, { colors } from "./plots/Pie.svelte";
|
import Pie, { colors } from "./plots/Pie.svelte";
|
||||||
import Histogram from "./plots/Histogram.svelte";
|
import Histogram from "./plots/Histogram.svelte";
|
||||||
import {
|
import {
|
||||||
@ -665,4 +666,9 @@
|
|||||||
{/key}
|
{/key}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Rooflineuplot/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
{/if}
|
{/if}
|
||||||
|
187
web/frontend/src/plots/Rooflineuplot.svelte
Normal file
187
web/frontend/src/plots/Rooflineuplot.svelte
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
<script>
|
||||||
|
import uPlot from 'uplot'
|
||||||
|
import { formatNumber } from '../units.js'
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
import { Card } from 'sveltestrap'
|
||||||
|
|
||||||
|
let plotWrapper = null
|
||||||
|
let uplot = null
|
||||||
|
let timeoutId = null
|
||||||
|
|
||||||
|
function randInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filledArr(len, val) {
|
||||||
|
let arr = Array(len);
|
||||||
|
|
||||||
|
if (typeof val == "function") {
|
||||||
|
for (let i = 0; i < len; ++i)
|
||||||
|
arr[i] = val(i);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (let i = 0; i < len; ++i)
|
||||||
|
arr[i] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let points = 10000;
|
||||||
|
let series = 5;
|
||||||
|
|
||||||
|
console.time("prep");
|
||||||
|
|
||||||
|
let data = filledArr(series, v => [
|
||||||
|
filledArr(points, i => randInt(0,500)),
|
||||||
|
filledArr(points, i => randInt(0,500)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
data[0] = null;
|
||||||
|
|
||||||
|
console.timeEnd("prep");
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
const drawPoints = (u, seriesIdx, idx0, idx1) => {
|
||||||
|
const size = 5 * devicePixelRatio;
|
||||||
|
|
||||||
|
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||||
|
let d = u.data[seriesIdx];
|
||||||
|
|
||||||
|
u.ctx.fillStyle = series.stroke();
|
||||||
|
|
||||||
|
let deg360 = 2 * Math.PI;
|
||||||
|
|
||||||
|
console.time("points");
|
||||||
|
|
||||||
|
let p = new Path2D();
|
||||||
|
|
||||||
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
|
let xVal = d[0][i];
|
||||||
|
let yVal = d[1][i];
|
||||||
|
|
||||||
|
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
|
||||||
|
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||||
|
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||||
|
|
||||||
|
p.moveTo(cx + size/2, cy);
|
||||||
|
arc(p, cx, cy, size/2, 0, deg360);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("points1");
|
||||||
|
|
||||||
|
u.ctx.fill(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let pxRatio;
|
||||||
|
|
||||||
|
function setPxRatio() {
|
||||||
|
pxRatio = devicePixelRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
function guardedRange(u, min, max) {
|
||||||
|
if (max == min) {
|
||||||
|
if (min == null) {
|
||||||
|
min = 0;
|
||||||
|
max = 100;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let delta = Math.abs(max) || 100;
|
||||||
|
max += delta;
|
||||||
|
min -= delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [min, max];
|
||||||
|
}
|
||||||
|
|
||||||
|
setPxRatio();
|
||||||
|
|
||||||
|
window.addEventListener('dppxchange', setPxRatio);
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
title: "Scatter Plot",
|
||||||
|
mode: 2,
|
||||||
|
width: 1920,
|
||||||
|
height: 600,
|
||||||
|
legend: {
|
||||||
|
live: false,
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
drawClear: [
|
||||||
|
u => {
|
||||||
|
u.series.forEach((s, i) => {
|
||||||
|
if (i > 0)
|
||||||
|
s._paths = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
time: false,
|
||||||
|
// auto: false,
|
||||||
|
// range: [0, 500],
|
||||||
|
// remove any scale padding, use raw data limits
|
||||||
|
range: guardedRange,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
// auto: false,
|
||||||
|
// range: [0, 500],
|
||||||
|
// remove any scale padding, use raw data limits
|
||||||
|
range: guardedRange,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
stroke: "red",
|
||||||
|
fill: "rgba(255,0,0,0.1)",
|
||||||
|
paths: (u, seriesIdx, idx0, idx1) => {
|
||||||
|
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
|
||||||
|
let d = u.data[seriesIdx];
|
||||||
|
|
||||||
|
console.log(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: "red",
|
||||||
|
fill: "rgba(255,0,0,0.1)",
|
||||||
|
paths: drawPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: "green",
|
||||||
|
fill: "rgba(0,255,0,0.1)",
|
||||||
|
paths: drawPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: "blue",
|
||||||
|
fill: "rgba(0,0,255,0.1)",
|
||||||
|
paths: drawPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: "magenta",
|
||||||
|
fill: "rgba(0,0,255,0.1)",
|
||||||
|
paths: drawPoints,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let u = new uPlot(opts, data, document.body);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if data != null}
|
||||||
|
<div bind:this={plotWrapper}/>
|
||||||
|
{:else}
|
||||||
|
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card>
|
||||||
|
{/if}
|
627
web/frontend/src/plots/Scatteruplot.svelte
Normal file
627
web/frontend/src/plots/Scatteruplot.svelte
Normal file
@ -0,0 +1,627 @@
|
|||||||
|
<script>
|
||||||
|
import Quadtree from '@timohausmann/quadtree-js' // https://github.com/timohausmann/quadtree-js
|
||||||
|
import uPlot from 'uplot'
|
||||||
|
import { formatNumber } from '../units.js'
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
import { Card } from 'sveltestrap'
|
||||||
|
|
||||||
|
let plotWrapper = null
|
||||||
|
let uplot = null
|
||||||
|
let timeoutId = null
|
||||||
|
|
||||||
|
function randInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filledArr(len, val) {
|
||||||
|
let arr = Array(len);
|
||||||
|
|
||||||
|
if (typeof val == "function") {
|
||||||
|
for (let i = 0; i < len; ++i)
|
||||||
|
arr[i] = val(i);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (let i = 0; i < len; ++i)
|
||||||
|
arr[i] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let points = 10000;
|
||||||
|
let series = 5;
|
||||||
|
|
||||||
|
console.time("prep");
|
||||||
|
|
||||||
|
let data = filledArr(series, v => [
|
||||||
|
filledArr(points, i => randInt(0,500)),
|
||||||
|
filledArr(points, i => randInt(0,500)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
data[0] = null;
|
||||||
|
|
||||||
|
console.timeEnd("prep");
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
const drawPoints = (u, seriesIdx, idx0, idx1) => {
|
||||||
|
const size = 5 * devicePixelRatio;
|
||||||
|
|
||||||
|
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||||
|
let d = u.data[seriesIdx];
|
||||||
|
|
||||||
|
u.ctx.fillStyle = series.stroke();
|
||||||
|
|
||||||
|
let deg360 = 2 * Math.PI;
|
||||||
|
|
||||||
|
console.time("points");
|
||||||
|
|
||||||
|
// let cir = new Path2D();
|
||||||
|
// cir.moveTo(0, 0);
|
||||||
|
// arc(cir, 0, 0, 3, 0, deg360);
|
||||||
|
|
||||||
|
// Create transformation matrix that moves 200 points to the right
|
||||||
|
// let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
|
||||||
|
// m.a = 1; m.b = 0;
|
||||||
|
// m.c = 0; m.d = 1;
|
||||||
|
// m.e = 200; m.f = 0;
|
||||||
|
|
||||||
|
|
||||||
|
let p = new Path2D();
|
||||||
|
|
||||||
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
|
let xVal = d[0][i];
|
||||||
|
let yVal = d[1][i];
|
||||||
|
|
||||||
|
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
|
||||||
|
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||||
|
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||||
|
|
||||||
|
p.moveTo(cx + size/2, cy);
|
||||||
|
// arc(p, cx, cy, 3, 0, deg360);
|
||||||
|
arc(p, cx, cy, size/2, 0, deg360);
|
||||||
|
|
||||||
|
// m.e = cx;
|
||||||
|
// m.f = cy;
|
||||||
|
// p.addPath(cir, m);
|
||||||
|
|
||||||
|
// qt.add({x: cx - 1.5, y: cy - 1.5, w: 3, h: 3, sidx: seriesIdx, didx: i});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("points");
|
||||||
|
|
||||||
|
u.ctx.fill(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawPoints2 = (u, seriesIdx, idx0, idx1) => {
|
||||||
|
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||||
|
let d = u.data[seriesIdx];
|
||||||
|
|
||||||
|
u.ctx.fillStyle = series.fill();
|
||||||
|
|
||||||
|
let deg360 = 2 * Math.PI;
|
||||||
|
|
||||||
|
console.time("points");
|
||||||
|
|
||||||
|
// let cir = new Path2D();
|
||||||
|
// cir.moveTo(0, 0);
|
||||||
|
// arc(cir, 0, 0, 3, 0, deg360);
|
||||||
|
|
||||||
|
// Create transformation matrix that moves 200 points to the right
|
||||||
|
// let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
|
||||||
|
// m.a = 1; m.b = 0;
|
||||||
|
// m.c = 0; m.d = 1;
|
||||||
|
// m.e = 200; m.f = 0;
|
||||||
|
|
||||||
|
|
||||||
|
let p = new Path2D();
|
||||||
|
|
||||||
|
let strokeWidth = 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
|
let cx = valToPosX(d[0][i], scaleX, xDim, xOff);
|
||||||
|
let cy = valToPosY(d[1][i], scaleY, yDim, yOff);
|
||||||
|
|
||||||
|
let size = d[2][i] * devicePixelRatio;
|
||||||
|
|
||||||
|
p.moveTo(cx + size, cy);
|
||||||
|
arc(p, cx, cy, size, 0, deg360);
|
||||||
|
qt.add({
|
||||||
|
x: cx - size - strokeWidth/2 - u.bbox.left,
|
||||||
|
y: cy - size - strokeWidth/2 - u.bbox.top,
|
||||||
|
w: size * 2 + strokeWidth,
|
||||||
|
h: size * 2 + strokeWidth,
|
||||||
|
sidx: seriesIdx,
|
||||||
|
didx: i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("points");
|
||||||
|
|
||||||
|
u.ctx.fill(p);
|
||||||
|
|
||||||
|
u.ctx.lineWidth = strokeWidth;
|
||||||
|
u.ctx.strokeStyle = series.stroke();
|
||||||
|
u.ctx.stroke(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// shape?
|
||||||
|
const makeDrawPoints3 = (opts) => {
|
||||||
|
let {/*size,*/ disp, each = () => {}} = opts;
|
||||||
|
|
||||||
|
return (u, seriesIdx, idx0, idx1) => {
|
||||||
|
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||||
|
let d = u.data[seriesIdx];
|
||||||
|
|
||||||
|
let strokeWidth = 1;
|
||||||
|
|
||||||
|
u.ctx.save();
|
||||||
|
|
||||||
|
u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
||||||
|
u.ctx.clip();
|
||||||
|
|
||||||
|
u.ctx.fillStyle = series.fill();
|
||||||
|
u.ctx.strokeStyle = series.stroke();
|
||||||
|
u.ctx.lineWidth = strokeWidth;
|
||||||
|
|
||||||
|
let deg360 = 2 * Math.PI;
|
||||||
|
|
||||||
|
console.time("points");
|
||||||
|
|
||||||
|
// let cir = new Path2D();
|
||||||
|
// cir.moveTo(0, 0);
|
||||||
|
// arc(cir, 0, 0, 3, 0, deg360);
|
||||||
|
|
||||||
|
// Create transformation matrix that moves 200 points to the right
|
||||||
|
// let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
|
||||||
|
// m.a = 1; m.b = 0;
|
||||||
|
// m.c = 0; m.d = 1;
|
||||||
|
// m.e = 200; m.f = 0;
|
||||||
|
|
||||||
|
// compute bubble dims
|
||||||
|
let sizes = disp.size.values(u, seriesIdx, idx0, idx1);
|
||||||
|
|
||||||
|
// todo: this depends on direction & orientation
|
||||||
|
// todo: calc once per redraw, not per path
|
||||||
|
let filtLft = u.posToVal(-maxSize / 2, scaleX.key);
|
||||||
|
let filtRgt = u.posToVal(u.bbox.width / devicePixelRatio + maxSize / 2, scaleX.key);
|
||||||
|
let filtBtm = u.posToVal(u.bbox.height / devicePixelRatio + maxSize / 2, scaleY.key);
|
||||||
|
let filtTop = u.posToVal(-maxSize / 2, scaleY.key);
|
||||||
|
|
||||||
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
|
let xVal = d[0][i];
|
||||||
|
let yVal = d[1][i];
|
||||||
|
let size = sizes[i] * devicePixelRatio;
|
||||||
|
|
||||||
|
if (xVal >= filtLft && xVal <= filtRgt && yVal >= filtBtm && yVal <= filtTop) {
|
||||||
|
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||||
|
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||||
|
|
||||||
|
u.ctx.moveTo(cx + size/2, cy);
|
||||||
|
u.ctx.beginPath();
|
||||||
|
u.ctx.arc(cx, cy, size/2, 0, deg360);
|
||||||
|
u.ctx.fill();
|
||||||
|
u.ctx.stroke();
|
||||||
|
|
||||||
|
each(u, seriesIdx, i,
|
||||||
|
cx - size/2 - strokeWidth/2,
|
||||||
|
cy - size/2 - strokeWidth/2,
|
||||||
|
size + strokeWidth,
|
||||||
|
size + strokeWidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("points");
|
||||||
|
|
||||||
|
u.ctx.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
|
||||||
|
let qt;
|
||||||
|
|
||||||
|
let pxRatio;
|
||||||
|
|
||||||
|
function setPxRatio() {
|
||||||
|
pxRatio = devicePixelRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
function guardedRange(u, min, max) {
|
||||||
|
if (max == min) {
|
||||||
|
if (min == null) {
|
||||||
|
min = 0;
|
||||||
|
max = 100;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let delta = Math.abs(max) || 100;
|
||||||
|
max += delta;
|
||||||
|
min -= delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [min, max];
|
||||||
|
}
|
||||||
|
|
||||||
|
setPxRatio();
|
||||||
|
|
||||||
|
window.addEventListener('dppxchange', setPxRatio);
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
title: "Scatter Plot",
|
||||||
|
mode: 2,
|
||||||
|
width: 1920,
|
||||||
|
height: 600,
|
||||||
|
legend: {
|
||||||
|
live: false,
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
drawClear: [
|
||||||
|
u => {
|
||||||
|
// qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||||
|
|
||||||
|
// qt.clear();
|
||||||
|
|
||||||
|
// force-clear the path cache to cause drawBars() to rebuild new quadtree
|
||||||
|
u.series.forEach((s, i) => {
|
||||||
|
if (i > 0)
|
||||||
|
s._paths = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
time: false,
|
||||||
|
// auto: false,
|
||||||
|
// range: [0, 500],
|
||||||
|
// remove any scale padding, use raw data limits
|
||||||
|
range: guardedRange,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
// auto: false,
|
||||||
|
// range: [0, 500],
|
||||||
|
// remove any scale padding, use raw data limits
|
||||||
|
range: guardedRange,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
stroke: "red",
|
||||||
|
fill: "rgba(255,0,0,0.1)",
|
||||||
|
paths: (u, seriesIdx, idx0, idx1) => {
|
||||||
|
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
|
||||||
|
let d = u.data[seriesIdx];
|
||||||
|
|
||||||
|
console.log(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: "red",
|
||||||
|
fill: "rgba(255,0,0,0.1)",
|
||||||
|
paths: drawPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: "green",
|
||||||
|
fill: "rgba(0,255,0,0.1)",
|
||||||
|
paths: drawPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: "blue",
|
||||||
|
fill: "rgba(0,0,255,0.1)",
|
||||||
|
paths: drawPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: "magenta",
|
||||||
|
fill: "rgba(0,0,255,0.1)",
|
||||||
|
paths: drawPoints,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let u = new uPlot(opts, data, document.body);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
points = 50;
|
||||||
|
|
||||||
|
// size range in pixels (diameter)
|
||||||
|
let minSize = 6;
|
||||||
|
let maxSize = 60;
|
||||||
|
|
||||||
|
let maxArea = Math.PI * (maxSize / 2) ** 2;
|
||||||
|
let minArea = Math.PI * (minSize / 2) ** 2;
|
||||||
|
|
||||||
|
// quadratic scaling (px area)
|
||||||
|
function getSize(value, minValue, maxValue) {
|
||||||
|
let pct = value / maxValue;
|
||||||
|
// clamp to min area
|
||||||
|
//let area = Math.max(maxArea * pct, minArea);
|
||||||
|
let area = maxArea * pct;
|
||||||
|
return Math.sqrt(area / Math.PI) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSizeMinMax(u) {
|
||||||
|
let minValue = Infinity;
|
||||||
|
let maxValue = -Infinity;
|
||||||
|
|
||||||
|
for (let i = 1; i < u.series.length; i++) {
|
||||||
|
let sizeData = u.data[i][2];
|
||||||
|
|
||||||
|
for (let j = 0; j < sizeData.length; j++) {
|
||||||
|
minValue = Math.min(minValue, sizeData[j]);
|
||||||
|
maxValue = Math.max(maxValue, sizeData[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [minValue, maxValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
let drawPoints3 = makeDrawPoints3({
|
||||||
|
disp: {
|
||||||
|
size: {
|
||||||
|
unit: 3, // raw CSS pixels
|
||||||
|
// discr: true,
|
||||||
|
values: (u, seriesIdx, idx0, idx1) => {
|
||||||
|
// TODO: only run once per setData() call
|
||||||
|
let [minValue, maxValue] = getSizeMinMax(u);
|
||||||
|
return u.data[seriesIdx][2].map(v => getSize(v, minValue, maxValue));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => {
|
||||||
|
// we get back raw canvas coords (included axes & padding). translate to the plotting area origin
|
||||||
|
lft -= u.bbox.left;
|
||||||
|
top -= u.bbox.top;
|
||||||
|
qt.add({x: lft, y: top, w: wid, h: hgt, sidx: seriesIdx, didx: dataIdx});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let data2 = filledArr(series, v => [
|
||||||
|
filledArr(points, i => randInt(0,500)),
|
||||||
|
filledArr(points, i => randInt(0,500)),
|
||||||
|
filledArr(points, i => randInt(1,10000)), // bubble size, population
|
||||||
|
filledArr(points, i => (Math.random() + 1).toString(36).substring(7)), // label / country name
|
||||||
|
]);
|
||||||
|
|
||||||
|
data2[0] = null;
|
||||||
|
data2[1][1] = data2[1][1].map(v => v == 0 ? v : v * -1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
const legendValue = (u, val, seriesIdx, idx) => {
|
||||||
|
let [x, y, size] = u.data[seriesIdx];
|
||||||
|
return `${x[idx]}, ${y[idx]}, ${size[idx]}`;
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
const legendValues = (u, seriesIdx, dataIdx) => {
|
||||||
|
// when data null, it's initial schema probe (also u.status == 0)
|
||||||
|
if (u.data == null || dataIdx == null || hRect == null || hRect.sidx != seriesIdx) {
|
||||||
|
return {
|
||||||
|
"Country": '<NUM COUNTRIES>',
|
||||||
|
"Population": '<TOTAL POP>',
|
||||||
|
"GDP": '<TOTAL GDP>',
|
||||||
|
"Income": '<AVG INCOME>'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let [x, y, size, label] = u.data[seriesIdx];
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Country": label[hRect.didx],
|
||||||
|
"Population": size[hRect.didx],
|
||||||
|
"GDP": '$' + x[hRect.didx],
|
||||||
|
"Income": '$' + y[hRect.didx],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// hovered
|
||||||
|
let hRect;
|
||||||
|
|
||||||
|
const opts2 = {
|
||||||
|
title: "Bubble Plot",
|
||||||
|
mode: 2,
|
||||||
|
width: 1920,
|
||||||
|
height: 600,
|
||||||
|
legend: {
|
||||||
|
// live: false,
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
dataIdx: (u, seriesIdx) => {
|
||||||
|
if (seriesIdx == 1) {
|
||||||
|
hRect = null;
|
||||||
|
|
||||||
|
let dist = Infinity;
|
||||||
|
let cx = u.cursor.left * pxRatio;
|
||||||
|
let cy = u.cursor.top * pxRatio;
|
||||||
|
|
||||||
|
// !!! https://github.com/timohausmann/quadtree-js/blob/master/docs/simple.html
|
||||||
|
// Rewrite mouseOver based on this example
|
||||||
|
|
||||||
|
// let overlaps = qt.retrieve({x:cx, y:cy, width:1, height:1})
|
||||||
|
|
||||||
|
// for(var i=0;i<overlaps.length;i=i+1) {
|
||||||
|
// overlaps[i].check = true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// OLD!
|
||||||
|
qt.get(cx, cy, 1, 1, o => {
|
||||||
|
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) {
|
||||||
|
let ocx = o.x + o.w / 2;
|
||||||
|
let ocy = o.y + o.h / 2;
|
||||||
|
|
||||||
|
let dx = ocx - cx;
|
||||||
|
let dy = ocy - cy;
|
||||||
|
|
||||||
|
let d = Math.sqrt(dx ** 2 + dy ** 2);
|
||||||
|
|
||||||
|
// test against radius for actual hover
|
||||||
|
if (d <= o.w / 2) {
|
||||||
|
// only hover bbox with closest distance
|
||||||
|
if (d <= dist) {
|
||||||
|
dist = d;
|
||||||
|
hRect = o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hRect && seriesIdx == hRect.sidx ? hRect.didx : null;
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
size: (u, seriesIdx) => {
|
||||||
|
return hRect && seriesIdx == hRect.sidx ? hRect.w / devicePixelRatio : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
drawClear: [
|
||||||
|
u => {
|
||||||
|
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||||
|
|
||||||
|
qt.clear();
|
||||||
|
|
||||||
|
// force-clear the path cache to cause drawBars() to rebuild new quadtree
|
||||||
|
u.series.forEach((s, i) => {
|
||||||
|
if (i > 0)
|
||||||
|
s._paths = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
label: "GDP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Income 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
side: 1,
|
||||||
|
scale: 'y2',
|
||||||
|
stroke: "red",
|
||||||
|
label: "Income 2",
|
||||||
|
grid: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
time: false,
|
||||||
|
// auto: false,
|
||||||
|
// range: [0, 500],
|
||||||
|
|
||||||
|
// remove any scale padding, use raw data limits
|
||||||
|
range: guardedRange,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
// auto: false,
|
||||||
|
// range: [0, 500],
|
||||||
|
|
||||||
|
// remove any scale padding, use raw data limits
|
||||||
|
range: guardedRange,
|
||||||
|
},
|
||||||
|
y2: {
|
||||||
|
dir: 1,
|
||||||
|
ori: 1,
|
||||||
|
range: guardedRange,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
facets: [
|
||||||
|
{
|
||||||
|
scale: 'x',
|
||||||
|
auto: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 'y2',
|
||||||
|
auto: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
label: "Region A",
|
||||||
|
stroke: "red",
|
||||||
|
fill: "rgba(255,0,0,0.3)",
|
||||||
|
paths: drawPoints3,
|
||||||
|
values: legendValues,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
facets: [
|
||||||
|
{
|
||||||
|
scale: 'x',
|
||||||
|
auto: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 'y',
|
||||||
|
auto: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
label: "Region B",
|
||||||
|
stroke: "green",
|
||||||
|
fill: "rgba(0,255,0,0.3)",
|
||||||
|
paths: drawPoints3,
|
||||||
|
values: legendValues,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
facets: [
|
||||||
|
{
|
||||||
|
scale: 'x',
|
||||||
|
auto: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 'y',
|
||||||
|
auto: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
label: "Region C",
|
||||||
|
stroke: "blue",
|
||||||
|
fill: "rgba(0,0,255,0.3)",
|
||||||
|
paths: drawPoints3,
|
||||||
|
values: legendValues,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
facets: [
|
||||||
|
{
|
||||||
|
scale: 'x',
|
||||||
|
auto: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 'y',
|
||||||
|
auto: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
label: "Region E",
|
||||||
|
stroke: "orange",
|
||||||
|
fill: "rgba(255,128,0,0.3)",
|
||||||
|
paths: drawPoints3,
|
||||||
|
values: legendValues,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let u2 = new uPlot(opts2, data2, document.body);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if data != null}
|
||||||
|
<div bind:this={plotWrapper}/>
|
||||||
|
{:else}
|
||||||
|
<Card class="mx-4" body color="warning">Cannot render scatter: No data!</Card>
|
||||||
|
{/if}
|
Loading…
Reference in New Issue
Block a user