Skip to content

Commit e9cafcc

Browse files
committed
feat: Heatmap
1 parent 01b7d46 commit e9cafcc

File tree

4 files changed

+353
-40
lines changed

4 files changed

+353
-40
lines changed

packages/app/src/SearchPage.tsx

+158-40
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import Link from 'next/link';
1313
import { useRouter } from 'next/router';
1414
import cx from 'classnames';
1515
import { clamp, sub } from 'date-fns';
16+
import { useAtom } from 'jotai';
17+
import { atomWithStorage } from 'jotai/utils';
1618
import { Button } from 'react-bootstrap';
1719
import { useHotkeys } from 'react-hotkeys-hook';
1820
import {
@@ -30,13 +32,16 @@ import {
3032
useQueryParams,
3133
withDefault,
3234
} from 'use-query-params';
33-
import { ActionIcon, Indicator } from '@mantine/core';
35+
import { ActionIcon, Indicator, Tooltip as MTooltip } from '@mantine/core';
3436
import { notifications } from '@mantine/notifications';
3537

3638
import { TimePicker } from '@/components/TimePicker';
3739

3840
import { ErrorBoundary } from './components/ErrorBoundary';
39-
import api from './api';
41+
import { Heatmap } from './components/Heatmap/Heatmap';
42+
import { Icon } from './components/Icon';
43+
import api, { useMultiSeriesChartV2 } from './api';
44+
import { convertDateRangeToGranularityString } from './ChartUtils';
4045
import CreateLogAlertModal from './CreateLogAlertModal';
4146
import { withAppNav } from './layout';
4247
import LogSidePanel from './LogSidePanel';
@@ -48,12 +53,18 @@ import { SearchPageFilters, ToggleFilterButton } from './SearchPage.components';
4853
import SearchPageActionBar from './SearchPageActionBar';
4954
import { Tags } from './Tags';
5055
import { useTimeQuery } from './timeQuery';
56+
import type { TimeChartSeries } from './types';
5157
import { useDisplayedColumns } from './useDisplayedColumns';
5258
import { FormatTime, useFormatTime } from './useFormatTime';
5359

5460
import 'react-modern-drawer/dist/index.css';
5561
import styles from '../styles/SearchPage.module.scss';
5662

63+
const chartModeAtom = atomWithStorage<'heatmap' | 'histogram'>(
64+
'hdx-search-page-chart-mode',
65+
'histogram',
66+
);
67+
5768
const HistogramBarChartTooltip = (props: any) => {
5869
const { active, payload, label } = props;
5970
if (active && payload && payload.length) {
@@ -119,7 +130,7 @@ const HDXHistogram = memo(
119130
const formatTime = useFormatTime();
120131

121132
return isHistogramResultsLoading ? (
122-
<div className="w-100 h-100 d-flex align-items-center justify-content-center">
133+
<div className="w-100 h-100 fs-8 text-slate-300 d-flex align-items-center justify-content-center">
123134
Loading Graph...
124135
</div>
125136
) : (
@@ -219,6 +230,79 @@ const HDXHistogram = memo(
219230
},
220231
);
221232

233+
function genLogScale(total_intervals: number, start: number, end: number) {
234+
const x = (Math.log(end) - Math.log(start)) / total_intervals;
235+
const factor = Math.exp(x);
236+
const result = [start];
237+
let i;
238+
239+
for (i = 1; i < total_intervals; i++) {
240+
result.push(result[result.length - 1] * factor);
241+
}
242+
result.push(end);
243+
return result;
244+
}
245+
246+
const HDXHeatmap = ({
247+
config,
248+
isLive,
249+
}: {
250+
config: {
251+
dateRange: [Date, Date];
252+
where: string;
253+
};
254+
isLive: boolean;
255+
}) => {
256+
const formatTime = useFormatTime();
257+
258+
const input = useMemo(() => {
259+
const scale = genLogScale(14, 1, 30 * 60 * 1000); // ms
260+
return {
261+
startDate: config.dateRange[0],
262+
endDate: config.dateRange[1],
263+
where: config.where,
264+
series: scale.map((v, i) => ({
265+
type: 'time' as const,
266+
table: 'logs' as const,
267+
aggFn: 'count' as const,
268+
where: `duration:>${scale[i - 1] || 0} AND duration:<${v} AND ${
269+
config.where
270+
}`,
271+
groupBy: [],
272+
})),
273+
seriesReturnType: 'column' as const,
274+
granularity: convertDateRangeToGranularityString(config.dateRange, 200),
275+
};
276+
}, [config]);
277+
278+
const { isFetching, isLoading, data } = api.useMultiSeriesChart(input, {
279+
keepPreviousData: true,
280+
});
281+
282+
const xLabels = useMemo(() => {
283+
return [formatTime(config.dateRange[0]), formatTime(config.dateRange[1])];
284+
}, [config.dateRange, formatTime]);
285+
286+
const yLabels = useMemo(() => ['0ms', '30m'], []);
287+
288+
if (isLoading) {
289+
return (
290+
<div className="w-100 fs-8 text-slate-300 h-100 d-flex align-items-center justify-content-center">
291+
Loading Graph...
292+
</div>
293+
);
294+
}
295+
296+
return (
297+
<Heatmap
298+
xLabels={xLabels}
299+
yLabels={yLabels}
300+
data={data}
301+
isFetching={isFetching}
302+
/>
303+
);
304+
};
305+
222306
const HistogramResultCounter = ({
223307
config: { dateRange, where },
224308
}: {
@@ -681,6 +765,8 @@ function SearchPage() {
681765
[displayedSearchQuery, displayedTimeInputValue, doSearch],
682766
);
683767

768+
const [chartMode, setChartMode] = useAtom(chartModeAtom);
769+
684770
return (
685771
<div style={{ height: '100vh' }}>
686772
<Head>
@@ -847,7 +933,31 @@ function SearchPage() {
847933
</ErrorBoundary>
848934
<div className="d-flex flex-column flex-grow-1">
849935
<div className="d-flex mx-4 mt-2 justify-content-between">
850-
<div className="fs-8 text-muted">
936+
<div className="fs-8 text-muted d-flex align-items-center gap-1">
937+
<MTooltip label="Histogram" color="gray">
938+
<ActionIcon
939+
color="gray"
940+
variant={chartMode === 'histogram' ? 'filled' : 'subtle'}
941+
size="sm"
942+
onClick={() => setChartMode('histogram')}
943+
>
944+
<Icon
945+
name="bar-chart-line-fill"
946+
className="fs-8 text-success"
947+
/>
948+
</ActionIcon>
949+
</MTooltip>
950+
<MTooltip color="gray" label="Heat map">
951+
<ActionIcon
952+
color="gray"
953+
size="sm"
954+
mr={4}
955+
variant={chartMode === 'heatmap' ? 'filled' : 'subtle'}
956+
onClick={() => setChartMode('heatmap')}
957+
>
958+
<Icon name="grid-fill" className="fs-8 text-success" />
959+
</ActionIcon>
960+
</MTooltip>
851961
{isReady ? (
852962
<HistogramResultCounter
853963
config={{
@@ -860,37 +970,41 @@ function SearchPage() {
860970
/>
861971
) : null}
862972
</div>
863-
<div className="d-flex">
864-
<Link
865-
href={generateSearchUrl(searchedQuery, [
866-
zoomOutFrom,
867-
zoomOutTo,
868-
])}
869-
className="text-muted-hover text-decoration-none fs-8 me-3"
870-
>
871-
<i className="bi bi-zoom-out me-1"></i>Zoom Out
872-
</Link>
873-
<Link
874-
href={generateSearchUrl(searchedQuery, [
875-
zoomInFrom,
876-
zoomInTo,
877-
])}
878-
className="text-muted-hover text-decoration-none fs-8 me-3"
879-
>
880-
<i className="bi bi-zoom-in me-1"></i>Zoom In
881-
</Link>
882-
<Link
883-
href={generateChartUrl({
884-
table: 'logs',
885-
aggFn: 'count',
886-
field: undefined,
887-
groupBy: ['level'],
888-
})}
889-
className="text-muted-hover text-decoration-none fs-8"
890-
>
891-
<i className="bi bi-plus-circle me-1"></i>Create Chart
892-
</Link>
893-
</div>
973+
{chartMode === 'histogram' ? (
974+
<div className="d-flex">
975+
<Link
976+
href={generateSearchUrl(searchedQuery, [
977+
zoomOutFrom,
978+
zoomOutTo,
979+
])}
980+
className="text-muted-hover text-decoration-none fs-8 me-3"
981+
>
982+
<i className="bi bi-zoom-out me-1"></i>Zoom Out
983+
</Link>
984+
<Link
985+
href={generateSearchUrl(searchedQuery, [
986+
zoomInFrom,
987+
zoomInTo,
988+
])}
989+
className="text-muted-hover text-decoration-none fs-8 me-3"
990+
>
991+
<i className="bi bi-zoom-in me-1"></i>Zoom In
992+
</Link>
993+
<Link
994+
href={generateChartUrl({
995+
table: 'logs',
996+
aggFn: 'count',
997+
field: undefined,
998+
groupBy: ['level'],
999+
})}
1000+
className="text-muted-hover text-decoration-none fs-8"
1001+
>
1002+
<i className="bi bi-plus-circle me-1"></i>Create Chart
1003+
</Link>
1004+
</div>
1005+
) : (
1006+
<div className="fs-8 text-slate-600">H Y P E R D X</div>
1007+
)}
8941008
</div>
8951009
<div style={{ height: 110 }} className="my-2 px-3 w-100">
8961010
{/* Hack, recharts will release real fix soon https://github.com/recharts/recharts/issues/172 */}
@@ -911,11 +1025,15 @@ function SearchPage() {
9111025
}}
9121026
>
9131027
{isReady ? (
914-
<HDXHistogram
915-
config={chartsConfig}
916-
onTimeRangeSelect={onTimeRangeSelect}
917-
isLive={isLive}
918-
/>
1028+
chartMode === 'histogram' ? (
1029+
<HDXHistogram
1030+
config={chartsConfig}
1031+
onTimeRangeSelect={onTimeRangeSelect}
1032+
isLive={isLive}
1033+
/>
1034+
) : (
1035+
<HDXHeatmap isLive={isLive} config={chartsConfig} />
1036+
)
9191037
) : null}
9201038
</div>
9211039
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.wrapper {
2+
position: relative;
3+
height: 100%;
4+
}
5+
6+
.xLabels,
7+
.yLabels {
8+
font-size: 10px;
9+
color: var(--mantine-color-gray-5);
10+
line-height: 1;
11+
}
12+
13+
.xLabels {
14+
position: absolute;
15+
bottom: 0;
16+
left: 20px;
17+
right: 0;
18+
display: flex;
19+
justify-content: space-between;
20+
}
21+
22+
.yLabels {
23+
position: absolute;
24+
top: 0;
25+
bottom: 16px;
26+
left: 0;
27+
display: flex;
28+
flex-direction: column-reverse;
29+
justify-content: space-between;
30+
padding: 0 5px;
31+
32+
> div {
33+
writing-mode: vertical-rl;
34+
}
35+
}
36+
37+
.heatmap {
38+
position: absolute;
39+
inset: 0 0 16px 22px;
40+
display: grid;
41+
grid-template-columns: repeat(100, 1fr);
42+
grid-template-rows: repeat(10, 1fr);
43+
place-items: stretch stretch;
44+
gap: 1px;
45+
}
46+
47+
.cell {
48+
transition: background-color 100ms;
49+
cursor: pointer;
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
3+
import { Heatmap } from './Heatmap';
4+
5+
const meta = {
6+
component: Heatmap,
7+
} satisfies Meta<typeof Heatmap>;
8+
9+
export default meta;
10+
11+
type Story = StoryObj<typeof meta>;
12+
13+
export const Default = () => (
14+
<Heatmap
15+
xLabels={['Jun 1 20:20:200', 'Jun 10 20:20:200']}
16+
yLabels={['0ms', '30m']}
17+
/>
18+
);

0 commit comments

Comments
 (0)