.pragma library
// Everything the widget needs to turn a Grafana dashboard into pictures,
// kept free of QML so it can be exercised from node against a real
// instance (see tools/check.mjs): time ranges, template variables, the
// /api/ds/query body, data frames → series, reducers, units, colours and
// thresholds. The panel components only draw what this hands them.
// ── time ──────────────────────────────────────────────────────────────
var UNIT_MS = { s: 1000, m: 60000, h: 3600000, d: 86400000, w: 604800000, M: 2592000000, y: 31536000000 }
// "now", "now-6h", "now-1d/d", an epoch in ms, or an ISO date → epoch ms
function parseTime(text, now, roundUp) {
if (text === undefined || text === null || text === "") return now
if (typeof text === "number") return text
var s = String(text).trim()
if (/^\d+$/.test(s)) return parseInt(s, 10)
var m = /^now(?:([+-])(\d+)([smhdwMy]))?(?:\/([smhdwMy]))?$/.exec(s)
if (!m) {
var t = Date.parse(s)
return isNaN(t) ? now : t
}
var t2 = now
if (m[1]) t2 += (m[1] === "-" ? -1 : 1) * parseInt(m[2], 10) * UNIT_MS[m[3]]
if (m[4]) t2 = roundTo(t2, m[4], roundUp)
return t2
}
function roundTo(t, unit, up) {
var d = new Date(t)
switch (unit) {
case "y": d.setMonth(0, 1); d.setHours(0, 0, 0, 0); if (up) d.setFullYear(d.getFullYear() + 1); break
case "M": d.setDate(1); d.setHours(0, 0, 0, 0); if (up) d.setMonth(d.getMonth() + 1); break
case "w": d.setDate(d.getDate() - d.getDay()); d.setHours(0, 0, 0, 0); if (up) d.setDate(d.getDate() + 7); break
case "d": d.setHours(0, 0, 0, 0); if (up) d.setDate(d.getDate() + 1); break
case "h": d.setMinutes(0, 0, 0); if (up) d.setHours(d.getHours() + 1); break
case "m": d.setSeconds(0, 0); if (up) d.setMinutes(d.getMinutes() + 1); break
case "s": d.setMilliseconds(0); if (up) d.setSeconds(d.getSeconds() + 1); break
}
return up ? d.getTime() - 1 : d.getTime()
}
function resolveRange(range, now) {
var from = parseTime(range.from || "now-6h", now, false)
var to = parseTime(range.to || "now", now, true)
return { from: from, to: Math.max(to, from + 1000) }
}
var RANGE_LABELS = {
"now-5m": "Last 5 minutes", "now-15m": "Last 15 minutes", "now-30m": "Last 30 minutes",
"now-1h": "Last 1 hour", "now-3h": "Last 3 hours", "now-6h": "Last 6 hours",
"now-12h": "Last 12 hours", "now-24h": "Last 24 hours", "now-2d": "Last 2 days",
"now-7d": "Last 7 days", "now-30d": "Last 30 days", "now-90d": "Last 90 days",
"now/d": "Today so far", "now-1d/d": "Yesterday"
}
function rangeLabel(range) {
if ((range.to || "now") === "now" && RANGE_LABELS[range.from]) return RANGE_LABELS[range.from]
return (range.from || "") + " → " + (range.to || "now")
}
// Grafana's own interval rounding, so step sizes land where it puts them
function roundInterval(ms) {
var steps = [
[15, 10], [35, 20], [75, 50], [150, 100], [350, 200], [750, 500], [1500, 1000],
[3500, 2000], [7500, 5000], [12500, 10000], [17500, 15000], [25000, 20000],
[45000, 30000], [90000, 60000], [210000, 120000], [450000, 300000], [750000, 600000],
[1050000, 900000], [1500000, 1200000], [2700000, 1800000], [5400000, 3600000],
[9000000, 7200000], [16200000, 10800000], [32400000, 21600000], [86400000, 43200000],
[604800000, 86400000], [1814400000, 604800000], [3628800000, 2592000000]
]
for (var i = 0; i < steps.length; i++) if (ms < steps[i][0]) return steps[i][1]
return 31536000000
}
function parseDuration(text) {
var m = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w|M|y)$/.exec(String(text || "").trim())
if (!m) return 0
return parseFloat(m[1]) * (m[2] === "ms" ? 1 : UNIT_MS[m[2]])
}
function intervalFor(range, maxDataPoints, minInterval) {
var raw = (range.to - range.from) / Math.max(1, maxDataPoints)
return Math.max(roundInterval(raw), parseDuration(minInterval) || 1)
}
// ── template variables & datasources ──────────────────────────────────
// {name: {value, selected, options, all, raw}} from the dashboard's
// templating list. `selected` is what the person picked (`selections`,
// else what the dashboard was saved with); `value` is what goes into a
// query, with "All" expanded. Datasource variables resolve to a uid
// through `datasources` (from /api/frontend/settings); query variables
// take their options from `fetched` (see varRequest/varOptions).
function variables(dashboard, datasources, selections, fetched) {
var out = {}
var list = (dashboard.templating && dashboard.templating.list) || []
for (var i = 0; i < list.length; i++) {
var v = list[i]
var cur = v.current || {}
var selected = selections && selections[v.name] !== undefined ? selections[v.name] : cur.value
if (v.type === "constant") selected = v.query
var options = optionsFor(v, datasources, fetched && fetched[v.name])
if (selected === undefined || selected === null || (Array.isArray(selected) && selected.length === 0)) {
selected = options.length ? options[0].value : ""
}
var value = selected
var all = value === "$__all" || (Array.isArray(value) && value.indexOf("$__all") !== -1)
if (all) {
var vals = options.map(function(o) { return o.value }).filter(function(x) { return x !== "$__all" })
value = v.allValue ? v.allValue : vals.length ? vals : ".*"
}
if (v.type === "datasource") {
var ds = findDatasource(datasources, Array.isArray(value) ? value[0] : value, v.query)
value = ds ? ds.uid : value
}
out[v.name] = { value: value, selected: selected, options: options, all: all, raw: v }
}
return out
}
// [{text, value}] a variable can be set to, "All" first when it has one
function optionsFor(v, datasources, fetched) {
var opts = []
if (v.type === "datasource") {
var re = regexOf(v.regex)
for (var name in datasources || {}) {
var ds = datasources[name]
if (ds.type === v.query && (!re || re.test(name))) opts.push({ text: name, value: name })
}
} else if (v.type === "custom" && !(v.options && v.options.length)) {
// "a, b, c" or "label : value"; a backslash keeps a comma literal
opts = String(v.query || "").replace(/\\,/g, "\u0000").split(",").map(function(s) {
s = s.replace(/\u0000/g, ",").trim()
var kv = /^(.+?)\s:\s(.+)$/.exec(s)
return kv ? { text: kv[1], value: kv[2] } : { text: s, value: s }
}).filter(function(o) { return o.value !== "" })
} else if (v.type === "interval") {
opts = String(v.query || "").split(",").map(function(s) { s = s.trim(); return { text: s, value: s } })
} else if (v.type === "query" && fetched) {
opts = fetched.slice()
} else {
opts = (v.options || []).filter(function(o) { return o.value !== "$__all" })
.map(function(o) { return { text: String(o.text), value: o.value } })
}
if (v.type === "query" && opts.length === 0 && v.current && v.current.value !== undefined && !fetched) {
// not fetched yet: offer what was saved so the picker isn't empty
var saved = [].concat(v.current.value).filter(function(x) { return x !== "$__all" })
var texts = [].concat(v.current.text)
opts = saved.map(function(x, i) { return { text: String(texts[i] !== undefined ? texts[i] : x), value: x } })
}
if (v.includeAll) opts.unshift({ text: "All", value: "$__all" })
return opts
}
function regexOf(text) {
if (!text) return null
var m = /^\/(.*)\/([gimsuy]*)$/.exec(text)
try { return m ? new RegExp(m[1], m[2].replace("g", "")) : new RegExp(text) } catch (e) { return null }
}
// The datasource resource request that lists a Prometheus query variable's
// options, or null for anything this widget can't evaluate. The query may
// reference other variables, so `vars` holds the ones before it.
function varRequest(v, vars, datasources, range) {
if (v.type !== "query") return null
var ref = v.datasource
var refUid = interpolate(typeof ref === "string" ? ref : (ref && ref.uid) || "", vars, false)
var ds = findDatasource(datasources, refUid || null, ref && ref.type)
if (!ds || ds.type !== "prometheus") return null
var q = interpolate(typeof v.query === "string" ? v.query : (v.query && v.query.query) || "", vars, true).trim()
var base = "/api/datasources/uid/" + encodeURIComponent(ds.uid) + "/resources/api/v1/"
var span = "start=" + Math.floor(range.from / 1000) + "&end=" + Math.floor(range.to / 1000)
var m
if ((m = /^label_values\((.+),\s*([\w.]+)\s*\)$/.exec(q))) {
return { kind: "values", path: base + "label/" + encodeURIComponent(m[2]) + "/values?match[]=" + encodeURIComponent(m[1]) + "&" + span }
}
if ((m = /^label_values\(\s*([\w.]+)\s*\)$/.exec(q))) {
return { kind: "values", path: base + "label/" + encodeURIComponent(m[1]) + "/values?" + span }
}
if ((m = /^label_names\((.*)\)$/.exec(q))) {
return { kind: "values", path: base + "labels?" + (m[1].trim() ? "match[]=" + encodeURIComponent(m[1]) + "&" : "") + span }
}
if ((m = /^metrics\((.*)\)$/.exec(q))) {
return { kind: "metrics", filter: m[1].trim(), path: base + "label/__name__/values?" + span }
}
if ((m = /^query_result\((.+)\)$/.exec(q))) {
return { kind: "query", path: base + "query?query=" + encodeURIComponent(m[1]) + "&time=" + Math.floor(range.to / 1000) }
}
return null
}
// [{text, value}] from the answer to varRequest, after the variable's own
// regex (with a capture group picking the value) and sort order.
function varOptions(v, req, response) {
var raw = []
var data = response && response.data
if (req.kind === "values" || req.kind === "metrics") {
raw = (data || []).map(String)
if (req.kind === "metrics" && req.filter) {
var fre = regexOf(req.filter)
if (fre) raw = raw.filter(function(x) { return fre.test(x) })
}
} else if (req.kind === "query") {
raw = ((data && data.result) || []).map(function(r) {
var labels = Object.keys(r.metric || {}).filter(function(k) { return k !== "__name__" })
return (r.metric.__name__ || "") + "{" + labels.map(function(k) { return k + "=\"" + r.metric[k] + "\"" }).join(",")
+ "} " + (r.value ? r.value[1] : "") + " " + (r.value ? Math.round(r.value[0] * 1000) : "")
})
}
var re = regexOf(v.regex)
var seen = {}
var out = []
for (var i = 0; i < raw.length; i++) {
var text = raw[i]
if (re) {
var m = re.exec(text)
if (!m) continue
text = m[1] !== undefined ? m[1] : m[0]
}
if (seen[text]) continue
seen[text] = true
out.push({ text: text, value: text })
}
var sort = v.sort || 0
if (sort === 1 || sort === 2) out.sort(function(a, b) { return a.text.localeCompare(b.text) })
if (sort === 3 || sort === 4) out.sort(function(a, b) { return (parseFloat(a.text) || 0) - (parseFloat(b.text) || 0) })
if (sort === 5 || sort === 6) out.sort(function(a, b) { return a.text.toLowerCase().localeCompare(b.text.toLowerCase()) })
if (sort === 2 || sort === 4 || sort === 6) out.reverse()
return out
}
// the label a picker shows for its current selection
function selectionText(variable) {
var sel = [].concat(variable.selected)
if (sel.indexOf("$__all") !== -1) return "All"
var names = sel.map(function(x) {
for (var i = 0; i < variable.options.length; i++) if (variable.options[i].value === x) return variable.options[i].text
return String(x)
})
return names.length > 2 ? names.slice(0, 2).join(", ") + " +" + (names.length - 2) : names.join(", ")
}
// by uid, then by name; `type` narrows a default when nothing matches
function findDatasource(datasources, ref, type) {
if (!datasources) return null
var list = []
for (var name in datasources) list.push(Object.assign({ name: name }, datasources[name]))
if (ref && ref !== "default") {
for (var i = 0; i < list.length; i++) if (list[i].uid === ref) return list[i]
for (var j = 0; j < list.length; j++) if (list[j].name === ref) return list[j]
}
for (var k = 0; k < list.length; k++) {
if (list[k].isDefault && (!type || list[k].type === type)) return list[k]
}
if (type) for (var n = 0; n < list.length; n++) if (list[n].type === type && list[n].uid) return list[n]
return null
}
function formatValue(value, format, regexDefault) {
if (Array.isArray(value)) {
if (value.length === 1) return formatValue(value[0], format, regexDefault)
switch (format) {
case "csv": return value.join(",")
case "pipe": return value.join("|")
case "json": return JSON.stringify(value)
case "raw": return value.join(",")
default:
return regexDefault ? "(" + value.map(regexEscape).join("|") + ")" : "{" + value.join(",") + "}"
}
}
if (format === "regex" && regexDefault) return regexEscape(String(value))
return value === undefined || value === null ? "" : String(value)
}
// Escaped for a regex inside a PromQL string literal, the way Grafana's
// Prometheus datasource does it: the backslashes themselves need escaping
// for the string, so each one is doubled.
function regexEscape(s) {
return String(s).replace(/\\/g, "\\\\\\\\").replace(/[$^*{}\[\]'+?.()|]/g, "\\\\$&")
}
// $var, ${var}, ${var:format}, [[var]]. Grafana's own $__ macros are left
// for the datasource backend, which interpolates them itself.
function interpolate(text, vars, regexDefault) {
if (typeof text !== "string") return text
return text.replace(/\$\{(\w+)(?::(\w+))?\}|\[\[(\w+)\]\]|\$(\w+)/g, function(whole, a, fmt, b, c) {
var name = a || b || c
if (name.indexOf("__") === 0) return whole
var v = vars[name]
if (!v) return whole
return formatValue(v.value, fmt, regexDefault)
})
}
function interpolateDeep(obj, vars, regexDefault) {
if (typeof obj === "string") return interpolate(obj, vars, regexDefault)
if (Array.isArray(obj)) return obj.map(function(x) { return interpolateDeep(x, vars, regexDefault) })
if (obj && typeof obj === "object") {
var out = {}
for (var k in obj) out[k] = interpolateDeep(obj[k], vars, regexDefault)
return out
}
return obj
}
// The /api/ds/query body for one panel, or {error} when it can't be built.
// Prometheus and Loki take a regex-joined multi value, which is what
// their own frontends do.
function buildQuery(panel, dashboard, vars, datasources, range, maxDataPoints) {
var targets = (panel.targets || []).filter(function(t) { return !t.hide })
if (targets.length === 0) return { error: "no queries" }
if (panel.scopedVars) vars = Object.assign({}, vars, panel.scopedVars)
var panelDs = panel.datasource
var interval = intervalFor(range, maxDataPoints, panel.interval)
var queries = []
for (var i = 0; i < targets.length; i++) {
var t = targets[i]
var ref = t.datasource || panelDs
if (ref && ref.uid === "-- Mixed --") ref = t.datasource
var refUid = typeof ref === "string" ? ref : ref && ref.uid
var refType = ref && typeof ref === "object" ? ref.type : undefined
refUid = interpolate(refUid || "", vars, false)
var ds = findDatasource(datasources, refUid || null, refType)
if (!ds || !ds.uid) return { error: "can't resolve datasource " + (refUid || "default") }
if (ds.uid === "-- Dashboard --" || ds.type === "datasource") return { error: "panels that reuse another panel's query aren't supported" }
var regexDefault = ds.type === "prometheus" || ds.type === "loki"
var q = interpolateDeep(t, vars, regexDefault)
q.datasource = { uid: ds.uid, type: ds.type }
q.refId = t.refId || String.fromCharCode(65 + i)
q.intervalMs = interval
q.maxDataPoints = maxDataPoints
queries.push(q)
}
return { body: { queries: queries, from: String(range.from), to: String(range.to) }, interval: interval }
}
// ── frames → series ───────────────────────────────────────────────────
// Prometheus' legend template, for frames that came back without one
function legend(format, labels) {
if (!format || format === "__auto") return null
return format.replace(/\{\{\s*(.+?)\s*\}\}/g, function(_, k) { return labels && labels[k] !== undefined ? labels[k] : "" })
}
function labelsText(labels) {
var keys = Object.keys(labels || {}).filter(function(k) { return k !== "__name__" })
var name = labels && labels.__name__ ? labels.__name__ : ""
if (keys.length === 0) return name
return name + "{" + keys.map(function(k) { return k + "=\"" + labels[k] + "\"" }).join(", ") + "}"
}
// [{name, refId, labels, times: [ms], values: [number|null], config}]
// One per number field. Frames without a time field (tables) give their
// number fields with no times, so reducers still work on them.
function seriesFromResults(results, panel) {
var out = []
var targetsByRef = {}
var targets = panel.targets || []
for (var i = 0; i < targets.length; i++) targetsByRef[targets[i].refId] = targets[i]
var refs = Object.keys(results || {}).sort()
for (var r = 0; r < refs.length; r++) {
var res = results[refs[r]]
var frames = (res && res.frames) || []
for (var f = 0; f < frames.length; f++) {
var frame = frames[f]
var fields = (frame.schema && frame.schema.fields) || []
var values = (frame.data && frame.data.values) || []
var timeIdx = -1
for (var k = 0; k < fields.length; k++) if (fields[k].type === "time") { timeIdx = k; break }
var times = timeIdx >= 0 ? values[timeIdx] || [] : []
for (var n = 0; n < fields.length; n++) {
var fld = fields[n]
if (fld.type !== "number") continue
var cfg = fld.config || {}
var t = targetsByRef[refs[r]] || {}
var name = cfg.displayNameFromDS || cfg.displayName
|| legend(t.legendFormat, fld.labels)
|| (fld.labels && Object.keys(fld.labels).length ? labelsText(fld.labels) : null)
|| (frame.schema.name && fields.length === 2 ? frame.schema.name : null)
|| fld.name
out.push({
name: name,
fieldName: fld.name,
refId: refs[r],
labels: fld.labels || {},
times: times,
values: values[n] || [],
config: cfg
})
}
}
}
return out
}
function resultErrors(results) {
var errs = []
for (var r in results || {}) if (results[r] && results[r].error) errs.push(results[r].error)
return errs
}
// ── reducers ──────────────────────────────────────────────────────────
function numbers(values) {
var out = []
for (var i = 0; i < values.length; i++) {
var v = values[i]
if (v !== null && v !== undefined && !isNaN(v)) out.push(v)
}
return out
}
function reduce(values, calc) {
var nums = numbers(values || [])
var i
switch (calc) {
case "last": return values.length ? values[values.length - 1] : null
case "first": return values.length ? values[0] : null
case "firstNotNull": return nums.length ? nums[0] : null
case "mean": if (!nums.length) return null; var s = 0; for (i = 0; i < nums.length; i++) s += nums[i]; return s / nums.length
case "sum": var t = 0; for (i = 0; i < nums.length; i++) t += nums[i]; return t
case "max": return nums.length ? Math.max.apply(null, nums) : null
case "min": return nums.length ? Math.min.apply(null, nums) : null
case "count": return values.length
case "range": return nums.length ? Math.max.apply(null, nums) - Math.min.apply(null, nums) : null
case "delta":
var d = 0
for (i = 1; i < nums.length; i++) d += nums[i] >= nums[i - 1] ? nums[i] - nums[i - 1] : nums[i]
return d
case "diff": return nums.length ? nums[nums.length - 1] - nums[0] : null
case "lastNotNull":
default: return nums.length ? nums[nums.length - 1] : null
}
}
var CALC_LABELS = {
last: "Last", lastNotNull: "Last *", first: "First", firstNotNull: "First *", mean: "Mean",
sum: "Total", max: "Max", min: "Min", count: "Count", range: "Range", delta: "Delta", diff: "Difference"
}
function calcLabel(c) { return CALC_LABELS[c] || c }
// ── field config ──────────────────────────────────────────────────────
// The panel's defaults with any matching overrides applied, for one field.
// `index` is the series' position, used by palette colours.
function fieldConfig(panel, series) {
var fc = panel.fieldConfig || {}
var cfg = Object.assign({}, fc.defaults || {})
cfg.custom = Object.assign({}, (fc.defaults && fc.defaults.custom) || {})
var overrides = fc.overrides || []
for (var i = 0; i < overrides.length; i++) {
var o = overrides[i]
if (!matches(o.matcher, series)) continue
var props = o.properties || []
for (var p = 0; p < props.length; p++) {
var id = props[p].id
if (id.indexOf("custom.") === 0) cfg.custom[id.substring(7)] = props[p].value
else cfg[id] = props[p].value
}
}
return cfg
}
function matches(matcher, series) {
if (!matcher) return false
var opt = matcher.options
switch (matcher.id) {
case "byName": return opt === series.name || opt === series.fieldName || opt === series.column
case "byRegexp":
try { return new RegExp(opt).test(series.name) } catch (e) { return false }
case "byFrameRefID": return opt === series.refId
case "byType": return opt === "number"
default: return false
}
}
function displayName(series, cfg) {
return cfg.displayName ? interpolate(cfg.displayName, { __series: { value: series.name } }) : series.name
}
// ── colours ───────────────────────────────────────────────────────────
var CLASSIC = ["#7EB26D", "#EAB839", "#6ED0E0", "#EF843C", "#E24D42", "#1F78C1", "#BA43A9", "#705DA0",
"#508642", "#CCA300", "#447EBC", "#C15C17", "#890F02", "#0A437C", "#6D1F62", "#584477",
"#B7DBAB", "#F4D598", "#70DBED", "#F9BA8F", "#F29191", "#82B5D8", "#E5A8E2", "#AEA2E0",
"#629E51", "#E5AC0E", "#64B0C8", "#E0752D", "#BF1B00", "#0A50A1", "#962D82", "#614D93"]
// super-light, light, base, semi-dark, dark
var HUES = {
red: ["#FFA6B0", "#FF7383", "#F2495C", "#E02F44", "#C4162A"],
orange: ["#FFCB7D", "#FFB357", "#FF9830", "#FF780A", "#FA6400"],
yellow: ["#FFF899", "#FFEE52", "#FADE2A", "#F2CC0C", "#E0B400"],
green: ["#C8F2C2", "#96D98D", "#73BF69", "#56A64B", "#37872D"],
blue: ["#C0D8FF", "#8AB8FF", "#5794F2", "#3274D9", "#1F60C4"],
purple: ["#DEB6F2", "#CA95E5", "#B877D9", "#A352CC", "#8F3BB8"]
}
var SHADES = { "super-light-": 0, "light-": 1, "": 2, "semi-dark-": 3, "dark-": 4 }
// "text" means the theme's text colour, which only the caller knows
function namedColor(name, textColor) {
if (!name) return textColor
if (name === "text") return textColor
if (name === "transparent") return "transparent"
if (name.charAt(0) === "#" || name.indexOf("rgb") === 0) return name
var m = /^(super-light-|light-|semi-dark-|dark-)?(red|orange|yellow|green|blue|purple)$/.exec(name)
if (m) return HUES[m[2]][SHADES[m[1] || ""]]
return name
}
var SCHEMES = {
"continuous-GrYlRd": ["#73BF69", "#FADE2A", "#F2495C"],
"continuous-RdYlGr": ["#F2495C", "#FADE2A", "#73BF69"],
"continuous-BlYlRd": ["#5794F2", "#FADE2A", "#F2495C"],
"continuous-YlRd": ["#FADE2A", "#F2495C"],
"continuous-BlPu": ["#5794F2", "#B877D9"],
"continuous-YlBl": ["#FADE2A", "#5794F2"],
"continuous-blues": ["#C0D8FF", "#1F60C4"],
"continuous-reds": ["#FFA6B0", "#C4162A"],
"continuous-greens": ["#C8F2C2", "#37872D"],
"continuous-purples": ["#DEB6F2", "#8F3BB8"]
}
function hexToRgb(hex) {
var h = hex.replace("#", "")
if (h.length === 3) h = h.replace(/(.)/g, "$1$1")
return [parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16)]
}
function mix(a, b, t) {
var x = hexToRgb(a), y = hexToRgb(b)
function ch(i) { var v = Math.round(x[i] + (y[i] - x[i]) * t).toString(16); return v.length < 2 ? "0" + v : v }
return "#" + ch(0) + ch(1) + ch(2)
}
function schemeColor(stops, t) {
t = Math.max(0, Math.min(1, isNaN(t) ? 0 : t))
if (stops.length === 1) return stops[0]
var pos = t * (stops.length - 1)
var i = Math.min(stops.length - 2, Math.floor(pos))
return mix(stops[i], stops[i + 1], pos - i)
}
function thresholdStep(thresholds, value, min, max) {
var steps = (thresholds && thresholds.steps) || []
if (steps.length === 0) return null
var v = value
if (thresholds.mode === "percentage" && min !== undefined && max !== undefined && max > min) {
v = (value - min) / (max - min) * 100
}
var hit = steps[0]
for (var i = 1; i < steps.length; i++) {
if (steps[i].value !== null && v >= steps[i].value) hit = steps[i]
}
return hit
}
// The colour a series (or one of its values) is drawn in.
function seriesColor(cfg, index, value, range, textColor) {
var c = cfg.color || { mode: "palette-classic" }
var mode = c.mode || "palette-classic"
if (mode === "fixed" || mode === "shades") return namedColor(c.fixedColor, textColor)
if (mode === "thresholds") {
var step = thresholdStep(cfg.thresholds, value === null || value === undefined ? -Infinity : value,
range ? range.min : undefined, range ? range.max : undefined)
return namedColor(step ? step.color : "green", textColor)
}
if (SCHEMES[mode]) {
if (!range || range.max === range.min) return SCHEMES[mode][SCHEMES[mode].length - 1]
return schemeColor(SCHEMES[mode], (value - range.min) / (range.max - range.min))
}
return CLASSIC[index % CLASSIC.length]
}
// a CSS colour with alpha, for fills
function withAlpha(color, alpha) {
if (!color || color === "transparent") return "rgba(0,0,0,0)"
if (color.charAt(0) === "#") {
var rgb = hexToRgb(color)
return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + alpha + ")"
}
return color
}
// ── units ─────────────────────────────────────────────────────────────
function decimalsFor(v, decimals) {
if (decimals !== undefined && decimals !== null) return decimals
var a = Math.abs(v)
if (a === 0 || a >= 100) return 0
if (a >= 10) return 1
if (a >= 1) return 2
return Math.min(4, Math.max(2, 1 - Math.floor(Math.log(a) / Math.LN10)))
}
function fixed(v, decimals) {
var d = decimalsFor(v, decimals)
var s = v.toFixed(d)
// auto decimals: don't print 3.00
if (decimals === undefined || decimals === null) s = s.indexOf(".") !== -1 ? s.replace(/\.?0+$/, "") : s
return s
}
function scaled(v, decimals, base, prefixes, suffix) {
var a = Math.abs(v)
var i = 0
while (a >= base && i < prefixes.length - 1) { a /= base; v /= base; i++ }
return { text: fixed(v, decimals), suffix: prefixes[i] + suffix }
}
function timeScaled(v, decimals, unit) {
// into the largest unit that keeps the number ≥ 1
var ms = v * { ns: 1e-6, "µs": 1e-3, ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 }[unit]
var a = Math.abs(ms)
var table = [[86400000 * 365, " year"], [86400000 * 7, " week"], [86400000, " day"], [3600000, " hour"],
[60000, " min"], [1000, " s"], [1, " ms"], [1e-3, " µs"], [1e-6, " ns"]]
if (a === 0) return { text: "0", suffix: " " + unit }
for (var i = 0; i < table.length; i++) {
if (a >= table[i][0]) {
var n = ms / table[i][0]
var suffix = table[i][1]
if ((suffix === " year" || suffix === " week" || suffix === " day" || suffix === " hour") && Math.abs(n) !== 1) suffix += "s"
return { text: fixed(n, decimals), suffix: suffix }
}
}
return { text: fixed(ms * 1e6, decimals), suffix: " ns" }
}
var SI = ["", " K", " M", " G", " T", " P"]
var SHORT = ["", " K", " Mil", " Bil", " Tri"]
// {text, suffix, prefix}; display is prefix + text + suffix
function formatParts(value, unit, decimals) {
if (value === null || value === undefined || (typeof value === "number" && isNaN(value))) return { text: "No data", suffix: "", prefix: "", empty: true }
if (typeof value !== "number") return { text: String(value), suffix: "", prefix: "" }
var v = value
var r
switch (unit || "short") {
case "none": return { text: fixed(v, decimals), suffix: "", prefix: "" }
case "percent": return { text: fixed(v, decimals), suffix: "%", prefix: "" }
case "percentunit": return { text: fixed(v * 100, decimals), suffix: "%", prefix: "" }
case "bytes": r = scaled(v, decimals, 1024, [" ", " Ki", " Mi", " Gi", " Ti", " Pi"], "B"); break
case "decbytes": r = scaled(v, decimals, 1000, [" ", " k", " M", " G", " T", " P"], "B"); break
case "bits": r = scaled(v, decimals, 1024, [" ", " Ki", " Mi", " Gi", " Ti"], "b"); break
case "decbits": r = scaled(v, decimals, 1000, [" ", " k", " M", " G", " T"], "b"); break
case "kbytes": r = scaled(v * 1024, decimals, 1024, [" ", " Ki", " Mi", " Gi", " Ti"], "B"); break
case "mbytes": r = scaled(v * 1048576, decimals, 1024, [" ", " Ki", " Mi", " Gi", " Ti"], "B"); break
case "Bps": r = scaled(v, decimals, 1000, [" ", " k", " M", " G", " T"], "B/s"); break
case "binBps": r = scaled(v, decimals, 1024, [" ", " Ki", " Mi", " Gi", " Ti"], "B/s"); break
case "bps": r = scaled(v, decimals, 1000, [" ", " k", " M", " G", " T"], "b/s"); break
case "binbps": r = scaled(v, decimals, 1024, [" ", " Ki", " Mi", " Gi", " Ti"], "b/s"); break
case "reqps": r = scaled(v, decimals, 1000, SHORT, ""); r.suffix += " req/s"; break
case "rps": r = scaled(v, decimals, 1000, SHORT, ""); r.suffix += " rps"; break
case "ops": r = scaled(v, decimals, 1000, SHORT, ""); r.suffix += " ops/s"; break
case "wps": r = scaled(v, decimals, 1000, SHORT, ""); r.suffix += " wps"; break
case "iops": r = scaled(v, decimals, 1000, SHORT, ""); r.suffix += " io/s"; break
case "opm": case "rpm": case "reqpm": r = scaled(v, decimals, 1000, SHORT, ""); r.suffix += " " + unit; break
case "hertz": r = scaled(v, decimals, 1000, [" ", " k", " M", " G"], "Hz"); break
case "celsius": return { text: fixed(v, decimals), suffix: "°C", prefix: "" }
case "fahrenheit": return { text: fixed(v, decimals), suffix: "°F", prefix: "" }
case "ns": case "µs": case "ms": case "s": case "m": case "h": case "d": r = timeScaled(v, decimals, unit); break
case "dtdurations": case "dtdurationms":
return { text: duration(unit === "dtdurations" ? v * 1000 : v), suffix: "", prefix: "" }
case "dateTimeAsIso": case "dateTimeAsSystem": case "dateTimeFromNow":
return { text: new Date(v).toISOString().replace("T", " ").substring(0, 19), suffix: "", prefix: "" }
case "currencyUSD": r = scaled(v, decimals, 1000, SHORT, ""); r.prefix = "$"; return r
case "currencyEUR": r = scaled(v, decimals, 1000, SHORT, ""); r.prefix = "€"; return r
case "short":
r = scaled(v, decimals, 1000, SHORT, ""); break
default:
// custom units: "suffix:x", "prefix:x", or anything else as a suffix
if (unit.indexOf("suffix:") === 0) return { text: fixed(v, decimals), suffix: unit.substring(7), prefix: "" }
if (unit.indexOf("prefix:") === 0) return { text: fixed(v, decimals), suffix: "", prefix: unit.substring(7) }
return { text: fixed(v, decimals), suffix: " " + unit, prefix: "" }
}
r.prefix = r.prefix || ""
if (r.suffix === " ") r.suffix = ""
r.suffix = r.suffix.replace(/^ +$/, "")
return r
}
function format(value, unit, decimals) {
var p = formatParts(value, unit, decimals)
return p.prefix + p.text + p.suffix
}
function duration(ms) {
var s = Math.floor(Math.abs(ms) / 1000)
var d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60), sec = s % 60
var parts = []
if (d) parts.push(d + "d")
if (h) parts.push(h + "h")
if (m && parts.length < 2) parts.push(m + "m")
if (parts.length < 2) parts.push(sec + "s")
return (ms < 0 ? "-" : "") + parts.slice(0, 2).join(" ")
}
// ── axes ──────────────────────────────────────────────────────────────
// About `count` round ticks covering [min, max]
function niceTicks(min, max, count) {
if (!isFinite(min) || !isFinite(max)) return { min: 0, max: 1, ticks: [0, 1] }
if (min === max) { var pad = Math.abs(min) * 0.1 || 1; min -= pad; max += pad }
var span = max - min
var raw = span / Math.max(1, count)
var mag = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10))
var norm = raw / mag
var step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag
var lo = Math.floor(min / step) * step
var hi = Math.ceil(max / step) * step
var ticks = []
for (var v = lo; v <= hi + step / 2; v += step) ticks.push(Math.abs(v) < step / 1e6 ? 0 : v)
return { min: lo, max: hi, ticks: ticks, step: step }
}
var TIME_STEPS = [1000, 5000, 10000, 15000, 30000, 60000, 120000, 300000, 600000, 900000, 1800000,
3600000, 7200000, 10800000, 21600000, 43200000, 86400000, 172800000, 604800000, 2592000000]
function timeTicks(from, to, count) {
var raw = (to - from) / Math.max(1, count)
var step = TIME_STEPS[TIME_STEPS.length - 1]
for (var i = 0; i < TIME_STEPS.length; i++) if (TIME_STEPS[i] >= raw) { step = TIME_STEPS[i]; break }
// align to local midnight-relative boundaries, like Grafana does
var tz = new Date(from).getTimezoneOffset() * 60000
var first = Math.ceil((from - tz) / step) * step + tz
var ticks = []
for (var t = first; t <= to; t += step) ticks.push(t)
return { ticks: ticks, step: step }
}
function pad2(n) { return n < 10 ? "0" + n : "" + n }
function timeLabel(t, step, span) {
var d = new Date(t)
var hm = pad2(d.getHours()) + ":" + pad2(d.getMinutes())
if (step < 60000) return hm + ":" + pad2(d.getSeconds())
if (span <= 86400000 * 1.5 || (step < 86400000 && (d.getHours() || d.getMinutes()))) return hm
return pad2(d.getMonth() + 1) + "/" + pad2(d.getDate())
}
function fullTime(t) {
var d = new Date(t)
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) + " "
+ pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds())
}
// ── tables ────────────────────────────────────────────────────────────
// {columns: [{name, unit?, numeric}], rows: [[cell]]}. Instant series
// with labels are joined into one row per label set, one "Value #A"
// column per query, the way Grafana's table shows several instant queries.
function tableFromResults(results, panel) {
var refs = Object.keys(results || {}).sort()
var labelKeys = []
var valueCols = []
var rowsByKey = {}
var order = []
var plainFrames = []
for (var r = 0; r < refs.length; r++) {
var frames = (results[refs[r]] && results[refs[r]].frames) || []
for (var f = 0; f < frames.length; f++) {
var fields = (frames[f].schema && frames[f].schema.fields) || []
var vals = (frames[f].data && frames[f].data.values) || []
var labelled = fields.some(function(x) { return x.type === "number" && x.labels && Object.keys(x.labels).length })
if (!labelled && refs.length === 1) { plainFrames.push(frames[f]); continue }
for (var n = 0; n < fields.length; n++) {
if (fields[n].type !== "number") continue
var labels = fields[n].labels || {}
var col = refs.length > 1 ? "Value #" + refs[r] : "Value"
if (valueCols.indexOf(col) === -1) valueCols.push(col)
var keys = Object.keys(labels).filter(function(k) { return k !== "__name__" }).sort()
for (var k = 0; k < keys.length; k++) if (labelKeys.indexOf(keys[k]) === -1) labelKeys.push(keys[k])
var id = keys.map(function(x) { return x + "=" + labels[x] }).join(",")
if (!rowsByKey[id]) { rowsByKey[id] = { labels: labels, values: {} }; order.push(id) }
rowsByKey[id].values[col] = reduce(vals[n] || [], "lastNotNull")
}
}
}
if (plainFrames.length > 0 && order.length === 0) {
var pf = plainFrames[0]
var pfields = pf.schema.fields
var pvals = pf.data.values
var count = pvals.length ? pvals[0].length : 0
var rows = []
for (var i = 0; i < count; i++) rows.push(pvals.map(function(c) { return c[i] }))
return {
columns: pfields.map(function(x) { return { name: (x.config && x.config.displayName) || x.name, field: x.name, type: x.type, refId: refs[0] } }),
rows: rows
}
}
var columns = labelKeys.map(function(k) { return { name: k, field: k, type: "string" } })
.concat(valueCols.map(function(c) { return { name: c, field: c, type: "number", refId: c.replace("Value #", "") } }))
var out = order.map(function(id) {
var row = rowsByKey[id]
return labelKeys.map(function(k) { return row.labels[k] !== undefined ? row.labels[k] : "" })
.concat(valueCols.map(function(c) { return row.values[c] !== undefined ? row.values[c] : null }))
})
return { columns: columns, rows: out }
}
// ── layout ────────────────────────────────────────────────────────────
// Flattens a dashboard into positioned items on the 24-column grid,
// honouring rows the person has folded (`folded[rowId]`, defaulting to
// what the dashboard was saved with). Items: {kind: "row"|"panel", panel,
// x, y, w, h, collapsed}.
function layout(dashboard, folded, vars) {
var panels = (dashboard.panels || []).slice().sort(function(a, b) {
var ga = a.gridPos || {}, gb = b.gridPos || {}
return (ga.y || 0) - (gb.y || 0) || (ga.x || 0) - (gb.x || 0)
})
var sections = []
var current = { row: null, panels: [] }
for (var i = 0; i < panels.length; i++) {
var p = panels[i]
if (p.type === "row") {
sections.push(current)
current = { row: p, panels: (p.panels || []).slice() }
} else {
current.panels.push(p)
}
}
sections.push(current)
var out = []
var cursor = 0
for (var s = 0; s < sections.length; s++) {
var sec = sections[s]
if (!sec.row && sec.panels.length === 0) continue
var collapsed = false
if (sec.row) {
var saved = !!sec.row.collapsed
collapsed = folded && folded[sec.row.id] !== undefined ? folded[sec.row.id] : saved
out.push({ kind: "row", panel: sec.row, x: 0, y: cursor, w: 24, h: 1, collapsed: collapsed, count: sec.panels.length })
cursor += 1
}
if (collapsed || sec.panels.length === 0) continue
sec.panels = expandRepeats(sec.panels, vars)
var minY = Infinity, maxY = 0
for (var j = 0; j < sec.panels.length; j++) {
var g = sec.panels[j].gridPos || {}
minY = Math.min(minY, g.y || 0)
}
for (var q = 0; q < sec.panels.length; q++) {
var gp = sec.panels[q].gridPos || { x: 0, y: 0, w: 12, h: 8 }
var y = cursor + (gp.y || 0) - minY
out.push({ kind: "panel", panel: sec.panels[q], x: gp.x || 0, y: y, w: gp.w || 12, h: gp.h || 8 })
maxY = Math.max(maxY, y + (gp.h || 8))
}
cursor = maxY
}
return out
}
// A panel repeated over a multi-value variable becomes one copy per value,
// each with that variable pinned (`scopedVars`) and a key of its own;
// panels below it in the section move down by however much room the
// copies took.
function expandRepeats(panels, vars) {
var out = []
var shifts = [] // [{fromY, by}]
for (var i = 0; i < panels.length; i++) {
var p = panels[i]
var g = p.gridPos || { x: 0, y: 0, w: 12, h: 8 }
var shift = 0
for (var s = 0; s < shifts.length; s++) if ((g.y || 0) >= shifts[s].fromY) shift += shifts[s].by
var v = p.repeat && vars ? vars[p.repeat] : null
var values = v ? [].concat(v.value) : []
if (!v || values.length <= 1) {
out.push(shift ? Object.assign({}, p, { gridPos: Object.assign({}, g, { y: (g.y || 0) + shift }) }) : p)
continue
}
var horizontal = p.repeatDirection !== "v"
var perRow = horizontal ? Math.min(values.length, p.maxPerRow || 4) : 1
var w = horizontal ? Math.max(1, Math.floor(24 / perRow)) : (g.w || 12)
var rows = Math.ceil(values.length / perRow)
for (var k = 0; k < values.length; k++) {
var scoped = {}
scoped[p.repeat] = { value: values[k] }
var copy = Object.assign({}, p, {
id: p.id + "#" + k,
scopedVars: Object.assign({}, p.scopedVars || {}, scoped),
gridPos: {
x: horizontal ? (k % perRow) * w : (g.x || 0),
y: (g.y || 0) + shift + Math.floor(k / perRow) * (g.h || 8),
w: w, h: g.h || 8
}
})
out.push(copy)
}
shifts.push({ fromY: (g.y || 0) + (g.h || 8), by: (rows - 1) * (g.h || 8) })
}
return out
}
// title with variables filled in, repeat values included
function panelTitle(panel, vars) {
var scoped = panel.scopedVars ? Object.assign({}, vars || {}, panel.scopedVars) : (vars || {})
return interpolate(panel.title || "", scoped, false)
}
// every non-row panel in a dashboard, nested ones included
function allPanels(dashboard) {
var out = []
function walk(list) {
for (var i = 0; i < (list || []).length; i++) {
if (list[i].type === "row") walk(list[i].panels)
else out.push(list[i])
}
}
walk(dashboard.panels)
return out
}
function findPanel(dashboard, id) {
var all = allPanels(dashboard)
for (var i = 0; i < all.length; i++) if (all[i].id === id) return all[i]
return null
}
var SUPPORTED = ["timeseries", "graph", "stat", "singlestat", "gauge", "bargauge", "table", "text", "piechart"]
function supported(type) {
return SUPPORTED.indexOf(type) !== -1
}
// node: export for tools/check.mjs; QML ignores this
if (typeof module !== "undefined") module.exports = {
parseTime: parseTime, resolveRange: resolveRange, rangeLabel: rangeLabel, intervalFor: intervalFor,
variables: variables, findDatasource: findDatasource, interpolate: interpolate, buildQuery: buildQuery,
seriesFromResults: seriesFromResults, resultErrors: resultErrors, reduce: reduce, fieldConfig: fieldConfig,
seriesColor: seriesColor, namedColor: namedColor, format: format, formatParts: formatParts, niceTicks: niceTicks,
timeTicks: timeTicks, timeLabel: timeLabel, tableFromResults: tableFromResults, layout: layout,
allPanels: allPanels, findPanel: findPanel, supported: supported, displayName: displayName,
varRequest: varRequest, varOptions: varOptions, selectionText: selectionText, panelTitle: panelTitle
}
neiam
/grafana-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
44.0 KiB · text
History
35c3541