mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-01-30 12:55:17 +01:00
210 lines
6.4 KiB
Svelte
210 lines
6.4 KiB
Svelte
|
<!--
|
||
|
@component
|
||
|
Properties:
|
||
|
- width, height: Number
|
||
|
- min, max: Number
|
||
|
- label: (x-Value) => String
|
||
|
- data: [{ value: Number, count: Number }]
|
||
|
-->
|
||
|
|
||
|
<div
|
||
|
on:mousemove={mousemove}
|
||
|
on:mouseleave={() => (infoText = '')}>
|
||
|
<span style="left: {paddingLeft + 5}px;">{infoText}</span>
|
||
|
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
||
|
</div>
|
||
|
|
||
|
<script>
|
||
|
import { onMount } from 'svelte'
|
||
|
|
||
|
export let data
|
||
|
export let width
|
||
|
export let height
|
||
|
export let min = null
|
||
|
export let max = null
|
||
|
export let label = formatNumber
|
||
|
|
||
|
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 paddingLeft = 35, paddingRight = 20, paddingTop = 20, paddingBottom = 20
|
||
|
|
||
|
let ctx, canvasElement
|
||
|
|
||
|
const maxCount = data.reduce((max, point) => Math.max(max, point.count), 0),
|
||
|
maxValue = data.reduce((max, point) => Math.max(max, point.value), 0.1)
|
||
|
|
||
|
function getStepSize(valueRange, pixelRange, minSpace) {
|
||
|
const proposition = valueRange / (pixelRange / minSpace)
|
||
|
const getStepSize = n => Math.pow(10, Math.floor(n / 3)) *
|
||
|
(n < 0 ? [1., 5., 2.][-n % 3] : [1., 2., 5.][n % 3])
|
||
|
|
||
|
let n = 0
|
||
|
let stepsize = getStepSize(n)
|
||
|
while (true) {
|
||
|
let bigger = getStepSize(n + 1)
|
||
|
if (proposition > bigger) {
|
||
|
n += 1
|
||
|
stepsize = bigger
|
||
|
} else {
|
||
|
return stepsize
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let infoText = ''
|
||
|
function mousemove(event) {
|
||
|
let rect = event.target.getBoundingClientRect()
|
||
|
let x = event.clientX - rect.left
|
||
|
if (x < paddingLeft || x > width - paddingRight) {
|
||
|
infoText = ''
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const w = width - paddingLeft - paddingRight
|
||
|
const barWidth = Math.round(w / (maxValue + 1))
|
||
|
x = Math.floor((x - paddingLeft) / (w - barWidth) * maxValue)
|
||
|
let point = data.find(point => point.value == x)
|
||
|
|
||
|
if (point)
|
||
|
infoText = `count: ${point.count} (value: ${label(x)})`
|
||
|
else
|
||
|
infoText = ''
|
||
|
}
|
||
|
|
||
|
function render() {
|
||
|
const h = height - paddingTop - paddingBottom
|
||
|
const w = width - paddingLeft - paddingRight
|
||
|
const barWidth = Math.ceil(w / (maxValue + 1))
|
||
|
|
||
|
if (Number.isNaN(barWidth))
|
||
|
return
|
||
|
|
||
|
const getCanvasX = (value) => (value / maxValue) * (w - barWidth) + paddingLeft + (barWidth / 2.)
|
||
|
const getCanvasY = (count) => (h - (count / maxCount) * h) + paddingTop
|
||
|
|
||
|
// X Axis
|
||
|
ctx.font = `${fontSize}px ${fontFamily}`
|
||
|
ctx.fillStyle = 'black'
|
||
|
ctx.textAlign = 'center'
|
||
|
if (min != null && max != null) {
|
||
|
const stepsizeX = getStepSize(max - min, w, 75)
|
||
|
let startX = 0
|
||
|
while (startX < min)
|
||
|
startX += stepsizeX
|
||
|
|
||
|
for (let x = startX; x < max; x += stepsizeX) {
|
||
|
let px = ((x - min) / (max - min)) * (w - barWidth) + paddingLeft + (barWidth / 2.)
|
||
|
ctx.fillText(`${formatNumber(x)}`, px, height - paddingBottom + 15)
|
||
|
}
|
||
|
} else {
|
||
|
const stepsizeX = getStepSize(maxValue, w, 120)
|
||
|
for (let x = 0; x <= maxValue; x += stepsizeX) {
|
||
|
ctx.fillText(label(x), getCanvasX(x), height - paddingBottom + 15)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Y Axis
|
||
|
ctx.fillStyle = 'black'
|
||
|
ctx.strokeStyle = '#bbbbbb'
|
||
|
ctx.textAlign = 'right'
|
||
|
ctx.beginPath()
|
||
|
const stepsizeY = getStepSize(maxCount, h, 50)
|
||
|
for (let y = stepsizeY; y <= maxCount; y += stepsizeY) {
|
||
|
const py = Math.floor(getCanvasY(y))
|
||
|
ctx.fillText(`${formatNumber(y)}`, paddingLeft - 5, py)
|
||
|
ctx.moveTo(paddingLeft, py)
|
||
|
ctx.lineTo(width, py)
|
||
|
}
|
||
|
ctx.stroke()
|
||
|
|
||
|
// Draw bars
|
||
|
ctx.fillStyle = '#0066cc'
|
||
|
for (let p of data) {
|
||
|
ctx.fillRect(
|
||
|
getCanvasX(p.value) - (barWidth / 2.),
|
||
|
getCanvasY(p.count),
|
||
|
barWidth,
|
||
|
(p.count / maxCount) * h)
|
||
|
}
|
||
|
|
||
|
// Fat lines left and below plotting area
|
||
|
ctx.strokeStyle = 'black'
|
||
|
ctx.beginPath()
|
||
|
ctx.moveTo(0, height - paddingBottom)
|
||
|
ctx.lineTo(width, height - paddingBottom)
|
||
|
ctx.moveTo(paddingLeft, 0)
|
||
|
ctx.lineTo(paddingLeft, height- paddingBottom)
|
||
|
ctx.stroke()
|
||
|
}
|
||
|
|
||
|
let mounted = false
|
||
|
onMount(() => {
|
||
|
mounted = true
|
||
|
canvasElement.width = width
|
||
|
canvasElement.height = height
|
||
|
ctx = canvasElement.getContext('2d')
|
||
|
render()
|
||
|
})
|
||
|
|
||
|
let timeoutId = null;
|
||
|
function sizeChanged() {
|
||
|
if (timeoutId != null)
|
||
|
clearTimeout(timeoutId)
|
||
|
|
||
|
timeoutId = setTimeout(() => {
|
||
|
timeoutId = null
|
||
|
if (!canvasElement)
|
||
|
return
|
||
|
|
||
|
canvasElement.width = width
|
||
|
canvasElement.height = height
|
||
|
ctx = canvasElement.getContext('2d')
|
||
|
render()
|
||
|
}, 250)
|
||
|
}
|
||
|
|
||
|
$: sizeChanged(width, height)
|
||
|
</script>
|
||
|
|
||
|
<style>
|
||
|
div {
|
||
|
position: relative;
|
||
|
}
|
||
|
div > span {
|
||
|
position: absolute;
|
||
|
top: 0px;
|
||
|
}
|
||
|
</style>
|
||
|
|
||
|
<script context="module">
|
||
|
import { formatNumber } from '../utils.js'
|
||
|
|
||
|
export function binsFromFootprint(weights, values, numBins) {
|
||
|
let min = 0, max = 0
|
||
|
if (values.length != 0) {
|
||
|
for (let x of values) {
|
||
|
min = Math.min(min, x)
|
||
|
max = Math.max(max, x)
|
||
|
}
|
||
|
max += 1 // So that we have an exclusive range.
|
||
|
}
|
||
|
|
||
|
if (numBins == null || numBins < 3)
|
||
|
numBins = 3
|
||
|
|
||
|
const bins = new Array(numBins).fill(0)
|
||
|
for (let i = 0; i < values.length; i++)
|
||
|
bins[Math.floor(((values[i] - min) / (max - min)) * numBins)] += weights ? weights[i] : 1
|
||
|
|
||
|
return {
|
||
|
label: idx => {
|
||
|
let start = min + (idx / numBins) * (max - min)
|
||
|
let stop = min + ((idx + 1) / numBins) * (max - min)
|
||
|
return `${formatNumber(start)} - ${formatNumber(stop)}`
|
||
|
},
|
||
|
bins: bins.map((count, idx) => ({ value: idx, count: count })),
|
||
|
min: min,
|
||
|
max: max
|
||
|
}
|
||
|
}
|
||
|
</script>
|