From 4ce23ce83ea38e83b63cc097ddfce3b78fd6fd88 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Tue, 31 Jan 2023 15:49:38 +0100 Subject: [PATCH] Start work on histogram visualization in the table view Signed-off-by: Julius Volz --- .../src/pages/graph/DataTable.test.tsx | 1 - .../react-app/src/pages/graph/DataTable.tsx | 75 +++++++---- .../src/pages/graph/HistogramChart.tsx | 79 ++++++++++++ web/ui/react-app/src/themes/_shared.scss | 122 +++++++++++++++++- web/ui/react-app/src/themes/dark.scss | 4 + web/ui/react-app/src/themes/light.scss | 4 + 6 files changed, 255 insertions(+), 30 deletions(-) create mode 100644 web/ui/react-app/src/pages/graph/HistogramChart.tsx diff --git a/web/ui/react-app/src/pages/graph/DataTable.test.tsx b/web/ui/react-app/src/pages/graph/DataTable.test.tsx index d6974cf90..85fcc50bb 100755 --- a/web/ui/react-app/src/pages/graph/DataTable.test.tsx +++ b/web/ui/react-app/src/pages/graph/DataTable.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import DataTable, { DataTableProps } from './DataTable'; -import HistogramString, { HistogramStringProps } from './DataTable'; import { Alert, Table } from 'reactstrap'; import SeriesName from './SeriesName'; diff --git a/web/ui/react-app/src/pages/graph/DataTable.tsx b/web/ui/react-app/src/pages/graph/DataTable.tsx index 901d7bb48..2d55f8522 100644 --- a/web/ui/react-app/src/pages/graph/DataTable.tsx +++ b/web/ui/react-app/src/pages/graph/DataTable.tsx @@ -1,12 +1,16 @@ import React, { FC, ReactNode } from 'react'; -import { Alert, Table } from 'reactstrap'; +import { Alert, Table, UncontrolledTooltip } from 'reactstrap'; import SeriesName from './SeriesName'; import { Metric, Histogram } from '../../types/types'; 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 { data: | null @@ -75,7 +79,13 @@ const DataTable: FC = ({ data, useLocalTime }) => { - {s.value && s.value[1]} + {s.value && s.value[1]} + {s.histogram && ( + <> + + {histogramTable(s.histogram[1])} + + )} ); @@ -100,7 +110,7 @@ const DataTable: FC = ({ data, useLocalTime }) => { const printedDatetime = moment.unix(h[0]).toISOString(useLocalTime); return ( - @{{h[0]}} + {histogramTable(h[1])} @{{h[0]}}
); @@ -159,29 +169,44 @@ const DataTable: FC = ({ data, useLocalTime }) => { ); }; -export interface HistogramStringProps { - h?: Histogram; -} +const leftDelim = (br: number): string => (br === 3 || br === 1 ? '[' : '('); +const rightDelim = (br: number): string => (br === 3 || br === 0 ? ']' : ')'); -export const HistogramString: FC = ({ h }) => { - if (!h) { - return <>; - } - const buckets: string[] = []; - - if (h.buckets) { - 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 bucketRangeString = ([boundaryRule, leftBoundary, rightBoundary, _]: [ + number, + string, + string, + string +]): string => { + return `${leftDelim(boundaryRule)}${leftBoundary} 🠒 ${rightBoundary}${rightDelim(boundaryRule)}`; }; +export const histogramTable = (h: Histogram): ReactNode => ( + + + + + + + + + + + + + + + + {h.buckets?.map((b, i) => ( + + + + + ))} + +
+ Histogram Sample +
count:{h.count}
sum:{h.sum}
{bucketRangeString(b)}{b[3]}
+); + export default DataTable; diff --git a/web/ui/react-app/src/pages/graph/HistogramChart.tsx b/web/ui/react-app/src/pages/graph/HistogramChart.tsx new file mode 100644 index 000000000..59ef688d6 --- /dev/null +++ b/web/ui/react-app/src/pages/graph/HistogramChart.tsx @@ -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 ( +
+
+ {[1, 0.75, 0.5, 0.25].map((i) => ( +
+ {formatter.format(countMax * i)} +
+ ))} +
+ 0 +
+
+
+
+ {[0, 0.25, 0.5, 0.75, 1].map((i) => ( + +
+
+
+
+
+ ))} + {buckets?.map((b, bIdx) => ( + +
+
+ + range: {bucketRangeString(b)} +
+ count: {b[3]} +
+
+
+ ))} +
+
+
+
+ 0 +
+ {[0.25, 0.5, 0.75, 1].map((i) => ( +
+
{formatter.format(rangeMax * i)}
+
+ ))} +
+
+
+ ); +}; + +export default HistogramChart; diff --git a/web/ui/react-app/src/themes/_shared.scss b/web/ui/react-app/src/themes/_shared.scss index 20658a8af..ffc67aeb8 100644 --- a/web/ui/react-app/src/themes/_shared.scss +++ b/web/ui/react-app/src/themes/_shared.scss @@ -50,14 +50,17 @@ max-width: 750px; overflow-wrap: break-word; } + .metrics-explorer .metric { cursor: pointer; margin: 0; padding: 5px; } + .metrics-explorer .metric:hover { background: $metrics-explorer-bg; } + button.expression-input-action-btn { color: $input-group-addon-color; background-color: $input-group-addon-bg; @@ -113,7 +116,7 @@ button.execute-btn { margin-top: 4px; } -input[type='checkbox']:checked + label { +input[type='checkbox']:checked+label { color: $checked-checkbox-color; } @@ -161,12 +164,121 @@ input[type='checkbox']:checked + label { margin: 10px 0 2px 0; } -.data-table > tbody > tr > td { +.data-table>tbody>tr>td { padding: 5px 0 5px 8px; font-size: 0.8em; 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 { position: absolute; border: 1px solid #ced4da; @@ -228,7 +340,7 @@ input[type='checkbox']:checked + label { width: 90px; } -.graph-controls > :not(:first-child) { +.graph-controls> :not(:first-child) { margin-left: 20px; } @@ -254,6 +366,7 @@ input[type='checkbox']:checked + label { border-radius: 3px; line-height: 1.7; } + .legend-item div { flex-wrap: wrap; } @@ -304,6 +417,7 @@ input[type='checkbox']:checked + label { cursor: crosshair; } +.histogram-tooltip, .graph-tooltip { background: rgba(0, 0, 0, 0.8); color: #fff; @@ -342,7 +456,7 @@ input[type='checkbox']:checked + label { padding: 10px; } -.badges-wrapper > span { +.badges-wrapper>span { margin-right: 5px; max-height: 20px; } diff --git a/web/ui/react-app/src/themes/dark.scss b/web/ui/react-app/src/themes/dark.scss index 0989c833c..390a98983 100644 --- a/web/ui/react-app/src/themes/dark.scss +++ b/web/ui/react-app/src/themes/dark.scss @@ -19,6 +19,10 @@ $clear-time-btn-bg: $secondary; $checked-checkbox-color: #60a5fa; +$histogram-chart-axis-color: $gray-300; +$histogram-chart-grid-color: $gray-600; +$histogram-chart-hover-color: $gray-700; + .bootstrap-dark { @import './shared'; } diff --git a/web/ui/react-app/src/themes/light.scss b/web/ui/react-app/src/themes/light.scss index 8e6b10e80..92b6537c0 100644 --- a/web/ui/react-app/src/themes/light.scss +++ b/web/ui/react-app/src/themes/light.scss @@ -18,6 +18,10 @@ $clear-time-btn-bg: $white; $checked-checkbox-color: #286090; +$histogram-chart-axis-color: $gray-700; +$histogram-chart-grid-color: $gray-500; +$histogram-chart-hover-color: $gray-400; + .bootstrap { @import './shared'; }