feat: Add uplot histogram, implemented in userview

- For testing
- add conversion function to utils
This commit is contained in:
Christoph Kluge 2023-07-26 13:44:06 +02:00
parent 2655bda644
commit 742c2e399e
5 changed files with 205 additions and 45 deletions

View File

@ -1,13 +1,13 @@
<script> <script>
import { onMount, getContext } from 'svelte' import { onMount, getContext } from 'svelte'
import { init } from './utils.js' import { init, convert2uplot } from './utils.js'
import { Table, Row, Col, Button, Icon, Card, Spinner, Input } from 'sveltestrap' import { Table, Row, Col, Button, Icon, Card, Spinner, Input } from 'sveltestrap'
import { queryStore, gql, getContextClient } from '@urql/svelte' import { queryStore, gql, getContextClient } from '@urql/svelte'
import Filters from './filters/Filters.svelte' import Filters from './filters/Filters.svelte'
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 Histogram from './plots/Histogram.svelte' import Histogramuplot from './plots/Histogramuplot.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'
@ -25,13 +25,6 @@
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
let w1, w2, histogramHeight = 250 let w1, w2, histogramHeight = 250
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
let resize = false
/* Resize Context
* A) Each viewport change triggers histogram rerender due to variable dimensions clearing canvas if not rerendered
* B) Opening filters (and some other things) triggers small change in viewport dimensions (Fix here?)
* A+B) Histogram rerenders if filters opened, high performance impact if dataload heavy
* Solution: Default to fixed histogram dimensions, allow user to enable automatic resizing
*/
const client = getContextClient(); const client = getContextClient();
$: stats = queryStore({ $: stats = queryStore({
@ -137,47 +130,31 @@
<th scope="row">Total Core Hours</th> <th scope="row">Total Core Hours</th>
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td> <td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
</tr> </tr>
<!-- <tr>
<th scope="row">Toggle Histogram Resizing</th>
<td><Input id="c3" value={resize} type="switch" on:change={() => (resize = !resize)}/></td>
</tr> -->
</tbody> </tbody>
</Table> </Table>
</Col> </Col>
<div class="col-4" style="text-align: center;" bind:clientWidth={w1}> <div class="col-4 text-center" bind:clientWidth={w1}>
<b>Duration Distribution</b> <b>Duration Distribution</b>
{#key $stats.data.jobsStatistics[0].histDuration} {#key $stats.data.jobsStatistics[0].histDuration}
{#if resize == true} <Histogramuplot
<Histogram data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
data={$stats.data.jobsStatistics[0].histDuration} width={w2 - 25} height={histogramHeight}
width={w1 - 25} height={histogramHeight} xlabel="Current Runtimes"
xlabel="Current Runtimes [h]" xunit="Hours"
ylabel="Number of Jobs"/> ylabel="Number of Jobs"
{:else} yunit="Jobs"/>
<Histogram
data={$stats.data.jobsStatistics[0].histDuration}
width={400} height={250}
xlabel="Current Runtimes [h]"
ylabel="Number of Jobs"/>
{/if}
{/key} {/key}
</div> </div>
<div class="col-4" style="text-align: center;" bind:clientWidth={w2}> <div class="col-4 text-center" bind:clientWidth={w2}>
<b>Number of Nodes Distribution</b> <b>Number of Nodes Distribution</b>
{#key $stats.data.jobsStatistics[0].histNumNodes} {#key $stats.data.jobsStatistics[0].histNumNodes}
{#if resize == true} <Histogramuplot
<Histogram data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
data={$stats.data.jobsStatistics[0].histNumNodes}
width={w2 - 25} height={histogramHeight} width={w2 - 25} height={histogramHeight}
xlabel="Allocated Nodes [#]" xlabel="Allocated Nodes"
ylabel="Number of Jobs" /> xunit="Nodes"
{:else} ylabel="Number of Jobs"
<Histogram yunit="Jobs"/>
data={$stats.data.jobsStatistics[0].histNumNodes}
width={400} height={250}
xlabel="Allocated Nodes [#]"
ylabel="Number of Jobs" />
{/if}
{/key} {/key}
</div> </div>
{/if} {/if}

View File

@ -0,0 +1,168 @@
<!--
@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 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 = (
style == drawStyles.bars ? (
bars({size: [0.75, 100]})
) :
() => null
)
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
}
let plotWrapper = null
let legendWrapper = null
let uplot = null
let timeoutId = null
function render() {
let opts = {
width: width,
height: height,
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)),
},
],
legend : {
mount: (self, legend) => {
legendWrapper.appendChild(legend)
},
markers: {
show: false,
stroke: "#000000"
}
},
series: [
{
label: xunit,
},
Object.assign({
label: yunit,
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}>
<div bind:this={legendWrapper}/>
</div>
{:else}
<Card class="mx-4" body color="warning">Cannot render histogram: No data!</Card>
{/if}

View File

@ -323,8 +323,9 @@
{#if series[0].data.length > 0} {#if series[0].data.length > 0}
<div bind:this={plotWrapper} class="cc-plot"></div> <div bind:this={plotWrapper} class="cc-plot"></div>
{:else} {:else}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">Cannot render plot: No series data returned for <code>{metric}</code></Card> <Card class="mx-4" body color="warning">Cannot render plot: No series data returned for <code>{metric}</code></Card>
{/if} {/if}
<style> <style>
.cc-plot { .cc-plot {
border-radius: 5px; border-radius: 5px;

View File

@ -6,11 +6,15 @@ const power = [1, 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21]
const prefix = ['', 'K', 'M', 'G', 'T', 'P', 'E'] const prefix = ['', 'K', 'M', 'G', 'T', 'P', 'E']
export function formatNumber(x) { export function formatNumber(x) {
for (let i = 0; i < prefix.length; i++) if ( isNaN(x) ) {
if (power[i] <= x && x < power[i+1]) return x // Return if String , used in Histograms
return `${Math.round((x / power[i]) * 100) / 100} ${prefix[i]}` } else {
for (let i = 0; i < prefix.length; i++)
if (power[i] <= x && x < power[i+1])
return `${Math.round((x / power[i]) * 100) / 100} ${prefix[i]}`
return Math.abs(x) >= 1000 ? x.toExponential() : x.toString() return Math.abs(x) >= 1000 ? x.toExponential() : x.toString()
}
} }
export function scaleNumbers(x, y , p = '') { export function scaleNumbers(x, y , p = '') {

View File

@ -313,3 +313,13 @@ export function checkMetricDisabled(m, c, s) { //[m]etric, [c]luster, [s]ubclust
} }
return false; return false;
} }
export function convert2uplot(canvasData) {
// initial use: Canvas Histogram Data to Uplot
let uplotData = [[],[]] // [X, Y1, Y2, ...]
canvasData.forEach( pair => {
uplotData[0].push(pair.value)
uplotData[1].push(pair.count)
})
return uplotData
}