From 47436fa4c1429b868eff5e0a7e30dd02b25cdc0d Mon Sep 17 00:00:00 2001 From: Marylia Gutierrez Date: Wed, 1 Jun 2022 18:48:14 -0400 Subject: [PATCH] ui: fixed on bar chart component This commit: - Updates z-index of legend, so it doesn't get on top of tooltip - Fixes the case when there is a single bar (previously not loading) - Tooltip shows the value with unit - The tooltip fixes to 1 decimal points when the value is a decimal - Adds new stories for single bar and chart with scale of `Duration` and `Bytes` Release note: None --- .../src/graphs/bargraph/barGraph.stories.tsx | 52 +++++++++++++++++-- .../src/graphs/bargraph/bargraph.module.scss | 2 +- .../cluster-ui/src/graphs/bargraph/bars.ts | 10 ++-- .../cluster-ui/src/graphs/bargraph/index.tsx | 6 +-- .../cluster-ui/src/graphs/bargraph/plugins.ts | 34 +++++++++--- .../workspaces/cluster-ui/src/util/format.ts | 33 ++++++++++-- pkg/ui/yarn-vendor | 2 +- 7 files changed, 116 insertions(+), 23 deletions(-) diff --git a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/barGraph.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/barGraph.stories.tsx index 070a2027b8b5..1a3071a62876 100644 --- a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/barGraph.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/barGraph.stories.tsx @@ -30,8 +30,15 @@ function genValuesInRange(range: [number, number], length: number): number[] { const mockData: AlignedData = [ generateTimestampsMillis(1546300800, 20), genValuesInRange([0, 100], 20), - genValuesInRange([0, 100], 20), - genValuesInRange([0, 100], 20), + genValuesInRange([0, 10000], 20), + genValuesInRange([0, 100000], 20), +]; +const mockDataSingle: AlignedData = [[1654115121], [0], [1], [2]]; +const mockDataDuration: AlignedData = [ + generateTimestampsMillis(1546300800, 20), + genValuesInRange([0, 1e7], 20), + genValuesInRange([0, 1e7], 20), + genValuesInRange([0, 1e7], 20), ]; const mockOpts: Partial = { @@ -58,7 +65,7 @@ storiesOf("BarGraphTimeSeries", module) alignedData={mockData} uPlotOptions={mockOpts} tooltip={ -
This is an example stacked bar graph axis unit = count.
+
This is an example stacked bar graph axis unit = Count.
} yAxisUnits={AxisUnits.Count} /> @@ -85,4 +92,43 @@ storiesOf("BarGraphTimeSeries", module) yAxisUnits={AxisUnits.Percentage} /> ); + }) + .add("with single stacked multi-series", () => { + return ( + This is an example stacked bar graph axis unit = Count. + } + yAxisUnits={AxisUnits.Count} + /> + ); + }) + .add("with duration stacked multi-series", () => { + return ( + This is an example stacked bar graph axis unit = Duration. + } + yAxisUnits={AxisUnits.Duration} + /> + ); + }) + .add("with bytes stacked multi-series", () => { + return ( + This is an example stacked bar graph axis unit = Bytes. + } + yAxisUnits={AxisUnits.Bytes} + /> + ); }); diff --git a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/bargraph.module.scss b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/bargraph.module.scss index da0982dda519..7562b5d96ee7 100644 --- a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/bargraph.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/bargraph.module.scss @@ -11,7 +11,7 @@ text-align: left; font-size: 10px; margin-top: 20px; - z-index: 100; + z-index: 1; width: fit-content; padding: 10px; } diff --git a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/bars.ts b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/bars.ts index dc9efd70d3fd..14e4f0c9b410 100644 --- a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/bars.ts +++ b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/bars.ts @@ -10,10 +10,7 @@ import { merge } from "lodash"; import uPlot, { Options, Band, AlignedData } from "uplot"; -import { - // AxisUnits, - AxisDomain, -} from "../utils/domain"; +import { AxisUnits, AxisDomain } from "../utils/domain"; import { barTooltipPlugin } from "./plugins"; const seriesPalette = [ @@ -89,12 +86,14 @@ export const getStackedBarOpts = ( userOptions: Partial, xAxisDomain: AxisDomain, yAxisDomain: AxisDomain, + yyAxisUnits: AxisUnits, colourPalette = seriesPalette, ): Options => { const options = getBarChartOpts( userOptions, xAxisDomain, yAxisDomain, + yyAxisUnits, colourPalette, ); @@ -140,6 +139,7 @@ export const getBarChartOpts = ( userOptions: Partial, xAxisDomain: AxisDomain, yAxisDomain: AxisDomain, + yAxisUnits: AxisUnits, colourPalette = seriesPalette, ): Options => { const { series, ...providedOpts } = userOptions; @@ -188,7 +188,7 @@ export const getBarChartOpts = ( ...s, })), ], - plugins: [barTooltipPlugin()], + plugins: [barTooltipPlugin(yAxisUnits)], }; const combinedOpts = merge(opts, providedOpts); diff --git a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/index.tsx b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/index.tsx index 9f3e974e4bea..12280349ef89 100644 --- a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/index.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/index.tsx @@ -44,9 +44,8 @@ export const BarGraphTimeSeries: React.FC = ({ yAxisUnits, }) => { const graphRef = useRef(null); - const samplingIntervalMillis = alignedData[0].length - ? alignedData[0][1] - alignedData[0][0] - : 0; + const samplingIntervalMillis = + alignedData[0].length > 1 ? alignedData[0][1] - alignedData[0][0] : 1e3; useEffect(() => { if (!alignedData) return; @@ -68,6 +67,7 @@ export const BarGraphTimeSeries: React.FC = ({ uPlotOptions, xAxisDomain, yAxisDomain, + yAxisUnits, colourPalette, ); diff --git a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/plugins.ts b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/plugins.ts index 72845e4c26b1..aad3690adeed 100644 --- a/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/plugins.ts +++ b/pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/plugins.ts @@ -9,14 +9,19 @@ // licenses/APL.txt. import uPlot, { Plugin } from "uplot"; -import { formatTimeStamp } from "../utils/domain"; +import { AxisUnits, formatTimeStamp } from "../utils/domain"; +import { Bytes, Duration, Percentage, Count } from "../../util"; // Fallback color for series stroke if one is not defined. const DEFAULT_STROKE = "#7e89a9"; // Generate a series legend within the provided div showing the data points // relative to the cursor position. -const generateSeriesLegend = (uPlot: uPlot, seriesLegend: HTMLDivElement) => { +const generateSeriesLegend = ( + uPlot: uPlot, + seriesLegend: HTMLDivElement, + yAxisUnits: AxisUnits, +) => { // idx is the closest data index to the cursor position. const { idx } = uPlot.cursor; @@ -62,8 +67,11 @@ const generateSeriesLegend = (uPlot: uPlot, seriesLegend: HTMLDivElement) => { value.style.fontFamily = "'Source Sans Pro', sans-serif"; value.textContent = series.value instanceof Function && dataValue - ? String(series.value(uPlot, dataValue, index, idx)) - : String(dataValue); + ? getFormattedValue( + Number(series.value(uPlot, dataValue, index, idx)), + yAxisUnits, + ) + : getFormattedValue(dataValue, yAxisUnits); container.appendChild(colorBox); container.appendChild(label); @@ -73,8 +81,22 @@ const generateSeriesLegend = (uPlot: uPlot, seriesLegend: HTMLDivElement) => { }); }; +// Formats the value according to its unit. +function getFormattedValue(value: number, yAxisUnits: AxisUnits): string { + switch (yAxisUnits) { + case AxisUnits.Bytes: + return Bytes(value); + case AxisUnits.Duration: + return Duration(value); + case AxisUnits.Percentage: + return Percentage(value, 1); + default: + return Count(value); + } +} + // Tooltip legend plugin for bar charts. -export function barTooltipPlugin(): Plugin { +export function barTooltipPlugin(yAxis: AxisUnits): Plugin { const cursorToolTip = { tooltip: document.createElement("div"), timeStamp: document.createElement("div"), @@ -91,7 +113,7 @@ export function barTooltipPlugin(): Plugin { timeStamp.textContent = formatTimeStamp(closestDataPointTimeMillis); // Generating the series legend based on current state of µPlot - generateSeriesLegend(u, seriesLegend); + generateSeriesLegend(u, seriesLegend, yAxis); // set the position of the Tooltip. Adjusting the tooltip away from the // cursor for readability. diff --git a/pkg/ui/workspaces/cluster-ui/src/util/format.ts b/pkg/ui/workspaces/cluster-ui/src/util/format.ts index b5fb75862ae6..5fa3feeae042 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/format.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/format.ts @@ -23,6 +23,7 @@ export const byteUnits: string[] = [ "YiB", ]; export const durationUnits: string[] = ["ns", "µs", "ms", "s"]; +export const countUnits: string[] = ["", "k", "m", "b"]; interface UnitValue { value: number; @@ -38,7 +39,7 @@ export function ComputePrefixExponent( value: number, prefixMultiple: number, prefixList: string[], -) { +): number { // Compute the metric prefix that will be used to label the axis. let maxUnits = Math.abs(value); let prefixScale: number; @@ -102,7 +103,7 @@ export function Bytes(bytes: number): string { * Cast bytes to provided scale units */ // tslint:disable-next-line: variable-name -export const BytesFitScale = (scale: string) => (bytes: number) => { +export const BytesFitScale = (scale: string) => (bytes: number): string => { if (!bytes) { return `0.00 ${scale}`; } @@ -156,7 +157,9 @@ export function Duration(nanoseconds: number): string { * Cast nanoseconds to provided scale units */ // tslint:disable-next-line: variable-name -export const DurationFitScale = (scale: string) => (nanoseconds: number) => { +export const DurationFitScale = (scale: string) => ( + nanoseconds: number, +): string => { if (!nanoseconds) { return `0.00 ${scale}`; } @@ -166,7 +169,7 @@ export const DurationFitScale = (scale: string) => (nanoseconds: number) => { export const DATE_FORMAT = "MMM DD, YYYY [at] H:mm"; -export function RenderCount(yesCount: Long, totalCount: Long) { +export function RenderCount(yesCount: Long, totalCount: Long): string { if (longToInt(yesCount) == 0) { return "No"; } @@ -176,3 +179,25 @@ export function RenderCount(yesCount: Long, totalCount: Long) { const noCount = longToInt(totalCount) - longToInt(yesCount); return `${longToInt(yesCount)} Yes / ${noCount} No`; } + +/** + * ComputeCountScale calculates an appropriate scale factor and unit to use + * to display a given count value, without actually converting the value. + */ +function ComputeCountScale(count: number): UnitValue { + const scale = ComputePrefixExponent(count, 1000, countUnits); + return { + value: Math.pow(1000, scale), + units: countUnits[scale], + }; +} + +/** + * Count creates a string representation for a count. + */ +export function Count(count: number): string { + const scale = ComputeCountScale(count); + const unitVal = count / scale.value; + const fractionDigits = Number.isInteger(unitVal) ? 0 : 1; + return unitVal.toFixed(fractionDigits) + " " + scale.units; +} diff --git a/pkg/ui/yarn-vendor b/pkg/ui/yarn-vendor index a5d175877c47..a65785ab41bb 160000 --- a/pkg/ui/yarn-vendor +++ b/pkg/ui/yarn-vendor @@ -1 +1 @@ -Subproject commit a5d175877c47c808dd460658fb41af599651ddaa +Subproject commit a65785ab41bbb53e67679bd05e7074b57fe691ae