forked from cockroachdb/cockroach
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ui: move shared graph components to cluster-ui
Part of cockroachdb#74516 This commit moves shared graph functions and components to cluster-ui package. This is to enable the new bar chart component to share axes utilities and containers with the older line graph component in db-console. Release note: None
- Loading branch information
Showing
27 changed files
with
666 additions
and
775 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Copyright 2022 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.txt. | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0, included in the file | ||
// licenses/APL.txt. | ||
|
||
export * from "./visualization"; | ||
export * from "./utils/domain"; |
342 changes: 342 additions & 0 deletions
342
pkg/ui/workspaces/cluster-ui/src/graphs/utils/domain.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
// Copyright 2022 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.txt. | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0, included in the file | ||
// licenses/APL.txt. | ||
|
||
import _ from "lodash"; | ||
import moment from "moment"; | ||
|
||
import { | ||
BytesFitScale, | ||
ComputeByteScale, | ||
ComputeDurationScale, | ||
DurationFitScale, | ||
} from "src/util/format"; | ||
|
||
/** | ||
* AxisUnits is an enumeration used to specify the type of units being displayed | ||
* on an Axis. | ||
*/ | ||
export enum AxisUnits { | ||
/** | ||
* Units are a simple count. | ||
*/ | ||
Count, | ||
/** | ||
* Units are a count of bytes. | ||
*/ | ||
Bytes, | ||
/** | ||
* Units are durations expressed in nanoseconds. | ||
*/ | ||
Duration, | ||
/** | ||
* Units are percentages expressed as fractional values of 1 (1.0 = 100%). | ||
*/ | ||
Percentage, | ||
} | ||
|
||
// The number of ticks to display on a Y axis. | ||
const Y_AXIS_TICK_COUNT = 3; | ||
|
||
// The number of ticks to display on an X axis. | ||
const X_AXIS_TICK_COUNT = 10; | ||
|
||
// A tuple of numbers for the minimum and maximum values of an axis. | ||
export type Extent = [number, number]; | ||
|
||
/** | ||
* AxisDomain is a class that describes the domain of a graph axis; this | ||
* includes the minimum/maximum extend, tick values, and formatting information | ||
* for axis values as displayed in various contexts. | ||
*/ | ||
export class AxisDomain { | ||
// the values at the ends of the axis. | ||
extent: Extent; | ||
// numbers at which an intermediate tick should be displayed on the axis. | ||
ticks: number[] = [0, 1]; | ||
// label returns the label for the axis. | ||
label = ""; | ||
// tickFormat returns a function used to format the tick values for display. | ||
tickFormat: (n: number) => string = n => n.toString(); | ||
// guideFormat returns a function used to format the axis values in the | ||
// chart's interactive guideline. | ||
guideFormat: (n: number) => string = n => n.toString(); | ||
|
||
// constructs a new AxisDomain with the given minimum and maximum value, with | ||
// ticks placed at intervals of the given increment in between the min and | ||
// max. Ticks are always "aligned" to values that are even multiples of | ||
// increment. Min and max are also aligned by default - the aligned min will | ||
// be <= the provided min, while the aligned max will be >= the provided max. | ||
constructor(extent: Extent, increment: number, alignMinMax = true) { | ||
let min = extent[0]; | ||
let max = extent[1]; | ||
if (alignMinMax) { | ||
min = min - (min % increment); | ||
if (max % increment !== 0) { | ||
max = max - (max % increment) + increment; | ||
} | ||
} | ||
|
||
this.extent = [min, max]; | ||
|
||
this.ticks = []; | ||
for ( | ||
let nextTick = min - (min % increment) + increment; | ||
nextTick < this.extent[1]; | ||
nextTick += increment | ||
) { | ||
this.ticks.push(nextTick); | ||
} | ||
} | ||
} | ||
|
||
const countIncrementTable = [ | ||
0.1, | ||
0.2, | ||
0.25, | ||
0.3, | ||
0.4, | ||
0.5, | ||
0.6, | ||
0.7, | ||
0.75, | ||
0.8, | ||
0.9, | ||
1.0, | ||
]; | ||
|
||
// converts a number from raw format to abbreviation k,m,b,t | ||
// e.g. 1500000 -> 1.5m | ||
// @params: num = number to abbreviate, fixed = max number of decimals | ||
const abbreviateNumber = (num: number, fixedDecimals: number) => { | ||
if (num === 0) { | ||
return "0"; | ||
} | ||
|
||
// number of decimal places to show | ||
const fixed = fixedDecimals < 0 ? 0 : fixedDecimals; | ||
|
||
const parts = num.toPrecision(2).split("e"); | ||
|
||
// get power: floor at decimals, ceiling at trillions | ||
const powerIdx = | ||
parts.length === 1 | ||
? 0 | ||
: Math.floor(Math.min(Number(parts[1].slice(1)), 14) / 3); | ||
|
||
// then divide by power | ||
let numAbbrev = Number( | ||
powerIdx < 1 | ||
? num.toFixed(fixed) | ||
: (num / Math.pow(10, powerIdx * 3)).toFixed(fixed), | ||
); | ||
|
||
numAbbrev = numAbbrev < 0 ? numAbbrev : Math.abs(numAbbrev); // enforce -0 is 0 | ||
|
||
const abbreviatedString = numAbbrev + ["", "k", "m", "b", "t"][powerIdx]; // append power | ||
|
||
return abbreviatedString; | ||
}; | ||
|
||
const formatPercentage = (n: number, fractionDigits: number) => { | ||
return `${(n * 100).toFixed(fractionDigits)}%`; | ||
}; | ||
|
||
// computeNormalizedIncrement computes a human-friendly increment between tick | ||
// values on an axis with a range of the given size. The provided size is taken | ||
// to be the minimum range needed to display all values present on the axis. | ||
// The increment is computed by dividing this minimum range into the correct | ||
// number of segments for the supplied tick count, and then increasing this | ||
// increment to the nearest human-friendly increment. | ||
// | ||
// "Human-friendly" increments are taken from the supplied countIncrementTable, | ||
// which should include decimal values between 0 and 1. | ||
function computeNormalizedIncrement( | ||
range: number, | ||
incrementTbl: number[] = countIncrementTable, | ||
) { | ||
if (range === 0) { | ||
throw new Error("cannot compute tick increment with zero range"); | ||
} | ||
|
||
let rawIncrement = range / (Y_AXIS_TICK_COUNT + 1); | ||
// Compute X such that 0 <= rawIncrement/10^x <= 1 | ||
let x = 0; | ||
while (rawIncrement > 1) { | ||
x++; | ||
rawIncrement = rawIncrement / 10; | ||
} | ||
const normalizedIncrementIdx = _.sortedIndex(incrementTbl, rawIncrement); | ||
return incrementTbl[normalizedIncrementIdx] * Math.pow(10, x); | ||
} | ||
|
||
function computeAxisDomain(extent: Extent, factor = 1): AxisDomain { | ||
const range = extent[1] - extent[0]; | ||
|
||
// Compute increment on min/max after conversion to the appropriate prefix unit. | ||
const increment = computeNormalizedIncrement(range / factor); | ||
|
||
// Create axis domain by multiplying computed increment by prefix factor. | ||
const axisDomain = new AxisDomain(extent, increment * factor); | ||
|
||
// If the tick increment is fractional (e.g. 0.2), we display a decimal | ||
// point. For non-fractional increments, we display with no decimal points | ||
// but with a metric prefix for large numbers (i.e. 1000 will display as "1k") | ||
let unitFormat: (v: number) => string; | ||
if (Math.floor(increment) !== increment) { | ||
unitFormat = (n: number) => n.toFixed(1); | ||
} else { | ||
unitFormat = (n: number) => abbreviateNumber(n, 4); | ||
} | ||
axisDomain.tickFormat = (v: number) => unitFormat(v / factor); | ||
|
||
return axisDomain; | ||
} | ||
|
||
function ComputeCountAxisDomain(extent: Extent): AxisDomain { | ||
const axisDomain = computeAxisDomain(extent); | ||
|
||
// For numbers larger than 1, the tooltip displays fractional values with | ||
// metric multiplicative prefixes (e.g. kilo, mega, giga). For numbers smaller | ||
// than 1, we simply display the fractional value without converting to a | ||
// fractional metric prefix; this is because the use of fractional metric | ||
// prefixes (i.e. milli, micro, nano) have proved confusing to users. | ||
const metricFormat = (n: number) => abbreviateNumber(n, 4); | ||
const decimalFormat = (n: number) => n.toFixed(4); | ||
axisDomain.guideFormat = (n: number) => { | ||
if (n < 1) { | ||
return decimalFormat(n); | ||
} | ||
return metricFormat(n); | ||
}; | ||
|
||
return axisDomain; | ||
} | ||
|
||
export function ComputeByteAxisDomain(extent: Extent): AxisDomain { | ||
// Compute an appropriate unit for the maximum value to be displayed. | ||
const scale = ComputeByteScale(extent[1]); | ||
const prefixFactor = scale.value; | ||
|
||
const axisDomain = computeAxisDomain(extent, prefixFactor); | ||
|
||
axisDomain.label = scale.units; | ||
|
||
axisDomain.guideFormat = BytesFitScale(scale.units); | ||
return axisDomain; | ||
} | ||
|
||
function ComputeDurationAxisDomain(extent: Extent): AxisDomain { | ||
const scale = ComputeDurationScale(extent[1]); | ||
const prefixFactor = scale.value; | ||
|
||
const axisDomain = computeAxisDomain(extent, prefixFactor); | ||
|
||
axisDomain.label = scale.units; | ||
|
||
axisDomain.guideFormat = DurationFitScale(scale.units); | ||
return axisDomain; | ||
} | ||
|
||
const percentIncrementTable = [0.25, 0.5, 0.75, 1.0]; | ||
|
||
function ComputePercentageAxisDomain(min: number, max: number) { | ||
const range = max - min; | ||
const increment = computeNormalizedIncrement(range, percentIncrementTable); | ||
const axisDomain = new AxisDomain([min, max], increment); | ||
axisDomain.label = "percentage"; | ||
axisDomain.tickFormat = (n: number) => formatPercentage(n, 1); | ||
axisDomain.guideFormat = (n: number) => formatPercentage(n, 2); | ||
return axisDomain; | ||
} | ||
|
||
const timeIncrementDurations = [ | ||
moment.duration(1, "m"), | ||
moment.duration(5, "m"), | ||
moment.duration(10, "m"), | ||
moment.duration(15, "m"), | ||
moment.duration(30, "m"), | ||
moment.duration(1, "h"), | ||
moment.duration(2, "h"), | ||
moment.duration(3, "h"), | ||
moment.duration(6, "h"), | ||
moment.duration(12, "h"), | ||
moment.duration(24, "h"), | ||
moment.duration(1, "week"), | ||
]; | ||
const timeIncrements: number[] = timeIncrementDurations.map(inc => | ||
inc.asMilliseconds(), | ||
); | ||
|
||
export function formatTimeStamp(timeMillis: number): string { | ||
return moment.utc(timeMillis).format("HH:mm:ss on MMM Do, YYYY"); | ||
} | ||
|
||
function ComputeTimeAxisDomain(extent: Extent): AxisDomain { | ||
// Compute increment; for time scales, this is taken from a table of allowed | ||
// values. | ||
let increment = 0; | ||
{ | ||
const rawIncrement = (extent[1] - extent[0]) / (X_AXIS_TICK_COUNT + 1); | ||
// Compute X such that 0 <= rawIncrement/10^x <= 1 | ||
const tbl = timeIncrements; | ||
let normalizedIncrementIdx = _.sortedIndex(tbl, rawIncrement); | ||
if (normalizedIncrementIdx === tbl.length) { | ||
normalizedIncrementIdx--; | ||
} | ||
increment = tbl[normalizedIncrementIdx]; | ||
} | ||
|
||
// Do not normalize min/max for time axis. | ||
const axisDomain = new AxisDomain(extent, increment, false); | ||
|
||
axisDomain.label = "time"; | ||
|
||
let tickDateFormatter: (d: Date) => string; | ||
if (increment < moment.duration(24, "hours").asMilliseconds()) { | ||
tickDateFormatter = (d: Date) => moment.utc(d).format("H:mm"); | ||
} else { | ||
tickDateFormatter = (d: Date) => moment.utc(d).format("MM/DD H:mm"); | ||
} | ||
axisDomain.tickFormat = (n: number) => { | ||
return tickDateFormatter(new Date(n)); | ||
}; | ||
|
||
axisDomain.guideFormat = formatTimeStamp; | ||
return axisDomain; | ||
} | ||
|
||
export function calculateYAxisDomain( | ||
axisUnits: AxisUnits, | ||
data: number[], | ||
): AxisDomain { | ||
const allDatapoints = data.concat([0, 1]); | ||
const yExtent = [ | ||
Math.min(...allDatapoints), | ||
Math.max(...allDatapoints), | ||
] as Extent; | ||
|
||
switch (axisUnits) { | ||
case AxisUnits.Bytes: | ||
return ComputeByteAxisDomain(yExtent); | ||
case AxisUnits.Duration: | ||
return ComputeDurationAxisDomain(yExtent); | ||
case AxisUnits.Percentage: | ||
return ComputePercentageAxisDomain(yExtent[0], yExtent[1]); | ||
default: | ||
return ComputeCountAxisDomain(yExtent); | ||
} | ||
} | ||
|
||
export function calculateXAxisDomain( | ||
startMillis: number, | ||
endMillis: number, | ||
): AxisDomain { | ||
return ComputeTimeAxisDomain([startMillis, endMillis] as Extent); | ||
} |
Oops, something went wrong.