mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-11-04 01:25:06 +01:00 
			
		
		
		
	feat: Add uplot histogram, implemented in userview
- For testing - add conversion function to utils
This commit is contained in:
		web/frontend/src
@@ -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,12 +6,16 @@ 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) {
 | 
				
			||||||
 | 
					    if ( isNaN(x) ) {
 | 
				
			||||||
 | 
					        return x // Return if String , used in Histograms
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
        for (let i = 0; i < prefix.length; i++)
 | 
					        for (let i = 0; i < prefix.length; i++)
 | 
				
			||||||
            if (power[i] <= x && x < power[i+1])
 | 
					            if (power[i] <= x && x < power[i+1])
 | 
				
			||||||
                return `${Math.round((x / power[i]) * 100) / 100} ${prefix[i]}`
 | 
					                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 = '') {
 | 
				
			||||||
    const oldPower  = power[prefix.indexOf(p)]
 | 
					    const oldPower  = power[prefix.indexOf(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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user