Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 67 additions & 90 deletions src/views/dashboard/metrics/components/metrics-chart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ a-card.metrics-chart(:bordered="false")
</template>

<script setup lang="ts">
import { ref, computed, watch, inject } from 'vue'
import { ref, computed, watch, inject, nextTick } from 'vue'
import { useElementSize } from '@vueuse/core'
import Chart from '@/components/raw-chart/index.vue'
import TimeRangeSelect from '@/components/time-range-select/index.vue'
Expand Down Expand Up @@ -97,17 +97,11 @@ a-card.metrics-chart(:bordered="false")
/** Fixed height of the line/bar plot area (excluding legend). */
const GRAPH_HEIGHT = 400
const LEGEND_ITEM_HEIGHT = 14
const LEGEND_ITEM_GAP = 10
const LEGEND_LIST_ROW_HEIGHT = LEGEND_ITEM_HEIGHT + 6
const LEGEND_LIST_PAGER_HEIGHT = 16
const LEGEND_ICON_WIDTH = 14
const LEGEND_CHAR_WIDTH = 6.5
const LEGEND_TO_GRID_GAP = 16
/** Space between legend block and canvas bottom */
const LEGEND_BOTTOM_INSET = 12
/** Above this count, legend uses a single-column list at the bottom */
const LEGEND_LIST_THRESHOLD = 5
const LEGEND_LIST_MAX_VISIBLE_ROWS = 8
const AXIS_LABEL_STYLE = { color: 'rgba(71, 52, 96, 0.45)', fontSize: 11 }
const SPLIT_LINE_STYLE = { type: 'solid' as const, color: 'rgba(71, 52, 96, 0.06)' }
const AXIS_LINE_STYLE = { lineStyle: { color: 'rgba(71, 52, 96, 0.12)' } }
Expand Down Expand Up @@ -208,11 +202,19 @@ a-card.metrics-chart(:bordered="false")
})

const showFullSeriesName = ref(false)
/** When set, only this series is shown (solo mode). Disables legend-hover dimming. */
const soloSeriesName = ref<string | null>(null)

const chartKey = computed(() => {
return `${query.value}-${step.value}-${showFullSeriesName.value}`
})

// Reset solo mode whenever series names are recomputed (query/step/showFullSeriesName change),
// otherwise the stale soloSeriesName no longer matches any legend name and hides everything.
watch(chartKey, () => {
soloSeriesName.value = null
})

const formatChartAxisTime = (value: number): string => {
const range = timeRange.value as [number, number] | undefined
if (!range || range.length !== 2) {
Expand All @@ -233,48 +235,54 @@ a-card.metrics-chart(:bordered="false")
return formatDateTime(value, 'TimestampMillisecond', 'HH:mm:ss') ?? ''
}

const estimateLegendAreaHeight = (
count: number,
avgNameLength: number,
containerWidth: number,
listLayout: boolean,
listMaxVisibleRows = LEGEND_LIST_MAX_VISIBLE_ROWS
) => {
const estimateLegendAreaHeight = (count: number) => {
if (count === 0) return 0

if (listLayout) {
const visibleRows = Math.min(count, listMaxVisibleRows)
const pagerHeight = count > listMaxVisibleRows ? LEGEND_LIST_PAGER_HEIGHT : 0
return visibleRows * LEGEND_LIST_ROW_HEIGHT + pagerHeight + 8 + LEGEND_BOTTOM_INSET
}

const maxWidth = Math.max(containerWidth * 0.92 - 24, 240)
const itemWidth = avgNameLength * LEGEND_CHAR_WIDTH + LEGEND_ICON_WIDTH + LEGEND_ITEM_GAP
const itemsPerRow = Math.max(1, Math.floor(maxWidth / itemWidth))
const rows = Math.ceil(count / itemsPerRow)
return rows * LEGEND_ITEM_HEIGHT + Math.max(0, rows - 1) * LEGEND_ITEM_GAP + 8
return count * LEGEND_LIST_ROW_HEIGHT + 8 + LEGEND_BOTTOM_INSET
}

const seriesCount = computed(() => seriesData.value.length)

const useLegendListLayout = computed(() => seriesCount.value >= LEGEND_LIST_THRESHOLD)
const legendAreaHeight = computed(() => estimateLegendAreaHeight(seriesCount.value))

const avgLegendNameLength = computed(() => (showFullSeriesName.value ? 56 : 28))
/** Plot area fixed; legend height is additive below the grid. */
const chartHeight = computed(() => GRAPH_HEIGHT + legendAreaHeight.value + LEGEND_TO_GRID_GAP)

const listLegendVisibleRows = computed(() => Math.min(seriesCount.value, LEGEND_LIST_MAX_VISIBLE_ROWS))
// Attach legendselectchanged event to implement solo mode:
// clicking a legend item shows only that series and hides all others.
// The actual selection state is driven by `selected` map in chartOption (recomputed when soloSeriesName changes).
// While in solo mode, legend-hover dimming is disabled via emphasis.focus in chartOption.
// Attach legendselectchanged event to implement solo mode:
// clicking a legend item shows only that series and hides all others.
// The actual selection state is driven by `selected` map in chartOption (recomputed when soloSeriesName changes).
// While in solo mode, legend-hover dimming is disabled via emphasis.focus in chartOption.
// Watch chartKey so the handler is re-attached when the chart is recreated
// (e.g., when toggling showFullSeriesName, which changes :key on the Chart component).
watch(
[hasData, chartKey],
() => {
if (!hasData.value) return
nextTick(() => {
const chartComponent = chartRef.value
if (!chartComponent) return
const instance = chartComponent.getInstance()
if (!instance) return

const handler = (params: any) => {
const { name } = params
if (name === soloSeriesName.value) {
soloSeriesName.value = null
} else {
soloSeriesName.value = name
}
}

const legendAreaHeight = computed(() =>
estimateLegendAreaHeight(
seriesCount.value,
avgLegendNameLength.value,
chartContainerWidth.value || 900,
useLegendListLayout.value,
LEGEND_LIST_MAX_VISIBLE_ROWS
)
instance.off('legendselectchanged', handler)
instance.on('legendselectchanged', handler)
})
},
{ immediate: true }
)

/** Plot area fixed; legend height is additive below the grid. */
const chartHeight = computed(() => GRAPH_HEIGHT + legendAreaHeight.value + LEGEND_TO_GRID_GAP)
const chartOption = computed<EChartsOption>(() => {
if (!hasData.value) return {}
// Option: control whether to display full series name or compact unique-label name
Expand Down Expand Up @@ -365,7 +373,7 @@ a-card.metrics-chart(:bordered="false")
width: 2,
},
emphasis: {
focus: 'series',
focus: soloSeriesName.value ? 'none' : 'series',
lineStyle: {
width: 2.5,
},
Expand All @@ -380,63 +388,32 @@ a-card.metrics-chart(:bordered="false")
})

const legendNames = series.map((s) => s.name as string)
const listLayout = useLegendListLayout.value
const listVisibleRows = listLegendVisibleRows.value
const legendRows = estimateLegendAreaHeight(
legendNames.length,
avgLegendNameLength.value,
chartContainerWidth.value || 900,
listLayout,
LEGEND_LIST_MAX_VISIBLE_ROWS
)
const gridBottom = legendRows + LEGEND_TO_GRID_GAP
const useLegendScroll =
(!listLayout && legendNames.length > 14) || (listLayout && legendNames.length > LEGEND_LIST_MAX_VISIBLE_ROWS)
const gridBottom = legendAreaHeight.value + LEGEND_TO_GRID_GAP

const legendTextStyle = {
fontSize: 11,
lineHeight: LEGEND_ITEM_HEIGHT,
color: 'rgba(71, 52, 96, 0.65)',
}

const listLegendWidth = Math.min(
(chartContainerWidth.value || 900) * 0.92,
Math.max(...legendNames.map((name) => name.length * LEGEND_CHAR_WIDTH + LEGEND_ICON_WIDTH + 20), 120)
)

const listLegendBlockHeight =
listVisibleRows * LEGEND_LIST_ROW_HEIGHT + (useLegendScroll ? LEGEND_LIST_PAGER_HEIGHT : 0)

const legendOption = listLayout
? {
type: useLegendScroll ? 'scroll' : 'plain',
orient: 'vertical',
left: 'center',
bottom: LEGEND_BOTTOM_INSET,
width: listLegendWidth,
height: Math.max(listLegendBlockHeight, LEGEND_LIST_ROW_HEIGHT),
itemWidth: LEGEND_ICON_WIDTH,
itemHeight: LEGEND_ITEM_HEIGHT,
itemGap: 6,
textStyle: legendTextStyle,
...(useLegendScroll
? {
pageIconSize: 10,
pageTextStyle: { fontSize: 10, color: 'rgba(71, 52, 96, 0.45)' },
}
: {}),
}
: {
type: useLegendScroll ? 'scroll' : 'plain',
orient: 'horizontal',
bottom: 0,
left: 'center',
width: '92%',
itemWidth: LEGEND_ICON_WIDTH,
itemHeight: 10,
itemGap: LEGEND_ITEM_GAP,
textStyle: legendTextStyle,
}
const legendOption = {
type: 'plain',
orient: 'vertical',
left: 'center',
bottom: LEGEND_BOTTOM_INSET,
width: Math.min(
(chartContainerWidth.value || 900) * 0.92,
Math.max(...legendNames.map((name) => name.length * 6.5 + LEGEND_ICON_WIDTH + 20), 120)
),
height: Math.max(legendAreaHeight.value - LEGEND_BOTTOM_INSET - 8, LEGEND_LIST_ROW_HEIGHT),
itemWidth: LEGEND_ICON_WIDTH,
itemHeight: LEGEND_ITEM_HEIGHT,
itemGap: 6,
textStyle: legendTextStyle,
selected: soloSeriesName.value
? Object.fromEntries(legendNames.map((n) => [n, n === soloSeriesName.value]))
: undefined,
}

return {
color: METRICS_CHART_COLORS,
Expand Down
Loading