mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BarChart: use uPlot's native bars pathbuilder (#36725)
This commit is contained in:
parent
395289f45d
commit
40a87a7851
@ -8,6 +8,12 @@
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
@ -33,7 +39,8 @@
|
||||
"hideFrom": {
|
||||
"graph": false,
|
||||
"legend": false,
|
||||
"tooltip": false
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0
|
||||
},
|
||||
@ -105,7 +112,8 @@
|
||||
"hideFrom": {
|
||||
"graph": false,
|
||||
"legend": false,
|
||||
"tooltip": false
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0
|
||||
},
|
||||
@ -177,7 +185,8 @@
|
||||
"hideFrom": {
|
||||
"graph": false,
|
||||
"legend": false,
|
||||
"tooltip": false
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0
|
||||
},
|
||||
@ -248,7 +257,8 @@
|
||||
"hideFrom": {
|
||||
"graph": false,
|
||||
"legend": false,
|
||||
"tooltip": false
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0
|
||||
},
|
||||
@ -319,7 +329,8 @@
|
||||
"hideFrom": {
|
||||
"graph": false,
|
||||
"legend": false,
|
||||
"tooltip": false
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0
|
||||
},
|
||||
@ -390,10 +401,12 @@
|
||||
"hideFrom": {
|
||||
"graph": false,
|
||||
"legend": false,
|
||||
"tooltip": false
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0
|
||||
},
|
||||
"decimals": 7,
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
@ -437,7 +450,7 @@
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nLong value, 15,10",
|
||||
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, -5\nLondon, 10, 1\nLong value, 15,10",
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
@ -462,7 +475,8 @@
|
||||
"hideFrom": {
|
||||
"graph": false,
|
||||
"legend": false,
|
||||
"tooltip": false
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0
|
||||
},
|
||||
@ -537,5 +551,5 @@
|
||||
"timezone": "",
|
||||
"title": "BarChart - Panel Tests - Value sizing",
|
||||
"uid": "WFlOM-jM1",
|
||||
"version": 3
|
||||
"version": 9
|
||||
}
|
@ -73,7 +73,7 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
|
||||
...this.props.config.getConfig(),
|
||||
};
|
||||
|
||||
pluginLog('UPlot', false, 'Reinitializing plot');
|
||||
pluginLog('UPlot', false, 'Reinitializing plot', config);
|
||||
const plot = new uPlot(config, this.props.data, this.plotContainer!.current!);
|
||||
|
||||
if (plotRef) {
|
||||
|
@ -113,7 +113,7 @@ Object {
|
||||
"size": undefined,
|
||||
"stroke": "#808080",
|
||||
},
|
||||
"pxAlign": false,
|
||||
"pxAlign": true,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"spanGaps": undefined,
|
||||
@ -238,7 +238,7 @@ Object {
|
||||
"size": undefined,
|
||||
"stroke": "#808080",
|
||||
},
|
||||
"pxAlign": false,
|
||||
"pxAlign": true,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"spanGaps": undefined,
|
||||
@ -363,7 +363,7 @@ Object {
|
||||
"size": undefined,
|
||||
"stroke": "#808080",
|
||||
},
|
||||
"pxAlign": false,
|
||||
"pxAlign": true,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"spanGaps": undefined,
|
||||
@ -488,7 +488,7 @@ Object {
|
||||
"size": undefined,
|
||||
"stroke": "#808080",
|
||||
},
|
||||
"pxAlign": false,
|
||||
"pxAlign": true,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"spanGaps": undefined,
|
||||
@ -613,7 +613,7 @@ Object {
|
||||
"size": undefined,
|
||||
"stroke": "#808080",
|
||||
},
|
||||
"pxAlign": false,
|
||||
"pxAlign": true,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"spanGaps": undefined,
|
||||
@ -738,7 +738,7 @@ Object {
|
||||
"size": undefined,
|
||||
"stroke": "#808080",
|
||||
},
|
||||
"pxAlign": false,
|
||||
"pxAlign": true,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"spanGaps": undefined,
|
||||
@ -863,7 +863,7 @@ Object {
|
||||
"size": undefined,
|
||||
"stroke": "#808080",
|
||||
},
|
||||
"pxAlign": false,
|
||||
"pxAlign": true,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"spanGaps": undefined,
|
||||
@ -988,7 +988,7 @@ Object {
|
||||
"size": undefined,
|
||||
"stroke": "#808080",
|
||||
},
|
||||
"pxAlign": false,
|
||||
"pxAlign": true,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"spanGaps": undefined,
|
||||
|
@ -1,20 +1,28 @@
|
||||
import uPlot, { Axis, Series } from 'uplot';
|
||||
import uPlot, { Axis } from 'uplot';
|
||||
import { pointWithin, Quadtree, Rect } from './quadtree';
|
||||
import { distribute, SPACE_BETWEEN } from './distribute';
|
||||
import { BarValueVisibility, ScaleDirection, ScaleOrientation } from '@grafana/ui/src/components/uPlot/config';
|
||||
import { CartesianCoords2D, GrafanaTheme2 } from '@grafana/data';
|
||||
import { calculateFontSize, measureText, PlotTooltipInterpolator, VizTextDisplayOptions } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { calculateFontSize, PlotTooltipInterpolator, VizTextDisplayOptions } from '@grafana/ui';
|
||||
|
||||
const groupDistr = SPACE_BETWEEN;
|
||||
const barDistr = SPACE_BETWEEN;
|
||||
|
||||
// min.max font size for value label
|
||||
const VALUE_MIN_FONT_SIZE = 8;
|
||||
const VALUE_MAX_FONT_SIZE = 30;
|
||||
// % of width/height of the bar that value should fit in when measuring size
|
||||
const BAR_FONT_SIZE_RATIO = 0.65;
|
||||
// distance between label and a horizontal bar
|
||||
const HORIZONTAL_BAR_LABEL_OFFSET = 10;
|
||||
// distance between label and a bar in % of bar width
|
||||
const LABEL_OFFSET_FACTOR_VT = 0.1;
|
||||
const LABEL_OFFSET_FACTOR_HZ = 0.15;
|
||||
// max distance
|
||||
const LABEL_OFFSET_MAX_VT = 5;
|
||||
const LABEL_OFFSET_MAX_HZ = 10;
|
||||
|
||||
// text baseline middle runs through the middle of lowercase letters
|
||||
// since bar values are numbers and uppercase-like, we want the middle of uppercase
|
||||
// this is a cheap fudge factor that skips expensive and inconsistent cross-browser measuring
|
||||
const MIDDLE_BASELINE_SHIFT = 0.1;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -31,247 +39,25 @@ export interface BarsOptions {
|
||||
onLeave?: (seriesIdx: number, valueIdx: number) => void;
|
||||
}
|
||||
|
||||
interface LabelDescriptor extends CartesianCoords2D {
|
||||
formattedValue: string;
|
||||
value: number;
|
||||
textAlign: CanvasTextAlign;
|
||||
textBaseline: CanvasTextBaseline;
|
||||
fontSize: number;
|
||||
barWidth: number;
|
||||
barHeight: number;
|
||||
textWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
const { xOri: ori, xDir: dir, groupWidth, barWidth, formatValue, showValue } = opts;
|
||||
const { xOri, xDir: dir, groupWidth, barWidth, formatValue, showValue } = opts;
|
||||
const isXHorizontal = xOri === ScaleOrientation.Horizontal;
|
||||
const hasAutoValueSize = !Boolean(opts.text?.valueSize);
|
||||
|
||||
let qt: Quadtree;
|
||||
let labelsSizing: Array<LabelDescriptor | null> = [];
|
||||
let hovered: Rect | undefined = undefined;
|
||||
|
||||
const drawBars: Series.PathBuilder = (u, sidx) => {
|
||||
return uPlot.orient(
|
||||
u,
|
||||
sidx,
|
||||
(series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect) => {
|
||||
const fill = new Path2D();
|
||||
const stroke = new Path2D();
|
||||
|
||||
let numGroups = dataX.length;
|
||||
let barsPerGroup = u.series.length - 1;
|
||||
|
||||
let y0Pos = valToPosY(0, scaleY, yDim, yOff);
|
||||
|
||||
const _dir = dir * (ori === 0 ? 1 : -1);
|
||||
|
||||
walkTwo(groupWidth, barWidth, sidx - 1, numGroups, barsPerGroup, xDim, null, (ix, x0, wid) => {
|
||||
let left = Math.round(xOff + (_dir === 1 ? x0 : xDim - x0 - wid));
|
||||
let barWid = Math.round(wid);
|
||||
const canvas = u.over;
|
||||
const bbox = canvas?.getBoundingClientRect();
|
||||
|
||||
if (dataY[ix] != null) {
|
||||
let yPos = valToPosY(dataY[ix]!, scaleY, yDim, yOff);
|
||||
let btm = Math.round(Math.max(yPos, y0Pos));
|
||||
let top = Math.round(Math.min(yPos, y0Pos));
|
||||
let barHgt = btm - top;
|
||||
let strokeWidth = series.width || 0;
|
||||
|
||||
if (strokeWidth) {
|
||||
rect(stroke, left + strokeWidth / 2, top + strokeWidth / 2, barWid - strokeWidth, barHgt - strokeWidth);
|
||||
}
|
||||
|
||||
rect(fill, left, top, barWid, barHgt);
|
||||
|
||||
let x = ori === ScaleOrientation.Horizontal ? Math.round(left - xOff) : Math.round(top - yOff);
|
||||
let y = ori === ScaleOrientation.Horizontal ? Math.round(top - yOff) : Math.round(left - xOff);
|
||||
let width = ori === ScaleOrientation.Horizontal ? barWid : barHgt;
|
||||
let height = ori === ScaleOrientation.Horizontal ? barHgt : barWid;
|
||||
|
||||
qt.add({ x, y, w: width, h: height, sidx: sidx, didx: ix });
|
||||
|
||||
// Collect labels sizes and placements
|
||||
const value = formatValue(sidx, dataY[ix]);
|
||||
let labelX = ori === ScaleOrientation.Horizontal ? Math.round(left) : Math.round(top);
|
||||
let labelY = ori === ScaleOrientation.Horizontal ? Math.round(top) : Math.round(left);
|
||||
|
||||
let availableSpaceForText;
|
||||
|
||||
if (ori === ScaleOrientation.Horizontal) {
|
||||
availableSpaceForText =
|
||||
dataY[ix]! >= 0 ? y / devicePixelRatio : bbox!.height - (y + height) / devicePixelRatio;
|
||||
} else {
|
||||
availableSpaceForText =
|
||||
dataY[ix]! >= 0 ? bbox!.width - (x + width) / devicePixelRatio : x / devicePixelRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snippet below is for debugging the available space for text. Leaving it for the future bugs...
|
||||
*/
|
||||
// u.ctx.beginPath();
|
||||
// u.ctx.strokeStyle = '#0000ff';
|
||||
|
||||
// if (dataY[ix]! >= 0) {
|
||||
// if (ori === ScaleOrientation.Horizontal) {
|
||||
// u.ctx.moveTo(left, top - availableSpaceForText * devicePixelRatio);
|
||||
// u.ctx.lineTo(left + width, top - availableSpaceForText * devicePixelRatio);
|
||||
// u.ctx.lineTo(left + width, top);
|
||||
// u.ctx.lineTo(left, top);
|
||||
// } else {
|
||||
// u.ctx.moveTo(top + width, left);
|
||||
// u.ctx.lineTo(top + width + availableSpaceForText * devicePixelRatio, left);
|
||||
// u.ctx.lineTo(top + width + availableSpaceForText * devicePixelRatio, left + height);
|
||||
// u.ctx.lineTo(top + width, left + height);
|
||||
// }
|
||||
// } else {
|
||||
// if (ori === ScaleOrientation.Horizontal) {
|
||||
// u.ctx.moveTo(left, top + height + availableSpaceForText * devicePixelRatio);
|
||||
// u.ctx.lineTo(left + width, top + height + availableSpaceForText * devicePixelRatio);
|
||||
// u.ctx.lineTo(left + width, top + height);
|
||||
// u.ctx.lineTo(left, top + height);
|
||||
// } else {
|
||||
// u.ctx.moveTo(top, left);
|
||||
// u.ctx.lineTo(top - availableSpaceForText * devicePixelRatio, left);
|
||||
// u.ctx.lineTo(top - availableSpaceForText * devicePixelRatio, left + height);
|
||||
// u.ctx.lineTo(top, left + height);
|
||||
// }
|
||||
// }
|
||||
// u.ctx.closePath();
|
||||
// u.ctx.stroke();
|
||||
|
||||
let fontSize = opts.text?.valueSize ?? VALUE_MIN_FONT_SIZE;
|
||||
|
||||
if (hasAutoValueSize) {
|
||||
const size =
|
||||
ori === ScaleOrientation.Horizontal
|
||||
? calculateFontSize(
|
||||
value,
|
||||
(width / devicePixelRatio) * BAR_FONT_SIZE_RATIO,
|
||||
availableSpaceForText * BAR_FONT_SIZE_RATIO,
|
||||
1
|
||||
)
|
||||
: calculateFontSize(
|
||||
value,
|
||||
availableSpaceForText,
|
||||
(height * BAR_FONT_SIZE_RATIO) / devicePixelRatio,
|
||||
1
|
||||
);
|
||||
fontSize = size > VALUE_MAX_FONT_SIZE ? VALUE_MAX_FONT_SIZE : size;
|
||||
}
|
||||
|
||||
const textAlign = ori === ScaleOrientation.Horizontal ? 'center' : 'left';
|
||||
const textBaseline = (ori === ScaleOrientation.Horizontal ? 'bottom' : 'alphabetic') as CanvasTextBaseline;
|
||||
const textMeasurement = measureText(value, fontSize * devicePixelRatio);
|
||||
let labelPosition: CartesianCoords2D = { x: labelX, y: labelY };
|
||||
|
||||
// Collect labels szes
|
||||
labelsSizing.push({
|
||||
formattedValue: value,
|
||||
value: dataY[ix]!,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
fontSize: Math.floor(fontSize),
|
||||
barWidth: width,
|
||||
barHeight: height,
|
||||
textWidth: textMeasurement.width,
|
||||
...labelPosition,
|
||||
});
|
||||
} else {
|
||||
labelsSizing.push(null);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
stroke,
|
||||
fill,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// uPlot hook to draw the labels on the bar chart
|
||||
const draw = (u: uPlot) => {
|
||||
let minFontSize = labelsSizing.reduce((min, s) => (s && s.fontSize < min ? s.fontSize : min), Infinity);
|
||||
|
||||
if (minFontSize === Infinity) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < labelsSizing.length; i++) {
|
||||
const label = labelsSizing[i];
|
||||
let x = 0,
|
||||
y = 0;
|
||||
if (label === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fontSize = hasAutoValueSize ? minFontSize : label.fontSize;
|
||||
|
||||
if (showValue === BarValueVisibility.Never) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (showValue !== BarValueVisibility.Always) {
|
||||
if (
|
||||
hasAutoValueSize &&
|
||||
((ori === ScaleOrientation.Horizontal && label.textWidth > label.barWidth) ||
|
||||
minFontSize < VALUE_MIN_FONT_SIZE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final labels positions according to unified text size
|
||||
const textMeasurement = measureText(label.formattedValue, fontSize * devicePixelRatio);
|
||||
|
||||
let actualLineHeight = textMeasurement.actualBoundingBoxAscent + textMeasurement.actualBoundingBoxDescent;
|
||||
|
||||
// fontBoundingBoxAscent is only supported in chrome & safari at the moment. (see: https://caniuse.com/?search=fontBoundingBoxAscent)
|
||||
// @ts-ignore
|
||||
if (textMeasurement.fontBoundingBoxAscent && textMeasurement.fontBoundingBoxDescent) {
|
||||
// @ts-ignore
|
||||
actualLineHeight = textMeasurement.fontBoundingBoxAscent + textMeasurement.fontBoundingBoxDescent;
|
||||
}
|
||||
|
||||
if (ori === ScaleOrientation.Horizontal) {
|
||||
x = label.x + label.barWidth / 2;
|
||||
y = label.y + (label.value >= 0 ? 0 : label.barHeight + actualLineHeight);
|
||||
} else {
|
||||
x =
|
||||
label.x +
|
||||
(label.value >= 0
|
||||
? label.barWidth + HORIZONTAL_BAR_LABEL_OFFSET
|
||||
: -textMeasurement.width - HORIZONTAL_BAR_LABEL_OFFSET);
|
||||
y =
|
||||
label.y +
|
||||
(label.barHeight + textMeasurement.actualBoundingBoxAscent + textMeasurement.actualBoundingBoxDescent) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snippet below is for debugging the available space for text. Leaving it for the future bugs...
|
||||
*/
|
||||
// u.ctx.beginPath();
|
||||
// u.ctx.fillStyle = '#0000ff';
|
||||
// u.ctx.arc(label.x, label.y, 10, 0, Math.PI * 2, true);
|
||||
// u.ctx.closePath();
|
||||
// u.ctx.fill();
|
||||
|
||||
u.ctx.fillStyle = theme.colors.text.primary;
|
||||
u.ctx.font = `${fontSize * devicePixelRatio}px ${theme.typography.fontFamily}`;
|
||||
u.ctx.textAlign = label.textAlign;
|
||||
u.ctx.textBaseline = label.textBaseline;
|
||||
u.ctx.fillText(label.formattedValue, x, y);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
let barMark = document.createElement('div');
|
||||
barMark.classList.add('bar-mark');
|
||||
barMark.style.position = 'absolute';
|
||||
barMark.style.background = 'rgba(255,255,255,0.4)';
|
||||
|
||||
const xSplits: Axis.Splits = (u: uPlot) => {
|
||||
const dim = ori === 0 ? u.bbox.width : u.bbox.height;
|
||||
const _dir = dir * (ori === 0 ? 1 : -1);
|
||||
const dim = isXHorizontal ? u.bbox.width : u.bbox.height;
|
||||
const _dir = dir * (isXHorizontal ? 1 : -1);
|
||||
|
||||
let splits: number[] = [];
|
||||
|
||||
@ -289,12 +75,63 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
|
||||
const xValues: Axis.Values = (u) => u.data[0].map((x) => formatValue(0, x));
|
||||
|
||||
let hovered: Rect | undefined = undefined;
|
||||
let distrTwo = (groupCount: number, barCount: number) => {
|
||||
let out = Array.from({ length: barCount }, () => ({
|
||||
offs: Array(groupCount).fill(0),
|
||||
size: Array(groupCount).fill(0),
|
||||
}));
|
||||
|
||||
let barMark = document.createElement('div');
|
||||
barMark.classList.add('bar-mark');
|
||||
barMark.style.position = 'absolute';
|
||||
barMark.style.background = 'rgba(255,255,255,0.4)';
|
||||
distribute(groupCount, groupWidth, groupDistr, null, (groupIdx, groupOffPct, groupDimPct) => {
|
||||
distribute(barCount, barWidth, barDistr, null, (barIdx, barOffPct, barDimPct) => {
|
||||
out[barIdx].offs[groupIdx] = groupOffPct + groupDimPct * barOffPct;
|
||||
out[barIdx].size[groupIdx] = groupDimPct * barDimPct;
|
||||
});
|
||||
});
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
let barsPctLayout: Array<null | { offs: number[]; size: number[] }> = [];
|
||||
let barRects: Rect[] = [];
|
||||
|
||||
// minimum available space for labels between bar end and plotting area bound (in canvas pixels)
|
||||
let vSpace = Infinity;
|
||||
let hSpace = Infinity;
|
||||
|
||||
let barsBuilder = uPlot.paths.bars!({
|
||||
disp: {
|
||||
x0: {
|
||||
unit: 2,
|
||||
values: (u, seriesIdx) => barsPctLayout[seriesIdx]!.offs,
|
||||
},
|
||||
size: {
|
||||
unit: 2,
|
||||
values: (u, seriesIdx) => barsPctLayout[seriesIdx]!.size,
|
||||
},
|
||||
},
|
||||
// collect rendered bar geometry
|
||||
each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => {
|
||||
// we get back raw canvas coords (included axes & padding)
|
||||
// translate to the plotting area origin
|
||||
lft -= u.bbox.left;
|
||||
top -= u.bbox.top;
|
||||
|
||||
let val = u.data[seriesIdx][dataIdx]!;
|
||||
|
||||
// accum min space abvailable for labels
|
||||
if (isXHorizontal) {
|
||||
vSpace = Math.min(vSpace, val < 0 ? u.bbox.height - (top + hgt) : top);
|
||||
hSpace = wid;
|
||||
} else {
|
||||
vSpace = hgt;
|
||||
hSpace = Math.min(hSpace, val < 0 ? lft : u.bbox.width - (lft + wid));
|
||||
}
|
||||
|
||||
let barRect = { x: lft, y: top, w: wid, h: hgt, sidx: seriesIdx, didx: dataIdx };
|
||||
qt.add(barRect);
|
||||
barRects.push(barRect);
|
||||
},
|
||||
});
|
||||
|
||||
const init = (u: uPlot) => {
|
||||
let over = u.over;
|
||||
@ -302,18 +139,91 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
over.appendChild(barMark);
|
||||
};
|
||||
|
||||
// Build bars
|
||||
const drawClear = (u: uPlot) => {
|
||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||
|
||||
qt.clear();
|
||||
|
||||
labelsSizing = [];
|
||||
|
||||
// clear the path cache to force drawBars() to rebuild new quadtree
|
||||
u.series.forEach((s) => {
|
||||
// @ts-ignore
|
||||
s._paths = null;
|
||||
});
|
||||
|
||||
barsPctLayout = ([null] as any).concat(distrTwo(u.data[0].length, u.data.length - 1));
|
||||
barRects.length = 0;
|
||||
vSpace = hSpace = Infinity;
|
||||
};
|
||||
|
||||
const LABEL_OFFSET_FACTOR = isXHorizontal ? LABEL_OFFSET_FACTOR_VT : LABEL_OFFSET_FACTOR_HZ;
|
||||
const LABEL_OFFSET_MAX = isXHorizontal ? LABEL_OFFSET_MAX_VT : LABEL_OFFSET_MAX_HZ;
|
||||
|
||||
// uPlot hook to draw the labels on the bar chart.
|
||||
const draw = (u: uPlot) => {
|
||||
// pre-cache formatted labels
|
||||
let texts = Array(barRects.length);
|
||||
let labelOffset = LABEL_OFFSET_MAX;
|
||||
|
||||
barRects.forEach((r, i) => {
|
||||
texts[i] = formatValue(r.sidx, u.data[r.sidx][r.didx]);
|
||||
labelOffset = Math.min(labelOffset, Math.round(LABEL_OFFSET_FACTOR * (isXHorizontal ? r.w : r.h)));
|
||||
});
|
||||
|
||||
let fontSize = opts.text?.valueSize ?? VALUE_MAX_FONT_SIZE;
|
||||
|
||||
if (hasAutoValueSize) {
|
||||
for (let i = 0; i < barRects.length; i++) {
|
||||
fontSize = Math.round(
|
||||
Math.min(
|
||||
fontSize,
|
||||
VALUE_MAX_FONT_SIZE,
|
||||
calculateFontSize(
|
||||
texts[i],
|
||||
hSpace * (isXHorizontal ? BAR_FONT_SIZE_RATIO : 1) - (isXHorizontal ? 0 : labelOffset),
|
||||
vSpace * (isXHorizontal ? 1 : BAR_FONT_SIZE_RATIO) - (isXHorizontal ? labelOffset : 0),
|
||||
1
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (fontSize < VALUE_MIN_FONT_SIZE && showValue !== BarValueVisibility.Always) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u.ctx.fillStyle = theme.colors.text.primary;
|
||||
u.ctx.font = `${fontSize}px ${theme.typography.fontFamily}`;
|
||||
|
||||
let middleShift = isXHorizontal ? 0 : -Math.round(MIDDLE_BASELINE_SHIFT * fontSize);
|
||||
|
||||
let curAlign: CanvasTextAlign, curBaseline: CanvasTextBaseline;
|
||||
|
||||
barRects.forEach((r, i) => {
|
||||
let value = u.data[r.sidx][r.didx];
|
||||
let text = texts[i];
|
||||
|
||||
if (value != null) {
|
||||
let align: CanvasTextAlign = isXHorizontal ? 'center' : value < 0 ? 'right' : 'left';
|
||||
let baseline: CanvasTextBaseline = isXHorizontal ? (value < 0 ? 'top' : 'alphabetic') : 'middle';
|
||||
|
||||
if (align !== curAlign) {
|
||||
u.ctx.textAlign = curAlign = align;
|
||||
}
|
||||
|
||||
if (baseline !== curBaseline) {
|
||||
u.ctx.textBaseline = curBaseline = baseline;
|
||||
}
|
||||
|
||||
u.ctx.fillText(
|
||||
text,
|
||||
u.bbox.left + (isXHorizontal ? r.x + r.w / 2 : value < 0 ? r.x - labelOffset : r.x + r.w + labelOffset),
|
||||
u.bbox.top +
|
||||
(isXHorizontal ? (value < 0 ? r.y + r.h + labelOffset : r.y - labelOffset) : r.y + r.h / 2 - middleShift)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// handle hover interaction with quadtree probing
|
||||
@ -367,41 +277,12 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
xValues,
|
||||
xSplits,
|
||||
|
||||
// pathbuilders
|
||||
drawBars,
|
||||
draw,
|
||||
barsBuilder,
|
||||
|
||||
// hooks
|
||||
init,
|
||||
drawClear,
|
||||
draw,
|
||||
interpolateTooltip,
|
||||
};
|
||||
}
|
||||
|
||||
type WalkTwoCb = null | ((idx: number, offPx: number, dimPx: number) => void);
|
||||
|
||||
function walkTwo(
|
||||
groupWidth: number,
|
||||
barWidth: number,
|
||||
yIdx: number,
|
||||
xCount: number,
|
||||
yCount: number,
|
||||
xDim: number,
|
||||
xDraw?: WalkTwoCb,
|
||||
yDraw?: WalkTwoCb
|
||||
) {
|
||||
distribute(xCount, groupWidth, groupDistr, null, (ix, offPct, dimPct) => {
|
||||
let groupOffPx = xDim * offPct;
|
||||
let groupWidPx = xDim * dimPct;
|
||||
|
||||
xDraw && xDraw(ix, groupOffPx, groupWidPx);
|
||||
|
||||
yDraw &&
|
||||
distribute(yCount, barWidth, barDistr, yIdx, (iy, offPct, dimPct) => {
|
||||
let barOffPx = groupWidPx * offPct;
|
||||
let barWidPx = groupWidPx * dimPct;
|
||||
|
||||
yDraw(ix, groupOffPx + barOffPx, barWidPx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -121,13 +121,13 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
||||
|
||||
builder.addSeries({
|
||||
scaleKey,
|
||||
pxAlign: false,
|
||||
pxAlign: true,
|
||||
lineWidth: customConfig.lineWidth,
|
||||
lineColor: seriesColor,
|
||||
fillOpacity: customConfig.fillOpacity,
|
||||
theme,
|
||||
colorMode,
|
||||
pathBuilder: config.drawBars,
|
||||
pathBuilder: config.barsBuilder,
|
||||
show: !customConfig.hideFrom?.viz,
|
||||
gradientMode: customConfig.gradientMode,
|
||||
thresholds: field.config.thresholds,
|
||||
|
Loading…
Reference in New Issue
Block a user