mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Start work on histogram visualization in the table view
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
a35e54cc56
commit
4ce23ce83e
|
@ -1,7 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import DataTable, { DataTableProps } from './DataTable';
|
import DataTable, { DataTableProps } from './DataTable';
|
||||||
import HistogramString, { HistogramStringProps } from './DataTable';
|
|
||||||
import { Alert, Table } from 'reactstrap';
|
import { Alert, Table } from 'reactstrap';
|
||||||
import SeriesName from './SeriesName';
|
import SeriesName from './SeriesName';
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import React, { FC, ReactNode } from 'react';
|
import React, { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
import { Alert, Table } from 'reactstrap';
|
import { Alert, Table, UncontrolledTooltip } from 'reactstrap';
|
||||||
|
|
||||||
import SeriesName from './SeriesName';
|
import SeriesName from './SeriesName';
|
||||||
import { Metric, Histogram } from '../../types/types';
|
import { Metric, Histogram } from '../../types/types';
|
||||||
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { Tooltip as ReTooltip, Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts';
|
||||||
|
import { parseValue } from './GraphHelpers';
|
||||||
|
import HistogramChart from './HistogramChart';
|
||||||
|
|
||||||
export interface DataTableProps {
|
export interface DataTableProps {
|
||||||
data:
|
data:
|
||||||
| null
|
| null
|
||||||
|
@ -75,7 +79,13 @@ const DataTable: FC<DataTableProps> = ({ data, useLocalTime }) => {
|
||||||
<SeriesName labels={s.metric} format={doFormat} />
|
<SeriesName labels={s.metric} format={doFormat} />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{s.value && s.value[1]} <HistogramString h={s.histogram && s.histogram[1]} />
|
{s.value && s.value[1]}
|
||||||
|
{s.histogram && (
|
||||||
|
<>
|
||||||
|
<HistogramChart histogram={s.histogram[1]} index={index} />
|
||||||
|
{histogramTable(s.histogram[1])}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
@ -100,7 +110,7 @@ const DataTable: FC<DataTableProps> = ({ data, useLocalTime }) => {
|
||||||
const printedDatetime = moment.unix(h[0]).toISOString(useLocalTime);
|
const printedDatetime = moment.unix(h[0]).toISOString(useLocalTime);
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={-hisIdx}>
|
<React.Fragment key={-hisIdx}>
|
||||||
<HistogramString h={h[1]} /> @{<span title={printedDatetime}>{h[0]}</span>}
|
{histogramTable(h[1])} @{<span title={printedDatetime}>{h[0]}</span>}
|
||||||
<br />
|
<br />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
@ -159,29 +169,44 @@ const DataTable: FC<DataTableProps> = ({ data, useLocalTime }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface HistogramStringProps {
|
const leftDelim = (br: number): string => (br === 3 || br === 1 ? '[' : '(');
|
||||||
h?: Histogram;
|
const rightDelim = (br: number): string => (br === 3 || br === 0 ? ']' : ')');
|
||||||
}
|
|
||||||
|
|
||||||
export const HistogramString: FC<HistogramStringProps> = ({ h }) => {
|
export const bucketRangeString = ([boundaryRule, leftBoundary, rightBoundary, _]: [
|
||||||
if (!h) {
|
number,
|
||||||
return <></>;
|
string,
|
||||||
}
|
string,
|
||||||
const buckets: string[] = [];
|
string
|
||||||
|
]): string => {
|
||||||
if (h.buckets) {
|
return `${leftDelim(boundaryRule)}${leftBoundary} 🠒 ${rightBoundary}${rightDelim(boundaryRule)}`;
|
||||||
for (const bucket of h.buckets) {
|
|
||||||
const left = bucket[0] === 3 || bucket[0] === 1 ? '[' : '(';
|
|
||||||
const right = bucket[0] === 3 || bucket[0] === 0 ? ']' : ')';
|
|
||||||
buckets.push(left + bucket[1] + ',' + bucket[2] + right + ':' + bucket[3] + ' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{'{'} count:{h.count} sum:{h.sum} {buckets} {'}'}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const histogramTable = (h: Histogram): ReactNode => (
|
||||||
|
<Table size="xs" responsive bordered>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ textAlign: 'center' }} colSpan={2}>
|
||||||
|
Histogram Sample
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>count:</th>
|
||||||
|
<td>{h.count}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>sum:</th>
|
||||||
|
<td>{h.sum}</td>
|
||||||
|
</tr>
|
||||||
|
{h.buckets?.map((b, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<th>{bucketRangeString(b)}</th>
|
||||||
|
<td>{b[3]}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
|
||||||
export default DataTable;
|
export default DataTable;
|
||||||
|
|
79
web/ui/react-app/src/pages/graph/HistogramChart.tsx
Normal file
79
web/ui/react-app/src/pages/graph/HistogramChart.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import { Histogram } from '../../types/types';
|
||||||
|
import { bucketRangeString } from './DataTable';
|
||||||
|
|
||||||
|
const HistogramChart: FC<{ histogram: Histogram; index: number }> = ({ index, histogram }) => {
|
||||||
|
const { buckets } = histogram;
|
||||||
|
const rangeMax = buckets ? parseFloat(buckets[buckets.length - 1][2]) : 0;
|
||||||
|
const countMax = buckets ? buckets.map((b) => parseFloat(b[3])).reduce((a, b) => Math.max(a, b)) : 0;
|
||||||
|
const formatter = Intl.NumberFormat('en', { notation: 'compact' });
|
||||||
|
return (
|
||||||
|
<div className="histogram-y-wrapper">
|
||||||
|
<div className="histogram-y-labels">
|
||||||
|
{[1, 0.75, 0.5, 0.25].map((i) => (
|
||||||
|
<div key={i} className="histogram-y-label">
|
||||||
|
{formatter.format(countMax * i)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div key={0} className="histogram-y-label" style={{ height: 0 }}>
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="histogram-x-wrapper">
|
||||||
|
<div className="histogram-container">
|
||||||
|
{[0, 0.25, 0.5, 0.75, 1].map((i) => (
|
||||||
|
<React.Fragment key={i}>
|
||||||
|
<div className="histogram-y-grid" style={{ bottom: i * 100 + '%' }}></div>
|
||||||
|
<div className="histogram-y-tick" style={{ bottom: i * 100 + '%' }}></div>
|
||||||
|
<div className="histogram-x-grid" style={{ left: i * 100 + '%' }}></div>
|
||||||
|
<div className="histogram-x-tick" style={{ left: i * 100 + '%' }}></div>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
{buckets?.map((b, bIdx) => (
|
||||||
|
<React.Fragment key={bIdx}>
|
||||||
|
<div
|
||||||
|
id={`bucket-${index}-${bIdx}`}
|
||||||
|
className="histogram-bucket-slot"
|
||||||
|
style={{
|
||||||
|
left: (parseFloat(b[1]) / rangeMax) * 100 + '%',
|
||||||
|
width: ((parseFloat(b[2]) - parseFloat(b[1])) / rangeMax) * 100 + '%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id={`bucket-${index}-${bIdx}`}
|
||||||
|
className="histogram-bucket"
|
||||||
|
style={{
|
||||||
|
height: (parseFloat(b[3]) / countMax) * 100 + '%',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<UncontrolledTooltip
|
||||||
|
style={{ maxWidth: 'unset', padding: 10, textAlign: 'left' }}
|
||||||
|
placement="bottom"
|
||||||
|
target={`bucket-${index}-${bIdx}`}
|
||||||
|
>
|
||||||
|
<strong>range:</strong> {bucketRangeString(b)}
|
||||||
|
<br />
|
||||||
|
<strong>count:</strong> {b[3]}
|
||||||
|
</UncontrolledTooltip>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
<div className="histogram-axes"></div>
|
||||||
|
</div>
|
||||||
|
<div className="histogram-x-labels">
|
||||||
|
<div key={0} className="histogram-x-label" style={{ width: 0 }}>
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
{[0.25, 0.5, 0.75, 1].map((i) => (
|
||||||
|
<div key={i} className="histogram-x-label">
|
||||||
|
<div style={{ position: 'absolute', right: i === 1 ? 0 : -18 }}>{formatter.format(rangeMax * i)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistogramChart;
|
|
@ -50,14 +50,17 @@
|
||||||
max-width: 750px;
|
max-width: 750px;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metrics-explorer .metric {
|
.metrics-explorer .metric {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metrics-explorer .metric:hover {
|
.metrics-explorer .metric:hover {
|
||||||
background: $metrics-explorer-bg;
|
background: $metrics-explorer-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.expression-input-action-btn {
|
button.expression-input-action-btn {
|
||||||
color: $input-group-addon-color;
|
color: $input-group-addon-color;
|
||||||
background-color: $input-group-addon-bg;
|
background-color: $input-group-addon-bg;
|
||||||
|
@ -167,6 +170,115 @@ input[type='checkbox']:checked + label {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.histogram-y-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 15px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-y-labels {
|
||||||
|
height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-y-label {
|
||||||
|
margin-right: 8px;
|
||||||
|
height: 25%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-x-wrapper {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-x-labels {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-x-label {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 5px;
|
||||||
|
width: 25%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-container {
|
||||||
|
margin-top: 9px;
|
||||||
|
position: relative;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-axes {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom: 1px solid $histogram-chart-axis-color;
|
||||||
|
border-left: 1px solid $histogram-chart-axis-color;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-y-grid {
|
||||||
|
position: absolute;
|
||||||
|
border-bottom: 1px dashed $histogram-chart-grid-color;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-y-tick {
|
||||||
|
position: absolute;
|
||||||
|
border-bottom: 1px solid $histogram-chart-axis-color;
|
||||||
|
left: -5px;
|
||||||
|
height: 0px;
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-x-grid {
|
||||||
|
position: absolute;
|
||||||
|
border-left: 1px dashed $histogram-chart-grid-color;
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-x-tick {
|
||||||
|
position: absolute;
|
||||||
|
border-left: 1px solid $histogram-chart-axis-color;
|
||||||
|
height: 5px;
|
||||||
|
width: 0;
|
||||||
|
bottom: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-bucket-slot {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-bucket {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
// background-color: #9090ff;
|
||||||
|
// border: 1px solid #aaaaff;
|
||||||
|
background-color: #2db453;
|
||||||
|
border: 1px solid #77de94;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-bucket-slot:hover {
|
||||||
|
background-color: $histogram-chart-hover-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-bucket-slot:hover .histogram-bucket {
|
||||||
|
background-color: #88e1a1;
|
||||||
|
border: 1px solid #77de94;
|
||||||
|
}
|
||||||
|
|
||||||
.autosuggest-dropdown {
|
.autosuggest-dropdown {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border: 1px solid #ced4da;
|
border: 1px solid #ced4da;
|
||||||
|
@ -254,6 +366,7 @@ input[type='checkbox']:checked + label {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-item div {
|
.legend-item div {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
@ -304,6 +417,7 @@ input[type='checkbox']:checked + label {
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.histogram-tooltip,
|
||||||
.graph-tooltip {
|
.graph-tooltip {
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
|
@ -19,6 +19,10 @@ $clear-time-btn-bg: $secondary;
|
||||||
|
|
||||||
$checked-checkbox-color: #60a5fa;
|
$checked-checkbox-color: #60a5fa;
|
||||||
|
|
||||||
|
$histogram-chart-axis-color: $gray-300;
|
||||||
|
$histogram-chart-grid-color: $gray-600;
|
||||||
|
$histogram-chart-hover-color: $gray-700;
|
||||||
|
|
||||||
.bootstrap-dark {
|
.bootstrap-dark {
|
||||||
@import './shared';
|
@import './shared';
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,10 @@ $clear-time-btn-bg: $white;
|
||||||
|
|
||||||
$checked-checkbox-color: #286090;
|
$checked-checkbox-color: #286090;
|
||||||
|
|
||||||
|
$histogram-chart-axis-color: $gray-700;
|
||||||
|
$histogram-chart-grid-color: $gray-500;
|
||||||
|
$histogram-chart-hover-color: $gray-400;
|
||||||
|
|
||||||
.bootstrap {
|
.bootstrap {
|
||||||
@import './shared';
|
@import './shared';
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue