mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-01 19:23:50 +02:00
Migrate RooflineHM and Scatter components
- With this commit, all SV4 components are migrated to SV5
This commit is contained in:
parent
48150ffc8b
commit
db674ec31d
@ -1,246 +1,262 @@
|
||||
<!--
|
||||
@component Roofline Model Plot as Heatmap of multiple Jobs based on Canvas
|
||||
@component Roofline Model Plot as Heatmap of multiple Jobs based on Canvas
|
||||
|
||||
Properties:
|
||||
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
|
||||
- **Note**: Object of first subCluster is used, how to handle multiple topologies within one cluster? [TODO]
|
||||
- `tiles [[Float!]!]?`: Data tiles to be rendered [Default: null]
|
||||
- `maxY Number?`: maximum flopRateSimd of all subClusters [Default: null]
|
||||
- `width Number?`: Plot width (reactively adaptive) [Default: 500]
|
||||
- `height Number?`: Plot height (reactively adaptive) [Default: 300]
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
const axesColor = '#aaaaaa'
|
||||
const tickFontSize = 10
|
||||
const labelFontSize = 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 = 40,
|
||||
paddingRight = 10,
|
||||
paddingTop = 10,
|
||||
paddingBottom = 50
|
||||
|
||||
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
||||
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
|
||||
return {
|
||||
x: x1 + a * (x2 - x1),
|
||||
y: y1 + a * (y2 - y1)
|
||||
}
|
||||
}
|
||||
|
||||
function axisStepFactor(i, size) {
|
||||
if (size && size < 500)
|
||||
return 10
|
||||
|
||||
if (i % 3 == 0)
|
||||
return 2
|
||||
else if (i % 3 == 1)
|
||||
return 2.5
|
||||
else
|
||||
return 2
|
||||
}
|
||||
|
||||
function render(ctx, data, subCluster, width, height, defaultMaxY) {
|
||||
if (width <= 0)
|
||||
return
|
||||
|
||||
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., subCluster?.flopRateSimd?.value || defaultMaxY]
|
||||
const w = width - paddingLeft - paddingRight
|
||||
const h = height - paddingTop - paddingBottom
|
||||
|
||||
// Helpers:
|
||||
const [log10minX, log10maxX, log10minY, log10maxY] =
|
||||
[Math.log10(minX), Math.log10(maxX), Math.log10(minY), Math.log10(maxY)]
|
||||
|
||||
/* Value -> Pixel-Coordinate */
|
||||
const getCanvasX = (x) => {
|
||||
x = Math.log10(x)
|
||||
x -= log10minX; x /= (log10maxX - log10minX)
|
||||
return Math.round((x * w) + paddingLeft)
|
||||
}
|
||||
const getCanvasY = (y) => {
|
||||
y = Math.log10(y)
|
||||
y -= log10minY
|
||||
y /= (log10maxY - log10minY)
|
||||
return Math.round((h - y * h) + paddingTop)
|
||||
}
|
||||
|
||||
// Axes
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.strokeStyle = axesColor
|
||||
ctx.font = `${tickFontSize}px ${fontFamily}`
|
||||
ctx.beginPath()
|
||||
for (let x = minX, i = 0; x <= maxX; i++) {
|
||||
let px = getCanvasX(x)
|
||||
let text = formatNumber(x)
|
||||
let textWidth = ctx.measureText(text).width
|
||||
ctx.fillText(text,
|
||||
Math.floor(px - (textWidth / 2)),
|
||||
height - paddingBottom + tickFontSize + 5)
|
||||
ctx.moveTo(px, paddingTop - 5)
|
||||
ctx.lineTo(px, height - paddingBottom + 5)
|
||||
|
||||
x *= axisStepFactor(i, w)
|
||||
}
|
||||
if (data.xLabel) {
|
||||
ctx.font = `${labelFontSize}px ${fontFamily}`
|
||||
let textWidth = ctx.measureText(data.xLabel).width
|
||||
ctx.fillText(data.xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20)
|
||||
}
|
||||
|
||||
ctx.textAlign = 'center'
|
||||
ctx.font = `${tickFontSize}px ${fontFamily}`
|
||||
for (let y = minY, i = 0; y <= maxY; i++) {
|
||||
let py = getCanvasY(y)
|
||||
ctx.moveTo(paddingLeft - 5, py)
|
||||
ctx.lineTo(width - paddingRight + 5, py)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(paddingLeft - 10, py)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
ctx.fillText(formatNumber(y), 0, 0)
|
||||
ctx.restore()
|
||||
|
||||
y *= axisStepFactor(i)
|
||||
}
|
||||
if (data.yLabel) {
|
||||
ctx.font = `${labelFontSize}px ${fontFamily}`
|
||||
ctx.save()
|
||||
ctx.translate(15, Math.floor(height / 2))
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
ctx.fillText(data.yLabel, 0, 0)
|
||||
ctx.restore()
|
||||
}
|
||||
ctx.stroke()
|
||||
|
||||
// Draw Data
|
||||
if (data.tiles) {
|
||||
const rows = data.tiles.length
|
||||
const cols = data.tiles[0].length
|
||||
|
||||
const tileWidth = Math.ceil(w / cols)
|
||||
const tileHeight = Math.ceil(h / rows)
|
||||
|
||||
let max = data.tiles.reduce((max, row) =>
|
||||
Math.max(max, row.reduce((max, val) =>
|
||||
Math.max(max, val)), 0), 0)
|
||||
|
||||
if (max == 0)
|
||||
max = 1
|
||||
|
||||
const tileColor = val => `rgba(255, 0, 0, ${(val / max)})`
|
||||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
for (let j = 0; j < cols; j++) {
|
||||
let px = paddingLeft + (j / cols) * w
|
||||
let py = paddingTop + (h - (i / rows) * h) - tileHeight
|
||||
|
||||
ctx.fillStyle = tileColor(data.tiles[i][j])
|
||||
ctx.fillRect(px, py, tileWidth, tileHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw roofs
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
if (subCluster != null) {
|
||||
const ycut = 0.01 * subCluster.memoryBandwidth.value
|
||||
const scalarKnee = (subCluster.flopRateScalar.value - ycut) / subCluster.memoryBandwidth.value
|
||||
const simdKnee = (subCluster.flopRateSimd.value - ycut) / subCluster.memoryBandwidth.value
|
||||
const scalarKneeX = getCanvasX(scalarKnee),
|
||||
simdKneeX = getCanvasX(simdKnee),
|
||||
flopRateScalarY = getCanvasY(subCluster.flopRateScalar.value),
|
||||
flopRateSimdY = getCanvasY(subCluster.flopRateSimd.value)
|
||||
|
||||
if (scalarKneeX < width - paddingRight) {
|
||||
ctx.moveTo(scalarKneeX, flopRateScalarY)
|
||||
ctx.lineTo(width - paddingRight, flopRateScalarY)
|
||||
}
|
||||
|
||||
if (simdKneeX < width - paddingRight) {
|
||||
ctx.moveTo(simdKneeX, flopRateSimdY)
|
||||
ctx.lineTo(width - paddingRight, flopRateSimdY)
|
||||
}
|
||||
|
||||
let x1 = getCanvasX(0.01),
|
||||
y1 = getCanvasY(ycut),
|
||||
x2 = getCanvasX(simdKnee),
|
||||
y2 = flopRateSimdY
|
||||
|
||||
let xAxisIntersect = lineIntersect(
|
||||
x1, y1, x2, y2,
|
||||
0, height - paddingBottom, width, height - paddingBottom)
|
||||
|
||||
if (xAxisIntersect.x > x1) {
|
||||
x1 = xAxisIntersect.x
|
||||
y1 = xAxisIntersect.y
|
||||
}
|
||||
|
||||
ctx.moveTo(x1, y1)
|
||||
ctx.lineTo(x2, y2)
|
||||
}
|
||||
ctx.stroke()
|
||||
}
|
||||
</script>
|
||||
Properties:
|
||||
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
|
||||
- **Note**: Object of first subCluster is used, how to handle multiple topologies within one cluster? [TODO]
|
||||
- `tiles [[Float!]!]?`: Data tiles to be rendered [Default: null]
|
||||
- `maxY Number?`: maximum flopRateSimd of all subClusters [Default: null]
|
||||
- `width Number?`: Plot width (reactively adaptive) [Default: 500]
|
||||
- `height Number?`: Plot height (reactively adaptive) [Default: 300]
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { formatNumber } from '../units.js'
|
||||
import { onMount } from 'svelte'
|
||||
import { formatNumber } from '../units.js'
|
||||
|
||||
export let subCluster = null
|
||||
export let tiles = null
|
||||
export let maxY = null
|
||||
export let width = 500
|
||||
export let height = 300
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
subCluster = null,
|
||||
tiles = null,
|
||||
maxY = null,
|
||||
width = 500,
|
||||
height = 300,
|
||||
} = $props();
|
||||
|
||||
console.assert(tiles, "you must provide tiles!")
|
||||
/* Check Before */
|
||||
console.assert(tiles, "you must provide tiles!")
|
||||
|
||||
let ctx, canvasElement, prevWidth = width, prevHeight = height
|
||||
const data = {
|
||||
tiles: tiles,
|
||||
xLabel: 'Intensity [FLOPS/byte]',
|
||||
yLabel: 'Performance [GFLOPS]'
|
||||
/* Const Init */
|
||||
const axesColor = '#aaaaaa';
|
||||
const tickFontSize = 10;
|
||||
const labelFontSize = 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 = 40;
|
||||
const paddingRight = 10;
|
||||
const paddingTop = 10;
|
||||
const paddingBottom = 5;
|
||||
|
||||
/* Var Init */
|
||||
let timeoutId = null;
|
||||
|
||||
/* State Init */
|
||||
let ctx = $state();
|
||||
let canvasElement = $state();
|
||||
let prevWidth = $state(width);
|
||||
let prevHeight = $state(height);
|
||||
|
||||
/* Derived */
|
||||
const data = $derived({
|
||||
tiles: tiles,
|
||||
xLabel: 'Intensity [FLOPS/byte]',
|
||||
yLabel: 'Performance [GFLOPS]'
|
||||
});
|
||||
|
||||
/* Effects */
|
||||
$effect(() =>{
|
||||
sizeChanged(width, height);
|
||||
});
|
||||
|
||||
/* Functions */
|
||||
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
||||
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
|
||||
return {
|
||||
x: x1 + a * (x2 - x1),
|
||||
y: y1 + a * (y2 - y1)
|
||||
}
|
||||
}
|
||||
|
||||
function axisStepFactor(i, size) {
|
||||
if (size && size < 500)
|
||||
return 10
|
||||
|
||||
if (i % 3 == 0)
|
||||
return 2
|
||||
else if (i % 3 == 1)
|
||||
return 2.5
|
||||
else
|
||||
return 2
|
||||
}
|
||||
|
||||
function render(ctx, data, subCluster, width, height, defaultMaxY) {
|
||||
if (width <= 0)
|
||||
return
|
||||
|
||||
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., subCluster?.flopRateSimd?.value || defaultMaxY]
|
||||
const w = width - paddingLeft - paddingRight
|
||||
const h = height - paddingTop - paddingBottom
|
||||
|
||||
// Helpers:
|
||||
const [log10minX, log10maxX, log10minY, log10maxY] =
|
||||
[Math.log10(minX), Math.log10(maxX), Math.log10(minY), Math.log10(maxY)]
|
||||
|
||||
/* Value -> Pixel-Coordinate */
|
||||
const getCanvasX = (x) => {
|
||||
x = Math.log10(x)
|
||||
x -= log10minX; x /= (log10maxX - log10minX)
|
||||
return Math.round((x * w) + paddingLeft)
|
||||
}
|
||||
const getCanvasY = (y) => {
|
||||
y = Math.log10(y)
|
||||
y -= log10minY
|
||||
y /= (log10maxY - log10minY)
|
||||
return Math.round((h - y * h) + paddingTop)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
ctx = canvasElement.getContext('2d')
|
||||
if (prevWidth != width || prevHeight != height) {
|
||||
sizeChanged()
|
||||
return
|
||||
// Axes
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.strokeStyle = axesColor
|
||||
ctx.font = `${tickFontSize}px ${fontFamily}`
|
||||
ctx.beginPath()
|
||||
for (let x = minX, i = 0; x <= maxX; i++) {
|
||||
let px = getCanvasX(x)
|
||||
let text = formatNumber(x)
|
||||
let textWidth = ctx.measureText(text).width
|
||||
ctx.fillText(text,
|
||||
Math.floor(px - (textWidth / 2)),
|
||||
height - paddingBottom + tickFontSize + 5)
|
||||
ctx.moveTo(px, paddingTop - 5)
|
||||
ctx.lineTo(px, height - paddingBottom + 5)
|
||||
|
||||
x *= axisStepFactor(i, w)
|
||||
}
|
||||
if (data.xLabel) {
|
||||
ctx.font = `${labelFontSize}px ${fontFamily}`
|
||||
let textWidth = ctx.measureText(data.xLabel).width
|
||||
ctx.fillText(data.xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20)
|
||||
}
|
||||
|
||||
ctx.textAlign = 'center'
|
||||
ctx.font = `${tickFontSize}px ${fontFamily}`
|
||||
for (let y = minY, i = 0; y <= maxY; i++) {
|
||||
let py = getCanvasY(y)
|
||||
ctx.moveTo(paddingLeft - 5, py)
|
||||
ctx.lineTo(width - paddingRight + 5, py)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(paddingLeft - 10, py)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
ctx.fillText(formatNumber(y), 0, 0)
|
||||
ctx.restore()
|
||||
|
||||
y *= axisStepFactor(i)
|
||||
}
|
||||
if (data.yLabel) {
|
||||
ctx.font = `${labelFontSize}px ${fontFamily}`
|
||||
ctx.save()
|
||||
ctx.translate(15, Math.floor(height / 2))
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
ctx.fillText(data.yLabel, 0, 0)
|
||||
ctx.restore()
|
||||
}
|
||||
ctx.stroke()
|
||||
|
||||
// Draw Data
|
||||
if (data.tiles) {
|
||||
const rows = data.tiles.length
|
||||
const cols = data.tiles[0].length
|
||||
|
||||
const tileWidth = Math.ceil(w / cols)
|
||||
const tileHeight = Math.ceil(h / rows)
|
||||
|
||||
let max = data.tiles.reduce((max, row) =>
|
||||
Math.max(max, row.reduce((max, val) =>
|
||||
Math.max(max, val)), 0), 0)
|
||||
|
||||
if (max == 0)
|
||||
max = 1
|
||||
|
||||
const tileColor = val => `rgba(255, 0, 0, ${(val / max)})`
|
||||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
for (let j = 0; j < cols; j++) {
|
||||
let px = paddingLeft + (j / cols) * w
|
||||
let py = paddingTop + (h - (i / rows) * h) - tileHeight
|
||||
|
||||
ctx.fillStyle = tileColor(data.tiles[i][j])
|
||||
ctx.fillRect(px, py, tileWidth, tileHeight)
|
||||
}
|
||||
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
render(ctx, data, subCluster, width, height, maxY)
|
||||
})
|
||||
|
||||
let timeoutId = null
|
||||
function sizeChanged() {
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
prevWidth = width
|
||||
prevHeight = height
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!canvasElement)
|
||||
return
|
||||
|
||||
timeoutId = null
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
render(ctx, data, subCluster, width, height, maxY)
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
|
||||
$: sizeChanged(width, height)
|
||||
// Draw roofs
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
if (subCluster != null) {
|
||||
const ycut = 0.01 * subCluster.memoryBandwidth.value
|
||||
const scalarKnee = (subCluster.flopRateScalar.value - ycut) / subCluster.memoryBandwidth.value
|
||||
const simdKnee = (subCluster.flopRateSimd.value - ycut) / subCluster.memoryBandwidth.value
|
||||
const scalarKneeX = getCanvasX(scalarKnee),
|
||||
simdKneeX = getCanvasX(simdKnee),
|
||||
flopRateScalarY = getCanvasY(subCluster.flopRateScalar.value),
|
||||
flopRateSimdY = getCanvasY(subCluster.flopRateSimd.value)
|
||||
|
||||
if (scalarKneeX < width - paddingRight) {
|
||||
ctx.moveTo(scalarKneeX, flopRateScalarY)
|
||||
ctx.lineTo(width - paddingRight, flopRateScalarY)
|
||||
}
|
||||
|
||||
if (simdKneeX < width - paddingRight) {
|
||||
ctx.moveTo(simdKneeX, flopRateSimdY)
|
||||
ctx.lineTo(width - paddingRight, flopRateSimdY)
|
||||
}
|
||||
|
||||
let x1 = getCanvasX(0.01),
|
||||
y1 = getCanvasY(ycut),
|
||||
x2 = getCanvasX(simdKnee),
|
||||
y2 = flopRateSimdY
|
||||
|
||||
let xAxisIntersect = lineIntersect(
|
||||
x1, y1, x2, y2,
|
||||
0, height - paddingBottom, width, height - paddingBottom)
|
||||
|
||||
if (xAxisIntersect.x > x1) {
|
||||
x1 = xAxisIntersect.x
|
||||
y1 = xAxisIntersect.y
|
||||
}
|
||||
|
||||
ctx.moveTo(x1, y1)
|
||||
ctx.lineTo(x2, y2)
|
||||
}
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
/* On Mount */
|
||||
onMount(() => {
|
||||
ctx = canvasElement.getContext('2d')
|
||||
if (prevWidth != width || prevHeight != height) {
|
||||
sizeChanged()
|
||||
return
|
||||
}
|
||||
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
render(ctx, data, subCluster, width, height, maxY)
|
||||
})
|
||||
|
||||
function sizeChanged() {
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
prevWidth = width
|
||||
prevHeight = height
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!canvasElement)
|
||||
return
|
||||
|
||||
timeoutId = null
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
render(ctx, data, subCluster, width, height, maxY)
|
||||
}, 250)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="cc-plot">
|
||||
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
|
||||
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
|
||||
</div>
|
@ -1,187 +1,194 @@
|
||||
<!--
|
||||
@component Scatter plot of two metrics at identical timesteps, based on canvas
|
||||
@component Scatter plot of two metrics at identical timesteps, based on canvas
|
||||
|
||||
Properties:
|
||||
- `X [Number]`: Data from first selected metric as X-values
|
||||
- `Y [Number]`: Data from second selected metric as Y-values
|
||||
- `S GraphQl.TimeWeights.X?`: Float to scale the data with [Default: null]
|
||||
- `color String`: Color of the drawn scatter circles
|
||||
- `width Number`:
|
||||
- `height Number`:
|
||||
- `xLabel String`:
|
||||
- `yLabel String`:
|
||||
Properties:
|
||||
- `X [Number]`: Data from first selected metric as X-values
|
||||
- `Y [Number]`: Data from second selected metric as Y-values
|
||||
- `S GraphQl.TimeWeights.X?`: Float to scale the data with [Default: null]
|
||||
- `color String`: Color of the drawn scatter circles
|
||||
- `width Number`:
|
||||
- `height Number`:
|
||||
- `xLabel String`:
|
||||
- `yLabel String`:
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
import { formatNumber } from '../units.js'
|
||||
|
||||
const axesColor = '#aaaaaa'
|
||||
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 = 40,
|
||||
paddingRight = 10,
|
||||
paddingTop = 10,
|
||||
paddingBottom = 50
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function render(ctx, X, Y, S, color, xLabel, yLabel, width, height) {
|
||||
if (width <= 0)
|
||||
return;
|
||||
|
||||
const [minX, minY] = [0., 0.];
|
||||
let maxX = X ? X.reduce((maxX, x) => Math.max(maxX, x), minX) : 1.0;
|
||||
let maxY = Y ? Y.reduce((maxY, y) => Math.max(maxY, y), minY) : 1.0;
|
||||
const w = width - paddingLeft - paddingRight;
|
||||
const h = height - paddingTop - paddingBottom;
|
||||
|
||||
if (maxX == 0 && maxY == 0) {
|
||||
maxX = 1;
|
||||
maxY = 1;
|
||||
}
|
||||
|
||||
/* Value -> Pixel-Coordinate */
|
||||
const getCanvasX = (x) => {
|
||||
x -= minX; x /= (maxX - minX);
|
||||
return Math.round((x * w) + paddingLeft);
|
||||
};
|
||||
const getCanvasY = (y) => {
|
||||
y -= minY; y /= (maxY - minY);
|
||||
return Math.round((h - y * h) + paddingTop);
|
||||
};
|
||||
|
||||
// Draw Data
|
||||
let size = 3
|
||||
if (S && X && Y) {
|
||||
let max = S.reduce((max, s, i) => (X[i] == null || Y[i] == null || Number.isNaN(X[i]) || Number.isNaN(Y[i])) ? max : Math.max(max, s), 0)
|
||||
size = (w / 15) / max
|
||||
}
|
||||
|
||||
ctx.fillStyle = color;
|
||||
if (X?.length > 0) {
|
||||
for (let i = 0; i < X.length; i++) {
|
||||
let x = X[i], y = Y[i];
|
||||
if (x == null || y == null || Number.isNaN(x) || Number.isNaN(y))
|
||||
continue;
|
||||
|
||||
const s = S ? S[i] * size : size;
|
||||
const px = getCanvasX(x);
|
||||
const py = getCanvasY(y);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, s, 0, Math.PI * 2, false);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Axes
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.strokeStyle = axesColor;
|
||||
ctx.font = `${fontSize}px ${fontFamily}`;
|
||||
ctx.beginPath();
|
||||
const stepsizeX = getStepSize(maxX, w, 75);
|
||||
for (let x = minX, i = 0; x <= maxX; i++) {
|
||||
let px = getCanvasX(x);
|
||||
let text = formatNumber(x);
|
||||
let textWidth = ctx.measureText(text).width;
|
||||
ctx.fillText(text,
|
||||
Math.floor(px - (textWidth / 2)),
|
||||
height - paddingBottom + fontSize + 5);
|
||||
ctx.moveTo(px, paddingTop - 5);
|
||||
ctx.lineTo(px, height - paddingBottom + 5);
|
||||
|
||||
x += stepsizeX;
|
||||
}
|
||||
if (xLabel) {
|
||||
let textWidth = ctx.measureText(xLabel).width;
|
||||
ctx.fillText(xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20);
|
||||
}
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
const stepsizeY = getStepSize(maxY, h, 75);
|
||||
for (let y = minY, i = 0; y <= maxY; i++) {
|
||||
let py = getCanvasY(y);
|
||||
ctx.moveTo(paddingLeft - 5, py);
|
||||
ctx.lineTo(width - paddingRight + 5, py);
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(paddingLeft - 10, py);
|
||||
ctx.rotate(-Math.PI / 2);
|
||||
ctx.fillText(formatNumber(y), 0, 0);
|
||||
ctx.restore();
|
||||
|
||||
y += stepsizeY;
|
||||
}
|
||||
if (yLabel) {
|
||||
ctx.save();
|
||||
ctx.translate(15, Math.floor(height / 2));
|
||||
ctx.rotate(-Math.PI / 2);
|
||||
ctx.fillText(yLabel, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { formatNumber } from '../units.js'
|
||||
|
||||
export let X;
|
||||
export let Y;
|
||||
export let S = null;
|
||||
export let color = '#0066cc';
|
||||
export let width = 250;
|
||||
export let height = 300;
|
||||
export let xLabel;
|
||||
export let yLabel;
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
X,
|
||||
Y,
|
||||
S = null,
|
||||
color = '#0066cc',
|
||||
width = 250,
|
||||
height = 300,
|
||||
xLabel,
|
||||
yLabel,
|
||||
} = $props();
|
||||
|
||||
let ctx;
|
||||
let canvasElement;
|
||||
/* Const Init */
|
||||
const axesColor = '#aaaaaa';
|
||||
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 = 40;
|
||||
const paddingRight = 10;
|
||||
const paddingTop = 10;
|
||||
const paddingBottom = 50;
|
||||
|
||||
onMount(() => {
|
||||
canvasElement.width = width;
|
||||
canvasElement.height = height;
|
||||
ctx = canvasElement.getContext('2d');
|
||||
render(ctx, X, Y, S, color, xLabel, yLabel, width, height);
|
||||
});
|
||||
/* Var Init */
|
||||
let timeoutId = null;
|
||||
|
||||
let timeoutId = null;
|
||||
function sizeChanged() {
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId);
|
||||
/* State Init */
|
||||
let ctx = $state();
|
||||
let canvasElement = $state();
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null;
|
||||
if (!canvasElement)
|
||||
return;
|
||||
/* Effects */
|
||||
$effect(() => {
|
||||
sizeChanged(width, height);
|
||||
});
|
||||
|
||||
canvasElement.width = width;
|
||||
canvasElement.height = height;
|
||||
ctx = canvasElement.getContext('2d');
|
||||
render(ctx, X, Y, S, color, xLabel, yLabel, width, height);
|
||||
}, 250);
|
||||
/* Functions */
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(ctx, X, Y, S, color, xLabel, yLabel, width, height);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function render(ctx, X, Y, S, color, xLabel, yLabel, width, height) {
|
||||
if (width <= 0)
|
||||
return;
|
||||
|
||||
const [minX, minY] = [0., 0.];
|
||||
let maxX = X ? X.reduce((maxX, x) => Math.max(maxX, x), minX) : 1.0;
|
||||
let maxY = Y ? Y.reduce((maxY, y) => Math.max(maxY, y), minY) : 1.0;
|
||||
const w = width - paddingLeft - paddingRight;
|
||||
const h = height - paddingTop - paddingBottom;
|
||||
|
||||
if (maxX == 0 && maxY == 0) {
|
||||
maxX = 1;
|
||||
maxY = 1;
|
||||
}
|
||||
|
||||
$: sizeChanged(width, height);
|
||||
/* Value -> Pixel-Coordinate */
|
||||
const getCanvasX = (x) => {
|
||||
x -= minX; x /= (maxX - minX);
|
||||
return Math.round((x * w) + paddingLeft);
|
||||
};
|
||||
const getCanvasY = (y) => {
|
||||
y -= minY; y /= (maxY - minY);
|
||||
return Math.round((h - y * h) + paddingTop);
|
||||
};
|
||||
|
||||
// Draw Data
|
||||
let size = 3
|
||||
if (S && X && Y) {
|
||||
let max = S.reduce((max, s, i) => (X[i] == null || Y[i] == null || Number.isNaN(X[i]) || Number.isNaN(Y[i])) ? max : Math.max(max, s), 0)
|
||||
size = (w / 15) / max
|
||||
}
|
||||
|
||||
ctx.fillStyle = color;
|
||||
if (X?.length > 0) {
|
||||
for (let i = 0; i < X.length; i++) {
|
||||
let x = X[i], y = Y[i];
|
||||
if (x == null || y == null || Number.isNaN(x) || Number.isNaN(y))
|
||||
continue;
|
||||
|
||||
const s = S ? S[i] * size : size;
|
||||
const px = getCanvasX(x);
|
||||
const py = getCanvasY(y);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, s, 0, Math.PI * 2, false);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Axes
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.strokeStyle = axesColor;
|
||||
ctx.font = `${fontSize}px ${fontFamily}`;
|
||||
ctx.beginPath();
|
||||
const stepsizeX = getStepSize(maxX, w, 75);
|
||||
for (let x = minX, i = 0; x <= maxX; i++) {
|
||||
let px = getCanvasX(x);
|
||||
let text = formatNumber(x);
|
||||
let textWidth = ctx.measureText(text).width;
|
||||
ctx.fillText(text,
|
||||
Math.floor(px - (textWidth / 2)),
|
||||
height - paddingBottom + fontSize + 5);
|
||||
ctx.moveTo(px, paddingTop - 5);
|
||||
ctx.lineTo(px, height - paddingBottom + 5);
|
||||
|
||||
x += stepsizeX;
|
||||
}
|
||||
if (xLabel) {
|
||||
let textWidth = ctx.measureText(xLabel).width;
|
||||
ctx.fillText(xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20);
|
||||
}
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
const stepsizeY = getStepSize(maxY, h, 75);
|
||||
for (let y = minY, i = 0; y <= maxY; i++) {
|
||||
let py = getCanvasY(y);
|
||||
ctx.moveTo(paddingLeft - 5, py);
|
||||
ctx.lineTo(width - paddingRight + 5, py);
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(paddingLeft - 10, py);
|
||||
ctx.rotate(-Math.PI / 2);
|
||||
ctx.fillText(formatNumber(y), 0, 0);
|
||||
ctx.restore();
|
||||
|
||||
y += stepsizeY;
|
||||
}
|
||||
if (yLabel) {
|
||||
ctx.save();
|
||||
ctx.translate(15, Math.floor(height / 2));
|
||||
ctx.rotate(-Math.PI / 2);
|
||||
ctx.fillText(yLabel, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/* On Mount */
|
||||
onMount(() => {
|
||||
canvasElement.width = width;
|
||||
canvasElement.height = height;
|
||||
ctx = canvasElement.getContext('2d');
|
||||
render(ctx, X, Y, S, color, xLabel, yLabel, width, height);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="cc-plot" bind:clientWidth={width}>
|
||||
<canvas bind:this={canvasElement} {width} {height}></canvas>
|
||||
<canvas bind:this={canvasElement} {width} {height}></canvas>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user