mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 13:29:05 +01:00
feat: Add uplot histogram, implemented in userview
- For testing - add conversion function to utils
This commit is contained in:
parent
2655bda644
commit
742c2e399e
@ -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}
|
||||||
|
168
web/frontend/src/plots/Histogramuplot.svelte
Normal file
168
web/frontend/src/plots/Histogramuplot.svelte
Normal 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}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
@ -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 = '') {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user