mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 05:19: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>
|
||||
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 { queryStore, gql, getContextClient } from '@urql/svelte'
|
||||
import Filters from './filters/Filters.svelte'
|
||||
import JobList from './joblist/JobList.svelte'
|
||||
import Sorting from './joblist/SortSelection.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 { scramble, scrambleNames } from './joblist/JobInfo.svelte'
|
||||
|
||||
@ -25,13 +25,6 @@
|
||||
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
|
||||
let w1, w2, histogramHeight = 250
|
||||
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();
|
||||
$: stats = queryStore({
|
||||
@ -137,47 +130,31 @@
|
||||
<th scope="row">Total Core Hours</th>
|
||||
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
|
||||
</tr>
|
||||
<!-- <tr>
|
||||
<th scope="row">Toggle Histogram Resizing</th>
|
||||
<td><Input id="c3" value={resize} type="switch" on:change={() => (resize = !resize)}/></td>
|
||||
</tr> -->
|
||||
</tbody>
|
||||
</Table>
|
||||
</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>
|
||||
{#key $stats.data.jobsStatistics[0].histDuration}
|
||||
{#if resize == true}
|
||||
<Histogram
|
||||
data={$stats.data.jobsStatistics[0].histDuration}
|
||||
width={w1 - 25} height={histogramHeight}
|
||||
xlabel="Current Runtimes [h]"
|
||||
ylabel="Number of Jobs"/>
|
||||
{:else}
|
||||
<Histogram
|
||||
data={$stats.data.jobsStatistics[0].histDuration}
|
||||
width={400} height={250}
|
||||
xlabel="Current Runtimes [h]"
|
||||
ylabel="Number of Jobs"/>
|
||||
{/if}
|
||||
<Histogramuplot
|
||||
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
||||
width={w2 - 25} height={histogramHeight}
|
||||
xlabel="Current Runtimes"
|
||||
xunit="Hours"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</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>
|
||||
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
||||
{#if resize == true}
|
||||
<Histogram
|
||||
data={$stats.data.jobsStatistics[0].histNumNodes}
|
||||
<Histogramuplot
|
||||
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
||||
width={w2 - 25} height={histogramHeight}
|
||||
xlabel="Allocated Nodes [#]"
|
||||
ylabel="Number of Jobs" />
|
||||
{:else}
|
||||
<Histogram
|
||||
data={$stats.data.jobsStatistics[0].histNumNodes}
|
||||
width={400} height={250}
|
||||
xlabel="Allocated Nodes [#]"
|
||||
ylabel="Number of Jobs" />
|
||||
{/if}
|
||||
xlabel="Allocated Nodes"
|
||||
xunit="Nodes"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
{/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}
|
||||
<div bind:this={plotWrapper} class="cc-plot"></div>
|
||||
{: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}
|
||||
|
||||
<style>
|
||||
.cc-plot {
|
||||
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']
|
||||
|
||||
export function formatNumber(x) {
|
||||
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]}`
|
||||
if ( isNaN(x) ) {
|
||||
return x // Return if String , used in Histograms
|
||||
} 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 = '') {
|
||||
|
@ -313,3 +313,13 @@ export function checkMetricDisabled(m, c, s) { //[m]etric, [c]luster, [s]ubclust
|
||||
}
|
||||
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