mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 05:19:05 +01:00
feat: Use chart.js for polarplot n jobview
This commit is contained in:
parent
f286872a33
commit
b42a11d30e
@ -20,12 +20,11 @@
|
|||||||
} from "sveltestrap";
|
} from "sveltestrap";
|
||||||
import PlotTable from "./PlotTable.svelte";
|
import PlotTable from "./PlotTable.svelte";
|
||||||
import Metric from "./Metric.svelte";
|
import Metric from "./Metric.svelte";
|
||||||
import PolarPlot from "./plots/Polar.svelte";
|
import Polar from "./plots/Polar.svelte";
|
||||||
import Roofline from "./plots/Roofline.svelte";
|
import Roofline from "./plots/Roofline.svelte";
|
||||||
import JobInfo from "./joblist/JobInfo.svelte";
|
import JobInfo from "./joblist/JobInfo.svelte";
|
||||||
import TagManagement from "./TagManagement.svelte";
|
import TagManagement from "./TagManagement.svelte";
|
||||||
import MetricSelection from "./MetricSelection.svelte";
|
import MetricSelection from "./MetricSelection.svelte";
|
||||||
import Zoom from "./Zoom.svelte";
|
|
||||||
import StatsTable from "./StatsTable.svelte";
|
import StatsTable from "./StatsTable.svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
@ -233,7 +232,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<Col>
|
<Col>
|
||||||
<PolarPlot
|
<Polar
|
||||||
width={polarPlotSize}
|
width={polarPlotSize}
|
||||||
height={polarPlotSize}
|
height={polarPlotSize}
|
||||||
metrics={ccconfig[
|
metrics={ccconfig[
|
||||||
@ -246,7 +245,7 @@
|
|||||||
<Col>
|
<Col>
|
||||||
<Roofline
|
<Roofline
|
||||||
width={fullWidth / 3 - 10}
|
width={fullWidth / 3 - 10}
|
||||||
height={polarPlotSize}
|
height={polarPlotSize + 20}
|
||||||
cluster={clusters
|
cluster={clusters
|
||||||
.find((c) => c.name == $initq.data.job.cluster)
|
.find((c) => c.name == $initq.data.job.cluster)
|
||||||
.subClusters.find(
|
.subClusters.find(
|
||||||
|
@ -1,22 +1,35 @@
|
|||||||
<div>
|
|
||||||
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { onMount, getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
|
import { Radar } from 'svelte-chartjs';
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
Filler,
|
||||||
|
PointElement,
|
||||||
|
RadialLinearScale,
|
||||||
|
LineElement
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
Filler,
|
||||||
|
PointElement,
|
||||||
|
RadialLinearScale,
|
||||||
|
LineElement
|
||||||
|
);
|
||||||
|
|
||||||
export let metrics
|
|
||||||
export let width
|
export let width
|
||||||
export let height
|
export let height
|
||||||
|
export let metrics
|
||||||
export let cluster
|
export let cluster
|
||||||
export let jobMetrics
|
export let jobMetrics
|
||||||
|
|
||||||
const fontSize = 12
|
|
||||||
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 metricConfig = getContext('metrics')
|
const metricConfig = getContext('metrics')
|
||||||
|
|
||||||
let ctx, canvasElement
|
|
||||||
|
|
||||||
const labels = metrics.filter(name => {
|
const labels = metrics.filter(name => {
|
||||||
if (!jobMetrics.find(m => m.name == name && m.scope == "node")) {
|
if (!jobMetrics.find(m => m.name == name && m.scope == "node")) {
|
||||||
console.warn(`PolarPlot: No metric data for '${name}'`)
|
console.warn(`PolarPlot: No metric data for '${name}'`)
|
||||||
@ -46,145 +59,37 @@
|
|||||||
return avg / metric.series.length
|
return avg / metric.series.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = [
|
const data = {
|
||||||
{
|
labels: labels,
|
||||||
name: 'Max',
|
datasets: [
|
||||||
values: getValuesForStat(getMax),
|
{
|
||||||
color: 'rgb(0, 102, 255)',
|
label: 'Max',
|
||||||
areaColor: 'rgba(0, 102, 255, 0.25)'
|
data: getValuesForStat(getMax),
|
||||||
},
|
fill: 1,
|
||||||
{
|
backgroundColor: 'rgba(0, 102, 255, 0.25)',
|
||||||
name: 'Avg',
|
borderColor: 'rgb(0, 102, 255)',
|
||||||
values: getValuesForStat(getAvg),
|
pointBackgroundColor: 'rgb(0, 102, 255)',
|
||||||
color: 'rgb(255, 153, 0)',
|
pointBorderColor: '#fff',
|
||||||
areaColor: 'rgba(255, 153, 0, 0.25)'
|
pointHoverBackgroundColor: '#fff',
|
||||||
}
|
pointHoverBorderColor: 'rgb(0, 102, 255)'
|
||||||
]
|
},
|
||||||
|
{
|
||||||
function render() {
|
label: 'Avg',
|
||||||
if (!width || Number.isNaN(width))
|
data: getValuesForStat(getAvg),
|
||||||
return
|
fill: true,
|
||||||
|
backgroundColor: 'rgba(255, 153, 0, 0.25)',
|
||||||
const centerX = width / 2
|
borderColor: 'rgb(255, 153, 0)',
|
||||||
const centerY = height / 2 - 15
|
pointBackgroundColor: 'rgb(255, 153, 0)',
|
||||||
const radius = (Math.min(width, height) / 2) - 50
|
pointBorderColor: '#fff',
|
||||||
|
pointHoverBackgroundColor: '#fff',
|
||||||
// Draw circles
|
pointHoverBorderColor: 'rgb(255, 153, 0)'
|
||||||
ctx.lineWidth = 1
|
|
||||||
ctx.strokeStyle = '#999999'
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(centerX, centerY, radius * 1.0, 0, Math.PI * 2, false)
|
|
||||||
ctx.stroke()
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(centerX, centerY, radius * 0.666, 0, Math.PI * 2, false)
|
|
||||||
ctx.stroke()
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(centerX, centerY, radius * 0.333, 0, Math.PI * 2, false)
|
|
||||||
ctx.stroke()
|
|
||||||
|
|
||||||
// Axis
|
|
||||||
ctx.font = `${fontSize}px ${fontFamily}`
|
|
||||||
ctx.textAlign = 'center'
|
|
||||||
ctx.fillText('1/3',
|
|
||||||
Math.floor(centerX + radius * 0.333),
|
|
||||||
Math.floor(centerY + 15))
|
|
||||||
ctx.fillText('2/3',
|
|
||||||
Math.floor(centerX + radius * 0.666),
|
|
||||||
Math.floor(centerY + 15))
|
|
||||||
ctx.fillText('1.0',
|
|
||||||
Math.floor(centerX + radius * 1.0),
|
|
||||||
Math.floor(centerY + 15))
|
|
||||||
|
|
||||||
// Label text and straight lines from center
|
|
||||||
for (let i = 0; i < labels.length; i++) {
|
|
||||||
const angle = 2 * Math.PI * ((i + 1) / labels.length)
|
|
||||||
const dx = Math.cos(angle) * radius
|
|
||||||
const dy = Math.sin(angle) * radius
|
|
||||||
ctx.fillText(labels[i],
|
|
||||||
Math.floor(centerX + dx * 1.1),
|
|
||||||
Math.floor(centerY + dy * 1.1))
|
|
||||||
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(centerX, centerY)
|
|
||||||
ctx.lineTo(centerX + dx, centerY + dy)
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let dataset of data) {
|
|
||||||
console.assert(dataset.values.length === labels.length, 'this will look confusing')
|
|
||||||
ctx.fillStyle = dataset.color
|
|
||||||
ctx.strokeStyle = dataset.color
|
|
||||||
const points = []
|
|
||||||
for (let i = 0; i < dataset.values.length; i++) {
|
|
||||||
const value = dataset.values[i]
|
|
||||||
const angle = 2 * Math.PI * ((i + 1) / labels.length)
|
|
||||||
const x = centerX + Math.cos(angle) * radius * value
|
|
||||||
const y = centerY + Math.sin(angle) * radius * value
|
|
||||||
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(x, y, 3, 0, Math.PI * 2, false)
|
|
||||||
ctx.fill()
|
|
||||||
|
|
||||||
points.push({ x, y })
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
// "Fill" the shape this dataset has
|
|
||||||
ctx.fillStyle = dataset.areaColor
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(points[0].x, points[0].y)
|
|
||||||
for (let p of points)
|
|
||||||
ctx.lineTo(p.x, p.y)
|
|
||||||
ctx.lineTo(points[0].x, points[0].y)
|
|
||||||
ctx.stroke()
|
|
||||||
ctx.fill()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legend at the bottom left corner
|
|
||||||
ctx.textAlign = 'left'
|
|
||||||
let paddingLeft = 0
|
|
||||||
for (let dataset of data) {
|
|
||||||
const text = `${dataset.name}: `
|
|
||||||
const textWidth = ctx.measureText(text).width
|
|
||||||
ctx.fillStyle = 'black'
|
|
||||||
ctx.fillText(text, paddingLeft, height - 20)
|
|
||||||
|
|
||||||
ctx.fillStyle = dataset.color
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(paddingLeft + textWidth + 5, height - 25, 5, 0, Math.PI * 2, false)
|
|
||||||
ctx.fill()
|
|
||||||
|
|
||||||
paddingLeft += textWidth + 15
|
|
||||||
}
|
|
||||||
ctx.fillStyle = 'black'
|
|
||||||
ctx.fillText(`Values relative to respective peak.`, 0, height - 7)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mounted = false
|
// No custom defined options but keep for clarity
|
||||||
onMount(() => {
|
const options = {}
|
||||||
canvasElement.width = width
|
|
||||||
canvasElement.height = height
|
|
||||||
ctx = canvasElement.getContext('2d')
|
|
||||||
render(ctx, data, width, height)
|
|
||||||
mounted = true
|
|
||||||
})
|
|
||||||
|
|
||||||
let timeoutId = null
|
|
||||||
function sizeChanged() {
|
|
||||||
if (!mounted)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (timeoutId != null)
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
timeoutId = null
|
|
||||||
|
|
||||||
canvasElement.width = width
|
|
||||||
canvasElement.height = height
|
|
||||||
ctx = canvasElement.getContext('2d')
|
|
||||||
render(ctx, data, width, height)
|
|
||||||
}, 250)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: sizeChanged(width, height)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Radar {data} {options} {width} {height}/>
|
||||||
|
Loading…
Reference in New Issue
Block a user