mirror of
https://github.com/KohakuBlueleaf/KohakuHub.git
synced 2026-04-28 09:57:43 -05:00
add hover sync
This commit is contained in:
@@ -10,11 +10,20 @@ const props = defineProps({
|
||||
availableMetrics: Array,
|
||||
initialConfig: Object,
|
||||
experimentId: String,
|
||||
tabName: String,
|
||||
hoverSyncEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:config", "remove"]);
|
||||
|
||||
console.log(`[${props.cardId}] Component created`);
|
||||
console.log(`[${props.cardId}] Hover sync props:`, {
|
||||
tabName: props.tabName,
|
||||
hoverSyncEnabled: props.hoverSyncEnabled,
|
||||
});
|
||||
|
||||
// Direct reference to props
|
||||
const cfg = props.initialConfig;
|
||||
@@ -488,6 +497,9 @@ function startResizeRight(e) {
|
||||
:smoothing-mode="props.initialConfig.smoothingMode"
|
||||
:smoothing-value="props.initialConfig.smoothingValue"
|
||||
:downsample-rate="props.initialConfig.downsampleRate"
|
||||
:tab-name="props.tabName"
|
||||
:chart-id="props.cardId"
|
||||
:hover-sync-enabled="props.hoverSyncEnabled"
|
||||
@update:smoothing-mode="(v) => emitConfig({ smoothingMode: v })"
|
||||
@update:smoothing-value="(v) => emitConfig({ smoothingValue: v })"
|
||||
@update:downsample-rate="(v) => emitConfig({ downsampleRate: v })"
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
import { ref, reactive, onMounted, watch, onUnmounted, computed } from "vue";
|
||||
import Plotly from "plotly.js-dist-min";
|
||||
import { useAnimationPreference } from "@/composables/useAnimationPreference";
|
||||
import { useHoverSync } from "@/composables/useHoverSync";
|
||||
|
||||
const { animationsEnabled } = useAnimationPreference();
|
||||
const { registerChart, unregisterChart } = useHoverSync();
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:smoothingMode",
|
||||
@@ -50,10 +52,24 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
// Hover sync props
|
||||
tabName: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
chartId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hoverSyncEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const plotDiv = ref(null);
|
||||
let myResizeObserver = null;
|
||||
let unregisterHoverSync = null;
|
||||
const showConfigModal = ref(false);
|
||||
const modalMouseDownTarget = ref(null);
|
||||
|
||||
@@ -94,6 +110,11 @@ onMounted(() => {
|
||||
createPlot();
|
||||
setupResizeObserver();
|
||||
watchThemeChanges();
|
||||
|
||||
// Use nextTick to ensure plotDiv is fully initialized after createPlot
|
||||
nextTick(() => {
|
||||
setupHoverSync();
|
||||
});
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
@@ -105,6 +126,9 @@ onUnmounted(() => {
|
||||
if (myResizeObserver) {
|
||||
myResizeObserver.disconnect();
|
||||
}
|
||||
if (unregisterHoverSync) {
|
||||
unregisterHoverSync();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -569,6 +593,53 @@ function watchThemeChanges() {
|
||||
onUnmounted(() => observer.disconnect());
|
||||
}
|
||||
|
||||
function setupHoverSync() {
|
||||
console.log(`[LinePlot] setupHoverSync called for chart ${props.chartId}`);
|
||||
console.log(`[LinePlot] Props:`, {
|
||||
hoverSyncEnabled: props.hoverSyncEnabled,
|
||||
tabName: props.tabName,
|
||||
chartId: props.chartId,
|
||||
xaxis: props.xaxis,
|
||||
hasPlotDiv: !!plotDiv.value,
|
||||
});
|
||||
|
||||
if (!props.hoverSyncEnabled) {
|
||||
console.log(
|
||||
`[LinePlot] Hover sync disabled via prop, skipping registration`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.tabName) {
|
||||
console.log(`[LinePlot] No tabName provided, skipping registration`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plotDiv.value) {
|
||||
console.log(`[LinePlot] plotDiv not available yet, skipping registration`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to get current x-axis type from props
|
||||
const getXAxisType = () => {
|
||||
console.log(`[LinePlot] getXAxisType called, returning:`, props.xaxis);
|
||||
return props.xaxis;
|
||||
};
|
||||
|
||||
// Register this chart for hover synchronization
|
||||
console.log(`[LinePlot] Calling registerChart for ${props.chartId}`);
|
||||
unregisterHoverSync = registerChart(
|
||||
props.tabName,
|
||||
props.chartId,
|
||||
plotDiv.value,
|
||||
getXAxisType,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[LinePlot] Successfully registered for hover sync: ${props.chartId} in tab ${props.tabName}`,
|
||||
);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetView,
|
||||
exportPNG,
|
||||
|
||||
448
src/kohaku-board-ui/src/composables/useHoverSync.js
Normal file
448
src/kohaku-board-ui/src/composables/useHoverSync.js
Normal file
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* Composable for synchronizing hover events across multiple Plotly charts
|
||||
* When hovering over one chart, all registered charts in the same tab will show tooltips at the same x-axis position
|
||||
*/
|
||||
|
||||
import { ref } from "vue";
|
||||
import Plotly from "plotly.js-dist-min";
|
||||
|
||||
const LOG_PREFIX = "[HoverSync]";
|
||||
|
||||
// Global state for hover synchronization
|
||||
const hoverSyncEnabled = ref(true);
|
||||
const registeredCharts = ref(new Map()); // Map<tabName, Set<chartInfo>>
|
||||
const isHovering = ref(false);
|
||||
|
||||
console.log(
|
||||
`${LOG_PREFIX} Module loaded, hoverSyncEnabled:`,
|
||||
hoverSyncEnabled.value,
|
||||
);
|
||||
|
||||
// Expose debug helper to window for console debugging
|
||||
if (typeof window !== "undefined") {
|
||||
window.__hoverSyncDebug = () => {
|
||||
const info = {
|
||||
enabled: hoverSyncEnabled.value,
|
||||
isHovering: isHovering.value,
|
||||
tabs: {},
|
||||
};
|
||||
|
||||
registeredCharts.value.forEach((charts, tabName) => {
|
||||
info.tabs[tabName] = {
|
||||
chartCount: charts.size,
|
||||
charts: Array.from(charts).map((c) => ({
|
||||
id: c.id,
|
||||
xAxisType: c.getXAxisType(),
|
||||
hasElement: !!c.element,
|
||||
hasData: !!(c.element && c.element.data),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`${LOG_PREFIX} Debug Info:`, info);
|
||||
return info;
|
||||
};
|
||||
console.log(
|
||||
`${LOG_PREFIX} Debug helper available: window.__hoverSyncDebug()`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a chart for hover synchronization
|
||||
* @param {string} tabName - The tab this chart belongs to
|
||||
* @param {string} chartId - Unique identifier for this chart
|
||||
* @param {HTMLElement} plotElement - The Plotly div element
|
||||
* @param {Function} getXAxisType - Function that returns the current x-axis type (e.g., 'step', 'walltime')
|
||||
*/
|
||||
export function useHoverSync() {
|
||||
const registerChart = (tabName, chartId, plotElement, getXAxisType) => {
|
||||
console.log(`${LOG_PREFIX} registerChart called:`, {
|
||||
tabName,
|
||||
chartId,
|
||||
hasPlotElement: !!plotElement,
|
||||
});
|
||||
|
||||
if (!registeredCharts.value.has(tabName)) {
|
||||
console.log(`${LOG_PREFIX} Creating new tab entry for:`, tabName);
|
||||
registeredCharts.value.set(tabName, new Set());
|
||||
}
|
||||
|
||||
const chartInfo = {
|
||||
id: chartId,
|
||||
element: plotElement,
|
||||
getXAxisType: getXAxisType,
|
||||
};
|
||||
|
||||
registeredCharts.value.get(tabName).add(chartInfo);
|
||||
console.log(
|
||||
`${LOG_PREFIX} Chart registered. Tab "${tabName}" now has ${registeredCharts.value.get(tabName).size} charts`,
|
||||
);
|
||||
|
||||
// Set up hover event listeners
|
||||
plotElement.on("plotly_hover", (eventData) => {
|
||||
console.log(`${LOG_PREFIX} plotly_hover event fired on chart:`, chartId);
|
||||
console.log(`${LOG_PREFIX} Event data:`, eventData);
|
||||
console.log(
|
||||
`${LOG_PREFIX} hoverSyncEnabled:`,
|
||||
hoverSyncEnabled.value,
|
||||
"isHovering:",
|
||||
isHovering.value,
|
||||
);
|
||||
|
||||
if (!hoverSyncEnabled.value) {
|
||||
console.log(`${LOG_PREFIX} Hover sync is disabled, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHovering.value) {
|
||||
console.log(
|
||||
`${LOG_PREFIX} Already processing a hover event, skipping to prevent loop`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isHovering.value = true;
|
||||
const xValue = eventData.points[0].x;
|
||||
const xAxisType = getXAxisType();
|
||||
|
||||
console.log(
|
||||
`${LOG_PREFIX} Processing hover: xValue=${xValue}, xAxisType=${xAxisType}`,
|
||||
);
|
||||
|
||||
// Trigger hover on all other charts in the same tab with matching x-axis type
|
||||
syncHoverAcrossCharts(tabName, chartId, xValue, xAxisType);
|
||||
|
||||
isHovering.value = false;
|
||||
});
|
||||
|
||||
plotElement.on("plotly_unhover", () => {
|
||||
console.log(
|
||||
`${LOG_PREFIX} plotly_unhover event fired on chart:`,
|
||||
chartId,
|
||||
);
|
||||
|
||||
if (!hoverSyncEnabled.value || isHovering.value) return;
|
||||
|
||||
isHovering.value = true;
|
||||
|
||||
// Clear hover on all charts in the same tab
|
||||
clearHoverAcrossCharts(tabName, chartId);
|
||||
|
||||
isHovering.value = false;
|
||||
});
|
||||
|
||||
console.log(`${LOG_PREFIX} Event listeners attached to chart:`, chartId);
|
||||
return () => unregisterChart(tabName, chartId);
|
||||
};
|
||||
|
||||
const unregisterChart = (tabName, chartId) => {
|
||||
console.log(`${LOG_PREFIX} unregisterChart called:`, { tabName, chartId });
|
||||
const charts = registeredCharts.value.get(tabName);
|
||||
if (charts) {
|
||||
const chartToRemove = Array.from(charts).find((c) => c.id === chartId);
|
||||
if (chartToRemove) {
|
||||
charts.delete(chartToRemove);
|
||||
console.log(
|
||||
`${LOG_PREFIX} Chart unregistered. Tab "${tabName}" now has ${charts.size} charts`,
|
||||
);
|
||||
}
|
||||
|
||||
if (charts.size === 0) {
|
||||
registeredCharts.value.delete(tabName);
|
||||
console.log(`${LOG_PREFIX} Tab "${tabName}" removed (no charts left)`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncHoverAcrossCharts = (tabName, sourceChartId, xValue, xAxisType) => {
|
||||
console.log(`${LOG_PREFIX} syncHoverAcrossCharts called:`, {
|
||||
tabName,
|
||||
sourceChartId,
|
||||
xValue,
|
||||
xAxisType,
|
||||
});
|
||||
|
||||
const charts = registeredCharts.value.get(tabName);
|
||||
if (!charts) {
|
||||
console.log(`${LOG_PREFIX} No charts registered for tab:`, tabName);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${LOG_PREFIX} Found ${charts.size} charts in tab "${tabName}"`,
|
||||
);
|
||||
|
||||
let syncedCount = 0;
|
||||
charts.forEach((chartInfo) => {
|
||||
console.log(
|
||||
`${LOG_PREFIX} Checking chart:`,
|
||||
chartInfo.id,
|
||||
"xAxisType:",
|
||||
chartInfo.getXAxisType(),
|
||||
);
|
||||
|
||||
// Skip the source chart and charts with different x-axis types
|
||||
if (chartInfo.id === sourceChartId) {
|
||||
console.log(`${LOG_PREFIX} Skipping source chart:`, chartInfo.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (chartInfo.getXAxisType() !== xAxisType) {
|
||||
console.log(
|
||||
`${LOG_PREFIX} Skipping chart ${chartInfo.id} - different x-axis type:`,
|
||||
chartInfo.getXAxisType(),
|
||||
"vs",
|
||||
xAxisType,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Find the closest point to the x value
|
||||
const plotData = chartInfo.element.data;
|
||||
if (!plotData || plotData.length === 0) {
|
||||
console.log(`${LOG_PREFIX} No plot data for chart:`, chartInfo.id);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${LOG_PREFIX} Chart ${chartInfo.id} has ${plotData.length} traces`,
|
||||
);
|
||||
|
||||
// Find points across all traces
|
||||
const hoverPoints = [];
|
||||
|
||||
plotData.forEach((trace, traceIndex) => {
|
||||
if (!trace.x || trace.x.length === 0) {
|
||||
console.log(`${LOG_PREFIX} Trace ${traceIndex} has no x data`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the closest point index
|
||||
let closestIndex = 0;
|
||||
let minDistance = Math.abs(trace.x[0] - xValue);
|
||||
|
||||
for (let i = 1; i < trace.x.length; i++) {
|
||||
const distance = Math.abs(trace.x[i] - xValue);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add if the distance is reasonable (not too far away)
|
||||
// This prevents showing tooltips when hovering at the edge where data doesn't exist
|
||||
const threshold =
|
||||
(Math.max(...trace.x) - Math.min(...trace.x)) * 0.05; // 5% of range
|
||||
console.log(
|
||||
`${LOG_PREFIX} Trace ${traceIndex}: closestIndex=${closestIndex}, minDistance=${minDistance}, threshold=${threshold}`,
|
||||
);
|
||||
|
||||
if (minDistance <= threshold) {
|
||||
hoverPoints.push({
|
||||
curveNumber: traceIndex,
|
||||
pointNumber: closestIndex,
|
||||
});
|
||||
console.log(
|
||||
`${LOG_PREFIX} Added hover point for trace ${traceIndex} at index ${closestIndex}`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`${LOG_PREFIX} Point too far away for trace ${traceIndex}, skipping`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (hoverPoints.length > 0) {
|
||||
console.log(
|
||||
`${LOG_PREFIX} Triggering hover on chart ${chartInfo.id} with points:`,
|
||||
hoverPoints,
|
||||
);
|
||||
console.log(`${LOG_PREFIX} Chart element:`, chartInfo.element);
|
||||
const hovermode = chartInfo.element.layout?.hovermode;
|
||||
console.log(`${LOG_PREFIX} Chart layout.hovermode:`, hovermode);
|
||||
console.log(`${LOG_PREFIX} Plotly.Fx available:`, !!Plotly.Fx);
|
||||
console.log(
|
||||
`${LOG_PREFIX} Plotly.Fx.hover available:`,
|
||||
!!Plotly.Fx?.hover,
|
||||
);
|
||||
|
||||
try {
|
||||
// For "x unified" mode, we need to use xval instead of point numbers
|
||||
if (hovermode === "x unified" || hovermode === "x") {
|
||||
// Use xval to trigger hover at specific x-coordinate
|
||||
const hoverData = hoverPoints.map((pt) => ({
|
||||
curveNumber: pt.curveNumber,
|
||||
xval: xValue, // Use the actual x-value instead of pointNumber
|
||||
}));
|
||||
console.log(
|
||||
`${LOG_PREFIX} Using xval-based hover for unified mode:`,
|
||||
hoverData,
|
||||
);
|
||||
|
||||
// Call Plotly.Fx.hover to show tooltips
|
||||
const result = Plotly.Fx.hover(chartInfo.element, hoverData);
|
||||
console.log(`${LOG_PREFIX} Plotly.Fx.hover() result:`, result);
|
||||
|
||||
// Plotly.Fx.hover doesn't trigger spikes, so we need to simulate a mouse event
|
||||
// to make Plotly render the spike lines
|
||||
try {
|
||||
const plotlyDiv = chartInfo.element;
|
||||
const plotArea = plotlyDiv.querySelector(".plot");
|
||||
const xaxis = plotlyDiv._fullLayout?.xaxis;
|
||||
const yaxis = plotlyDiv._fullLayout?.yaxis;
|
||||
|
||||
if (plotArea && xaxis && yaxis) {
|
||||
// Convert data x-value to pixel position
|
||||
const xPixel = xaxis.l2p(xValue) + xaxis._offset;
|
||||
|
||||
// Get a reasonable y-pixel position (middle of plot area)
|
||||
const yPixel = yaxis._offset + yaxis._length / 2;
|
||||
|
||||
console.log(
|
||||
`${LOG_PREFIX} Calculated pixel position: x=${xPixel}, y=${yPixel}`,
|
||||
);
|
||||
console.log(
|
||||
`${LOG_PREFIX} xaxis range:`,
|
||||
xaxis.range,
|
||||
"offset:",
|
||||
xaxis._offset,
|
||||
);
|
||||
|
||||
// Get bounding rect for proper client coordinates
|
||||
const rect = plotArea.getBoundingClientRect();
|
||||
|
||||
// Create and dispatch a mousemove event at the calculated position
|
||||
const mouseEvent = new MouseEvent("mousemove", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
clientX: rect.left + xPixel,
|
||||
clientY: rect.top + yPixel,
|
||||
screenX: window.screenX + rect.left + xPixel,
|
||||
screenY: window.screenY + rect.top + yPixel,
|
||||
});
|
||||
|
||||
plotArea.dispatchEvent(mouseEvent);
|
||||
console.log(
|
||||
`${LOG_PREFIX} Dispatched mousemove event to trigger spikes`,
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`${LOG_PREFIX} Missing plotArea or axis layout:`,
|
||||
{
|
||||
hasPlotArea: !!plotArea,
|
||||
hasXaxis: !!xaxis,
|
||||
hasYaxis: !!yaxis,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (spikeError) {
|
||||
console.warn(
|
||||
`${LOG_PREFIX} Failed to trigger spike with mouse event:`,
|
||||
spikeError,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Use point number for other hover modes
|
||||
const result = Plotly.Fx.hover(chartInfo.element, hoverPoints);
|
||||
console.log(`${LOG_PREFIX} Plotly.Fx.hover() result:`, result);
|
||||
}
|
||||
syncedCount++;
|
||||
} catch (hoverError) {
|
||||
console.error(
|
||||
`${LOG_PREFIX} Plotly.Fx.hover() threw error:`,
|
||||
hoverError,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`${LOG_PREFIX} No valid hover points found for chart:`,
|
||||
chartInfo.id,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${LOG_PREFIX} Failed to sync hover for chart ${chartInfo.id}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`${LOG_PREFIX} Synced hover to ${syncedCount} charts`);
|
||||
};
|
||||
|
||||
const clearHoverAcrossCharts = (tabName, sourceChartId) => {
|
||||
console.log(`${LOG_PREFIX} clearHoverAcrossCharts called:`, {
|
||||
tabName,
|
||||
sourceChartId,
|
||||
});
|
||||
|
||||
const charts = registeredCharts.value.get(tabName);
|
||||
if (!charts) return;
|
||||
|
||||
charts.forEach((chartInfo) => {
|
||||
if (chartInfo.id === sourceChartId) return;
|
||||
|
||||
try {
|
||||
// Use Plotly.Fx.unhover to clear hover state
|
||||
Plotly.Fx.unhover(chartInfo.element);
|
||||
console.log(`${LOG_PREFIX} Cleared hover for chart:`, chartInfo.id);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${LOG_PREFIX} Failed to clear hover for chart ${chartInfo.id}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const toggleHoverSync = () => {
|
||||
hoverSyncEnabled.value = !hoverSyncEnabled.value;
|
||||
console.log(
|
||||
`${LOG_PREFIX} Hover sync toggled. New value:`,
|
||||
hoverSyncEnabled.value,
|
||||
);
|
||||
};
|
||||
|
||||
const setHoverSync = (enabled) => {
|
||||
hoverSyncEnabled.value = enabled;
|
||||
console.log(`${LOG_PREFIX} Hover sync set to:`, enabled);
|
||||
};
|
||||
|
||||
const clearAllRegistrations = () => {
|
||||
console.log(`${LOG_PREFIX} Clearing all chart registrations`);
|
||||
registeredCharts.value.clear();
|
||||
};
|
||||
|
||||
const getDebugInfo = () => {
|
||||
const info = {
|
||||
enabled: hoverSyncEnabled.value,
|
||||
isHovering: isHovering.value,
|
||||
tabs: {},
|
||||
};
|
||||
|
||||
registeredCharts.value.forEach((charts, tabName) => {
|
||||
info.tabs[tabName] = {
|
||||
chartCount: charts.size,
|
||||
charts: Array.from(charts).map((c) => ({
|
||||
id: c.id,
|
||||
xAxisType: c.getXAxisType(),
|
||||
hasElement: !!c.element,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
return {
|
||||
registerChart,
|
||||
unregisterChart,
|
||||
toggleHoverSync,
|
||||
setHoverSync,
|
||||
clearAllRegistrations,
|
||||
hoverSyncEnabled,
|
||||
getDebugInfo, // For debugging
|
||||
};
|
||||
}
|
||||
@@ -2,9 +2,11 @@
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import ConfigurableChartCard from "@/components/ConfigurableChartCard.vue";
|
||||
import { useAnimationPreference } from "@/composables/useAnimationPreference";
|
||||
import { useHoverSync } from "@/composables/useHoverSync";
|
||||
|
||||
const route = useRoute();
|
||||
const { animationsEnabled } = useAnimationPreference();
|
||||
const { hoverSyncEnabled, toggleHoverSync } = useHoverSync();
|
||||
const metricDataCache = ref({});
|
||||
const availableMetrics = ref([]);
|
||||
|
||||
@@ -847,6 +849,23 @@ function onDragEnd(evt) {
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ tab.name }}</span>
|
||||
<!-- Hover sync toggle button for active tab -->
|
||||
<el-button
|
||||
v-if="tab.name === activeTab && !isEditingTabs"
|
||||
size="small"
|
||||
circle
|
||||
@click.stop="toggleHoverSync"
|
||||
:title="
|
||||
hoverSyncEnabled ? 'Disable Hover Sync' : 'Enable Hover Sync'
|
||||
"
|
||||
:type="hoverSyncEnabled ? 'primary' : 'default'"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
hoverSyncEnabled ? 'i-ep-connection' : 'i-ep-connection'
|
||||
"
|
||||
></i>
|
||||
</el-button>
|
||||
<!-- Global settings button for active tab -->
|
||||
<el-button
|
||||
v-if="tab.name === activeTab && !isEditingTabs"
|
||||
@@ -919,6 +938,8 @@ function onDragEnd(evt) {
|
||||
...card.config,
|
||||
height: isMobile ? defaultCardHeight : card.config.height,
|
||||
}"
|
||||
:tab-name="activeTab"
|
||||
:hover-sync-enabled="hoverSyncEnabled"
|
||||
@update:config="updateCard"
|
||||
@remove="removeCard(card.id)"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user