mirror of
https://github.com/prometheus/prometheus.git
synced 2025-01-13 06:47:28 -08:00
feat (ui): Add Native Histogram rendering to new UI (#14431)
* feat: scaffold new ui native histogram Signed-off-by: Manik Rana <manikrana54@gmail.com> * feat: add native histogram rendering Signed-off-by: Manik Rana <manikrana54@gmail.com> * feat: add tooltip Signed-off-by: Manik Rana <manikrana54@gmail.com> * fix:revert package-lock changes Signed-off-by: Manik Rana <manikrana54@gmail.com> * chore: fix tab spacing Signed-off-by: Manik Rana <manikrana54@gmail.com> * fix: apply suggestions and fixes Signed-off-by: Manik Rana <manikrana54@gmail.com> * chore: lint Signed-off-by: Manik Rana <manikrana54@gmail.com> --------- Signed-off-by: Manik Rana <manikrana54@gmail.com>
This commit is contained in:
parent
0eea8645fa
commit
373f09796d
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "prometheus",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
|
@ -8,3 +8,16 @@
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.histogramSummaryWrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramSummary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
import { FC, useEffect, useId } from "react";
|
import { FC, ReactNode, useEffect, useId, useState } from "react";
|
||||||
import { Table, Alert, Skeleton, Box, LoadingOverlay } from "@mantine/core";
|
import {
|
||||||
|
Table,
|
||||||
|
Alert,
|
||||||
|
Skeleton,
|
||||||
|
Box,
|
||||||
|
LoadingOverlay,
|
||||||
|
SegmentedControl,
|
||||||
|
ScrollArea,
|
||||||
|
} from "@mantine/core";
|
||||||
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
|
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
InstantQueryResult,
|
InstantQueryResult,
|
||||||
|
@ -13,6 +21,9 @@ import dayjs from "dayjs";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import { useAppSelector } from "../../state/hooks";
|
import { useAppSelector } from "../../state/hooks";
|
||||||
import { formatTimestamp } from "../../lib/formatTime";
|
import { formatTimestamp } from "../../lib/formatTime";
|
||||||
|
import HistogramChart from "./HistogramChart";
|
||||||
|
import { Histogram } from "../../types/types";
|
||||||
|
import { bucketRangeString } from "./HistogramHelpers";
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
const maxFormattableSeries = 1000;
|
const maxFormattableSeries = 1000;
|
||||||
|
@ -34,6 +45,8 @@ export interface DataTableProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
|
const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
|
||||||
|
const [scale, setScale] = useState<string>("exponential");
|
||||||
|
|
||||||
const { data, error, isFetching, isLoading, refetch } =
|
const { data, error, isFetching, isLoading, refetch } =
|
||||||
useAPIQuery<InstantQueryResult>({
|
useAPIQuery<InstantQueryResult>({
|
||||||
key: useId(),
|
key: useId(),
|
||||||
|
@ -101,7 +114,7 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
|
||||||
}}
|
}}
|
||||||
styles={{ loader: { width: "100%", height: "100%" } }}
|
styles={{ loader: { width: "100%", height: "100%" } }}
|
||||||
/>
|
/>
|
||||||
<Table highlightOnHover fz="xs">
|
<Table fz="xs">
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{resultType === "vector" ? (
|
{resultType === "vector" ? (
|
||||||
limitSeries<InstantSample>(result).map((s, idx) => (
|
limitSeries<InstantSample>(result).map((s, idx) => (
|
||||||
|
@ -111,7 +124,35 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td className={classes.numberCell}>
|
<Table.Td className={classes.numberCell}>
|
||||||
{s.value && s.value[1]}
|
{s.value && s.value[1]}
|
||||||
{s.histogram && "TODO HISTOGRAM DISPLAY"}
|
{s.histogram && (
|
||||||
|
<>
|
||||||
|
<HistogramChart
|
||||||
|
histogram={s.histogram[1]}
|
||||||
|
index={idx}
|
||||||
|
scale={scale}
|
||||||
|
/>
|
||||||
|
<div className={classes.histogramSummaryWrapper}>
|
||||||
|
<div className={classes.histogramSummary}>
|
||||||
|
<span>
|
||||||
|
<strong>Total count:</strong> {s.histogram[1].count}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong>Sum:</strong> {s.histogram[1].sum}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={classes.histogramSummary}>
|
||||||
|
<span>x-axis scale:</span>
|
||||||
|
<SegmentedControl
|
||||||
|
size={"xs"}
|
||||||
|
value={scale}
|
||||||
|
onChange={setScale}
|
||||||
|
data={["exponential", "linear"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{histogramTable(s.histogram[1])}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))
|
))
|
||||||
|
@ -161,4 +202,44 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const histogramTable = (h: Histogram): ReactNode => (
|
||||||
|
<Table>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th style={{ textAlign: "center" }} colSpan={2}>
|
||||||
|
Bucket counts
|
||||||
|
</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table.Tr
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table.Th>Range</Table.Th>
|
||||||
|
<Table.Th>Count</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
<ScrollArea w={"100%"} h={250}>
|
||||||
|
{h.buckets?.map((b, i) => (
|
||||||
|
<Table.Tr key={i}>
|
||||||
|
<Table.Td style={{ textAlign: "left" }}>
|
||||||
|
{bucketRangeString(b)}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{b[3]}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</ScrollArea>
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
|
||||||
export default DataTable;
|
export default DataTable;
|
||||||
|
|
106
web/ui/mantine-ui/src/pages/query/HistogramChart.module.css
Normal file
106
web/ui/mantine-ui/src/pages/query/HistogramChart.module.css
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
.histogramYWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 15px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramYLabels {
|
||||||
|
height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramYLabel {
|
||||||
|
margin-right: 8px;
|
||||||
|
height: 25%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramXWrapper {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramXLabels {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramXLabel {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 5px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramContainer {
|
||||||
|
margin-top: 9px;
|
||||||
|
position: relative;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramAxes {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom: 1px solid var(--mantine-color-gray-7);
|
||||||
|
border-left: 1px solid var(--mantine-color-gray-7);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramYGrid {
|
||||||
|
position: absolute;
|
||||||
|
border-bottom: 1px dashed var(--mantine-color-gray-6);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramYTick {
|
||||||
|
position: absolute;
|
||||||
|
border-bottom: 1px solid var(--mantine-color-gray-7);
|
||||||
|
left: -5px;
|
||||||
|
height: 0px;
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramXGrid {
|
||||||
|
position: absolute;
|
||||||
|
border-left: 1px dashed var(--mantine-color-gray-6);
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramXTick {
|
||||||
|
position: absolute;
|
||||||
|
border-left: 1px solid var(--mantine-color-gray-7);
|
||||||
|
height: 5px;
|
||||||
|
width: 0;
|
||||||
|
bottom: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramBucketSlot {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramBucket {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #2db453;
|
||||||
|
border: 1px solid #77de94;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramBucketSlot:hover {
|
||||||
|
background-color: var(--mantine-color-gray-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogramBucketSlot:hover .histogramBucket {
|
||||||
|
background-color: #88e1a1;
|
||||||
|
border: 1px solid #77de94;
|
||||||
|
}
|
309
web/ui/mantine-ui/src/pages/query/HistogramChart.tsx
Normal file
309
web/ui/mantine-ui/src/pages/query/HistogramChart.tsx
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Histogram } from "../../types/types";
|
||||||
|
import {
|
||||||
|
calculateDefaultExpBucketWidth,
|
||||||
|
findMinPositive,
|
||||||
|
findMaxNegative,
|
||||||
|
findZeroAxisLeft,
|
||||||
|
showZeroAxis,
|
||||||
|
findZeroBucket,
|
||||||
|
bucketRangeString,
|
||||||
|
} from "./HistogramHelpers";
|
||||||
|
import classes from "./HistogramChart.module.css";
|
||||||
|
import { Tooltip } from "@mantine/core";
|
||||||
|
|
||||||
|
interface HistogramChartProps {
|
||||||
|
histogram: Histogram;
|
||||||
|
index: number;
|
||||||
|
scale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistogramChart: FC<HistogramChartProps> = ({
|
||||||
|
index,
|
||||||
|
histogram,
|
||||||
|
scale,
|
||||||
|
}) => {
|
||||||
|
const { buckets } = histogram;
|
||||||
|
if (!buckets || buckets.length === 0) {
|
||||||
|
return <div>No data</div>;
|
||||||
|
}
|
||||||
|
const formatter = Intl.NumberFormat("en", { notation: "compact" });
|
||||||
|
|
||||||
|
// For linear scales, the count of a histogram bucket is represented by its area rather than its height. This means it considers
|
||||||
|
// both the count and the range (width) of the bucket. For this, we can set the height of the bucket proportional
|
||||||
|
// to its frequency density (fd). The fd is the count of the bucket divided by the width of the bucket.
|
||||||
|
const fds = [];
|
||||||
|
for (const bucket of buckets) {
|
||||||
|
const left = parseFloat(bucket[1]);
|
||||||
|
const right = parseFloat(bucket[2]);
|
||||||
|
const count = parseFloat(bucket[3]);
|
||||||
|
const width = right - left;
|
||||||
|
|
||||||
|
// This happens when a user want observations of precisely zero to be included in the zero bucket
|
||||||
|
if (width === 0) {
|
||||||
|
fds.push(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fds.push(count / width);
|
||||||
|
}
|
||||||
|
const fdMax = Math.max(...fds);
|
||||||
|
|
||||||
|
const first = buckets[0];
|
||||||
|
const last = buckets[buckets.length - 1];
|
||||||
|
|
||||||
|
const rangeMax = parseFloat(last[2]);
|
||||||
|
const rangeMin = parseFloat(first[1]);
|
||||||
|
const countMax = Math.max(...buckets.map((b) => parseFloat(b[3])));
|
||||||
|
|
||||||
|
const defaultExpBucketWidth = calculateDefaultExpBucketWidth(last, buckets);
|
||||||
|
|
||||||
|
const maxPositive = rangeMax > 0 ? rangeMax : 0;
|
||||||
|
const minPositive = findMinPositive(buckets);
|
||||||
|
const maxNegative = findMaxNegative(buckets);
|
||||||
|
const minNegative = parseFloat(first[1]) < 0 ? parseFloat(first[1]) : 0;
|
||||||
|
|
||||||
|
// Calculate the borders of positive and negative buckets in the exponential scale from left to right
|
||||||
|
const startNegative =
|
||||||
|
minNegative !== 0 ? -Math.log(Math.abs(minNegative)) : 0;
|
||||||
|
const endNegative = maxNegative !== 0 ? -Math.log(Math.abs(maxNegative)) : 0;
|
||||||
|
const startPositive = minPositive !== 0 ? Math.log(minPositive) : 0;
|
||||||
|
const endPositive = maxPositive !== 0 ? Math.log(maxPositive) : 0;
|
||||||
|
|
||||||
|
// Calculate the width of negative, positive, and all exponential bucket ranges on the x-axis
|
||||||
|
const xWidthNegative = endNegative - startNegative;
|
||||||
|
const xWidthPositive = endPositive - startPositive;
|
||||||
|
const xWidthTotal = xWidthNegative + defaultExpBucketWidth + xWidthPositive;
|
||||||
|
|
||||||
|
const zeroBucketIdx = findZeroBucket(buckets);
|
||||||
|
const zeroAxisLeft = findZeroAxisLeft(
|
||||||
|
scale,
|
||||||
|
rangeMin,
|
||||||
|
rangeMax,
|
||||||
|
minPositive,
|
||||||
|
maxNegative,
|
||||||
|
zeroBucketIdx,
|
||||||
|
xWidthNegative,
|
||||||
|
xWidthTotal,
|
||||||
|
defaultExpBucketWidth,
|
||||||
|
);
|
||||||
|
const zeroAxis = showZeroAxis(zeroAxisLeft);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.histogramYWrapper}>
|
||||||
|
<div className={classes.histogramYLabels}>
|
||||||
|
{[1, 0.75, 0.5, 0.25].map((i) => (
|
||||||
|
<div key={i} className={classes.histogramYLabel}>
|
||||||
|
{scale === "linear" ? "" : formatter.format(countMax * i)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div key={0} className={classes.histogramYLabel} style={{ height: 0 }}>
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.histogramXWrapper}>
|
||||||
|
<div className={classes.histogramContainer}>
|
||||||
|
{[0, 0.25, 0.5, 0.75, 1].map((i) => (
|
||||||
|
<React.Fragment key={i}>
|
||||||
|
<div
|
||||||
|
className={classes.histogramYGrid}
|
||||||
|
style={{ bottom: i * 100 + "%" }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={classes.histogramYTick}
|
||||||
|
style={{ bottom: i * 100 + "%" }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={classes.histogramXGrid}
|
||||||
|
style={{ left: i * 100 + "%" }}
|
||||||
|
></div>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
<div className={classes.histogramXTick} style={{ left: "0%" }}></div>
|
||||||
|
<div
|
||||||
|
className={classes.histogramXTick}
|
||||||
|
style={{ left: zeroAxisLeft }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={classes.histogramXGrid}
|
||||||
|
style={{ left: zeroAxisLeft }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={classes.histogramXTick}
|
||||||
|
style={{ left: "100%" }}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<RenderHistogramBars
|
||||||
|
buckets={buckets}
|
||||||
|
scale={scale}
|
||||||
|
rangeMin={rangeMin}
|
||||||
|
rangeMax={rangeMax}
|
||||||
|
index={index}
|
||||||
|
fds={fds}
|
||||||
|
fdMax={fdMax}
|
||||||
|
countMax={countMax}
|
||||||
|
defaultExpBucketWidth={defaultExpBucketWidth}
|
||||||
|
minPositive={minPositive}
|
||||||
|
maxNegative={maxNegative}
|
||||||
|
startPositive={startPositive}
|
||||||
|
startNegative={startNegative}
|
||||||
|
xWidthNegative={xWidthNegative}
|
||||||
|
xWidthTotal={xWidthTotal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={classes.histogramAxes}></div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.histogramXLabels}>
|
||||||
|
<div className={classes.histogramXLabel}>
|
||||||
|
<React.Fragment>
|
||||||
|
<div style={{ position: "absolute", left: 0 }}>
|
||||||
|
{formatter.format(rangeMin)}
|
||||||
|
</div>
|
||||||
|
{rangeMin < 0 && zeroAxis && (
|
||||||
|
<div style={{ position: "absolute", left: zeroAxisLeft }}>
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ position: "absolute", right: 0 }}>
|
||||||
|
{formatter.format(rangeMax)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RenderHistogramProps {
|
||||||
|
buckets: [number, string, string, string][];
|
||||||
|
scale: string;
|
||||||
|
rangeMin: number;
|
||||||
|
rangeMax: number;
|
||||||
|
index: number;
|
||||||
|
fds: number[];
|
||||||
|
fdMax: number;
|
||||||
|
countMax: number;
|
||||||
|
defaultExpBucketWidth: number;
|
||||||
|
minPositive: number;
|
||||||
|
maxNegative: number;
|
||||||
|
startPositive: number;
|
||||||
|
startNegative: number;
|
||||||
|
xWidthNegative: number;
|
||||||
|
xWidthTotal: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderHistogramBars: FC<RenderHistogramProps> = ({
|
||||||
|
buckets,
|
||||||
|
scale,
|
||||||
|
rangeMin,
|
||||||
|
rangeMax,
|
||||||
|
index,
|
||||||
|
fds,
|
||||||
|
fdMax,
|
||||||
|
countMax,
|
||||||
|
defaultExpBucketWidth,
|
||||||
|
minPositive,
|
||||||
|
maxNegative,
|
||||||
|
startPositive,
|
||||||
|
startNegative,
|
||||||
|
xWidthNegative,
|
||||||
|
xWidthTotal,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{buckets.map((b, bIdx) => {
|
||||||
|
const left = parseFloat(b[1]);
|
||||||
|
const right = parseFloat(b[2]);
|
||||||
|
const count = parseFloat(b[3]);
|
||||||
|
const bucketIdx = `bucket-${index}-${bIdx}-${Math.ceil(parseFloat(b[3]) * 100)}`;
|
||||||
|
|
||||||
|
const logWidth = Math.abs(
|
||||||
|
Math.log(Math.abs(right)) - Math.log(Math.abs(left)),
|
||||||
|
);
|
||||||
|
const expBucketWidth =
|
||||||
|
logWidth === 0 ? defaultExpBucketWidth : logWidth;
|
||||||
|
|
||||||
|
let bucketWidth = "";
|
||||||
|
let bucketLeft = "";
|
||||||
|
let bucketHeight = "";
|
||||||
|
|
||||||
|
switch (scale) {
|
||||||
|
case "linear": {
|
||||||
|
bucketWidth = ((right - left) / (rangeMax - rangeMin)) * 100 + "%";
|
||||||
|
bucketLeft =
|
||||||
|
((left - rangeMin) / (rangeMax - rangeMin)) * 100 + "%";
|
||||||
|
if (left === 0 && right === 0) {
|
||||||
|
bucketLeft = "0%"; // do not render zero-width zero bucket
|
||||||
|
bucketWidth = "0%";
|
||||||
|
}
|
||||||
|
bucketHeight = (fds[bIdx] / fdMax) * 100 + "%";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "exponential": {
|
||||||
|
let adjust = 0; // if buckets are all positive/negative, we need to remove the width of the zero bucket
|
||||||
|
if (minPositive === 0 || maxNegative === 0) {
|
||||||
|
adjust = defaultExpBucketWidth;
|
||||||
|
}
|
||||||
|
bucketWidth = (expBucketWidth / (xWidthTotal - adjust)) * 100 + "%";
|
||||||
|
if (left < 0) {
|
||||||
|
// negative buckets boundary
|
||||||
|
bucketLeft =
|
||||||
|
(-(Math.log(Math.abs(left)) + startNegative) /
|
||||||
|
(xWidthTotal - adjust)) *
|
||||||
|
100 +
|
||||||
|
"%";
|
||||||
|
} else {
|
||||||
|
// positive buckets boundary
|
||||||
|
bucketLeft =
|
||||||
|
((Math.log(left) -
|
||||||
|
startPositive +
|
||||||
|
defaultExpBucketWidth +
|
||||||
|
xWidthNegative -
|
||||||
|
adjust) /
|
||||||
|
(xWidthTotal - adjust)) *
|
||||||
|
100 +
|
||||||
|
"%";
|
||||||
|
}
|
||||||
|
if (left < 0 && right > 0) {
|
||||||
|
// if the bucket crosses the zero axis
|
||||||
|
bucketLeft = (xWidthNegative / xWidthTotal) * 100 + "%";
|
||||||
|
}
|
||||||
|
if (left === 0 && right === 0) {
|
||||||
|
// do not render zero width zero bucket
|
||||||
|
bucketLeft = "0%";
|
||||||
|
bucketWidth = "0%";
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketHeight = (count / countMax) * 100 + "%";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid scale");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={`range: ${bucketRangeString(b)}`} key={bIdx}>
|
||||||
|
<div
|
||||||
|
id={bucketIdx}
|
||||||
|
className={classes.histogramBucketSlot}
|
||||||
|
style={{
|
||||||
|
left: bucketLeft,
|
||||||
|
width: bucketWidth,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id={bucketIdx}
|
||||||
|
className={classes.histogramBucket}
|
||||||
|
style={{
|
||||||
|
height: bucketHeight,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistogramChart;
|
144
web/ui/mantine-ui/src/pages/query/HistogramHelpers.ts
Normal file
144
web/ui/mantine-ui/src/pages/query/HistogramHelpers.ts
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
// Calculates a default width of exponential histogram bucket ranges. If the last bucket is [0, 0],
|
||||||
|
// the width is calculated using the second to last bucket. returns error if the last bucket is [-0, 0],
|
||||||
|
export function calculateDefaultExpBucketWidth(
|
||||||
|
last: [number, string, string, string],
|
||||||
|
buckets: [number, string, string, string][]
|
||||||
|
): number {
|
||||||
|
if (parseFloat(last[2]) === 0 || parseFloat(last[1]) === 0) {
|
||||||
|
if (buckets.length > 1) {
|
||||||
|
return Math.abs(
|
||||||
|
Math.log(Math.abs(parseFloat(buckets[buckets.length - 2][2]))) -
|
||||||
|
Math.log(Math.abs(parseFloat(buckets[buckets.length - 2][1])))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Only one bucket in histogram ([-0, 0]). Cannot calculate defaultExpBucketWidth."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Math.abs(
|
||||||
|
Math.log(Math.abs(parseFloat(last[2]))) -
|
||||||
|
Math.log(Math.abs(parseFloat(last[1])))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the lowest positive value from the bucket ranges
|
||||||
|
// Returns 0 if no positive values are found or if there are no buckets.
|
||||||
|
export function findMinPositive(buckets: [number, string, string, string][]) {
|
||||||
|
if (!buckets || buckets.length === 0) {
|
||||||
|
return 0; // no buckets
|
||||||
|
}
|
||||||
|
for (let i = 0; i < buckets.length; i++) {
|
||||||
|
const right = parseFloat(buckets[i][2]);
|
||||||
|
const left = parseFloat(buckets[i][1]);
|
||||||
|
|
||||||
|
if (left > 0) {
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
if (left < 0 && right > 0) {
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
if (i === buckets.length - 1) {
|
||||||
|
if (right > 0) {
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0; // all buckets are negative
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the lowest negative value from the bucket ranges
|
||||||
|
// Returns 0 if no negative values are found or if there are no buckets.
|
||||||
|
export function findMaxNegative(buckets: [number, string, string, string][]) {
|
||||||
|
if (!buckets || buckets.length === 0) {
|
||||||
|
return 0; // no buckets
|
||||||
|
}
|
||||||
|
for (let i = 0; i < buckets.length; i++) {
|
||||||
|
const right = parseFloat(buckets[i][2]);
|
||||||
|
const left = parseFloat(buckets[i][1]);
|
||||||
|
const prevRight = i > 0 ? parseFloat(buckets[i - 1][2]) : 0;
|
||||||
|
|
||||||
|
if (right >= 0) {
|
||||||
|
if (i === 0) {
|
||||||
|
if (left < 0) {
|
||||||
|
return left; // return the first negative bucket
|
||||||
|
}
|
||||||
|
return 0; // all buckets are positive
|
||||||
|
}
|
||||||
|
return prevRight; // return the last negative bucket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("findmaxneg returning: ", buckets[buckets.length - 1][2]);
|
||||||
|
return parseFloat(buckets[buckets.length - 1][2]); // all buckets are negative
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the left position of the zero axis as a percentage string.
|
||||||
|
export function findZeroAxisLeft(
|
||||||
|
scale: string,
|
||||||
|
rangeMin: number,
|
||||||
|
rangeMax: number,
|
||||||
|
minPositive: number,
|
||||||
|
maxNegative: number,
|
||||||
|
zeroBucketIdx: number,
|
||||||
|
widthNegative: number,
|
||||||
|
widthTotal: number,
|
||||||
|
expBucketWidth: number
|
||||||
|
): string {
|
||||||
|
if (scale === "linear") {
|
||||||
|
return ((0 - rangeMin) / (rangeMax - rangeMin)) * 100 + "%";
|
||||||
|
} else {
|
||||||
|
if (maxNegative === 0) {
|
||||||
|
return "0%";
|
||||||
|
}
|
||||||
|
if (minPositive === 0) {
|
||||||
|
return "100%";
|
||||||
|
}
|
||||||
|
if (zeroBucketIdx === -1) {
|
||||||
|
// if there is no zero bucket, we must zero axis between buckets around zero
|
||||||
|
return (widthNegative / widthTotal) * 100 + "%";
|
||||||
|
}
|
||||||
|
if ((widthNegative + 0.5 * expBucketWidth) / widthTotal > 0) {
|
||||||
|
return ((widthNegative + 0.5 * expBucketWidth) / widthTotal) * 100 + "%";
|
||||||
|
} else {
|
||||||
|
return "0%";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if the zero axis should be shown such that the zero label does not overlap with the range labels.
|
||||||
|
// The zero axis is shown if it is between 5% and 95% of the graph.
|
||||||
|
export function showZeroAxis(zeroAxisLeft: string) {
|
||||||
|
const axisNumber = parseFloat(zeroAxisLeft.slice(0, -1));
|
||||||
|
if (5 < axisNumber && axisNumber < 95) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the index of the bucket whose range includes zero
|
||||||
|
export function findZeroBucket(
|
||||||
|
buckets: [number, string, string, string][]
|
||||||
|
): number {
|
||||||
|
for (let i = 0; i < buckets.length; i++) {
|
||||||
|
const left = parseFloat(buckets[i][1]);
|
||||||
|
const right = parseFloat(buckets[i][2]);
|
||||||
|
if (left <= 0 && right >= 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftDelim = (br: number): string => (br === 3 || br === 1 ? "[" : "(");
|
||||||
|
const rightDelim = (br: number): string => (br === 3 || br === 0 ? "]" : ")");
|
||||||
|
|
||||||
|
export const bucketRangeString = ([
|
||||||
|
boundaryRule,
|
||||||
|
leftBoundary,
|
||||||
|
rightBoundary,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_,
|
||||||
|
]: [number, string, string, string]): string => {
|
||||||
|
return `${leftDelim(boundaryRule)}${leftBoundary} -> ${rightBoundary}${rightDelim(boundaryRule)}`;
|
||||||
|
};
|
5
web/ui/mantine-ui/src/types/types.ts
Normal file
5
web/ui/mantine-ui/src/types/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface Histogram {
|
||||||
|
count: string;
|
||||||
|
sum: string;
|
||||||
|
buckets?: [number, string, string, string][];
|
||||||
|
}
|
Loading…
Reference in a new issue