From 8b4e7ede041c9f33aff812ddc968391567265e18 Mon Sep 17 00:00:00 2001
From: Yury Molodov <yurymolodov@gmail.com>
Date: Fri, 18 Oct 2024 02:28:23 +0200
Subject: [PATCH] vmui/logs: fix display of hits chart (#7167)

### Describe Your Changes

Fixed the display of hits chart in VictoriaLogs.
See #7133

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

(cherry picked from commit 36a86c3aafc2928921fa854cc3dafa4d88de0208)
---
 .../ExploreLogsBarChart.tsx                   | 46 +++++++++++++------
 .../ExploreLogs/hooks/useFetchLogHits.ts      |  8 +---
 app/vmui/packages/vmui/src/utils/logs.ts      | 12 +++++
 docs/VictoriaLogs/CHANGELOG.md                |  2 +
 4 files changed, 48 insertions(+), 20 deletions(-)

diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/ExploreLogsBarChart.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/ExploreLogsBarChart.tsx
index 506cf418d7..326e9febdc 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/ExploreLogsBarChart.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/ExploreLogsBarChart.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useMemo } from "preact/compat";
+import React, { FC, useCallback, useMemo } from "preact/compat";
 import "./style.scss";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
 import classNames from "classnames";
@@ -10,6 +10,7 @@ import BarHitsChart from "../../../components/Chart/BarHitsChart/BarHitsChart";
 import Alert from "../../../components/Main/Alert/Alert";
 import { TimeParams } from "../../../types";
 import LineLoader from "../../../components/Main/LineLoader/LineLoader";
+import { getHitsTimeParams } from "../../../utils/logs";
 
 interface Props {
   query: string;
@@ -24,26 +25,43 @@ const ExploreLogsBarChart: FC<Props> = ({ logHits, period, error, isLoading, onA
   const { isMobile } = useDeviceDetect();
   const timeDispatch = useTimeDispatch();
 
-  const getXAxis = (timestamps: string[]): number[] => {
-    return (timestamps.map(t => t ? dayjs(t).unix() : null)
-      .filter(Boolean) as number[])
-      .sort((a, b) => a - b);
-  };
-
-  const getYAxes = (logHits: LogHits[], timestamps: string[]) => {
+  const getYAxes = (logHits: LogHits[], timestamps: number[]) => {
     return logHits.map(hits => {
-      return timestamps.map(t => {
-        const index = hits.timestamps.findIndex(ts => ts === t);
-        return index === -1 ? null : hits.values[index] || null;
+      const timestampValueMap = new Map();
+      hits.timestamps.forEach((ts, idx) => {
+        const unixTime = dayjs(ts).unix();
+        timestampValueMap.set(unixTime, hits.values[idx] || null);
       });
+
+      return timestamps.map(t => timestampValueMap.get(t) || null);
     });
   };
 
+  const generateTimestamps = useCallback((date: dayjs.Dayjs) => {
+    const result: number[] = [];
+    const { start, end, step } = getHitsTimeParams(period);
+    const stepsToFirstTimestamp = Math.ceil(start.diff(date, "milliseconds") / step);
+    let firstTimestamp = date.add(stepsToFirstTimestamp * step, "milliseconds");
+
+    // If the first timestamp is before 'start', set it to 'start'
+    if (firstTimestamp.isBefore(start)) {
+      firstTimestamp = start.clone();
+    }
+
+    // Calculate the total number of steps from 'firstTimestamp' to 'end'
+    const totalSteps = Math.floor(end.diff(firstTimestamp, "milliseconds") / step);
+
+    for (let i = 0; i <= totalSteps; i++) {
+      result.push(firstTimestamp.add(i * step, "milliseconds").unix());
+    }
+
+    return result;
+  }, [period]);
+
   const data = useMemo(() => {
     if (!logHits.length) return [[], []] as AlignedData;
-    const timestamps = Array.from(new Set(logHits.map(l => l.timestamps).flat()));
-    const xAxis = getXAxis(timestamps);
-    const yAxes = getYAxes(logHits, timestamps);
+    const xAxis = generateTimestamps(dayjs(logHits[0].timestamps[0]));
+    const yAxes = getYAxes(logHits, xAxis);
     return [xAxis, ...yAxes] as AlignedData;
   }, [logHits]);
 
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogHits.ts b/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogHits.ts
index af784db47a..c46d48c766 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogHits.ts
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogHits.ts
@@ -2,9 +2,8 @@ import { useCallback, useMemo, useRef, useState } from "preact/compat";
 import { getLogHitsUrl } from "../../../api/logs";
 import { ErrorTypes, TimeParams } from "../../../types";
 import { LogHits } from "../../../api/types";
-import dayjs from "dayjs";
-import { LOGS_BARS_VIEW } from "../../../constants/logs";
 import { useSearchParams } from "react-router-dom";
+import { getHitsTimeParams } from "../../../utils/logs";
 
 export const useFetchLogHits = (server: string, query: string) => {
   const [searchParams] = useSearchParams();
@@ -17,10 +16,7 @@ export const useFetchLogHits = (server: string, query: string) => {
   const url = useMemo(() => getLogHitsUrl(server), [server]);
 
   const getOptions = (query: string, period: TimeParams, signal: AbortSignal) => {
-    const start = dayjs(period.start * 1000);
-    const end = dayjs(period.end * 1000);
-    const totalSeconds = end.diff(start, "milliseconds");
-    const step = Math.ceil(totalSeconds / LOGS_BARS_VIEW) || 1;
+    const { start, end, step } = getHitsTimeParams(period);
 
     return {
       signal,
diff --git a/app/vmui/packages/vmui/src/utils/logs.ts b/app/vmui/packages/vmui/src/utils/logs.ts
index 13e3d246c9..bcf9708375 100644
--- a/app/vmui/packages/vmui/src/utils/logs.ts
+++ b/app/vmui/packages/vmui/src/utils/logs.ts
@@ -1,4 +1,16 @@
+import { TimeParams } from "../types";
+import dayjs from "dayjs";
+import { LOGS_BARS_VIEW } from "../constants/logs";
+
 export const getStreamPairs = (value: string): string[] => {
   const pairs = /^{.+}$/.test(value) ? value.slice(1, -1).split(",") : [value];
   return pairs.filter(Boolean);
 };
+
+export const getHitsTimeParams = (period: TimeParams) => {
+  const start = dayjs(period.start * 1000);
+  const end = dayjs(period.end * 1000);
+  const totalSeconds = end.diff(start, "milliseconds");
+  const step = Math.ceil(totalSeconds / LOGS_BARS_VIEW) || 1;
+  return { start, end, step };
+};
diff --git a/docs/VictoriaLogs/CHANGELOG.md b/docs/VictoriaLogs/CHANGELOG.md
index 42e0793365..f9a40d7caf 100644
--- a/docs/VictoriaLogs/CHANGELOG.md
+++ b/docs/VictoriaLogs/CHANGELOG.md
@@ -20,6 +20,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
 * FEATURE: improve performance for [`top`](https://docs.victoriametrics.com/victorialogs/logsql/#top-pipe), [`uniq`](https://docs.victoriametrics.com/victorialogs/logsql/#uniq-pipe) and [`field_values`](https://docs.victoriametrics.com/victorialogs/logsql/#field_values-pipe) pipes on systems with many CPU cores when it is applied to [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) with big number of unique values. For example, `_time:1d | top 5 (user_id)` should be executed much faster when `user_id` field contains millions of unique values.
 * FEATURE: improve performance for [`field_names` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#field_names-pipe) when it is applied to logs with hundreds of [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
 
+* BUGFIX: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): fix display of hits chart. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7133).
+
 ## [v0.36.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.36.0-victorialogs)
 
 Released at 2024-10-16