diff --git a/@codexteam/ui/dev/pages/components/Chart.vue b/@codexteam/ui/dev/pages/components/Chart.vue
index ee008846..d07f1cfe 100644
--- a/@codexteam/ui/dev/pages/components/Chart.vue
+++ b/@codexteam/ui/dev/pages/components/Chart.vue
@@ -65,13 +65,6 @@
Static series legend under the chart: color dot + label for each line.
Off by default so existing layouts stay the same.
-
- Try it:
-
-
@@ -87,6 +80,11 @@
:is-disabled="false"
:items="paletteColorItems"
/>
+ legend:
+
- Multiple Lines
+ Shared scale
+
+
+ Both series hit 50 on the same tick. They must sit on one horizontal line.
+
+
+
+
+ Single spike
+
+ One non-zero point among zeros — should still show as a marker.
+
+
+
+ Stacked
+
+
+ Many-series on top (tooltip can overlap the chart below). Some ticks have only one or two non-zero series.
+
+
@@ -121,7 +161,7 @@
@@ -137,7 +177,7 @@
@@ -225,9 +265,9 @@ const detalizationSelected = ref({
});
/**
- * Toggle static series legend in the preview
+ * Legend toggle for the Single Line preview
*/
-const legendEnabled = ref(true);
+const legendEnabled = ref(false);
/**
* Available detalization options for the Select component
@@ -337,6 +377,60 @@ const multipleLinesData = computed(() => {
];
});
+/**
+ * Two ranges, same count on one tick — must share Y
+ */
+const sharedScaleData = computed(() => {
+ const timestamps = demoTimestamps.value;
+ const mid = Math.floor(timestamps.length / 2);
+
+ return [
+ {
+ label: 'wide',
+ color: ChartLineColor.Red,
+ data: timestamps.map((timestamp, index) => ({
+ timestamp,
+ count: index === mid ? 50 : 180,
+ })),
+ },
+ {
+ label: 'narrow',
+ color: ChartLineColor.Blue,
+ data: timestamps.map((timestamp, index) => ({
+ timestamp,
+ count: index === mid ? 50 : 20,
+ })),
+ },
+ ];
+});
+
+/**
+ * One event among zeros
+ */
+const spikeData = computed(() => {
+ const timestamps = demoTimestamps.value;
+ const mid = Math.floor(timestamps.length / 2);
+
+ return [
+ {
+ label: 'incident',
+ color: ChartLineColor.Orange,
+ data: timestamps.map((timestamp, index) => ({
+ timestamp,
+ count: index === mid ? 90 : 0,
+ })),
+ },
+ ];
+});
+
+/**
+ * Long labels to check tooltip ellipsis
+ */
+const longPreviewLabels: Partial> = {
+ [ChartLineColor.Red]: 'accepted-events-from-the-ingestion-pipeline',
+ [ChartLineColor.LightGrey]: 'rate-limited-requests-waiting-in-queue',
+};
+
/**
* All hardcoded palette tokens stacked for comparison
*/
@@ -344,10 +438,20 @@ const paletteSeriesData = computed(() => {
const timestamps = demoTimestamps.value;
const bases = [150, 130, 110, 95, 80, 70, 55, 40, 25];
- return paletteTokens.map((color, index) => ({
- label: color,
- data: generateData(timestamps, bases[index] ?? 50),
+ return paletteTokens.map((color, seriesIndex) => ({
+ label: longPreviewLabels[color] ?? color,
color,
+ data: timestamps.map((timestamp, tick) => {
+ const activeSeries = (tick % 7) + 1;
+ const count = seriesIndex < activeSeries
+ ? Math.floor(Math.random() * (bases[seriesIndex] ?? 50)) + 10
+ : 0;
+
+ return {
+ timestamp,
+ count,
+ };
+ }),
}));
});
@@ -438,5 +542,9 @@ const paletteSeriesData = computed(() => {
&--tall {
padding-bottom: 240px;
}
+
+ &--stack {
+ gap: var(--spacing-m);
+ }
}
diff --git a/@codexteam/ui/package.json b/@codexteam/ui/package.json
index d54b572f..70c7ea3f 100644
--- a/@codexteam/ui/package.json
+++ b/@codexteam/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@codexteam/ui",
- "version": "0.2.6",
+ "version": "0.2.7",
"type": "module",
"sideEffects": [
"*.css",
diff --git a/@codexteam/ui/src/vue/components/chart/Chart.vue b/@codexteam/ui/src/vue/components/chart/Chart.vue
index 93bfe1ca..8d9b488b 100644
--- a/@codexteam/ui/src/vue/components/chart/Chart.vue
+++ b/@codexteam/ui/src/vue/components/chart/Chart.vue
@@ -1,5 +1,10 @@
-
+
-
-
+
- {{ formatSpacedNumber(item.value) }}
+ :class="$style['chart__pointer-tooltip-dot']"
+ :style="{ backgroundColor: getCursorColor(item.prepared.line) }"
+ />
+
+
+ {{ formatSpacedNumber(item.value) }}
+
+
+ {{ item.prepared.line.label }}
+
-
- {{ item.prepared.line.label }}
-
-
+
@@ -103,6 +115,7 @@
v-for="(preparedLine, index) in preparedLines"
:key="`legend-${preparedLine.line.label}-${index}`"
:class="$style['chart__legend-item']"
+ :title="preparedLine.line.label"
>
{
- return props.lines.map((line, index) => {
- let min = Infinity;
- let max = 0;
- let allZeros = true;
+const chartValueScale = computed((): { min: number; max: number } => {
+ let max = 0;
+ for (const line of props.lines) {
for (const item of line.data ?? []) {
- const v = item.count ?? 0;
+ const value = item.count ?? 0;
- if (v < min) {
- min = v;
- }
- if (v > max) {
- max = v;
- }
- if (v !== 0) {
- allZeros = false;
+ if (value > max) {
+ max = value;
}
}
+ }
- const safeMin = min === Infinity ? 0 : min;
- const safeMax = max * 1.5;
+ return {
+ min: 0,
+ max: max > 0 ? max * 1.5 : 1,
+ };
+});
- return { line,
- min: safeMin,
- max: safeMax,
- allZeros,
- index };
- });
+/**
+ * Precomputed line data with shared min/max
+ */
+const preparedLines = computed((): PreparedLine[] => {
+ const { min, max } = chartValueScale.value;
+
+ return props.lines.map((line, index) => ({
+ line,
+ min,
+ max,
+ index,
+ }));
});
/**
@@ -375,7 +388,6 @@ const tooltipLines = computed((): Array<{
}
return preparedLines.value
- .filter(prepared => !prepared.allZeros)
.map(prepared => ({
prepared,
value: getLineValueAtHoveredIndex(prepared.line, hoveredIndex.value),
@@ -383,6 +395,11 @@ const tooltipLines = computed((): Array<{
.sort((a, b) => b.value - a.value);
});
+/**
+ * Tooltip width cap, same as CSS max-width
+ */
+const TOOLTIP_MAX_WIDTH_PX = 220;
+
/**
* Tooltip min-width based on the longest visible series row
*/
@@ -394,20 +411,15 @@ const tooltipMinWidth = computed((): number => {
const dateLabel = formatTimestamp(firstLineData.value[hoveredIndex.value].timestamp * 1000);
let maxLength = dateLabel.length;
- for (const prepared of preparedLines.value) {
- if (prepared.allZeros) {
- continue;
- }
-
- const value = formatSpacedNumber(getLineValueAtHoveredIndex(prepared.line, hoveredIndex.value));
- const row = `${value} ${prepared.line.label}`;
+ for (const item of tooltipLines.value) {
+ const row = `${formatSpacedNumber(item.value)} ${item.prepared.line.label}`;
if (row.length > maxLength) {
maxLength = row.length;
}
}
- return maxLength * 5.6 + 28;
+ return Math.min(TOOLTIP_MAX_WIDTH_PX, maxLength * 5.6 + 28);
});
/**
@@ -665,7 +677,7 @@ onBeforeUnmount(() => {
.chart {
position: relative;
- isolation: isolate;
+ z-index: 0;
display: flex;
flex-direction: column;
overflow: visible;
@@ -676,6 +688,10 @@ onBeforeUnmount(() => {
--legend-block-padding: var(--spacing-s);
}
+.chart--raised {
+ z-index: var(--z-popover);
+}
+
.chart__plot {
position: relative;
display: flex;
@@ -751,6 +767,7 @@ onBeforeUnmount(() => {
gap: var(--spacing-xs);
padding-block: var(--tooltip-block-padding);
padding-inline: var(--spacing-xs);
+ max-width: 220px;
color: var(--base--text);
@apply --text-ui-small;
white-space: nowrap;
@@ -793,15 +810,41 @@ onBeforeUnmount(() => {
text-align: center;
}
+.chart__pointer-tooltip-list {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ gap: var(--spacing-xs);
+ line-height: 1;
+
+ &:has(> :nth-child(6)) {
+ max-height: calc(5lh + 4 * var(--spacing-xs));
+ overflow-x: hidden;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ scrollbar-width: thin;
+
+ &::-webkit-scrollbar {
+ width: var(--spacing-xxs);
+ }
+ }
+}
+
.chart__pointer-tooltip-number {
display: flex;
align-items: center;
+ flex-shrink: 0;
+ min-width: 0;
gap: var(--spacing-xs);
+ height: 1lh;
+ line-height: 1;
}
.chart__pointer-tooltip-metrics {
display: flex;
align-items: baseline;
+ min-width: 0;
+ flex: 1;
gap: var(--spacing-xs);
}
@@ -811,12 +854,16 @@ onBeforeUnmount(() => {
}
.chart__pointer-tooltip-value {
+ flex-shrink: 0;
color: var(--base--text);
animation: tooltip-value-in 500ms ease;
}
.chart__pointer-tooltip-label {
+ min-width: 0;
+ overflow: hidden;
color: var(--base--text-secondary);
+ text-overflow: ellipsis;
}
.chart__legend-dot,