mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-09-08 17:53:00 +02:00
Merge pull request #199 from ClusterCockpit/197_apply_chartjs_update
197 apply chartjs update
This commit is contained in:
@@ -2,11 +2,12 @@
|
||||
import { init, convert2uplot } from './utils.js'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { queryStore, gql, getContextClient } from '@urql/svelte'
|
||||
import { Row, Col, Spinner, Card, Table } from 'sveltestrap'
|
||||
import { Row, Col, Spinner, Card, Table, Icon } from 'sveltestrap'
|
||||
import Filters from './filters/Filters.svelte'
|
||||
import PlotSelection from './PlotSelection.svelte'
|
||||
import Histogramuplot from './plots/Histogramuplot.svelte'
|
||||
import Histogram, { binsFromFootprint } from './plots/Histogram.svelte'
|
||||
import Pie, { colors } from './plots/Pie.svelte'
|
||||
import { binsFromFootprint } from './plots/Histogram.svelte'
|
||||
import ScatterPlot from './plots/Scatter.svelte'
|
||||
import PlotTable from './PlotTable.svelte'
|
||||
import Roofline from './plots/Roofline.svelte'
|
||||
@@ -30,7 +31,7 @@
|
||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||
let jobFilters = [];
|
||||
let rooflineMaxY;
|
||||
let colWidth;
|
||||
let colWidth1, colWidth2, colWidth3, colWidth4;
|
||||
let numBins = 50;
|
||||
let maxY = -1;
|
||||
const ccconfig = getContext('cc-config')
|
||||
@@ -135,82 +136,104 @@
|
||||
</Col>
|
||||
</Row>
|
||||
{:else if $statsQuery.data}
|
||||
<Row>
|
||||
<div class="col-3" bind:clientWidth={colWidth}>
|
||||
<div style="height: 40%">
|
||||
<Table>
|
||||
<tr>
|
||||
<th scope="col">Total Jobs</th>
|
||||
<td>{$statsQuery.data.stats[0].totalJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Short Jobs</th>
|
||||
<td>{$statsQuery.data.stats[0].shortJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Total Walltime</th>
|
||||
<td>{$statsQuery.data.stats[0].totalWalltime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Total Core Hours</th>
|
||||
<td>{$statsQuery.data.stats[0].totalCoreHours}</td>
|
||||
</tr>
|
||||
</Table>
|
||||
<Row cols={3} class="mb-4">
|
||||
<Col>
|
||||
<Table>
|
||||
<tr>
|
||||
<th scope="col">Total Jobs</th>
|
||||
<td>{$statsQuery.data.stats[0].totalJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Short Jobs</th>
|
||||
<td>{$statsQuery.data.stats[0].shortJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Total Walltime</th>
|
||||
<td>{$statsQuery.data.stats[0].totalWalltime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Total Core Hours</th>
|
||||
<td>{$statsQuery.data.stats[0].totalCoreHours}</td>
|
||||
</tr>
|
||||
</Table>
|
||||
</Col>
|
||||
<Col>
|
||||
<div bind:clientWidth={colWidth1}>
|
||||
<h5>Top Users</h5>
|
||||
{#key $statsQuery.data.topUsers}
|
||||
<Pie
|
||||
size={colWidth1}
|
||||
sliceLabel='Hours'
|
||||
quantities={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.count)}
|
||||
entities={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.name)}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
<div style="height: 60%;">
|
||||
{#key $statsQuery.data.topUsers}
|
||||
<h4>Top Users (by node hours)</h4>
|
||||
<Histogram
|
||||
width={colWidth - 25} height={300 * 0.5} small={true}
|
||||
data={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))}
|
||||
label={(x) => x < $statsQuery.data.topUsers.length ? $statsQuery.data.topUsers[Math.floor(x)].name : 'No Users'}
|
||||
ylabel="Node Hours [h]"/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Table>
|
||||
<tr class="mb-2"><th>Legend</th><th>User Name</th><th>Node Hours</th></tr>
|
||||
{#each $statsQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }, i}
|
||||
<tr>
|
||||
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
|
||||
<th scope="col"><a href="/monitoring/user/{name}?cluster={cluster.name}">{name}</a></th>
|
||||
<td>{count}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</Table>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row cols={3} class="mb-2">
|
||||
<Col>
|
||||
{#if $rooflineQuery.fetching}
|
||||
<Spinner />
|
||||
{:else if $rooflineQuery.error}
|
||||
<Card body color="danger">{$rooflineQuery.error.message}</Card>
|
||||
{:else if $rooflineQuery.data && cluster}
|
||||
<div bind:clientWidth={colWidth2}>
|
||||
{#key $rooflineQuery.data}
|
||||
<Roofline
|
||||
width={colWidth2} height={300}
|
||||
tiles={$rooflineQuery.data.rooflineHeatmap}
|
||||
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
|
||||
maxY={rooflineMaxY} />
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
</div>
|
||||
{/if}
|
||||
</Col>
|
||||
<Col>
|
||||
<div bind:clientWidth={colWidth3}>
|
||||
{#key $statsQuery.data.stats[0].histDuration}
|
||||
<Histogramuplot
|
||||
width={colWidth3} height={300}
|
||||
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
|
||||
width={colWidth - 25}
|
||||
title="Duration Distribution"
|
||||
xlabel="Current Runtimes"
|
||||
xunit="Hours"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-3">
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div bind:clientWidth={colWidth4}>
|
||||
{#key $statsQuery.data.stats[0].histNumNodes}
|
||||
<Histogramuplot
|
||||
width={colWidth4} height={300}
|
||||
data={convert2uplot($statsQuery.data.stats[0].histNumNodes)}
|
||||
width={colWidth - 25}
|
||||
title="Number of Nodes Distribution"
|
||||
xlabel="Allocated Nodes"
|
||||
xunit="Nodes"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-3">
|
||||
{#if $rooflineQuery.fetching}
|
||||
<Spinner />
|
||||
{:else if $rooflineQuery.error}
|
||||
<Card body color="danger">{$rooflineQuery.error.message}</Card>
|
||||
{:else if $rooflineQuery.data && cluster}
|
||||
{#key $rooflineQuery.data}
|
||||
<Roofline
|
||||
width={colWidth - 25}
|
||||
tiles={$rooflineQuery.data.rooflineHeatmap}
|
||||
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
|
||||
maxY={rooflineMaxY} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
|
||||
<br/>
|
||||
<hr class="my-6"/>
|
||||
|
||||
{#if $footprintsQuery.error}
|
||||
<Row>
|
||||
<Col>
|
||||
@@ -284,7 +307,7 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
h4 {
|
||||
h5 {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
@@ -20,12 +20,11 @@
|
||||
} from "sveltestrap";
|
||||
import PlotTable from "./PlotTable.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 JobInfo from "./joblist/JobInfo.svelte";
|
||||
import TagManagement from "./TagManagement.svelte";
|
||||
import MetricSelection from "./MetricSelection.svelte";
|
||||
import Zoom from "./Zoom.svelte";
|
||||
import StatsTable from "./StatsTable.svelte";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
@@ -134,7 +133,6 @@
|
||||
jobTags,
|
||||
fullWidth,
|
||||
statsTable;
|
||||
$: polarPlotSize = Math.min(fullWidth / 3 - 10, 300);
|
||||
$: document.title = $initq.fetching
|
||||
? "Loading..."
|
||||
: $initq.error
|
||||
@@ -246,9 +244,8 @@
|
||||
{/if}
|
||||
{/if}
|
||||
<Col>
|
||||
<PolarPlot
|
||||
width={polarPlotSize}
|
||||
height={polarPlotSize}
|
||||
<Polar
|
||||
size={fullWidth / 4.1}
|
||||
metrics={ccconfig[
|
||||
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
|
||||
] || ccconfig[`job_view_polarPlotMetrics`]}
|
||||
@@ -259,7 +256,7 @@
|
||||
<Col>
|
||||
<Roofline
|
||||
width={fullWidth / 3 - 10}
|
||||
height={polarPlotSize}
|
||||
height={fullWidth / 5}
|
||||
cluster={clusters
|
||||
.find((c) => c.name == $initq.data.job.cluster)
|
||||
.subClusters.find(
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import Refresher from './joblist/Refresher.svelte'
|
||||
import Roofline, { transformPerNodeData } from './plots/Roofline.svelte'
|
||||
import Histogram from './plots/Histogram.svelte'
|
||||
import Pie, { colors } from './plots/Pie.svelte'
|
||||
import Histogramuplot from './plots/Histogramuplot.svelte'
|
||||
import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap'
|
||||
import { init, convert2uplot } from './utils.js'
|
||||
@@ -160,21 +160,24 @@
|
||||
<Row cols={4}>
|
||||
<Col class="p-2">
|
||||
<div bind:clientWidth={colWidth1}>
|
||||
<h4 class="mb-3 text-center">Top Users</h4>
|
||||
<h4 class="text-center">Top Users</h4>
|
||||
{#key $mainQuery.data}
|
||||
<Histogram
|
||||
width={colWidth1 - 25}
|
||||
data={$mainQuery.data.topUsers.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))}
|
||||
label={(x) => x < $mainQuery.data.topUsers.length ? $mainQuery.data.topUsers[Math.floor(x)].name : '0'}
|
||||
xlabel="User Name" ylabel="Number of Jobs" />
|
||||
<Pie
|
||||
size={colWidth1}
|
||||
sliceLabel='Jobs'
|
||||
quantities={$mainQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.count)}
|
||||
entities={$mainQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.name)}
|
||||
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</Col>
|
||||
<Col class="px-4 py-2">
|
||||
<Table>
|
||||
<tr class="mb-2"><th>User Name</th><th>Number of Nodes</th></tr>
|
||||
{#each $mainQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }}
|
||||
<tr class="mb-2"><th>Legend</th><th>User Name</th><th>Number of Nodes</th></tr>
|
||||
{#each $mainQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }, i}
|
||||
<tr>
|
||||
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
|
||||
<th scope="col"><a href="/monitoring/user/{name}?cluster={cluster}&state=running">{name}</a></th>
|
||||
<td>{count}</td>
|
||||
</tr>
|
||||
@@ -182,20 +185,22 @@
|
||||
</Table>
|
||||
</Col>
|
||||
<Col class="p-2">
|
||||
<h4 class="mb-3 text-center">Top Projects</h4>
|
||||
<h4 class="text-center">Top Projects</h4>
|
||||
{#key $mainQuery.data}
|
||||
<Histogram
|
||||
width={colWidth1 - 25}
|
||||
data={$mainQuery.data.topProjects.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))}
|
||||
label={(x) => x < $mainQuery.data.topProjects.length ? $mainQuery.data.topProjects[Math.floor(x)].name : '0'}
|
||||
xlabel="Project Code" ylabel="Number of Jobs" />
|
||||
<Pie
|
||||
size={colWidth1}
|
||||
sliceLabel='Jobs'
|
||||
quantities={$mainQuery.data.topProjects.sort((a, b) => b.count - a.count).map((tp) => tp.count)}
|
||||
entities={$mainQuery.data.topProjects.sort((a, b) => b.count - a.count).map((tp) => tp.name)}
|
||||
/>
|
||||
{/key}
|
||||
</Col>
|
||||
<Col class="px-4 py-2">
|
||||
<Table>
|
||||
<tr class="mb-2"><th>Project Code</th><th>Number of Nodes</th></tr>
|
||||
{#each $mainQuery.data.topProjects.sort((a, b) => b.count - a.count) as { name, count }}
|
||||
<tr class="mb-2"><th>Legend</th><th>Project Code</th><th>Number of Nodes</th></tr>
|
||||
{#each $mainQuery.data.topProjects.sort((a, b) => b.count - a.count) as { name, count }, i}
|
||||
<tr>
|
||||
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
|
||||
<th scope="col"><a href="/monitoring/jobs/?cluster={cluster}&state=running&project={name}&projectMatch=eq">{name}</a></th>
|
||||
<td>{count}</td>
|
||||
</tr>
|
||||
@@ -232,4 +237,4 @@
|
||||
{/key}
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
{/if}
|
@@ -137,7 +137,7 @@
|
||||
{#key $stats.data.jobsStatistics[0].histDuration}
|
||||
<Histogramuplot
|
||||
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
||||
width={w2 - 25} height={histogramHeight}
|
||||
width={w1 - 25} height={histogramHeight}
|
||||
title="Duration Distribution"
|
||||
xlabel="Current Runtimes"
|
||||
xunit="Hours"
|
||||
|
79
web/frontend/src/plots/Pie.svelte
Normal file
79
web/frontend/src/plots/Pie.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<script context="module">
|
||||
// http://tsitsul.in/blog/coloropt/ : 12 colors normal
|
||||
export const colors = [
|
||||
'rgb(235,172,35)',
|
||||
'rgb(184,0,88)',
|
||||
'rgb(0,140,249)',
|
||||
'rgb(0,110,0)',
|
||||
'rgb(0,187,173)',
|
||||
'rgb(209,99,230)',
|
||||
'rgb(178,69,2)',
|
||||
'rgb(255,146,135)',
|
||||
'rgb(89,84,214)',
|
||||
'rgb(0,198,248)',
|
||||
'rgb(135,133,0)',
|
||||
'rgb(0,167,108)',
|
||||
'rgb(189,189,189)'
|
||||
]
|
||||
</script>
|
||||
<script>
|
||||
import { Pie } from 'svelte-chartjs';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement,
|
||||
CategoryScale
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement,
|
||||
CategoryScale
|
||||
);
|
||||
|
||||
export let size
|
||||
export let sliceLabel
|
||||
export let quantities
|
||||
export let entities
|
||||
export let displayLegend = false
|
||||
|
||||
const data = {
|
||||
labels: entities,
|
||||
datasets: [
|
||||
{
|
||||
label: sliceLabel,
|
||||
data: quantities,
|
||||
fill: 1,
|
||||
backgroundColor: colors.slice(0, quantities.length),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const options = {
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: displayLegend
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="chart-container" style="--container-width: {size}; --container-height: {size}">
|
||||
<Pie {data} {options}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
height: var(--container-height);
|
||||
width: var(--container-width);
|
||||
}
|
||||
</style>
|
@@ -1,22 +1,34 @@
|
||||
<div>
|
||||
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
||||
</div>
|
||||
|
||||
<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 size
|
||||
export let metrics
|
||||
export let width
|
||||
export let height
|
||||
export let cluster
|
||||
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')
|
||||
|
||||
let ctx, canvasElement
|
||||
|
||||
const labels = metrics.filter(name => {
|
||||
if (!jobMetrics.find(m => m.name == name && m.scope == "node")) {
|
||||
console.warn(`PolarPlot: No metric data for '${name}'`)
|
||||
@@ -46,145 +58,49 @@
|
||||
return avg / metric.series.length
|
||||
}
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'Max',
|
||||
values: getValuesForStat(getMax),
|
||||
color: 'rgb(0, 102, 255)',
|
||||
areaColor: 'rgba(0, 102, 255, 0.25)'
|
||||
},
|
||||
{
|
||||
name: 'Avg',
|
||||
values: getValuesForStat(getAvg),
|
||||
color: 'rgb(255, 153, 0)',
|
||||
areaColor: 'rgba(255, 153, 0, 0.25)'
|
||||
}
|
||||
]
|
||||
|
||||
function render() {
|
||||
if (!width || Number.isNaN(width))
|
||||
return
|
||||
|
||||
const centerX = width / 2
|
||||
const centerY = height / 2 - 15
|
||||
const radius = (Math.min(width, height) / 2) - 50
|
||||
|
||||
// Draw circles
|
||||
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 })
|
||||
const data = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Max',
|
||||
data: getValuesForStat(getMax),
|
||||
fill: 1,
|
||||
backgroundColor: 'rgba(0, 102, 255, 0.25)',
|
||||
borderColor: 'rgb(0, 102, 255)',
|
||||
pointBackgroundColor: 'rgb(0, 102, 255)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgb(0, 102, 255)'
|
||||
},
|
||||
{
|
||||
label: 'Avg',
|
||||
data: getValuesForStat(getAvg),
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(255, 153, 0, 0.25)',
|
||||
borderColor: 'rgb(255, 153, 0)',
|
||||
pointBackgroundColor: 'rgb(255, 153, 0)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgb(255, 153, 0)'
|
||||
}
|
||||
|
||||
// "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
|
||||
onMount(() => {
|
||||
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)
|
||||
// No custom defined options but keep for clarity
|
||||
const options = {
|
||||
maintainAspectRatio: false,
|
||||
animation: false
|
||||
}
|
||||
|
||||
$: sizeChanged(width, height)
|
||||
</script>
|
||||
|
||||
<div class="chart-container">
|
||||
<Radar {data} {options} width={size} height={size}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chart-container {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user