mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-01 11:13:50 +02:00
Migrate jobCompare and comparison plot
This commit is contained in:
parent
1e039cb1bf
commit
dceb92ba8e
@ -23,94 +23,91 @@
|
|||||||
import { formatTime, roundTwoDigits } from "./units.js";
|
import { formatTime, roundTwoDigits } from "./units.js";
|
||||||
import Comparogram from "./plots/Comparogram.svelte";
|
import Comparogram from "./plots/Comparogram.svelte";
|
||||||
|
|
||||||
const ccconfig = getContext("cc-config"),
|
/* Svelte 5 Props */
|
||||||
// initialized = getContext("initialized"),
|
let {
|
||||||
globalMetrics = getContext("globalMetrics");
|
matchedCompareJobs = $bindable(0),
|
||||||
|
metrics = $bindable(ccconfig?.plot_list_selectedMetrics),
|
||||||
|
filterBuffer = [],
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let matchedCompareJobs = 0;
|
/* Const Init */
|
||||||
export let metrics = ccconfig.plot_list_selectedMetrics;
|
const client = getContextClient();
|
||||||
export let filterBuffer = [];
|
const ccconfig = getContext("cc-config");
|
||||||
|
const globalMetrics = getContext("globalMetrics");
|
||||||
let filter = [...filterBuffer] || [];
|
// const initialized = getContext("initialized");
|
||||||
let comparePlotData = {};
|
|
||||||
let compareTableData = [];
|
|
||||||
let compareTableSorting = {};
|
|
||||||
let jobIds = [];
|
|
||||||
let jobClusters = [];
|
|
||||||
let tableJobIDFilter = "";
|
|
||||||
|
|
||||||
/*uPlot*/
|
|
||||||
let plotSync = uPlot.sync("compareJobsView");
|
|
||||||
|
|
||||||
/* GQL */
|
|
||||||
const client = getContextClient();
|
|
||||||
// Pull All Series For Metrics Statistics Only On Node Scope
|
// Pull All Series For Metrics Statistics Only On Node Scope
|
||||||
const compareQuery = gql`
|
const compareQuery = gql`
|
||||||
query ($filter: [JobFilter!]!, $metrics: [String!]!) {
|
query ($filter: [JobFilter!]!, $metrics: [String!]!) {
|
||||||
jobsMetricStats(filter: $filter, metrics: $metrics) {
|
jobsMetricStats(filter: $filter, metrics: $metrics) {
|
||||||
id
|
id
|
||||||
jobId
|
jobId
|
||||||
startTime
|
startTime
|
||||||
duration
|
duration
|
||||||
cluster
|
cluster
|
||||||
subCluster
|
subCluster
|
||||||
numNodes
|
numNodes
|
||||||
numHWThreads
|
numHWThreads
|
||||||
numAccelerators
|
numAccelerators
|
||||||
stats {
|
stats {
|
||||||
name
|
name
|
||||||
data {
|
data {
|
||||||
min
|
min
|
||||||
avg
|
avg
|
||||||
max
|
max
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/* REACTIVES */
|
/* Var Init*/
|
||||||
|
let plotSync = uPlot.sync("compareJobsView");
|
||||||
|
|
||||||
$: compareData = queryStore({
|
/* State Init */
|
||||||
client: client,
|
let filter = $state([...filterBuffer] || []);
|
||||||
query: compareQuery,
|
let tableJobIDFilter = $state("");
|
||||||
variables:{ filter, metrics },
|
|
||||||
});
|
|
||||||
|
|
||||||
$: matchedCompareJobs = $compareData.data != null ? $compareData.data.jobsMetricStats.length : -1;
|
/* Derived*/
|
||||||
|
const compareData = $derived(queryStore({
|
||||||
$: if ($compareData.data != null) {
|
client: client,
|
||||||
jobIds = [];
|
query: compareQuery,
|
||||||
jobClusters = [];
|
variables:{ filter, metrics },
|
||||||
comparePlotData = {};
|
})
|
||||||
compareTableData = [...$compareData.data.jobsMetricStats];
|
);
|
||||||
jobs2uplot($compareData.data.jobsMetricStats, metrics);
|
let jobIds = $derived($compareData?.data ? $compareData.data.jobsMetricStats.map((jms) => jms.jobId) : []);
|
||||||
}
|
let jobClusters = $derived($compareData?.data ? $compareData.data.jobsMetricStats.map((jms) => `${jms.cluster} ${jms.subCluster}`) : []);
|
||||||
|
let compareTableData = $derived($compareData?.data ? [...$compareData.data.jobsMetricStats] : []);
|
||||||
$: if ((!$compareData.fetching && !$compareData.error) && metrics) {
|
let comparePlotData = $derived($compareData?.data ? jobs2uplot($compareData.data.jobsMetricStats, metrics) : {});
|
||||||
|
let compareTableSorting = $derived.by(() => {
|
||||||
|
let pendingSort = {};
|
||||||
// Meta
|
// Meta
|
||||||
compareTableSorting['meta'] = {
|
pendingSort['meta'] = {
|
||||||
startTime: { dir: "down", active: true },
|
startTime: { dir: "down", active: true },
|
||||||
duration: { dir: "up", active: false },
|
duration: { dir: "up", active: false },
|
||||||
cluster: { dir: "up", active: false },
|
cluster: { dir: "up", active: false },
|
||||||
};
|
};
|
||||||
// Resources
|
// Resources
|
||||||
compareTableSorting['resources'] = {
|
pendingSort['resources'] = {
|
||||||
Nodes: { dir: "up", active: false },
|
Nodes: { dir: "up", active: false },
|
||||||
Threads: { dir: "up", active: false },
|
Threads: { dir: "up", active: false },
|
||||||
Accs: { dir: "up", active: false },
|
Accs: { dir: "up", active: false },
|
||||||
};
|
};
|
||||||
// Metrics
|
|
||||||
for (let metric of metrics) {
|
for (let metric of metrics) {
|
||||||
compareTableSorting[metric] = {
|
pendingSort[metric] = {
|
||||||
min: { dir: "up", active: false },
|
min: { dir: "up", active: false },
|
||||||
avg: { dir: "up", active: false },
|
avg: { dir: "up", active: false },
|
||||||
max: { dir: "up", active: false },
|
max: { dir: "up", active: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
return pendingSort;
|
||||||
|
});
|
||||||
|
|
||||||
/* FUNCTIONS */
|
/* Effect */
|
||||||
|
$effect(() => {
|
||||||
|
matchedCompareJobs = $compareData?.data != null ? $compareData.data.jobsMetricStats.length : -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
// (Re-)query and optionally set new filters; Query will be started reactively.
|
// (Re-)query and optionally set new filters; Query will be started reactively.
|
||||||
export function queryJobs(filters) {
|
export function queryJobs(filters) {
|
||||||
if (filters != null) {
|
if (filters != null) {
|
||||||
@ -178,42 +175,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function jobs2uplot(jobs, metrics) {
|
function jobs2uplot(jobs, metrics) {
|
||||||
|
// Proxy Init
|
||||||
|
let pendingComparePlotData = {};
|
||||||
// Resources Init
|
// Resources Init
|
||||||
comparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS]
|
pendingComparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS]
|
||||||
// Metric Init
|
// Metric Init
|
||||||
for (let m of metrics) {
|
for (let m of metrics) {
|
||||||
// Get Unit
|
// Get Unit
|
||||||
const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit
|
const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit
|
||||||
const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
|
const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
|
||||||
comparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX]
|
pendingComparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate jobs if exists
|
// Iterate jobs if exists
|
||||||
if (jobs) {
|
if (jobs) {
|
||||||
let plotIndex = 0
|
let plotIndex = 0
|
||||||
jobs.forEach((j) => {
|
jobs.forEach((j) => {
|
||||||
// Collect JobIDs & Clusters for X-Ticks and Legend
|
|
||||||
jobIds.push(j.jobId)
|
|
||||||
jobClusters.push(`${j.cluster} ${j.subCluster}`)
|
|
||||||
// Resources
|
// Resources
|
||||||
comparePlotData['resources'].data[0].push(plotIndex)
|
pendingComparePlotData['resources'].data[0].push(plotIndex)
|
||||||
comparePlotData['resources'].data[1].push(j.startTime)
|
pendingComparePlotData['resources'].data[1].push(j.startTime)
|
||||||
comparePlotData['resources'].data[2].push(j.duration)
|
pendingComparePlotData['resources'].data[2].push(j.duration)
|
||||||
comparePlotData['resources'].data[3].push(j.numNodes)
|
pendingComparePlotData['resources'].data[3].push(j.numNodes)
|
||||||
comparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
|
pendingComparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
|
||||||
comparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
|
pendingComparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
|
||||||
// Metrics
|
// Metrics
|
||||||
for (let s of j.stats) {
|
for (let s of j.stats) {
|
||||||
comparePlotData[s.name].data[0].push(plotIndex)
|
pendingComparePlotData[s.name].data[0].push(plotIndex)
|
||||||
comparePlotData[s.name].data[1].push(j.startTime)
|
pendingComparePlotData[s.name].data[1].push(j.startTime)
|
||||||
comparePlotData[s.name].data[2].push(j.duration)
|
pendingComparePlotData[s.name].data[2].push(j.duration)
|
||||||
comparePlotData[s.name].data[3].push(s.data.min)
|
pendingComparePlotData[s.name].data[3].push(s.data.min)
|
||||||
comparePlotData[s.name].data[4].push(s.data.avg)
|
pendingComparePlotData[s.name].data[4].push(s.data.avg)
|
||||||
comparePlotData[s.name].data[5].push(s.data.max)
|
pendingComparePlotData[s.name].data[5].push(s.data.max)
|
||||||
}
|
}
|
||||||
plotIndex++
|
plotIndex++
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return {...pendingComparePlotData};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapt for Persisting Job Selections in DB later down the line
|
// Adapt for Persisting Job Selections in DB later down the line
|
||||||
@ -242,7 +239,6 @@
|
|||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $compareData.fetching}
|
{#if $compareData.fetching}
|
||||||
@ -269,7 +265,7 @@
|
|||||||
xticks={jobIds}
|
xticks={jobIds}
|
||||||
xinfo={jobClusters}
|
xinfo={jobClusters}
|
||||||
ylabel={'Resource Counts'}
|
ylabel={'Resource Counts'}
|
||||||
data={comparePlotData['resources'].data}
|
data={comparePlotData['resources']?.data}
|
||||||
{plotSync}
|
{plotSync}
|
||||||
forResources
|
forResources
|
||||||
/>
|
/>
|
||||||
@ -285,8 +281,8 @@
|
|||||||
xinfo={jobClusters}
|
xinfo={jobClusters}
|
||||||
ylabel={m}
|
ylabel={m}
|
||||||
metric={m}
|
metric={m}
|
||||||
yunit={comparePlotData[m].unit}
|
yunit={comparePlotData[m]?.unit}
|
||||||
data={comparePlotData[m].data}
|
data={comparePlotData[m]?.data}
|
||||||
{plotSync}
|
{plotSync}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -318,7 +314,7 @@
|
|||||||
</InputGroupText>
|
</InputGroupText>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</th>
|
</th>
|
||||||
<th on:click={() => sortBy('meta', 'startTime')}>
|
<th onclick={() => sortBy('meta', 'startTime')}>
|
||||||
Sort
|
Sort
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime']
|
name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime']
|
||||||
@ -327,7 +323,7 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th on:click={() => sortBy('meta', 'duration')}>
|
<th onclick={() => sortBy('meta', 'duration')}>
|
||||||
Sort
|
Sort
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration']
|
name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration']
|
||||||
@ -336,7 +332,7 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th on:click={() => sortBy('meta', 'cluster')}>
|
<th onclick={() => sortBy('meta', 'cluster')}>
|
||||||
Sort
|
Sort
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster']
|
name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster']
|
||||||
@ -346,7 +342,7 @@
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
{#each ["Nodes", "Threads", "Accs"] as res}
|
{#each ["Nodes", "Threads", "Accs"] as res}
|
||||||
<th on:click={() => sortBy('resources', res)}>
|
<th onclick={() => sortBy('resources', res)}>
|
||||||
{res}
|
{res}
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res]
|
name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res]
|
||||||
@ -358,7 +354,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{#each metrics as metric}
|
{#each metrics as metric}
|
||||||
{#each ["min", "avg", "max"] as stat}
|
{#each ["min", "avg", "max"] as stat}
|
||||||
<th on:click={() => sortBy(metric, stat)}>
|
<th onclick={() => sortBy(metric, stat)}>
|
||||||
{stat.charAt(0).toUpperCase() + stat.slice(1)}
|
{stat.charAt(0).toUpperCase() + stat.slice(1)}
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat]
|
name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat]
|
||||||
|
@ -18,86 +18,30 @@
|
|||||||
import { getContext, onMount, onDestroy } from "svelte";
|
import { getContext, onMount, onDestroy } from "svelte";
|
||||||
import { Card } from "@sveltestrap/sveltestrap";
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let metric = "";
|
|
||||||
export let width = 0;
|
|
||||||
export let height = 300;
|
|
||||||
export let data = null;
|
|
||||||
export let xlabel = "";
|
|
||||||
export let xticks = [];
|
|
||||||
export let xinfo = [];
|
|
||||||
export let ylabel = "";
|
|
||||||
export let yunit = "";
|
|
||||||
export let title = "";
|
|
||||||
export let forResources = false;
|
|
||||||
export let plotSync;
|
|
||||||
|
|
||||||
// NOTE: Metric Thresholds non-required, Cluster Mixing Allowed
|
// NOTE: Metric Thresholds non-required, Cluster Mixing Allowed
|
||||||
|
|
||||||
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
metric = "",
|
||||||
|
width = 0,
|
||||||
|
height = 300,
|
||||||
|
data = null,
|
||||||
|
xlabel = "",
|
||||||
|
xticks = [],
|
||||||
|
xinfo = [],
|
||||||
|
ylabel = "",
|
||||||
|
yunit = "",
|
||||||
|
title = "",
|
||||||
|
forResources = false,
|
||||||
|
plotSync,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const clusterCockpitConfig = getContext("cc-config");
|
const clusterCockpitConfig = getContext("cc-config");
|
||||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
|
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
|
||||||
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
|
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
|
||||||
|
|
||||||
// UPLOT PLUGIN // converts the legend into a simple tooltip
|
// UPLOT SERIES INIT //
|
||||||
function legendAsTooltipPlugin({
|
|
||||||
className,
|
|
||||||
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
|
||||||
} = {}) {
|
|
||||||
let legendEl;
|
|
||||||
|
|
||||||
function init(u, opts) {
|
|
||||||
legendEl = u.root.querySelector(".u-legend");
|
|
||||||
|
|
||||||
legendEl.classList.remove("u-inline");
|
|
||||||
className && legendEl.classList.add(className);
|
|
||||||
|
|
||||||
uPlot.assign(legendEl.style, {
|
|
||||||
minWidth: "100px",
|
|
||||||
textAlign: "left",
|
|
||||||
pointerEvents: "none",
|
|
||||||
display: "none",
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
zIndex: 100,
|
|
||||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
|
||||||
...style,
|
|
||||||
});
|
|
||||||
|
|
||||||
// hide series color markers:
|
|
||||||
const idents = legendEl.querySelectorAll(".u-marker");
|
|
||||||
for (let i = 0; i < idents.length; i++)
|
|
||||||
idents[i].style.display = "none";
|
|
||||||
|
|
||||||
const overEl = u.over;
|
|
||||||
overEl.style.overflow = "visible";
|
|
||||||
|
|
||||||
// move legend into plot bounds
|
|
||||||
overEl.appendChild(legendEl);
|
|
||||||
|
|
||||||
// show/hide tooltip on enter/exit
|
|
||||||
overEl.addEventListener("mouseenter", () => {
|
|
||||||
legendEl.style.display = null;
|
|
||||||
});
|
|
||||||
overEl.addEventListener("mouseleave", () => {
|
|
||||||
legendEl.style.display = "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(u) {
|
|
||||||
const { left, top } = u.cursor;
|
|
||||||
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
|
|
||||||
legendEl.style.transform =
|
|
||||||
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hooks: {
|
|
||||||
init: init,
|
|
||||||
setCursor: update,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const plotSeries = [
|
const plotSeries = [
|
||||||
{
|
{
|
||||||
label: "JobID",
|
label: "JobID",
|
||||||
@ -122,6 +66,7 @@
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// UPLOT SCALES INIT //
|
||||||
if (forResources) {
|
if (forResources) {
|
||||||
const resSeries = [
|
const resSeries = [
|
||||||
{
|
{
|
||||||
@ -177,11 +122,13 @@
|
|||||||
plotSeries.push(...statsSeries)
|
plotSeries.push(...statsSeries)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// UPLOT BAND COLORS //
|
||||||
const plotBands = [
|
const plotBands = [
|
||||||
{ series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
|
{ series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
|
||||||
{ series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
|
{ series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// UPLOT OPTIONS //
|
||||||
const opts = {
|
const opts = {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@ -259,11 +206,83 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// RENDER HANDLING
|
/* Var Init */
|
||||||
let plotWrapper = null;
|
|
||||||
let uplot = null;
|
|
||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
|
let uplot = null;
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let plotWrapper = $state(null);
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
$effect(() => {
|
||||||
|
if (plotWrapper) {
|
||||||
|
onSizeChange(width, height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
// UPLOT PLUGIN // converts the legend into a simple tooltip
|
||||||
|
function legendAsTooltipPlugin({
|
||||||
|
className,
|
||||||
|
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||||
|
} = {}) {
|
||||||
|
let legendEl;
|
||||||
|
|
||||||
|
function init(u, opts) {
|
||||||
|
legendEl = u.root.querySelector(".u-legend");
|
||||||
|
|
||||||
|
legendEl.classList.remove("u-inline");
|
||||||
|
className && legendEl.classList.add(className);
|
||||||
|
|
||||||
|
uPlot.assign(legendEl.style, {
|
||||||
|
minWidth: "100px",
|
||||||
|
textAlign: "left",
|
||||||
|
pointerEvents: "none",
|
||||||
|
display: "none",
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||||
|
...style,
|
||||||
|
});
|
||||||
|
|
||||||
|
// hide series color markers:
|
||||||
|
const idents = legendEl.querySelectorAll(".u-marker");
|
||||||
|
for (let i = 0; i < idents.length; i++)
|
||||||
|
idents[i].style.display = "none";
|
||||||
|
|
||||||
|
const overEl = u.over;
|
||||||
|
overEl.style.overflow = "visible";
|
||||||
|
|
||||||
|
// move legend into plot bounds
|
||||||
|
overEl.appendChild(legendEl);
|
||||||
|
|
||||||
|
// show/hide tooltip on enter/exit
|
||||||
|
overEl.addEventListener("mouseenter", () => {
|
||||||
|
legendEl.style.display = null;
|
||||||
|
});
|
||||||
|
overEl.addEventListener("mouseleave", () => {
|
||||||
|
legendEl.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(u) {
|
||||||
|
const { left, top } = u.cursor;
|
||||||
|
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
|
||||||
|
legendEl.style.transform =
|
||||||
|
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hooks: {
|
||||||
|
init: init,
|
||||||
|
setCursor: update,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// RENDER HANDLING
|
||||||
function render(ren_width, ren_height) {
|
function render(ren_width, ren_height) {
|
||||||
if (!uplot) {
|
if (!uplot) {
|
||||||
opts.width = ren_width;
|
opts.width = ren_width;
|
||||||
@ -284,22 +303,18 @@
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* On Mount */
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (plotWrapper) {
|
if (plotWrapper) {
|
||||||
render(width, height);
|
render(width, height);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* On Destroy */
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (timeoutId != null) clearTimeout(timeoutId);
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
if (uplot) uplot.destroy();
|
if (uplot) uplot.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// This updates plot on all size changes if wrapper (== data) exists
|
|
||||||
$: if (plotWrapper) {
|
|
||||||
onSizeChange(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Define $width Wrapper and NoData Card -->
|
<!-- Define $width Wrapper and NoData Card -->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user