mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 13:29:05 +01:00
Cleanup and fixes on new plots
This commit is contained in:
parent
cf04f420e0
commit
05b43c0f21
@ -5,9 +5,9 @@
|
|||||||
import { Row, Col, Spinner, Card, Table, Icon } from 'sveltestrap'
|
import { Row, Col, Spinner, Card, Table, Icon } from 'sveltestrap'
|
||||||
import Filters from './filters/Filters.svelte'
|
import Filters from './filters/Filters.svelte'
|
||||||
import PlotSelection from './PlotSelection.svelte'
|
import PlotSelection from './PlotSelection.svelte'
|
||||||
import Histogramuplot from './plots/Histogramuplot.svelte'
|
import Histogram from './plots/Histogram.svelte'
|
||||||
import Pie, { colors } from './plots/Pie.svelte'
|
import Pie, { colors } from './plots/Pie.svelte'
|
||||||
import { binsFromFootprint } from './plots/Histogram.svelte'
|
import { binsFromFootprint } from './utils.js'
|
||||||
import ScatterPlot from './plots/Scatter.svelte'
|
import ScatterPlot from './plots/Scatter.svelte'
|
||||||
import PlotTable from './PlotTable.svelte'
|
import PlotTable from './PlotTable.svelte'
|
||||||
import Roofline from './plots/Roofline.svelte'
|
import Roofline from './plots/Roofline.svelte'
|
||||||
@ -204,7 +204,7 @@
|
|||||||
<Col>
|
<Col>
|
||||||
<div bind:clientWidth={colWidth3}>
|
<div bind:clientWidth={colWidth3}>
|
||||||
{#key $statsQuery.data.stats[0].histDuration}
|
{#key $statsQuery.data.stats[0].histDuration}
|
||||||
<Histogramuplot
|
<Histogram
|
||||||
width={colWidth3} height={300}
|
width={colWidth3} height={300}
|
||||||
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
|
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
|
||||||
title="Duration Distribution"
|
title="Duration Distribution"
|
||||||
@ -218,7 +218,7 @@
|
|||||||
<Col>
|
<Col>
|
||||||
<div bind:clientWidth={colWidth4}>
|
<div bind:clientWidth={colWidth4}>
|
||||||
{#key $statsQuery.data.stats[0].histNumNodes}
|
{#key $statsQuery.data.stats[0].histNumNodes}
|
||||||
<Histogramuplot
|
<Histogram
|
||||||
width={colWidth4} height={300}
|
width={colWidth4} height={300}
|
||||||
data={convert2uplot($statsQuery.data.stats[0].histNumNodes)}
|
data={convert2uplot($statsQuery.data.stats[0].histNumNodes)}
|
||||||
title="Number of Nodes Distribution"
|
title="Number of Nodes Distribution"
|
||||||
@ -261,7 +261,7 @@
|
|||||||
$footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))}
|
$footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))}
|
||||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
|
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
|
||||||
|
|
||||||
<Histogramuplot
|
<Histogram
|
||||||
data={convert2uplot(item.bins)}
|
data={convert2uplot(item.bins)}
|
||||||
width={width} height={250}
|
width={width} height={250}
|
||||||
title="Average Distribution of '{item.metric}'"
|
title="Average Distribution of '{item.metric}'"
|
||||||
@ -289,6 +289,7 @@
|
|||||||
<PlotTable
|
<PlotTable
|
||||||
let:item
|
let:item
|
||||||
let:width
|
let:width
|
||||||
|
renderFor="analysis"
|
||||||
items={metricsInScatterplots.map(([m1, m2]) => ({
|
items={metricsInScatterplots.map(([m1, m2]) => ({
|
||||||
m1, f1: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m1).data,
|
m1, f1: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m1).data,
|
||||||
m2, f2: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m2).data }))}
|
m2, f2: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m2).data }))}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
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 Pie, { colors } from './plots/Pie.svelte'
|
import Pie, { colors } from './plots/Pie.svelte'
|
||||||
import Histogramuplot from './plots/Histogramuplot.svelte'
|
import Histogram from './plots/Histogram.svelte'
|
||||||
import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap'
|
import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap'
|
||||||
import { init, convert2uplot } from './utils.js'
|
import { init, convert2uplot } from './utils.js'
|
||||||
import { scaleNumbers } from './units.js'
|
import { scaleNumbers } from './units.js'
|
||||||
@ -213,7 +213,7 @@
|
|||||||
<Col class="p-2">
|
<Col class="p-2">
|
||||||
<div bind:clientWidth={colWidth2}>
|
<div bind:clientWidth={colWidth2}>
|
||||||
{#key $mainQuery.data.stats}
|
{#key $mainQuery.data.stats}
|
||||||
<Histogramuplot
|
<Histogram
|
||||||
data={convert2uplot($mainQuery.data.stats[0].histDuration)}
|
data={convert2uplot($mainQuery.data.stats[0].histDuration)}
|
||||||
width={colWidth2 - 25}
|
width={colWidth2 - 25}
|
||||||
title="Duration Distribution"
|
title="Duration Distribution"
|
||||||
@ -226,7 +226,7 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col class="p-2">
|
<Col class="p-2">
|
||||||
{#key $mainQuery.data.stats}
|
{#key $mainQuery.data.stats}
|
||||||
<Histogramuplot
|
<Histogram
|
||||||
data={convert2uplot($mainQuery.data.stats[0].histNumNodes)}
|
data={convert2uplot($mainQuery.data.stats[0].histNumNodes)}
|
||||||
width={colWidth2 - 25}
|
width={colWidth2 - 25}
|
||||||
title="Number of Nodes Distribution"
|
title="Number of Nodes Distribution"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
import JobList from './joblist/JobList.svelte'
|
import JobList from './joblist/JobList.svelte'
|
||||||
import Sorting from './joblist/SortSelection.svelte'
|
import Sorting from './joblist/SortSelection.svelte'
|
||||||
import Refresher from './joblist/Refresher.svelte'
|
import Refresher from './joblist/Refresher.svelte'
|
||||||
import Histogramuplot from './plots/Histogramuplot.svelte'
|
import Histogram from './plots/Histogram.svelte'
|
||||||
import MetricSelection from './MetricSelection.svelte'
|
import MetricSelection from './MetricSelection.svelte'
|
||||||
import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
|
import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
|
||||||
|
|
||||||
@ -135,7 +135,7 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<div class="col-4 text-center" bind:clientWidth={w1}>
|
<div class="col-4 text-center" bind:clientWidth={w1}>
|
||||||
{#key $stats.data.jobsStatistics[0].histDuration}
|
{#key $stats.data.jobsStatistics[0].histDuration}
|
||||||
<Histogramuplot
|
<Histogram
|
||||||
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
||||||
width={w1 - 25} height={histogramHeight}
|
width={w1 - 25} height={histogramHeight}
|
||||||
title="Duration Distribution"
|
title="Duration Distribution"
|
||||||
@ -147,7 +147,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-4 text-center" bind:clientWidth={w2}>
|
<div class="col-4 text-center" bind:clientWidth={w2}>
|
||||||
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
||||||
<Histogramuplot
|
<Histogram
|
||||||
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
||||||
width={w2 - 25} height={histogramHeight}
|
width={w2 - 25} height={histogramHeight}
|
||||||
title="Number of Nodes Distribution"
|
title="Number of Nodes Distribution"
|
||||||
|
@ -1,229 +1,216 @@
|
|||||||
<!--
|
<!--
|
||||||
@component
|
@component
|
||||||
Properties:
|
Properties:
|
||||||
- width, height: Number
|
- Todo
|
||||||
- min, max: Number
|
|
||||||
- label: (x-Value) => String
|
|
||||||
- data: [{ value: Number, count: Number }]
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<div
|
|
||||||
on:mousemove={mousemove}
|
|
||||||
on:mouseleave={() => (infoText = '')}>
|
|
||||||
<span style="left: {paddingLeft + 5}px;">{infoText}</span>
|
|
||||||
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
import uPlot from 'uplot'
|
||||||
|
import { formatNumber } from '../units.js'
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
import { Card } from 'sveltestrap'
|
||||||
|
|
||||||
export let data
|
export let data
|
||||||
export let width = 500
|
export let width = 500
|
||||||
export let height = 300
|
export let height = 300
|
||||||
|
export let title = ''
|
||||||
export let xlabel = ''
|
export let xlabel = ''
|
||||||
|
export let xunit = 'X'
|
||||||
export let ylabel = ''
|
export let ylabel = ''
|
||||||
export let min = null
|
export let yunit = 'Y'
|
||||||
export let max = null
|
|
||||||
export let small = false
|
|
||||||
export let label = formatNumber
|
|
||||||
|
|
||||||
const fontSize = 12
|
const { bars } = uPlot.paths
|
||||||
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
|
|
||||||
const paddingLeft = 50, paddingRight = 20, paddingTop = 20, paddingBottom = 20
|
|
||||||
|
|
||||||
let ctx, canvasElement
|
const drawStyles = {
|
||||||
|
bars: 1,
|
||||||
|
points: 2,
|
||||||
|
};
|
||||||
|
|
||||||
const maxCount = data.reduce((max, point) => Math.max(max, point.count), 0),
|
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
|
||||||
maxValue = data.reduce((max, point) => Math.max(max, point.value), 0.1)
|
let s = u.series[seriesIdx];
|
||||||
|
let style = s.drawStyle;
|
||||||
|
|
||||||
function getStepSize(valueRange, pixelRange, minSpace) {
|
let renderer = ( // If bars to wide, change here
|
||||||
const proposition = valueRange / (pixelRange / minSpace)
|
style == drawStyles.bars ? (
|
||||||
const getStepSize = n => Math.pow(10, Math.floor(n / 3)) *
|
bars({size: [0.75, 100]})
|
||||||
(n < 0 ? [1., 5., 2.][-n % 3] : [1., 2., 5.][n % 3])
|
) :
|
||||||
|
() => null
|
||||||
|
)
|
||||||
|
|
||||||
let n = 0
|
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||||
let stepsize = getStepSize(n)
|
|
||||||
while (true) {
|
|
||||||
let bigger = getStepSize(n + 1)
|
|
||||||
if (proposition > bigger) {
|
|
||||||
n += 1
|
|
||||||
stepsize = bigger
|
|
||||||
} else {
|
|
||||||
return stepsize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let infoText = ''
|
// converts the legend into a simple tooltip
|
||||||
function mousemove(event) {
|
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
|
||||||
let rect = event.target.getBoundingClientRect()
|
let legendEl;
|
||||||
let x = event.clientX - rect.left
|
|
||||||
if (x < paddingLeft || x > width - paddingRight) {
|
function init(u, opts) {
|
||||||
infoText = ''
|
legendEl = u.root.querySelector(".u-legend");
|
||||||
return
|
|
||||||
|
legendEl.classList.remove("u-inline");
|
||||||
|
className && legendEl.classList.add(className);
|
||||||
|
|
||||||
|
uPlot.assign(legendEl.style, {
|
||||||
|
textAlign: "left",
|
||||||
|
pointerEvents: "none",
|
||||||
|
display: "none",
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||||
|
...style
|
||||||
|
});
|
||||||
|
|
||||||
|
// hide series color markers
|
||||||
|
const idents = legendEl.querySelectorAll(".u-marker");
|
||||||
|
|
||||||
|
for (let i = 0; i < idents.length; i++)
|
||||||
|
idents[i].style.display = "none";
|
||||||
|
|
||||||
|
const overEl = u.over;
|
||||||
|
overEl.style.overflow = "visible";
|
||||||
|
|
||||||
|
// move legend into plot bounds
|
||||||
|
overEl.appendChild(legendEl);
|
||||||
|
|
||||||
|
// show/hide tooltip on enter/exit
|
||||||
|
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
|
||||||
|
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
|
||||||
|
|
||||||
|
// let tooltip exit plot
|
||||||
|
// overEl.style.overflow = "visible";
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = width - paddingLeft - paddingRight
|
function update(u) {
|
||||||
const barWidth = Math.round(w / (maxValue + 1))
|
const { left, top } = u.cursor;
|
||||||
x = Math.floor((x - paddingLeft) / (w - barWidth) * maxValue)
|
legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)";
|
||||||
let point = data.find(point => point.value == x)
|
|
||||||
|
|
||||||
if (point)
|
|
||||||
infoText = `count: ${point.count} (value: ${label(x)})`
|
|
||||||
else
|
|
||||||
infoText = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hooks: {
|
||||||
|
init: init,
|
||||||
|
setCursor: update,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let plotWrapper = null
|
||||||
|
let uplot = null
|
||||||
|
let timeoutId = null
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const labelOffset = Math.floor(height * 0.1)
|
let opts = {
|
||||||
const h = height - paddingTop - paddingBottom - labelOffset
|
width: width,
|
||||||
const w = width - paddingLeft - paddingRight
|
height: height,
|
||||||
const barGap = 5
|
title: title,
|
||||||
const barWidth = Math.ceil(w / (maxValue + 1)) - barGap
|
plugins: [
|
||||||
|
legendAsTooltipPlugin()
|
||||||
if (Number.isNaN(barWidth))
|
],
|
||||||
return
|
cursor: {
|
||||||
|
points: {
|
||||||
const getCanvasX = (value) => (value / maxValue) * (w - barWidth) + paddingLeft + (barWidth / 2.)
|
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
|
||||||
const getCanvasY = (count) => (h - (count / maxCount) * h) + paddingTop
|
width: (u, seriesIdx, size) => size / 4,
|
||||||
|
stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '90',
|
||||||
// X Axis
|
fill: (u, seriesIdx) => "#fff",
|
||||||
ctx.font = `bold ${fontSize}px ${fontFamily}`
|
|
||||||
ctx.fillStyle = 'black'
|
|
||||||
if (xlabel != '') {
|
|
||||||
let textWidth = ctx.measureText(xlabel).width
|
|
||||||
ctx.fillText(xlabel, Math.floor((width / 2) - (textWidth / 2) + barGap), height - Math.floor(labelOffset / 2))
|
|
||||||
}
|
}
|
||||||
ctx.textAlign = 'center'
|
},
|
||||||
ctx.font = `${fontSize}px ${fontFamily}`
|
scales: {
|
||||||
if (min != null && max != null) {
|
x: {
|
||||||
const stepsizeX = getStepSize(max - min, w, 75)
|
time: false
|
||||||
let startX = 0
|
},
|
||||||
while (startX < min)
|
},
|
||||||
startX += stepsizeX
|
axes: [
|
||||||
|
{
|
||||||
|
stroke: "#000000",
|
||||||
|
// scale: 'x',
|
||||||
|
label: xlabel,
|
||||||
|
labelGap: 10,
|
||||||
|
size: 25,
|
||||||
|
incrs: [1, 2, 5, 6, 10, 12, 50, 100, 500, 1000, 5000, 10000],
|
||||||
|
border: {
|
||||||
|
show: true,
|
||||||
|
stroke: "#000000",
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
size: 5 / devicePixelRatio,
|
||||||
|
stroke: "#000000",
|
||||||
|
},
|
||||||
|
values: (_, t) => t.map(v => formatNumber(v)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: "#000000",
|
||||||
|
// scale: 'y',
|
||||||
|
label: ylabel,
|
||||||
|
labelGap: 10,
|
||||||
|
size: 35,
|
||||||
|
border: {
|
||||||
|
show: true,
|
||||||
|
stroke: "#000000",
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
size: 5 / devicePixelRatio,
|
||||||
|
stroke: "#000000",
|
||||||
|
},
|
||||||
|
values: (_, t) => t.map(v => formatNumber(v)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
label: xunit !== '' ? xunit : null,
|
||||||
|
},
|
||||||
|
Object.assign({
|
||||||
|
label: yunit !== '' ? yunit : null,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
drawStyle: drawStyles.points,
|
||||||
|
lineInterpolation: null,
|
||||||
|
paths,
|
||||||
|
}, {
|
||||||
|
drawStyle: drawStyles.bars,
|
||||||
|
lineInterpolation: null,
|
||||||
|
stroke: "#85abce",
|
||||||
|
fill: "#85abce", // + "1A", // Transparent Fill
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
for (let x = startX; x < max; x += stepsizeX) {
|
uplot = new uPlot(opts, data, plotWrapper)
|
||||||
let px = ((x - min) / (max - min)) * (w - barWidth) + paddingLeft + (barWidth / 2.)
|
|
||||||
ctx.fillText(`${formatNumber(x)}`, px, height - paddingBottom - Math.floor(labelOffset / 2))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const stepsizeX = getStepSize(maxValue, w, 120)
|
|
||||||
for (let x = 0; x <= maxValue; x += stepsizeX) {
|
|
||||||
ctx.fillText(label(x), getCanvasX(x), height - paddingBottom - Math.floor(labelOffset / (small ? 8 : 2)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Y Axis
|
|
||||||
ctx.fillStyle = 'black'
|
|
||||||
ctx.strokeStyle = '#bbbbbb'
|
|
||||||
ctx.font = `bold ${fontSize}px ${fontFamily}`
|
|
||||||
if (ylabel != '') {
|
|
||||||
ctx.save()
|
|
||||||
ctx.translate(15, Math.floor(h / 2))
|
|
||||||
ctx.rotate(-Math.PI / 2)
|
|
||||||
ctx.fillText(ylabel, 0, 0)
|
|
||||||
ctx.restore()
|
|
||||||
}
|
|
||||||
ctx.textAlign = 'right'
|
|
||||||
ctx.font = `${fontSize}px ${fontFamily}`
|
|
||||||
ctx.beginPath()
|
|
||||||
const stepsizeY = getStepSize(maxCount, h, 50)
|
|
||||||
for (let y = stepsizeY; y <= maxCount; y += stepsizeY) {
|
|
||||||
const py = Math.floor(getCanvasY(y))
|
|
||||||
ctx.fillText(`${formatNumber(y)}`, paddingLeft - 5, py)
|
|
||||||
ctx.moveTo(paddingLeft, py)
|
|
||||||
ctx.lineTo(width, py)
|
|
||||||
}
|
|
||||||
ctx.stroke()
|
|
||||||
|
|
||||||
// Draw bars
|
|
||||||
ctx.fillStyle = '#85abce'
|
|
||||||
for (let p of data) {
|
|
||||||
ctx.fillRect(
|
|
||||||
getCanvasX(p.value) - (barWidth / 2.),
|
|
||||||
getCanvasY(p.count),
|
|
||||||
barWidth,
|
|
||||||
(p.count / maxCount) * h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fat lines left and below plotting area
|
|
||||||
ctx.strokeStyle = 'black'
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(0, height - paddingBottom - labelOffset)
|
|
||||||
ctx.lineTo(width, height - paddingBottom - labelOffset)
|
|
||||||
ctx.moveTo(paddingLeft, 0)
|
|
||||||
ctx.lineTo(paddingLeft, height - Math.floor(labelOffset / 2))
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
let mounted = false
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mounted = true
|
|
||||||
canvasElement.width = width
|
|
||||||
canvasElement.height = height
|
|
||||||
ctx = canvasElement.getContext('2d')
|
|
||||||
render()
|
render()
|
||||||
})
|
})
|
||||||
|
|
||||||
let timeoutId = null;
|
onDestroy(() => {
|
||||||
|
if (uplot)
|
||||||
|
uplot.destroy()
|
||||||
|
|
||||||
|
if (timeoutId != null)
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
})
|
||||||
|
|
||||||
function sizeChanged() {
|
function sizeChanged() {
|
||||||
if (timeoutId != null)
|
if (timeoutId != null)
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
timeoutId = null
|
timeoutId = null
|
||||||
if (!canvasElement)
|
if (uplot)
|
||||||
return
|
uplot.destroy()
|
||||||
|
|
||||||
canvasElement.width = width
|
|
||||||
canvasElement.height = height
|
|
||||||
ctx = canvasElement.getContext('2d')
|
|
||||||
render()
|
render()
|
||||||
}, 250)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: sizeChanged(width, height)
|
$: sizeChanged(width, height)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
{#if data.length > 0}
|
||||||
div {
|
<div bind:this={plotWrapper}/>
|
||||||
position: relative;
|
{:else}
|
||||||
}
|
<Card class="mx-4" body color="warning">Cannot render histogram: No data!</Card>
|
||||||
div > span {
|
{/if}
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script context="module">
|
|
||||||
import { formatNumber } from '../units.js'
|
|
||||||
|
|
||||||
export function binsFromFootprint(weights, values, numBins) {
|
|
||||||
let min = 0, max = 0
|
|
||||||
if (values.length != 0) {
|
|
||||||
for (let x of values) {
|
|
||||||
min = Math.min(min, x)
|
|
||||||
max = Math.max(max, x)
|
|
||||||
}
|
|
||||||
max += 1 // So that we have an exclusive range.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numBins == null || numBins < 3)
|
|
||||||
numBins = 3
|
|
||||||
|
|
||||||
const bins = new Array(numBins).fill(0)
|
|
||||||
for (let i = 0; i < values.length; i++)
|
|
||||||
bins[Math.floor(((values[i] - min) / (max - min)) * numBins)] += weights ? weights[i] : 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: idx => {
|
|
||||||
let start = min + (idx / numBins) * (max - min)
|
|
||||||
let stop = min + ((idx + 1) / numBins) * (max - min)
|
|
||||||
return `${formatNumber(start)} - ${formatNumber(stop)}`
|
|
||||||
},
|
|
||||||
bins: bins.map((count, idx) => ({ value: idx, count: count })),
|
|
||||||
min: min,
|
|
||||||
max: max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
@ -1,216 +0,0 @@
|
|||||||
<!--
|
|
||||||
@component
|
|
||||||
Properties:
|
|
||||||
- Todo
|
|
||||||
-->
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import uPlot from 'uplot'
|
|
||||||
import { formatNumber } from '../units.js'
|
|
||||||
import { onMount, onDestroy } from 'svelte'
|
|
||||||
import { Card } from 'sveltestrap'
|
|
||||||
|
|
||||||
export let data
|
|
||||||
export let width = 500
|
|
||||||
export let height = 300
|
|
||||||
export let title = ''
|
|
||||||
export let xlabel = ''
|
|
||||||
export let xunit = 'X'
|
|
||||||
export let ylabel = ''
|
|
||||||
export let yunit = 'Y'
|
|
||||||
|
|
||||||
const { bars } = uPlot.paths
|
|
||||||
|
|
||||||
const drawStyles = {
|
|
||||||
bars: 1,
|
|
||||||
points: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
|
|
||||||
let s = u.series[seriesIdx];
|
|
||||||
let style = s.drawStyle;
|
|
||||||
|
|
||||||
let renderer = ( // If bars to wide, change here
|
|
||||||
style == drawStyles.bars ? (
|
|
||||||
bars({size: [0.75, 100]})
|
|
||||||
) :
|
|
||||||
() => null
|
|
||||||
)
|
|
||||||
|
|
||||||
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
|
||||||
}
|
|
||||||
|
|
||||||
// converts the legend into a simple tooltip
|
|
||||||
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
|
|
||||||
let legendEl;
|
|
||||||
|
|
||||||
function init(u, opts) {
|
|
||||||
legendEl = u.root.querySelector(".u-legend");
|
|
||||||
|
|
||||||
legendEl.classList.remove("u-inline");
|
|
||||||
className && legendEl.classList.add(className);
|
|
||||||
|
|
||||||
uPlot.assign(legendEl.style, {
|
|
||||||
textAlign: "left",
|
|
||||||
pointerEvents: "none",
|
|
||||||
display: "none",
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
zIndex: 100,
|
|
||||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
|
||||||
...style
|
|
||||||
});
|
|
||||||
|
|
||||||
// hide series color markers
|
|
||||||
const idents = legendEl.querySelectorAll(".u-marker");
|
|
||||||
|
|
||||||
for (let i = 0; i < idents.length; i++)
|
|
||||||
idents[i].style.display = "none";
|
|
||||||
|
|
||||||
const overEl = u.over;
|
|
||||||
overEl.style.overflow = "visible";
|
|
||||||
|
|
||||||
// move legend into plot bounds
|
|
||||||
overEl.appendChild(legendEl);
|
|
||||||
|
|
||||||
// show/hide tooltip on enter/exit
|
|
||||||
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
|
|
||||||
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
|
|
||||||
|
|
||||||
// let tooltip exit plot
|
|
||||||
// overEl.style.overflow = "visible";
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(u) {
|
|
||||||
const { left, top } = u.cursor;
|
|
||||||
legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hooks: {
|
|
||||||
init: init,
|
|
||||||
setCursor: update,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let plotWrapper = null
|
|
||||||
let uplot = null
|
|
||||||
let timeoutId = null
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
let opts = {
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
title: title,
|
|
||||||
plugins: [
|
|
||||||
legendAsTooltipPlugin()
|
|
||||||
],
|
|
||||||
cursor: {
|
|
||||||
points: {
|
|
||||||
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
|
|
||||||
width: (u, seriesIdx, size) => size / 4,
|
|
||||||
stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '90',
|
|
||||||
fill: (u, seriesIdx) => "#fff",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
time: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axes: [
|
|
||||||
{
|
|
||||||
stroke: "#000000",
|
|
||||||
// scale: 'x',
|
|
||||||
label: xlabel,
|
|
||||||
labelGap: 10,
|
|
||||||
size: 25,
|
|
||||||
incrs: [1, 2, 5, 6, 10, 12, 50, 100, 500, 1000, 5000, 10000],
|
|
||||||
border: {
|
|
||||||
show: true,
|
|
||||||
stroke: "#000000",
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
size: 5 / devicePixelRatio,
|
|
||||||
stroke: "#000000",
|
|
||||||
},
|
|
||||||
values: (_, t) => t.map(v => formatNumber(v)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
stroke: "#000000",
|
|
||||||
// scale: 'y',
|
|
||||||
label: ylabel,
|
|
||||||
labelGap: 10,
|
|
||||||
size: 35,
|
|
||||||
border: {
|
|
||||||
show: true,
|
|
||||||
stroke: "#000000",
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
size: 5 / devicePixelRatio,
|
|
||||||
stroke: "#000000",
|
|
||||||
},
|
|
||||||
values: (_, t) => t.map(v => formatNumber(v)),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
label: xunit !== '' ? xunit : null,
|
|
||||||
},
|
|
||||||
Object.assign({
|
|
||||||
label: yunit !== '' ? yunit : null,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
drawStyle: drawStyles.points,
|
|
||||||
lineInterpolation: null,
|
|
||||||
paths,
|
|
||||||
}, {
|
|
||||||
drawStyle: drawStyles.bars,
|
|
||||||
lineInterpolation: null,
|
|
||||||
stroke: "#85abce",
|
|
||||||
fill: "#85abce", // + "1A", // Transparent Fill
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
uplot = new uPlot(opts, data, plotWrapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
render()
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (uplot)
|
|
||||||
uplot.destroy()
|
|
||||||
|
|
||||||
if (timeoutId != null)
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
})
|
|
||||||
|
|
||||||
function sizeChanged() {
|
|
||||||
if (timeoutId != null)
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
timeoutId = null
|
|
||||||
if (uplot)
|
|
||||||
uplot.destroy()
|
|
||||||
|
|
||||||
render()
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: sizeChanged(width, height)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if data.length > 0}
|
|
||||||
<div bind:this={plotWrapper}/>
|
|
||||||
{:else}
|
|
||||||
<Card class="mx-4" body color="warning">Cannot render histogram: No data!</Card>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
|||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend,
|
||||||
|
Filler,
|
||||||
ArcElement,
|
ArcElement,
|
||||||
CategoryScale
|
CategoryScale
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
@ -31,6 +32,7 @@
|
|||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend,
|
||||||
|
Filler,
|
||||||
ArcElement,
|
ArcElement,
|
||||||
CategoryScale
|
CategoryScale
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from "@urql/svelte";
|
} from "@urql/svelte";
|
||||||
import { setContext, getContext, hasContext, onDestroy, tick } from "svelte";
|
import { setContext, getContext, hasContext, onDestroy, tick } from "svelte";
|
||||||
import { readable } from "svelte/store";
|
import { readable } from "svelte/store";
|
||||||
|
import { formatNumber } from './units.js'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Call this function only at component initialization time!
|
* Call this function only at component initialization time!
|
||||||
@ -323,3 +324,45 @@ export function convert2uplot(canvasData) {
|
|||||||
})
|
})
|
||||||
return uplotData
|
return uplotData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function binsFromFootprint(weights, values, numBins) {
|
||||||
|
let min = 0, max = 0
|
||||||
|
if (values.length != 0) {
|
||||||
|
for (let x of values) {
|
||||||
|
min = Math.min(min, x)
|
||||||
|
max = Math.max(max, x)
|
||||||
|
}
|
||||||
|
max += 1 // So that we have an exclusive range.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numBins == null || numBins < 3)
|
||||||
|
numBins = 3
|
||||||
|
|
||||||
|
const bins = new Array(numBins).fill(0)
|
||||||
|
for (let i = 0; i < values.length; i++)
|
||||||
|
bins[Math.floor(((values[i] - min) / (max - min)) * numBins)] += weights ? weights[i] : 1
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// label: idx => {
|
||||||
|
// let start = min + (idx / numBins) * (max - min)
|
||||||
|
// let stop = min + ((idx + 1) / numBins) * (max - min)
|
||||||
|
// return `${formatNumber(start)} - ${formatNumber(stop)}`
|
||||||
|
// },
|
||||||
|
// bins: bins.map((count, idx) => ({ value: idx, count: count })),
|
||||||
|
// min: min,
|
||||||
|
// max: max
|
||||||
|
// }
|
||||||
|
|
||||||
|
return {
|
||||||
|
bins: bins.map((count, idx) => ({
|
||||||
|
value: idx => { // Get rounded down next integer to bins' Start-Stop Mean Value
|
||||||
|
let start = min + (idx / numBins) * (max - min)
|
||||||
|
let stop = min + ((idx + 1) / numBins) * (max - min)
|
||||||
|
return `${formatNumber(Math.floor((start+stop)/2))}`
|
||||||
|
},
|
||||||
|
count: count
|
||||||
|
})),
|
||||||
|
min: min,
|
||||||
|
max: max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user