Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 47 additions & 45 deletions jest/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,56 +12,58 @@ class ResizeObserver {
disconnect() {}
}

window.ResizeObserver = ResizeObserver
if (typeof window !== "undefined") {
window.ResizeObserver = ResizeObserver

Element.prototype.getBoundingClientRect = jest.fn(() => {
return {
width: 120,
height: 120,
top: 0,
left: 0,
bottom: 120,
right: 120,
x: 0,
y: 0,
}
})
Element.prototype.getBoundingClientRect = jest.fn(() => {
return {
width: 120,
height: 120,
top: 0,
left: 0,
bottom: 120,
right: 120,
x: 0,
y: 0,
}
})

Object.defineProperties(HTMLElement.prototype, {
offsetHeight: {
get() {
return parseFloat(this.style.height) || 500
Object.defineProperties(HTMLElement.prototype, {
offsetHeight: {
get() {
return parseFloat(this.style.height) || 500
},
configurable: true,
},
configurable: true,
},
offsetWidth: {
get() {
return parseFloat(this.style.width) || 500
offsetWidth: {
get() {
return parseFloat(this.style.width) || 500
},
configurable: true,
},
configurable: true,
},
scrollHeight: {
get() {
return parseFloat(this.style.minHeight) || parseFloat(this.style.height) || 500
scrollHeight: {
get() {
return parseFloat(this.style.minHeight) || parseFloat(this.style.height) || 500
},
configurable: true,
},
configurable: true,
},
scrollWidth: {
get() {
return parseFloat(this.style.width) || 500
scrollWidth: {
get() {
return parseFloat(this.style.width) || 500
},
configurable: true,
},
configurable: true,
},
clientHeight: {
get() {
return parseFloat(this.style.height) || 500
clientHeight: {
get() {
return parseFloat(this.style.height) || 500
},
configurable: true,
},
configurable: true,
},
clientWidth: {
get() {
return parseFloat(this.style.width) || 500
clientWidth: {
get() {
return parseFloat(this.style.width) || 500
},
configurable: true,
},
configurable: true,
},
})
})
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@netdata/charts",
"version": "6.12.6",
"version": "6.12.7",
"description": "Netdata frontend SDK and chart utilities",
"main": "dist/index.js",
"module": "dist/es6/index.js",
Expand Down
4 changes: 2 additions & 2 deletions src/components/drawer/compare/statValue.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react"
import { Flex, Text, TextMicro, TextSmall } from "@netdata/netdata-ui"
import { stripRateUnit } from "@/helpers/units"
import { useValueWithUnit } from "@/components/provider"

const getDisplayUnit = (unit, integrated) =>
integrated && typeof unit === "string" && unit.endsWith("/s") ? unit.slice(0, -2) : unit
const getDisplayUnit = (unit, integrated) => (integrated ? stripRateUnit(unit) : unit)

const StatValue = ({ value, valueKey, prominent, justifyContent }) => {
const { convertedValue, convertedUnit } = useValueWithUnit(value, {
Expand Down
20 changes: 10 additions & 10 deletions src/components/drawer/correlate/sparklineCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ const defaultWidth = 120
const defaultHeight = 24
const linePadding = 4

export const getSparklinePoints = (values, width, height) => {
export const getSparklinePoints = (values, width, height, range = {}) => {
const finiteValues = values.filter(Number.isFinite)
if (!finiteValues.length) return []

const min = Math.min(...finiteValues)
const max = Math.max(...finiteValues)
const range = max - min
const min = Number.isFinite(range.min) ? range.min : Math.min(...finiteValues)
const max = Number.isFinite(range.max) ? range.max : Math.max(...finiteValues)
const span = max - min
const xRange = Math.max(width - linePadding * 2, 1)
const yRange = Math.max(height - linePadding * 2, 1)
const divisor = Math.max(values.length - 1, 1)
Expand All @@ -21,13 +21,13 @@ export const getSparklinePoints = (values, width, height) => {

return {
x: linePadding + (index / divisor) * xRange,
y: range ? linePadding + ((max - value) / range) * yRange : height / 2,
y: span ? linePadding + ((max - value) / span) * yRange : height / 2,
}
})
}

export const drawSparkline = (context, values, width, height, color) => {
const points = getSparklinePoints(values, width, height)
export const drawSparkline = (context, values, width, height, color, range) => {
const points = getSparklinePoints(values, width, height, range)
context.clearRect(0, 0, width, height)
if (!points.length) return

Expand All @@ -52,7 +52,7 @@ export const drawSparkline = (context, values, width, height, color) => {
context.stroke()
}

const SparklineCanvas = ({ values, color, height = defaultHeight }) => {
const SparklineCanvas = ({ values, color, height = defaultHeight, range }) => {
const canvasRef = useRef(null)
const frameRef = useRef(null)

Expand All @@ -72,8 +72,8 @@ const SparklineCanvas = ({ values, color, height = defaultHeight }) => {
if (!context) return

context.setTransform(ratio, 0, 0, ratio, 0, 0)
drawSparkline(context, values, width, height, color)
}, [color, height, values])
drawSparkline(context, values, width, height, color, range)
}, [color, height, range, values])

useLayoutEffect(() => {
draw()
Expand Down
11 changes: 11 additions & 0 deletions src/components/drawer/correlate/sparklineCanvas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,15 @@ describe("getSparklinePoints", () => {
])
expect(getSparklinePoints([null, NaN], 100, 20)).toEqual([])
})

it("supports a shared range without changing existing callers", () => {
expect(getSparklinePoints([0, 50], 100, 24, { min: 0, max: 100 })).toEqual([
{ x: 4, y: 20 },
{ x: 96, y: 12 },
])
expect(getSparklinePoints([0, 50], 100, 24)).toEqual([
{ x: 4, y: 20 },
{ x: 96, y: 4 },
])
})
})
25 changes: 21 additions & 4 deletions src/components/drawer/correlate/sparklineData.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getAlias } from "@/helpers/units"
import { getPointValue } from "@/sdk/makeChart/getPointValue"
import { normalizeDataQueryUnits } from "@/sdk/dataQuery/response"

const maxConcurrentRequests = 4
const maxCachedResponses = 100
Expand All @@ -13,15 +14,19 @@ const requestAttributeKeys = [
"before",
"context",
"contextScope",
"dimensionsScope",
"eliminateZeroDimensions",
"groupBy",
"groupByLabel",
"groupingMethod",
"groupingTime",
"host",
"liveAnchor",
"limit",
"nodesScope",
"nulls2zero",
"points",
"postAggregationMethod",
"postGroupBy",
"postGroupByLabel",
"renderedAt",
Expand All @@ -31,6 +36,11 @@ const requestAttributeKeys = [
"selectedLabels",
"selectedNodes",
"showPostAggregations",
"sparklineRateVolume",
"tier",
"timeout",
"timeGroupOptions",
"unaligned",
]

const states = new WeakMap()
Expand Down Expand Up @@ -120,13 +130,15 @@ export const getSparklineBatchAttributes = (chart, dimensions, overrides = {}) =
}
}

export const normalizeSparklinePayload = rawPayload => {
export const normalizeSparklinePayload = (rawPayload, { rateVolume = false, timeGroup } = {}) => {
const result = rawPayload?.result
if (!result || !Array.isArray(result.labels) || !Array.isArray(result.data))
throw new Error("Invalid sparkline response")

const dimensionUnits = rawPayload.view?.dimensions?.units || []
const defaultUnit = rawPayload.view?.units || rawPayload.db?.units || ""
const normalizedUnits = normalizeDataQueryUnits(rawPayload, { rateVolume, timeGroup })
if (!normalizedUnits.available) throw new Error("Unsupported sparkline units")
const dimensionUnits = normalizedUnits.units
const defaultUnit = dimensionUnits[0] || rawPayload.db?.units || ""
const seriesByDimension = new Map()

for (let column = 1; column < result.labels.length; column++) {
Expand Down Expand Up @@ -208,7 +220,12 @@ const drainQueue = state => {
attrs: entry.attrs,
signal: entry.controller.signal,
})
.then(normalizeSparklinePayload)
.then(payload =>
normalizeSparklinePayload(payload, {
rateVolume: entry.attrs.sparklineRateVolume,
timeGroup: entry.attrs.groupingMethod,
})
)
.then(value => {
entry.status = "fulfilled"
entry.resolve(value)
Expand Down
86 changes: 86 additions & 0 deletions src/components/drawer/correlate/sparklineData.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getSparklineBatchAttributes,
getSparklineBatchDimensions,
getSparklineDataFetcher,
getSparklineRequestKey,
normalizeSparklinePayload,
sparklineRequestLimits,
} from "./sparklineData"
Expand Down Expand Up @@ -57,6 +58,35 @@ const makeDimension = (index, overrides = {}) => ({
const makeAttrs = (owner, index) => getSparklineBatchAttributes(owner, [makeDimension(index)])

describe("sparkline batch requests", () => {
it("keys every attribute that can change a fleet trend response", () => {
const attrs = {
contextScope: ["context"],
dimensionsScope: ["dimension"],
limit: 1,
nulls2zero: false,
tier: 0,
timeGroupOptions: ["absolute"],
unaligned: true,
}

expect(getSparklineRequestKey(attrs)).not.toBe(
getSparklineRequestKey({
...attrs,
dimensionsScope: ["other-dimension"],
})
)
expect(getSparklineRequestKey(attrs)).not.toBe(
getSparklineRequestKey({
...attrs,
timeGroupOptions: ["percentage"],
})
)
expect(getSparklineRequestKey(attrs)).not.toBe(getSparklineRequestKey({ ...attrs, limit: 2 }))
expect(getSparklineRequestKey(attrs)).not.toBe(
getSparklineRequestKey({ ...attrs, nulls2zero: true })
)
})

it("builds deterministic bounded batches for one context and node", () => {
const dimensions = Array.from({ length: 120 }, (_, index) => makeDimension(index)).reverse()
dimensions.push(makeDimension(200, { context: "other-context" }))
Expand Down Expand Up @@ -131,6 +161,62 @@ describe("normalizeSparklinePayload", () => {
it("rejects responses without chart labels and data", () => {
expect(() => normalizeSparklinePayload({})).toThrow("Invalid sparkline response")
})

it("falls back to the first dimension unit for columns beyond heterogeneous units", () => {
const payload = {
view: {
units: "percentage",
dimensions: { units: ["microseconds", "bytes"] },
},
result: {
labels: ["time", "latency", "traffic", "extra"],
point: { value: 0, anomalyRate: 1 },
data: [
[1000, [1, 0], [2, 0], [3, 0]],
[1005, [3, 0], [8, 0], [9, 0]],
],
},
}

const result = normalizeSparklinePayload(payload)

expect(result.get("latency").unit).toBe("us")
expect(result.get("traffic").unit).toBe("By")
expect(result.get("extra").unit).toBe("us")
})

it("rejects a Volume trend when rate and non-rate dimension units are mixed", () => {
expect(() =>
normalizeSparklinePayload(
makePayload(["latency", "traffic"], { units: ["MiB/s", "requests"] }),
{ rateVolume: true, timeGroup: "sum" }
)
).toThrow("Unsupported sparkline units")
})

it("rejects a Volume trend when the time grouping is not a sum", () => {
expect(() =>
normalizeSparklinePayload(makePayload(["traffic"], { units: ["MiB/s"] }), {
rateVolume: true,
timeGroup: "average",
})
).toThrow("Unsupported sparkline units")
})

it("normalizes canonical rate units for a Volume trend and rejects unknown units", () => {
expect(
normalizeSparklinePayload(makePayload(["traffic"], { units: ["MiB/s"] }), {
rateVolume: true,
timeGroup: "sum",
}).get("traffic").unit
).toBe("MiBy")
expect(() =>
normalizeSparklinePayload(makePayload(["traffic"], { units: [""] }), {
rateVolume: true,
timeGroup: "sum",
})
).toThrow("Unsupported sparkline units")
})
})

describe("getSparklineDataFetcher", () => {
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/units/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const unitsMissing = u => {
return typeof allUnits.units[alias] === "undefined"
}

export const isRateUnit = unit => typeof unit === "string" && unit.endsWith("/s")

export const stripRateUnit = unit => (isRateUnit(unit) ? unit.slice(0, -2) : unit)

const unitOrEmpty = u => (u === null || typeof u === "undefined" ? "" : u)

export const getUnitConfig = u => {
Expand Down
Loading