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 61bc55486b..dbc1b18b8e 100755 --- a/web/ui/react-app/src/pages/graph/DataTable.test.tsx +++ b/web/ui/react-app/src/pages/graph/DataTable.test.tsx @@ -71,7 +71,7 @@ describe('DataTable', () => { const table = dataTable.find(Table); table.find('tr').forEach((row, idx) => { expect(row.find(SeriesName)).toHaveLength(1); - expect(row.find('td').at(1).text()).toEqual(`${idx} `); + expect(row.find('td').at(1).text()).toEqual(`${idx}`); }); }); }); @@ -129,42 +129,21 @@ describe('DataTable', () => { }, useLocalTime: false, }; - const dataTable = mount(); + const dataTable = shallow(); it('renders a table', () => { - const table = dataTable.find(Table); + const table = dataTable.find(Table).first(); expect(table.prop('hover')).toBe(true); expect(table.prop('size')).toEqual('sm'); expect(table.prop('className')).toEqual('data-table'); - expect(table.find('tbody')).toHaveLength(1); + expect(table.find('tbody')).toHaveLength(dataTableProps.data?.result.length as number); }); it('renders rows', () => { const table = dataTable.find(Table); - const histogramData = [{ - count: '10', - sum: '3.3', - buckets: [ - [1, '-1', '-0.5', '2'], - [3, '-0.5', '0.5', '3'], - [0, '0.5', '1', '5'], - ] - }, - { - count: '5', - sum: '1.11', - buckets: [ - [0, '0.5', '1', '2'], - [0, '1', '2', '3'], - ], - }]; table.find('tr').forEach((row, idx) => { const seriesNameComponent = dataTable.find('SeriesName'); expect(seriesNameComponent).toHaveLength(dataTableProps.data?.result.length as number); - - const histogramStringComponent = row.find('HistogramString'); - expect(histogramStringComponent).toHaveLength(1); - expect(histogramStringComponent.prop('h')).toEqual(histogramData[idx]); }); }); }); diff --git a/web/ui/react-app/src/pages/graph/DataTable.tsx b/web/ui/react-app/src/pages/graph/DataTable.tsx index 1885c7a80f..add28f182b 100644 --- a/web/ui/react-app/src/pages/graph/DataTable.tsx +++ b/web/ui/react-app/src/pages/graph/DataTable.tsx @@ -1,12 +1,14 @@ import React, { FC, ReactNode } from 'react'; -import { Alert, Table } from 'reactstrap'; +import { Alert, Button, ButtonGroup, Table } from 'reactstrap'; import SeriesName from './SeriesName'; import { Metric, Histogram } from '../../types/types'; import moment from 'moment'; +import HistogramChart from './HistogramChart'; + export interface DataTableProps { data: | null @@ -54,6 +56,8 @@ const limitSeries = (series: S[]): S[] = }; const DataTable: FC = ({ data, useLocalTime }) => { + const [scale, setScale] = React.useState<'linear' | 'exponential'>('exponential'); + if (data === null) { return No data queried yet; } @@ -75,7 +79,42 @@ const DataTable: FC = ({ data, useLocalTime }) => { - {s.value && s.value[1]} + {s.value && s.value[1]} + {s.histogram && ( + <> + +
+
+ + Total count: {s.histogram[1].count} + + + Sum: {s.histogram[1].sum} + +
+
+ x-axis scale: + + + + +
+
+ {histogramTable(s.histogram[1])} + + )} ); @@ -100,7 +139,7 @@ const DataTable: FC = ({ data, useLocalTime }) => { const printedDatetime = moment.unix(h[0]).toISOString(useLocalTime); return ( - @{{h[0]}} + {histogramTable(h[1])} @{{h[0]}}
); @@ -159,29 +198,39 @@ 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 +
RangeCount
{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 0000000000..ae171c5e43 --- /dev/null +++ b/web/ui/react-app/src/pages/graph/HistogramChart.tsx @@ -0,0 +1,92 @@ +import React, { FC } from 'react'; +import { UncontrolledTooltip } from 'reactstrap'; +import { Histogram } from '../../types/types'; +import { bucketRangeString } from './DataTable'; + +type ScaleType = 'linear' | 'exponential'; + +const HistogramChart: FC<{ histogram: Histogram; index: number; scale: ScaleType }> = ({ index, histogram, scale }) => { + 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' }); + const positiveBuckets = buckets?.filter((b) => parseFloat(b[1]) >= 0); // we only want to show buckets with range >= 0 + const xLabelTicks = scale === 'linear' ? [0.25, 0.5, 0.75, 1] : [1]; + 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) => ( + +
+
+
+
+
+ ))} + {positiveBuckets?.map((b, bIdx) => { + const bucketIdx = `bucket-${index}-${bIdx}-${Math.ceil(parseFloat(b[3]) * 100)}`; + const bucketLeft = + scale === 'linear' ? (parseFloat(b[1]) / rangeMax) * 100 + '%' : (bIdx / positiveBuckets.length) * 100 + '%'; + const bucketWidth = + scale === 'linear' + ? ((parseFloat(b[2]) - parseFloat(b[1])) / rangeMax) * 100 + '%' + : 100 / positiveBuckets.length + '%'; + return ( + +
+
+ + range: {bucketRangeString(b)} +
+ count: {b[3]} +
+
+
+ ); + })} +
+
+
+
+ 0 +
+ {xLabelTicks.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 8745a6d775..e43901ca6b 100644 --- a/web/ui/react-app/src/themes/_shared.scss +++ b/web/ui/react-app/src/themes/_shared.scss @@ -111,7 +111,7 @@ button.execute-btn { } .navbar-brand svg.animate path { - animation: flamecolor 4s ease-in 1 forwards,flame 1s ease-in infinite; + animation: flamecolor 4s ease-in 1 forwards, flame 1s ease-in infinite; } .navbar-brand { @@ -122,6 +122,126 @@ input[type='checkbox']:checked + label { color: $checked-checkbox-color; } +.histogram-summary-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; +} + +.histogram-summary { + display: flex; + align-items: center; + gap: 1rem; +} + +.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: 100%; + 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: #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; +} + .custom-control-label { cursor: pointer; } @@ -151,7 +271,7 @@ input[type='checkbox']:checked + label { .alert { padding: 10px; - margin-bottom: .2rem; + margin-bottom: 0.2rem; } .nav-tabs .nav-link { @@ -364,14 +484,24 @@ input[type='checkbox']:checked + label { } @keyframes flamecolor { - 100% { - fill: #e95224; - } + 100% { + fill: #e95224; + } } @keyframes flame { - 0% { d: path("M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 8.712,-21.285 5.569,-29.308 3.059,0.249 6.331,6.456 6.552,16.161 3.252,-4.494 4.613,-12.701 4.613,-17.733 0,-5.21 3.433,-11.262 6.867,-11.469 -3.061,5.045 0.793,9.37 4.219,20.099 1.285,4.03 1.121,10.812 2.113,15.113 C 63.797,33.534 65.333,20.5 71,16 c -2.5,5.667 0.37,12.758 2.333,16.167 3.167,5.5 5.087,9.667 5.087,17.548 0,5.284 -1.951,10.259 -5.242,14.148 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z"); - } - 50% { d: path("M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 1.640181,-23.047128 7.294982,-29.291475 C 39.391377,29.509803 45.435,26.752 45.656,36.457 c 3.252,-4.494 7.100362,-8.366957 7.100362,-13.398957 0,-5.21 0.137393,-8.650513 -3.479689,-15.0672265 7.834063,1.6180944 8.448052,4.2381285 11.874052,14.9671285 1.285,4.03 1.325275,15.208055 2.317275,19.509055 0.329,-8.933 6.441001,-14.01461 5.163951,-21.391003 5.755224,5.771457 4.934508,10.495521 7.126537,14.288218 3.167,5.5 2.382625,7.496239 2.382625,15.377239 0,5.284 -1.672113,9.232546 -4.963113,13.121546 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z"); } - 100% { - d: path("M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 8.712,-21.285 5.569,-29.308 3.059,0.249 6.331,6.456 6.552,16.161 3.252,-4.494 4.613,-12.701 4.613,-17.733 0,-5.21 3.433,-11.262 6.867,-11.469 -3.061,5.045 0.793,9.37 4.219,20.099 1.285,4.03 1.121,10.812 2.113,15.113 C 63.797,33.534 65.333,20.5 71,16 c -2.5,5.667 0.37,12.758 2.333,16.167 3.167,5.5 5.087,9.667 5.087,17.548 0,5.284 -1.951,10.259 -5.242,14.148 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z"); } + 0% { + d: path( + 'M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 8.712,-21.285 5.569,-29.308 3.059,0.249 6.331,6.456 6.552,16.161 3.252,-4.494 4.613,-12.701 4.613,-17.733 0,-5.21 3.433,-11.262 6.867,-11.469 -3.061,5.045 0.793,9.37 4.219,20.099 1.285,4.03 1.121,10.812 2.113,15.113 C 63.797,33.534 65.333,20.5 71,16 c -2.5,5.667 0.37,12.758 2.333,16.167 3.167,5.5 5.087,9.667 5.087,17.548 0,5.284 -1.951,10.259 -5.242,14.148 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z' + ); + } + 50% { + d: path( + 'M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 1.640181,-23.047128 7.294982,-29.291475 C 39.391377,29.509803 45.435,26.752 45.656,36.457 c 3.252,-4.494 7.100362,-8.366957 7.100362,-13.398957 0,-5.21 0.137393,-8.650513 -3.479689,-15.0672265 7.834063,1.6180944 8.448052,4.2381285 11.874052,14.9671285 1.285,4.03 1.325275,15.208055 2.317275,19.509055 0.329,-8.933 6.441001,-14.01461 5.163951,-21.391003 5.755224,5.771457 4.934508,10.495521 7.126537,14.288218 3.167,5.5 2.382625,7.496239 2.382625,15.377239 0,5.284 -1.672113,9.232546 -4.963113,13.121546 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z' + ); + } + 100% { + d: path( + 'M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 8.712,-21.285 5.569,-29.308 3.059,0.249 6.331,6.456 6.552,16.161 3.252,-4.494 4.613,-12.701 4.613,-17.733 0,-5.21 3.433,-11.262 6.867,-11.469 -3.061,5.045 0.793,9.37 4.219,20.099 1.285,4.03 1.121,10.812 2.113,15.113 C 63.797,33.534 65.333,20.5 71,16 c -2.5,5.667 0.37,12.758 2.333,16.167 3.167,5.5 5.087,9.667 5.087,17.548 0,5.284 -1.951,10.259 -5.242,14.148 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z' + ); + } } diff --git a/web/ui/react-app/src/themes/dark.scss b/web/ui/react-app/src/themes/dark.scss index 0989c833ca..2212028947 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-700; +$histogram-chart-grid-color: $gray-600; +$histogram-chart-hover-color: $gray-400; + .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 8e6b10e804..c6f8a68d88 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-600; +$histogram-chart-hover-color: $gray-400; + .bootstrap { @import './shared'; }