Skip to content

Commit

Permalink
feat: Heatmap
Browse files Browse the repository at this point in the history
  • Loading branch information
ernestii committed Jul 17, 2024
1 parent 01b7d46 commit dad6f32
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 39 deletions.
152 changes: 113 additions & 39 deletions packages/app/src/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import Link from 'next/link';
import { useRouter } from 'next/router';
import cx from 'classnames';
import { clamp, sub } from 'date-fns';
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { Button } from 'react-bootstrap';
import { useHotkeys } from 'react-hotkeys-hook';
import {
Expand All @@ -30,12 +32,14 @@ import {
useQueryParams,
withDefault,
} from 'use-query-params';
import { ActionIcon, Indicator } from '@mantine/core';
import { ActionIcon, Indicator, Tooltip as MTooltip } from '@mantine/core';
import { notifications } from '@mantine/notifications';

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

import { ErrorBoundary } from './components/ErrorBoundary';
import { Heatmap } from './components/Heatmap/Heatmap';
import { Icon } from './components/Icon';
import api from './api';
import CreateLogAlertModal from './CreateLogAlertModal';
import { withAppNav } from './layout';
Expand All @@ -54,6 +58,11 @@ import { FormatTime, useFormatTime } from './useFormatTime';
import 'react-modern-drawer/dist/index.css';
import styles from '../styles/SearchPage.module.scss';

const chartModeAtom = atomWithStorage<'heatmap' | 'histogram'>(
'hdx-search-page-chart-mode',
'histogram',
);

const HistogramBarChartTooltip = (props: any) => {
const { active, payload, label } = props;
if (active && payload && payload.length) {
Expand Down Expand Up @@ -119,7 +128,7 @@ const HDXHistogram = memo(
const formatTime = useFormatTime();

return isHistogramResultsLoading ? (
<div className="w-100 h-100 d-flex align-items-center justify-content-center">
<div className="w-100 h-100 fs-8 text-slate-300 d-flex align-items-center justify-content-center">
Loading Graph...
</div>
) : (
Expand Down Expand Up @@ -219,6 +228,37 @@ const HDXHistogram = memo(
},
);

const HDXHeatmap = ({
config,
isLive,
}: {
config: {
dateRange: [Date, Date];
where: string;
};
isLive: boolean;
}) => {
const formatTime = useFormatTime();

const isLoading = false;

const xLabels = useMemo(() => {
return [formatTime(config.dateRange[0]), formatTime(config.dateRange[1])];
}, [config.dateRange, formatTime]);

const yLabels = useMemo(() => ['0ms', '30m'], []);

if (isLoading) {
return (
<div className="w-100 fs-8 text-slate-300 h-100 d-flex align-items-center justify-content-center">
Loading Graph...
</div>
);
}

return <Heatmap xLabels={xLabels} yLabels={yLabels} />;
};

const HistogramResultCounter = ({
config: { dateRange, where },
}: {
Expand Down Expand Up @@ -681,6 +721,8 @@ function SearchPage() {
[displayedSearchQuery, displayedTimeInputValue, doSearch],
);

const [chartMode, setChartMode] = useAtom(chartModeAtom);

return (
<div style={{ height: '100vh' }}>
<Head>
Expand Down Expand Up @@ -847,7 +889,31 @@ function SearchPage() {
</ErrorBoundary>
<div className="d-flex flex-column flex-grow-1">
<div className="d-flex mx-4 mt-2 justify-content-between">
<div className="fs-8 text-muted">
<div className="fs-8 text-muted d-flex align-items-center gap-1">
<MTooltip label="Histogram" color="gray">
<ActionIcon
color="gray"
variant={chartMode === 'histogram' ? 'filled' : 'subtle'}
size="sm"
onClick={() => setChartMode('histogram')}
>
<Icon
name="bar-chart-line-fill"
className="fs-8 text-success"
/>
</ActionIcon>
</MTooltip>
<MTooltip color="gray" label="Heat map">
<ActionIcon
color="gray"
size="sm"
mr={4}
variant={chartMode === 'heatmap' ? 'filled' : 'subtle'}
onClick={() => setChartMode('heatmap')}
>
<Icon name="grid-fill" className="fs-8 text-success" />
</ActionIcon>
</MTooltip>
{isReady ? (
<HistogramResultCounter
config={{
Expand All @@ -860,37 +926,41 @@ function SearchPage() {
/>
) : null}
</div>
<div className="d-flex">
<Link
href={generateSearchUrl(searchedQuery, [
zoomOutFrom,
zoomOutTo,
])}
className="text-muted-hover text-decoration-none fs-8 me-3"
>
<i className="bi bi-zoom-out me-1"></i>Zoom Out
</Link>
<Link
href={generateSearchUrl(searchedQuery, [
zoomInFrom,
zoomInTo,
])}
className="text-muted-hover text-decoration-none fs-8 me-3"
>
<i className="bi bi-zoom-in me-1"></i>Zoom In
</Link>
<Link
href={generateChartUrl({
table: 'logs',
aggFn: 'count',
field: undefined,
groupBy: ['level'],
})}
className="text-muted-hover text-decoration-none fs-8"
>
<i className="bi bi-plus-circle me-1"></i>Create Chart
</Link>
</div>
{chartMode === 'histogram' ? (
<div className="d-flex">
<Link
href={generateSearchUrl(searchedQuery, [
zoomOutFrom,
zoomOutTo,
])}
className="text-muted-hover text-decoration-none fs-8 me-3"
>
<i className="bi bi-zoom-out me-1"></i>Zoom Out
</Link>
<Link
href={generateSearchUrl(searchedQuery, [
zoomInFrom,
zoomInTo,
])}
className="text-muted-hover text-decoration-none fs-8 me-3"
>
<i className="bi bi-zoom-in me-1"></i>Zoom In
</Link>
<Link
href={generateChartUrl({
table: 'logs',
aggFn: 'count',
field: undefined,
groupBy: ['level'],
})}
className="text-muted-hover text-decoration-none fs-8"
>
<i className="bi bi-plus-circle me-1"></i>Create Chart
</Link>
</div>
) : (
<div className="fs-8 text-slate-600">H Y P E R D X</div>
)}
</div>
<div style={{ height: 110 }} className="my-2 px-3 w-100">
{/* Hack, recharts will release real fix soon https://github.com/recharts/recharts/issues/172 */}
Expand All @@ -911,11 +981,15 @@ function SearchPage() {
}}
>
{isReady ? (
<HDXHistogram
config={chartsConfig}
onTimeRangeSelect={onTimeRangeSelect}
isLive={isLive}
/>
chartMode === 'histogram' ? (
<HDXHistogram
config={chartsConfig}
onTimeRangeSelect={onTimeRangeSelect}
isLive={isLive}
/>
) : (
<HDXHeatmap isLive={isLive} config={chartsConfig} />
)
) : null}
</div>
</div>
Expand Down
50 changes: 50 additions & 0 deletions packages/app/src/components/Heatmap/Heatmap.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.wrapper {
position: relative;
height: 100%;
}

.xLabels,
.yLabels {
font-size: 10px;
color: var(--mantine-color-gray-5);
line-height: 1;
}

.xLabels {
position: absolute;
bottom: 0;
left: 20px;
right: 0;
display: flex;
justify-content: space-between;
}

.yLabels {
position: absolute;
top: 0;
bottom: 16px;
left: 0;
display: flex;
flex-direction: column-reverse;
justify-content: space-between;
padding: 0 5px;

> div {
writing-mode: vertical-rl;
}
}

.heatmap {
position: absolute;
inset: 0 0 16px 22px;
display: grid;
grid-template-columns: repeat(100, 1fr);
grid-template-rows: repeat(10, 1fr);
place-items: stretch stretch;
gap: 1px;
}

.cell {
transition: background-color 100ms;
cursor: pointer;
}
18 changes: 18 additions & 0 deletions packages/app/src/components/Heatmap/Heatmap.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Heatmap } from './Heatmap';

const meta = {
component: Heatmap,
} satisfies Meta<typeof Heatmap>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default = () => (
<Heatmap
xLabels={['Jun 1 20:20:200', 'Jun 10 20:20:200']}
yLabels={['0ms', '30m']}
/>
);
86 changes: 86 additions & 0 deletions packages/app/src/components/Heatmap/Heatmap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';

import classes from './Heatmap.module.scss';

function percentageToColor(percentage: number, warn = false) {
if (percentage === 0) {
return 'transparent';
}

if (warn) {
return `rgba(255, 200, 0, ${Math.max(percentage, 0.4)})`;
}

return `rgba(80, 250, 123, ${Math.max(percentage, 0.2)})`;
}

const w = 140;
const h = 8;

// ts_bucket, duration_bucket, count
type HeatmapDataPoint = {
ts_bucket: number;
duration_bucket: number;
count: number;
};
type HeatmapData = HeatmapDataPoint[];

const generateMockHeatmapData = (w = 100, h = 10): HeatmapData => {
return Array.from({ length: w }, (_, i) =>
Array.from({ length: h }, (_, j) => ({
ts_bucket: i,
duration_bucket: j,
count: Math.random() > 0.5 ? 0 : Math.random(),
})),
).flat();
};

const MOCK_DATA = generateMockHeatmapData(w, h);

export const Heatmap = ({
xLabels,
yLabels,
}: {
// data: HeatmapData;
xLabels?: string[];
yLabels?: string[];
}) => {
return (
<div className={classes.wrapper}>
<div className={classes.yLabels}>
{yLabels?.map(label => (
<div key={label} className={classes.yLabel}>
{label}
</div>
))}
</div>
<div className={classes.xLabels}>
{xLabels?.map(label => (
<div key={label} className={classes.xLabel}>
{label}
</div>
))}
</div>
<div
className={classes.heatmap}
style={{
gridTemplateColumns: `repeat(${w}, 1fr)`,
gridTemplateRows: `repeat(${h}, 1fr)`,
}}
>
{MOCK_DATA.map(({ ts_bucket, duration_bucket, count }) => (
<div
key={`${ts_bucket + 1}-${duration_bucket + 1}`}
className={classes.heatmapCell}
style={{
backgroundColor: percentageToColor(count, duration_bucket < 3),
gridArea: `${duration_bucket + 1} / ${
ts_bucket + 1
} / span 1 / span 1`,
}}
/>
))}
</div>
</div>
);
};

0 comments on commit dad6f32

Please sign in to comment.