mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Timeline: Text align option (#34087)
* Timeline: Text align option, but does not work * working text alignment * Refactoring and fixing rendering text values on the right edge that does not fit Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
@@ -17,6 +17,7 @@ export function pointWithin(px: number, py: number, rlft: number, rtop: number,
|
||||
export class Quadtree {
|
||||
o: Rect[];
|
||||
q: Quads | null;
|
||||
|
||||
constructor(public x: number, public y: number, public w: number, public h: number, public l: number = 0) {
|
||||
this.o = [];
|
||||
this.q = null;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { PanelContext, PanelContextRoot, GraphNG, GraphNGProps, BarValueVisibility } from '@grafana/ui';
|
||||
import { DataFrame, FieldType, TimeRange } from '@grafana/data';
|
||||
import { preparePlotConfigBuilder } from './utils';
|
||||
import { TimelineMode } from './types';
|
||||
import { TimelineMode, TimelineValueAlignment } from './types';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
@@ -11,10 +11,11 @@ export interface TimelineProps extends Omit<GraphNGProps, 'prepConfig' | 'propsT
|
||||
mode: TimelineMode;
|
||||
rowHeight: number;
|
||||
showValue: BarValueVisibility;
|
||||
alignValue: TimelineValueAlignment;
|
||||
colWidth?: number;
|
||||
}
|
||||
|
||||
const propsToDiff = ['mode', 'rowHeight', 'colWidth', 'showValue'];
|
||||
const propsToDiff = ['mode', 'rowHeight', 'colWidth', 'showValue', 'alignValue'];
|
||||
|
||||
export class TimelineChart extends React.Component<TimelineProps> {
|
||||
static contextType = PanelContextRoot;
|
||||
|
||||
@@ -39,7 +39,7 @@ export const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
height={height}
|
||||
{...options}
|
||||
>
|
||||
{(config, alignedDataFrame) => <ZoomPlugin config={config} onZoom={onChangeTimeRange} />}
|
||||
{(config) => <ZoomPlugin config={config} onZoom={onChangeTimeRange} />}
|
||||
</TimelineChart>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -79,6 +79,19 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
|
||||
},
|
||||
defaultValue: BarValueVisibility.Always,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'alignValue',
|
||||
name: 'Align value',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: 'left', label: 'Left' },
|
||||
{ value: 'center', label: 'Center' },
|
||||
{ value: 'right', label: 'Right' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'center',
|
||||
showIf: ({ mode }) => mode === TimelineMode.Changes,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'rowHeight',
|
||||
name: 'Row height',
|
||||
|
||||
@@ -2,7 +2,7 @@ import uPlot, { Series, Cursor } from 'uplot';
|
||||
import { FIXED_UNIT } from '@grafana/ui/src/components/GraphNG/GraphNG';
|
||||
import { Quadtree, Rect, pointWithin } from 'app/plugins/panel/barchart/quadtree';
|
||||
import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute';
|
||||
import { TimelineMode } from './types';
|
||||
import { TimelineMode, TimelineValueAlignment } from './types';
|
||||
import { GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||
import { BarValueVisibility } from '@grafana/ui';
|
||||
|
||||
@@ -23,6 +23,11 @@ function walk(rowHeight: number, yIdx: number | null, count: number, dim: number
|
||||
});
|
||||
}
|
||||
|
||||
interface TimelineBoxRect extends Rect {
|
||||
left: number;
|
||||
strokeWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -33,6 +38,7 @@ export interface TimelineCoreOptions {
|
||||
colWidth?: number;
|
||||
theme: GrafanaTheme2;
|
||||
showValue: BarValueVisibility;
|
||||
alignValue: TimelineValueAlignment;
|
||||
isDiscrete: (seriesIdx: number) => boolean;
|
||||
colorLookup: (seriesIdx: number, value: any) => string;
|
||||
label: (seriesIdx: number) => string;
|
||||
@@ -55,6 +61,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
rowHeight = 0,
|
||||
colWidth = 0,
|
||||
showValue,
|
||||
alignValue,
|
||||
theme,
|
||||
label,
|
||||
fill,
|
||||
@@ -78,6 +85,15 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
return mark;
|
||||
});
|
||||
|
||||
// alignement-aware text position cache filled by drawPaths->putBox for use in drawPoints
|
||||
let boxRectsBySeries: TimelineBoxRect[][];
|
||||
|
||||
const resetBoxRectsBySeries = (count: number) => {
|
||||
boxRectsBySeries = Array(numSeries)
|
||||
.fill(null)
|
||||
.map((v) => Array(count).fill(null));
|
||||
};
|
||||
|
||||
const font = `500 ${Math.round(12 * devicePixelRatio)}px ${theme.typography.fontFamily}`;
|
||||
const hovered: Array<Rect | null> = Array(numSeries).fill(null);
|
||||
|
||||
@@ -108,10 +124,10 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
rect: uPlot.RectH,
|
||||
xOff: number,
|
||||
yOff: number,
|
||||
lft: number,
|
||||
left: number,
|
||||
top: number,
|
||||
wid: number,
|
||||
hgt: number,
|
||||
boxWidth: number,
|
||||
boxHeight: number,
|
||||
strokeWidth: number,
|
||||
seriesIdx: number,
|
||||
valueIdx: number,
|
||||
@@ -126,7 +142,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
fillPaths.set(fillStyle, (fillPath = new Path2D()));
|
||||
}
|
||||
|
||||
rect(fillPath, lft, top, wid, hgt);
|
||||
rect(fillPath, left, top, boxWidth, boxHeight);
|
||||
|
||||
if (strokeWidth) {
|
||||
let strokeStyle = stroke(seriesIdx + 1, value);
|
||||
@@ -136,30 +152,41 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
strokePaths.set(strokeStyle, (strokePath = new Path2D()));
|
||||
}
|
||||
|
||||
rect(strokePath, lft + strokeWidth / 2, top + strokeWidth / 2, wid - strokeWidth, hgt - strokeWidth);
|
||||
rect(
|
||||
strokePath,
|
||||
left + strokeWidth / 2,
|
||||
top + strokeWidth / 2,
|
||||
boxWidth - strokeWidth,
|
||||
boxHeight - strokeWidth
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
rect(ctx, lft, top, wid, hgt);
|
||||
rect(ctx, left, top, boxWidth, boxHeight);
|
||||
ctx.fillStyle = fill(seriesIdx, value);
|
||||
ctx.fill();
|
||||
|
||||
if (strokeWidth) {
|
||||
ctx.beginPath();
|
||||
rect(ctx, lft + strokeWidth / 2, top + strokeWidth / 2, wid - strokeWidth, hgt - strokeWidth);
|
||||
rect(ctx, left + strokeWidth / 2, top + strokeWidth / 2, boxWidth - strokeWidth, boxHeight - strokeWidth);
|
||||
ctx.strokeStyle = stroke(seriesIdx, value);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
qt.add({
|
||||
x: round(lft - xOff),
|
||||
const boxRect = (boxRectsBySeries[seriesIdx][valueIdx] = {
|
||||
x: round(left - xOff),
|
||||
y: round(top - yOff),
|
||||
w: wid,
|
||||
h: hgt,
|
||||
w: boxWidth,
|
||||
h: boxHeight,
|
||||
sidx: seriesIdx + 1,
|
||||
didx: valueIdx,
|
||||
// These two are needed for later text positioning
|
||||
left: left,
|
||||
strokeWidth,
|
||||
});
|
||||
|
||||
qt.add(boxRect);
|
||||
}
|
||||
|
||||
const drawPaths: Series.PathBuilder = (u, sidx, idx0, idx1) => {
|
||||
@@ -259,7 +286,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
u.ctx.clip();
|
||||
|
||||
u.ctx.font = font;
|
||||
u.ctx.textAlign = mode === TimelineMode.Changes ? 'left' : 'center';
|
||||
u.ctx.textAlign = alignValue;
|
||||
u.ctx.textBaseline = 'middle';
|
||||
|
||||
uPlot.orient(
|
||||
@@ -285,13 +312,14 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
|
||||
for (let ix = 0; ix < dataY.length; ix++) {
|
||||
if (dataY[ix] != null) {
|
||||
let x = valToPosX(dataX[ix], scaleX, xDim, xOff);
|
||||
const boxRect = boxRectsBySeries[sidx - 1][ix];
|
||||
|
||||
// For the left aligned values shift them 2 pixels of edge
|
||||
if (mode === TimelineMode.Changes) {
|
||||
x += 2;
|
||||
// Todo refine this to better know when to not render text (when values do not fit)
|
||||
if (boxRect.w < 20) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const x = getTextPositionOffet(boxRect, alignValue);
|
||||
const valueColor = colorLookup(sidx, dataY[ix]);
|
||||
|
||||
u.ctx.fillStyle = theme.colors.getContrastText(valueColor, 3);
|
||||
@@ -318,6 +346,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||
|
||||
qt.clear();
|
||||
resetBoxRectsBySeries(u.data[0].length);
|
||||
|
||||
// force-clear the path cache to cause drawBars() to rebuild new quadtree
|
||||
u.series.forEach((s) => {
|
||||
@@ -440,3 +469,16 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
setCursor,
|
||||
};
|
||||
}
|
||||
|
||||
function getTextPositionOffet(rect: TimelineBoxRect, alignValue: TimelineValueAlignment) {
|
||||
// left or right aligned values shift 2 pixels inside edge
|
||||
const textPadding = alignValue === 'left' ? 2 : alignValue === 'right' ? -2 : 0;
|
||||
const { left, w, strokeWidth } = rect;
|
||||
|
||||
return (
|
||||
left +
|
||||
strokeWidth / 2 +
|
||||
(alignValue === 'center' ? w / 2 - strokeWidth / 2 : alignValue === 'right' ? w - strokeWidth / 2 : 0) +
|
||||
textPadding
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@ export interface TimelineOptions {
|
||||
showValue: BarValueVisibility;
|
||||
rowHeight: number;
|
||||
colWidth?: number;
|
||||
alignValue: TimelineValueAlignment;
|
||||
}
|
||||
|
||||
export type TimelineValueAlignment = 'center' | 'left' | 'right';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
|
||||
@@ -9,13 +9,7 @@ import {
|
||||
Field,
|
||||
FALLBACK_COLOR,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
UPlotConfigBuilder,
|
||||
FIXED_UNIT,
|
||||
SeriesVisibilityChangeMode,
|
||||
BarValueVisibility,
|
||||
UPlotConfigPrepFn,
|
||||
} from '@grafana/ui';
|
||||
import { UPlotConfigBuilder, FIXED_UNIT, SeriesVisibilityChangeMode, UPlotConfigPrepFn } from '@grafana/ui';
|
||||
import { TimelineCoreOptions, getConfig } from './timeline';
|
||||
import {
|
||||
AxisPlacement,
|
||||
@@ -25,7 +19,7 @@ import {
|
||||
} from '@grafana/ui/src/components/uPlot/config';
|
||||
import { measureText } from '@grafana/ui/src/utils/measureText';
|
||||
|
||||
import { TimelineFieldConfig, TimelineMode } from './types';
|
||||
import { TimelineFieldConfig, TimelineOptions } from './types';
|
||||
|
||||
const defaultConfig: TimelineFieldConfig = {
|
||||
lineWidth: 0,
|
||||
@@ -49,21 +43,16 @@ export function preparePlotFrame(data: DataFrame[], dimFields: XYFieldMatchers)
|
||||
});
|
||||
}
|
||||
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
mode: TimelineMode;
|
||||
rowHeight: number;
|
||||
colWidth?: number;
|
||||
showValue: BarValueVisibility;
|
||||
}> = ({
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
|
||||
frame,
|
||||
theme,
|
||||
timeZone,
|
||||
getTimeRange,
|
||||
|
||||
mode,
|
||||
rowHeight,
|
||||
colWidth,
|
||||
showValue,
|
||||
alignValue,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder(timeZone);
|
||||
|
||||
@@ -101,6 +90,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
rowHeight: rowHeight!,
|
||||
colWidth: colWidth,
|
||||
showValue: showValue!,
|
||||
alignValue,
|
||||
theme,
|
||||
label: (seriesIdx) => getFieldDisplayName(frame.fields[seriesIdx], frame),
|
||||
fill: colorLookup,
|
||||
|
||||
Reference in New Issue
Block a user