123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- import { orderBy } from 'lodash';
- import { Padding } from 'uplot';
- import {
- ArrayVector,
- DataFrame,
- Field,
- FieldType,
- formattedValueToString,
- getDisplayProcessor,
- getFieldColorModeForField,
- getFieldSeriesColor,
- GrafanaTheme2,
- outerJoinDataFrames,
- reduceField,
- VizOrientation,
- } from '@grafana/data';
- import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames';
- import {
- AxisPlacement,
- ScaleDirection,
- ScaleDistribution,
- ScaleOrientation,
- StackingMode,
- VizLegendOptions,
- } from '@grafana/schema';
- import { FIXED_UNIT, measureText, UPlotConfigBuilder, UPlotConfigPrepFn, UPLOT_AXIS_FONT_SIZE } from '@grafana/ui';
- import { getStackingGroups } from '@grafana/ui/src/components/uPlot/utils';
- import { findField } from 'app/features/dimensions';
- import { BarsOptions, getConfig } from './bars';
- import { BarChartFieldConfig, PanelOptions, defaultBarChartFieldConfig } from './models.gen';
- import { BarChartDisplayValues, BarChartDisplayWarning } from './types';
- function getBarCharScaleOrientation(orientation: VizOrientation) {
- if (orientation === VizOrientation.Vertical) {
- return {
- xOri: ScaleOrientation.Horizontal,
- xDir: ScaleDirection.Right,
- yOri: ScaleOrientation.Vertical,
- yDir: ScaleDirection.Up,
- };
- }
- return {
- xOri: ScaleOrientation.Vertical,
- xDir: ScaleDirection.Down,
- yOri: ScaleOrientation.Horizontal,
- yDir: ScaleDirection.Right,
- };
- }
- export interface BarChartOptionsEX extends PanelOptions {
- rawValue: (seriesIdx: number, valueIdx: number) => number | null;
- getColor?: (seriesIdx: number, valueIdx: number, value: any) => string | null;
- fillOpacity?: number;
- }
- export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
- frame,
- theme,
- orientation,
- showValue,
- groupWidth,
- barWidth,
- barRadius = 0,
- stacking,
- text,
- rawValue,
- getColor,
- fillOpacity,
- allFrames,
- xTickLabelRotation,
- xTickLabelMaxLength,
- xTickLabelSpacing = 0,
- legend,
- }) => {
- const builder = new UPlotConfigBuilder();
- const defaultValueFormatter = (seriesIdx: number, value: any) => {
- return shortenValue(formattedValueToString(frame.fields[seriesIdx].display!(value)), xTickLabelMaxLength);
- };
- // bar orientation -> x scale orientation & direction
- const vizOrientation = getBarCharScaleOrientation(orientation);
- const formatValue = defaultValueFormatter;
- // Use bar width when only one field
- if (frame.fields.length === 2) {
- groupWidth = barWidth;
- barWidth = 1;
- }
- const opts: BarsOptions = {
- xOri: vizOrientation.xOri,
- xDir: vizOrientation.xDir,
- groupWidth,
- barWidth,
- barRadius,
- stacking,
- rawValue,
- getColor,
- fillOpacity,
- formatValue,
- text,
- showValue,
- legend,
- xSpacing: xTickLabelSpacing,
- xTimeAuto: frame.fields[0]?.type === FieldType.time && !frame.fields[0].config.unit?.startsWith('time:'),
- };
- const config = getConfig(opts, theme);
- builder.setCursor(config.cursor);
- builder.addHook('init', config.init);
- builder.addHook('drawClear', config.drawClear);
- builder.addHook('draw', config.draw);
- builder.setTooltipInterpolator(config.interpolateTooltip);
- if (vizOrientation.xOri === ScaleOrientation.Horizontal && xTickLabelRotation !== 0) {
- builder.setPadding(getRotationPadding(frame, xTickLabelRotation, xTickLabelMaxLength));
- }
- builder.setPrepData(config.prepData);
- builder.addScale({
- scaleKey: 'x',
- isTime: false,
- range: config.xRange,
- distribution: ScaleDistribution.Ordinal,
- orientation: vizOrientation.xOri,
- direction: vizOrientation.xDir,
- });
- const xFieldAxisPlacement =
- frame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden
- ? vizOrientation.xOri === ScaleOrientation.Horizontal
- ? AxisPlacement.Bottom
- : AxisPlacement.Left
- : AxisPlacement.Hidden;
- const xFieldAxisShow = frame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden;
- builder.addAxis({
- scaleKey: 'x',
- isTime: false,
- placement: xFieldAxisPlacement,
- label: frame.fields[0].config.custom?.axisLabel,
- splits: config.xSplits,
- values: config.xValues,
- grid: { show: false },
- ticks: { show: false },
- gap: 15,
- tickLabelRotation: xTickLabelRotation * -1,
- theme,
- show: xFieldAxisShow,
- });
- let seriesIndex = 0;
- const legendOrdered = isLegendOrdered(legend);
- // iterate the y values
- for (let i = 1; i < frame.fields.length; i++) {
- const field = frame.fields[i];
- seriesIndex++;
- const customConfig: BarChartFieldConfig = { ...defaultBarChartFieldConfig, ...field.config.custom };
- const scaleKey = field.config.unit || FIXED_UNIT;
- const colorMode = getFieldColorModeForField(field);
- const scaleColor = getFieldSeriesColor(field, theme);
- const seriesColor = scaleColor.color;
- // make barcharts start at 0 unless explicitly overridden
- let softMin = customConfig.axisSoftMin;
- let softMax = customConfig.axisSoftMax;
- if (softMin == null && field.config.min == null) {
- softMin = 0;
- }
- if (softMax == null && field.config.max == null) {
- softMax = 0;
- }
- builder.addSeries({
- scaleKey,
- pxAlign: true,
- lineWidth: customConfig.lineWidth,
- lineColor: seriesColor,
- fillOpacity: customConfig.fillOpacity,
- theme,
- colorMode,
- pathBuilder: config.barsBuilder,
- show: !customConfig.hideFrom?.viz,
- gradientMode: customConfig.gradientMode,
- thresholds: field.config.thresholds,
- hardMin: field.config.min,
- hardMax: field.config.max,
- softMin,
- softMax,
- // The following properties are not used in the uPlot config, but are utilized as transport for legend config
- // PlotLegend currently gets unfiltered DataFrame[], so index must be into that field array, not the prepped frame's which we're iterating here
- dataFrameFieldIndex: {
- fieldIndex: legendOrdered
- ? i
- : allFrames[0].fields.findIndex(
- (f) => f.type === FieldType.number && f.state?.seriesIndex === seriesIndex - 1
- ),
- frameIndex: 0,
- },
- });
- // The builder will manage unique scaleKeys and combine where appropriate
- builder.addScale({
- scaleKey,
- min: field.config.min,
- max: field.config.max,
- softMin,
- softMax,
- orientation: vizOrientation.yOri,
- direction: vizOrientation.yDir,
- distribution: customConfig.scaleDistribution?.type,
- log: customConfig.scaleDistribution?.log,
- });
- if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
- let placement = customConfig.axisPlacement;
- if (!placement || placement === AxisPlacement.Auto) {
- placement = AxisPlacement.Left;
- }
- if (vizOrientation.xOri === 1) {
- if (placement === AxisPlacement.Left) {
- placement = AxisPlacement.Bottom;
- }
- if (placement === AxisPlacement.Right) {
- placement = AxisPlacement.Top;
- }
- }
- builder.addAxis({
- scaleKey,
- label: customConfig.axisLabel,
- size: customConfig.axisWidth,
- placement,
- formatValue: (v) => formattedValueToString(field.display!(v)),
- theme,
- grid: { show: customConfig.axisGridShow },
- });
- }
- }
- let stackingGroups = getStackingGroups(frame);
- builder.setStackingGroups(stackingGroups);
- return builder;
- };
- function shortenValue(value: string, length: number) {
- if (value.length > length) {
- return value.substring(0, length).concat('...');
- } else {
- return value;
- }
- }
- function getRotationPadding(frame: DataFrame, rotateLabel: number, valueMaxLength: number): Padding {
- const values = frame.fields[0].values;
- const fontSize = UPLOT_AXIS_FONT_SIZE;
- const displayProcessor = frame.fields[0].display ?? ((v) => v);
- let maxLength = 0;
- for (let i = 0; i < values.length; i++) {
- let size = measureText(
- shortenValue(formattedValueToString(displayProcessor(values.get(i))), valueMaxLength),
- fontSize
- );
- maxLength = size.width > maxLength ? size.width : maxLength;
- }
- // Add padding to the right if the labels are rotated in a way that makes the last label extend outside the graph.
- const paddingRight =
- rotateLabel > 0
- ? Math.cos((rotateLabel * Math.PI) / 180) *
- measureText(
- shortenValue(formattedValueToString(displayProcessor(values.get(values.length - 1))), valueMaxLength),
- fontSize
- ).width
- : 0;
- // Add padding to the left if the labels are rotated in a way that makes the first label extend outside the graph.
- const paddingLeft =
- rotateLabel < 0
- ? Math.cos((rotateLabel * -1 * Math.PI) / 180) *
- measureText(shortenValue(formattedValueToString(displayProcessor(values.get(0))), valueMaxLength), fontSize)
- .width
- : 0;
- // Add padding to the bottom to avoid clipping the rotated labels.
- const paddingBottom = Math.sin(((rotateLabel >= 0 ? rotateLabel : rotateLabel * -1) * Math.PI) / 180) * maxLength;
- return [0, paddingRight, paddingBottom, paddingLeft];
- }
- /** @internal */
- export function prepareBarChartDisplayValues(
- series: DataFrame[],
- theme: GrafanaTheme2,
- options: PanelOptions
- ): BarChartDisplayValues | BarChartDisplayWarning {
- if (!series?.length) {
- return { warn: 'No data in response' };
- }
- // Bar chart requires a single frame
- const frame =
- series.length === 1
- ? maybeSortFrame(
- series[0],
- series[0].fields.findIndex((f) => f.type === FieldType.time)
- )
- : outerJoinDataFrames({ frames: series });
- if (!frame) {
- return { warn: 'Unable to join data' };
- }
- // Color by a field different than the input
- let colorByField: Field | undefined = undefined;
- if (options.colorByField) {
- colorByField = findField(frame, options.colorByField);
- if (!colorByField) {
- return { warn: 'Color field not found' };
- }
- }
- let xField: Field | undefined = undefined;
- if (options.xField) {
- xField = findField(frame, options.xField);
- if (!xField) {
- return { warn: 'Configured x field not found' };
- }
- }
- let stringField: Field | undefined = undefined;
- let timeField: Field | undefined = undefined;
- let fields: Field[] = [];
- for (const field of frame.fields) {
- if (field === xField) {
- continue;
- }
- switch (field.type) {
- case FieldType.string:
- if (!stringField) {
- stringField = field;
- }
- break;
- case FieldType.time:
- if (!timeField) {
- timeField = field;
- }
- break;
- case FieldType.number: {
- const copy = {
- ...field,
- state: {
- ...field.state,
- seriesIndex: fields.length, // off by one?
- },
- config: {
- ...field.config,
- custom: {
- ...field.config.custom,
- stacking: {
- group: '_',
- mode: options.stacking,
- },
- },
- },
- values: new ArrayVector(
- field.values.toArray().map((v) => {
- if (!(Number.isFinite(v) || v == null)) {
- return null;
- }
- return v;
- })
- ),
- };
- if (options.stacking === StackingMode.Percent) {
- copy.config.unit = 'percentunit';
- copy.display = getDisplayProcessor({ field: copy, theme });
- }
- fields.push(copy);
- }
- }
- }
- let firstField = xField;
- if (!firstField) {
- firstField = stringField || timeField;
- }
- if (!firstField) {
- return {
- warn: 'Bar charts requires a string or time field',
- };
- }
- if (!fields.length) {
- return {
- warn: 'No numeric fields found',
- };
- }
- // Show the first number value
- if (colorByField && fields.length > 1) {
- const firstNumber = fields.find((f) => f !== colorByField);
- if (firstNumber) {
- fields = [firstNumber];
- }
- }
- if (isLegendOrdered(options.legend)) {
- const sortKey = options.legend.sortBy!.toLowerCase();
- const reducers = options.legend.calcs ?? [sortKey];
- fields = orderBy(
- fields,
- (field) => {
- return reduceField({ field, reducers })[sortKey];
- },
- options.legend.sortDesc ? 'desc' : 'asc'
- );
- }
- // String field is first
- fields.unshift(firstField);
- return {
- aligned: frame,
- colorByField,
- viz: [
- {
- length: firstField.values.length,
- fields: fields, // ideally: fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.viz)),
- },
- ],
- };
- }
- export const isLegendOrdered = (options: VizLegendOptions) => Boolean(options?.sortBy && options.sortDesc !== null);
|