mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BarChartPanel: Adds support for Tooltip in BarChartPanel (#33938)
* Adds support for Tooltip in BarChartPanel * Revert some formatting * Remove BarChart story
This commit is contained in:
parent
60c32dc96a
commit
bf2c45db01
@ -1,66 +0,0 @@
|
|||||||
import { toDataFrame, FieldType, VizOrientation } from '@grafana/data';
|
|
||||||
import React from 'react';
|
|
||||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
|
||||||
import { BarChart } from './BarChart';
|
|
||||||
import { LegendDisplayMode } from '../VizLegend/models.gen';
|
|
||||||
import { prepDataForStorybook } from '../../utils/storybook/data';
|
|
||||||
import { useTheme2 } from '../../themes';
|
|
||||||
import { select } from '@storybook/addon-knobs';
|
|
||||||
import { BarChartOptions, BarValueVisibility } from './types';
|
|
||||||
import { StackingMode } from '../uPlot/config';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Visualizations/BarChart',
|
|
||||||
component: BarChart,
|
|
||||||
decorators: [withCenteredStory],
|
|
||||||
parameters: {
|
|
||||||
docs: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const getKnobs = () => {
|
|
||||||
return {
|
|
||||||
legendPlacement: select(
|
|
||||||
'Legend placement',
|
|
||||||
{
|
|
||||||
bottom: 'bottom',
|
|
||||||
right: 'right',
|
|
||||||
},
|
|
||||||
'bottom'
|
|
||||||
),
|
|
||||||
orientation: select(
|
|
||||||
'Bar orientation',
|
|
||||||
{
|
|
||||||
vertical: VizOrientation.Vertical,
|
|
||||||
horizontal: VizOrientation.Horizontal,
|
|
||||||
},
|
|
||||||
VizOrientation.Vertical
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Basic: React.FC = () => {
|
|
||||||
const { legendPlacement, orientation } = getKnobs();
|
|
||||||
|
|
||||||
const theme = useTheme2();
|
|
||||||
const frame = toDataFrame({
|
|
||||||
fields: [
|
|
||||||
{ name: 'x', type: FieldType.string, values: ['group 1', 'group 2'] },
|
|
||||||
{ name: 'a', type: FieldType.number, values: [10, 20] },
|
|
||||||
{ name: 'b', type: FieldType.number, values: [30, 10] },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = prepDataForStorybook([frame], theme);
|
|
||||||
|
|
||||||
const options: BarChartOptions = {
|
|
||||||
orientation: orientation,
|
|
||||||
legend: { displayMode: LegendDisplayMode.List, placement: legendPlacement, calcs: [] },
|
|
||||||
stacking: StackingMode.None,
|
|
||||||
showValue: BarValueVisibility.Always,
|
|
||||||
barWidth: 0.97,
|
|
||||||
groupWidth: 0.7,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <BarChart data={data} width={600} height={400} {...options} />;
|
|
||||||
};
|
|
@ -1,100 +1,52 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AlignedData } from 'uplot';
|
|
||||||
import { DataFrame, TimeRange } from '@grafana/data';
|
import { DataFrame, TimeRange } from '@grafana/data';
|
||||||
import { VizLayout } from '../VizLayout/VizLayout';
|
|
||||||
import { Themeable2 } from '../../types';
|
|
||||||
import { UPlotChart } from '../uPlot/Plot';
|
|
||||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
|
||||||
import { GraphNGLegendEvent } from '../GraphNG/types';
|
|
||||||
import { BarChartOptions } from './types';
|
import { BarChartOptions } from './types';
|
||||||
import { withTheme2 } from '../../themes/ThemeContext';
|
import { withTheme2 } from '../../themes/ThemeContext';
|
||||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
||||||
import { pluginLog, preparePlotData } from '../uPlot/utils';
|
|
||||||
import { LegendDisplayMode } from '../VizLegend/models.gen';
|
import { LegendDisplayMode } from '../VizLegend/models.gen';
|
||||||
import { PlotLegend } from '../uPlot/PlotLegend';
|
import { PlotLegend } from '../uPlot/PlotLegend';
|
||||||
|
import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG';
|
||||||
|
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface BarChartProps extends Themeable2, BarChartOptions {
|
export interface BarChartProps
|
||||||
height: number;
|
extends BarChartOptions,
|
||||||
width: number;
|
Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'> {}
|
||||||
data: DataFrame[];
|
|
||||||
structureRev?: number; // a number that will change when the data[] structure changes
|
|
||||||
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BarChartState {
|
const propsToDiff: string[] = ['orientation', 'barWidth', 'groupWidth', 'showValue'];
|
||||||
data: AlignedData;
|
|
||||||
alignedDataFrame: DataFrame;
|
|
||||||
config?: UPlotConfigBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnthemedBarChart extends React.Component<BarChartProps, BarChartState> {
|
class UnthemedBarChart extends React.Component<BarChartProps> {
|
||||||
constructor(props: BarChartProps) {
|
prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => {
|
||||||
super(props);
|
const { eventBus } = this.context;
|
||||||
const alignedDataFrame = preparePlotFrame(props.data);
|
const { theme, timeZone, orientation, barWidth, showValue, groupWidth, stacking, legend, tooltip } = this.props;
|
||||||
if (!alignedDataFrame) {
|
return preparePlotConfigBuilder({
|
||||||
return;
|
frame: alignedFrame,
|
||||||
}
|
getTimeRange,
|
||||||
const data = preparePlotData(alignedDataFrame);
|
theme,
|
||||||
const config = preparePlotConfigBuilder(alignedDataFrame, this.props.theme, this.props);
|
timeZone,
|
||||||
this.state = {
|
eventBus,
|
||||||
alignedDataFrame,
|
orientation,
|
||||||
data,
|
barWidth,
|
||||||
config,
|
showValue,
|
||||||
};
|
groupWidth,
|
||||||
}
|
stacking,
|
||||||
|
legend,
|
||||||
|
tooltip,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: BarChartProps) {
|
renderLegend = (config: UPlotConfigBuilder) => {
|
||||||
const { data, orientation, groupWidth, barWidth, showValue, structureRev } = this.props;
|
const { legend, onLegendClick, frames } = this.props;
|
||||||
const { alignedDataFrame } = this.state;
|
|
||||||
let shouldConfigUpdate = false;
|
|
||||||
let stateUpdate = {} as BarChartState;
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.state.config === undefined ||
|
|
||||||
orientation !== prevProps.orientation ||
|
|
||||||
groupWidth !== prevProps.groupWidth ||
|
|
||||||
barWidth !== prevProps.barWidth ||
|
|
||||||
showValue !== prevProps.showValue
|
|
||||||
) {
|
|
||||||
shouldConfigUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data !== prevProps.data || shouldConfigUpdate) {
|
|
||||||
const hasStructureChanged = structureRev !== prevProps.structureRev || !structureRev;
|
|
||||||
const alignedData = preparePlotFrame(data);
|
|
||||||
|
|
||||||
if (!alignedData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stateUpdate = {
|
|
||||||
alignedDataFrame: alignedData,
|
|
||||||
data: preparePlotData(alignedData),
|
|
||||||
};
|
|
||||||
if (shouldConfigUpdate || hasStructureChanged) {
|
|
||||||
pluginLog('BarChart', false, 'updating config');
|
|
||||||
const builder = preparePlotConfigBuilder(alignedDataFrame, this.props.theme, this.props);
|
|
||||||
stateUpdate = { ...stateUpdate, config: builder };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(stateUpdate).length > 0) {
|
|
||||||
this.setState(stateUpdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLegend() {
|
|
||||||
const { legend, onLegendClick, data } = this.props;
|
|
||||||
const { config } = this.state;
|
|
||||||
|
|
||||||
if (!config || legend.displayMode === LegendDisplayMode.Hidden) {
|
if (!config || legend.displayMode === LegendDisplayMode.Hidden) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlotLegend
|
<PlotLegend
|
||||||
data={data}
|
data={frames}
|
||||||
config={config}
|
config={config}
|
||||||
onLegendClick={onLegendClick}
|
onLegendClick={onLegendClick}
|
||||||
maxHeight="35%"
|
maxHeight="35%"
|
||||||
@ -102,31 +54,21 @@ class UnthemedBarChart extends React.Component<BarChartProps, BarChartState> {
|
|||||||
{...legend}
|
{...legend}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { width, height } = this.props;
|
|
||||||
const { config, data } = this.state;
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VizLayout width={width} height={height} legend={this.renderLegend()}>
|
<GraphNG
|
||||||
{(vizWidth: number, vizHeight: number) => (
|
{...this.props}
|
||||||
<UPlotChart
|
frames={this.props.frames}
|
||||||
data={data}
|
prepConfig={this.prepConfig}
|
||||||
config={config}
|
propsToDiff={propsToDiff}
|
||||||
width={vizWidth}
|
preparePlotFrame={preparePlotFrame}
|
||||||
height={vizHeight}
|
renderLegend={this.renderLegend as any}
|
||||||
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</VizLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BarChart = withTheme2(UnthemedBarChart);
|
export const BarChart = withTheme2(UnthemedBarChart);
|
||||||
BarChart.displayName = 'GraphNG';
|
BarChart.displayName = 'BarChart';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`GraphNG utils preparePlotConfigBuilder orientation 1`] = `
|
exports[`BarChart utils preparePlotConfigBuilder orientation 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"axes": Array [
|
"axes": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -61,13 +61,10 @@ Object {
|
|||||||
},
|
},
|
||||||
"points": Object {
|
"points": Object {
|
||||||
"fill": [Function],
|
"fill": [Function],
|
||||||
"show": false,
|
|
||||||
"size": [Function],
|
"size": [Function],
|
||||||
"stroke": [Function],
|
"stroke": [Function],
|
||||||
"width": [Function],
|
"width": [Function],
|
||||||
},
|
},
|
||||||
"x": false,
|
|
||||||
"y": false,
|
|
||||||
},
|
},
|
||||||
"hooks": Object {
|
"hooks": Object {
|
||||||
"drawClear": Array [
|
"drawClear": Array [
|
||||||
@ -76,9 +73,6 @@ Object {
|
|||||||
"init": Array [
|
"init": Array [
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
"setCursor": Array [
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"scales": Object {
|
"scales": Object {
|
||||||
"m/s": Object {
|
"m/s": Object {
|
||||||
@ -100,9 +94,7 @@ Object {
|
|||||||
"time": false,
|
"time": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"select": Object {
|
"select": undefined,
|
||||||
"show": false,
|
|
||||||
},
|
|
||||||
"series": Array [
|
"series": Array [
|
||||||
Object {},
|
Object {},
|
||||||
Object {
|
Object {
|
||||||
@ -126,7 +118,7 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`GraphNG utils preparePlotConfigBuilder orientation 2`] = `
|
exports[`BarChart utils preparePlotConfigBuilder orientation 2`] = `
|
||||||
Object {
|
Object {
|
||||||
"axes": Array [
|
"axes": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -187,13 +179,10 @@ Object {
|
|||||||
},
|
},
|
||||||
"points": Object {
|
"points": Object {
|
||||||
"fill": [Function],
|
"fill": [Function],
|
||||||
"show": false,
|
|
||||||
"size": [Function],
|
"size": [Function],
|
||||||
"stroke": [Function],
|
"stroke": [Function],
|
||||||
"width": [Function],
|
"width": [Function],
|
||||||
},
|
},
|
||||||
"x": false,
|
|
||||||
"y": false,
|
|
||||||
},
|
},
|
||||||
"hooks": Object {
|
"hooks": Object {
|
||||||
"drawClear": Array [
|
"drawClear": Array [
|
||||||
@ -202,9 +191,6 @@ Object {
|
|||||||
"init": Array [
|
"init": Array [
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
"setCursor": Array [
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"scales": Object {
|
"scales": Object {
|
||||||
"m/s": Object {
|
"m/s": Object {
|
||||||
@ -226,9 +212,7 @@ Object {
|
|||||||
"time": false,
|
"time": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"select": Object {
|
"select": undefined,
|
||||||
"show": false,
|
|
||||||
},
|
|
||||||
"series": Array [
|
"series": Array [
|
||||||
Object {},
|
Object {},
|
||||||
Object {
|
Object {
|
||||||
@ -252,7 +236,7 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`GraphNG utils preparePlotConfigBuilder orientation 3`] = `
|
exports[`BarChart utils preparePlotConfigBuilder orientation 3`] = `
|
||||||
Object {
|
Object {
|
||||||
"axes": Array [
|
"axes": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -313,13 +297,10 @@ Object {
|
|||||||
},
|
},
|
||||||
"points": Object {
|
"points": Object {
|
||||||
"fill": [Function],
|
"fill": [Function],
|
||||||
"show": false,
|
|
||||||
"size": [Function],
|
"size": [Function],
|
||||||
"stroke": [Function],
|
"stroke": [Function],
|
||||||
"width": [Function],
|
"width": [Function],
|
||||||
},
|
},
|
||||||
"x": false,
|
|
||||||
"y": false,
|
|
||||||
},
|
},
|
||||||
"hooks": Object {
|
"hooks": Object {
|
||||||
"drawClear": Array [
|
"drawClear": Array [
|
||||||
@ -328,9 +309,6 @@ Object {
|
|||||||
"init": Array [
|
"init": Array [
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
"setCursor": Array [
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"scales": Object {
|
"scales": Object {
|
||||||
"m/s": Object {
|
"m/s": Object {
|
||||||
@ -352,9 +330,7 @@ Object {
|
|||||||
"time": false,
|
"time": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"select": Object {
|
"select": undefined,
|
||||||
"show": false,
|
|
||||||
},
|
|
||||||
"series": Array [
|
"series": Array [
|
||||||
Object {},
|
Object {},
|
||||||
Object {
|
Object {
|
||||||
@ -378,7 +354,7 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`GraphNG utils preparePlotConfigBuilder stacking 1`] = `
|
exports[`BarChart utils preparePlotConfigBuilder stacking 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"axes": Array [
|
"axes": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -439,13 +415,10 @@ Object {
|
|||||||
},
|
},
|
||||||
"points": Object {
|
"points": Object {
|
||||||
"fill": [Function],
|
"fill": [Function],
|
||||||
"show": false,
|
|
||||||
"size": [Function],
|
"size": [Function],
|
||||||
"stroke": [Function],
|
"stroke": [Function],
|
||||||
"width": [Function],
|
"width": [Function],
|
||||||
},
|
},
|
||||||
"x": false,
|
|
||||||
"y": false,
|
|
||||||
},
|
},
|
||||||
"hooks": Object {
|
"hooks": Object {
|
||||||
"drawClear": Array [
|
"drawClear": Array [
|
||||||
@ -454,9 +427,6 @@ Object {
|
|||||||
"init": Array [
|
"init": Array [
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
"setCursor": Array [
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"scales": Object {
|
"scales": Object {
|
||||||
"m/s": Object {
|
"m/s": Object {
|
||||||
@ -478,9 +448,7 @@ Object {
|
|||||||
"time": false,
|
"time": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"select": Object {
|
"select": undefined,
|
||||||
"show": false,
|
|
||||||
},
|
|
||||||
"series": Array [
|
"series": Array [
|
||||||
Object {},
|
Object {},
|
||||||
Object {
|
Object {
|
||||||
@ -504,7 +472,7 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`GraphNG utils preparePlotConfigBuilder stacking 2`] = `
|
exports[`BarChart utils preparePlotConfigBuilder stacking 2`] = `
|
||||||
Object {
|
Object {
|
||||||
"axes": Array [
|
"axes": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -565,13 +533,10 @@ Object {
|
|||||||
},
|
},
|
||||||
"points": Object {
|
"points": Object {
|
||||||
"fill": [Function],
|
"fill": [Function],
|
||||||
"show": false,
|
|
||||||
"size": [Function],
|
"size": [Function],
|
||||||
"stroke": [Function],
|
"stroke": [Function],
|
||||||
"width": [Function],
|
"width": [Function],
|
||||||
},
|
},
|
||||||
"x": false,
|
|
||||||
"y": false,
|
|
||||||
},
|
},
|
||||||
"hooks": Object {
|
"hooks": Object {
|
||||||
"drawClear": Array [
|
"drawClear": Array [
|
||||||
@ -580,9 +545,6 @@ Object {
|
|||||||
"init": Array [
|
"init": Array [
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
"setCursor": Array [
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"scales": Object {
|
"scales": Object {
|
||||||
"m/s": Object {
|
"m/s": Object {
|
||||||
@ -604,9 +566,7 @@ Object {
|
|||||||
"time": false,
|
"time": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"select": Object {
|
"select": undefined,
|
||||||
"show": false,
|
|
||||||
},
|
|
||||||
"series": Array [
|
"series": Array [
|
||||||
Object {},
|
Object {},
|
||||||
Object {
|
Object {
|
||||||
@ -630,7 +590,7 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`GraphNG utils preparePlotConfigBuilder stacking 3`] = `
|
exports[`BarChart utils preparePlotConfigBuilder stacking 3`] = `
|
||||||
Object {
|
Object {
|
||||||
"axes": Array [
|
"axes": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -691,13 +651,10 @@ Object {
|
|||||||
},
|
},
|
||||||
"points": Object {
|
"points": Object {
|
||||||
"fill": [Function],
|
"fill": [Function],
|
||||||
"show": false,
|
|
||||||
"size": [Function],
|
"size": [Function],
|
||||||
"stroke": [Function],
|
"stroke": [Function],
|
||||||
"width": [Function],
|
"width": [Function],
|
||||||
},
|
},
|
||||||
"x": false,
|
|
||||||
"y": false,
|
|
||||||
},
|
},
|
||||||
"hooks": Object {
|
"hooks": Object {
|
||||||
"drawClear": Array [
|
"drawClear": Array [
|
||||||
@ -706,9 +663,6 @@ Object {
|
|||||||
"init": Array [
|
"init": Array [
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
"setCursor": Array [
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"scales": Object {
|
"scales": Object {
|
||||||
"m/s": Object {
|
"m/s": Object {
|
||||||
@ -730,9 +684,7 @@ Object {
|
|||||||
"time": false,
|
"time": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"select": Object {
|
"select": undefined,
|
||||||
"show": false,
|
|
||||||
},
|
|
||||||
"series": Array [
|
"series": Array [
|
||||||
Object {},
|
Object {},
|
||||||
Object {
|
Object {
|
||||||
@ -756,7 +708,7 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`GraphNG utils preparePlotConfigBuilder value visibility 1`] = `
|
exports[`BarChart utils preparePlotConfigBuilder value visibility 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"axes": Array [
|
"axes": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -817,13 +769,10 @@ Object {
|
|||||||
},
|
},
|
||||||
"points": Object {
|
"points": Object {
|
||||||
"fill": [Function],
|
"fill": [Function],
|
||||||
"show": false,
|
|
||||||
"size": [Function],
|
"size": [Function],
|
||||||
"stroke": [Function],
|
"stroke": [Function],
|
||||||
"width": [Function],
|
"width": [Function],
|
||||||
},
|
},
|
||||||
"x": false,
|
|
||||||
"y": false,
|
|
||||||
},
|
},
|
||||||
"hooks": Object {
|
"hooks": Object {
|
||||||
"drawClear": Array [
|
"drawClear": Array [
|
||||||
@ -832,9 +781,6 @@ Object {
|
|||||||
"init": Array [
|
"init": Array [
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
"setCursor": Array [
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"scales": Object {
|
"scales": Object {
|
||||||
"m/s": Object {
|
"m/s": Object {
|
||||||
@ -856,9 +802,7 @@ Object {
|
|||||||
"time": false,
|
"time": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"select": Object {
|
"select": undefined,
|
||||||
"show": false,
|
|
||||||
},
|
|
||||||
"series": Array [
|
"series": Array [
|
||||||
Object {},
|
Object {},
|
||||||
Object {
|
Object {
|
||||||
@ -882,7 +826,7 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`GraphNG utils preparePlotConfigBuilder value visibility 2`] = `
|
exports[`BarChart utils preparePlotConfigBuilder value visibility 2`] = `
|
||||||
Object {
|
Object {
|
||||||
"axes": Array [
|
"axes": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -943,13 +887,10 @@ Object {
|
|||||||
},
|
},
|
||||||
"points": Object {
|
"points": Object {
|
||||||
"fill": [Function],
|
"fill": [Function],
|
||||||
"show": false,
|
|
||||||
"size": [Function],
|
"size": [Function],
|
||||||
"stroke": [Function],
|
"stroke": [Function],
|
||||||
"width": [Function],
|
"width": [Function],
|
||||||
},
|
},
|
||||||
"x": false,
|
|
||||||
"y": false,
|
|
||||||
},
|
},
|
||||||
"hooks": Object {
|
"hooks": Object {
|
||||||
"drawClear": Array [
|
"drawClear": Array [
|
||||||
@ -958,9 +899,6 @@ Object {
|
|||||||
"init": Array [
|
"init": Array [
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
"setCursor": Array [
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"scales": Object {
|
"scales": Object {
|
||||||
"m/s": Object {
|
"m/s": Object {
|
||||||
@ -982,9 +920,7 @@ Object {
|
|||||||
"time": false,
|
"time": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"select": Object {
|
"select": undefined,
|
||||||
"show": false,
|
|
||||||
},
|
|
||||||
"series": Array [
|
"series": Array [
|
||||||
Object {},
|
Object {},
|
||||||
Object {
|
Object {
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import uPlot, { Axis, Series, Cursor, Select } from 'uplot';
|
import uPlot, { Axis, Series } from 'uplot';
|
||||||
import { Quadtree, Rect, pointWithin } from './quadtree';
|
import { Quadtree, Rect, pointWithin } from './quadtree';
|
||||||
import { distribute, SPACE_BETWEEN } from './distribute';
|
import { distribute, SPACE_BETWEEN } from './distribute';
|
||||||
|
import { TooltipInterpolator } from '../uPlot/types';
|
||||||
const pxRatio = devicePixelRatio;
|
import { ScaleDirection, ScaleOrientation } from '../uPlot/config';
|
||||||
|
|
||||||
const groupDistr = SPACE_BETWEEN;
|
const groupDistr = SPACE_BETWEEN;
|
||||||
const barDistr = SPACE_BETWEEN;
|
const barDistr = SPACE_BETWEEN;
|
||||||
|
const font = Math.round(10 * devicePixelRatio) + 'px Arial';
|
||||||
const font = Math.round(10 * pxRatio) + 'px Arial';
|
|
||||||
|
|
||||||
type WalkTwoCb = null | ((idx: number, offPx: number, dimPx: number) => void);
|
type WalkTwoCb = null | ((idx: number, offPx: number, dimPx: number) => void);
|
||||||
|
|
||||||
@ -41,8 +40,8 @@ function walkTwo(
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export interface BarsOptions {
|
export interface BarsOptions {
|
||||||
xOri: 1 | 0;
|
xOri: ScaleOrientation;
|
||||||
xDir: 1 | -1;
|
xDir: ScaleDirection;
|
||||||
groupWidth: number;
|
groupWidth: number;
|
||||||
barWidth: number;
|
barWidth: number;
|
||||||
formatValue?: (seriesIdx: number, value: any) => string;
|
formatValue?: (seriesIdx: number, value: any) => string;
|
||||||
@ -54,11 +53,11 @@ export interface BarsOptions {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function getConfig(opts: BarsOptions) {
|
export function getConfig(opts: BarsOptions) {
|
||||||
const { xOri: ori, xDir: dir, groupWidth, barWidth, formatValue, onHover, onLeave } = opts;
|
const { xOri: ori, xDir: dir, groupWidth, barWidth, formatValue } = opts;
|
||||||
|
|
||||||
let qt: Quadtree;
|
let qt: Quadtree;
|
||||||
|
|
||||||
const drawBars: Series.PathBuilder = (u, sidx, i0, i1) => {
|
const drawBars: Series.PathBuilder = (u, sidx) => {
|
||||||
return uPlot.orient(
|
return uPlot.orient(
|
||||||
u,
|
u,
|
||||||
sidx,
|
sidx,
|
||||||
@ -112,29 +111,14 @@ export function getConfig(opts: BarsOptions) {
|
|||||||
const drawPoints: Series.Points.Show =
|
const drawPoints: Series.Points.Show =
|
||||||
formatValue == null
|
formatValue == null
|
||||||
? false
|
? false
|
||||||
: (u, sidx, i0, i1) => {
|
: (u, sidx) => {
|
||||||
u.ctx.font = font;
|
u.ctx.font = font;
|
||||||
u.ctx.fillStyle = 'white';
|
u.ctx.fillStyle = 'white';
|
||||||
|
|
||||||
uPlot.orient(
|
uPlot.orient(
|
||||||
u,
|
u,
|
||||||
sidx,
|
sidx,
|
||||||
(
|
(series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
|
||||||
series,
|
|
||||||
dataX,
|
|
||||||
dataY,
|
|
||||||
scaleX,
|
|
||||||
scaleY,
|
|
||||||
valToPosX,
|
|
||||||
valToPosY,
|
|
||||||
xOff,
|
|
||||||
yOff,
|
|
||||||
xDim,
|
|
||||||
yDim,
|
|
||||||
moveTo,
|
|
||||||
lineTo,
|
|
||||||
rect
|
|
||||||
) => {
|
|
||||||
let numGroups = dataX.length;
|
let numGroups = dataX.length;
|
||||||
let barsPerGroup = u.series.length - 1;
|
let barsPerGroup = u.series.length - 1;
|
||||||
|
|
||||||
@ -148,13 +132,11 @@ export function getConfig(opts: BarsOptions) {
|
|||||||
if (dataY[ix] != null) {
|
if (dataY[ix] != null) {
|
||||||
let yPos = valToPosY(dataY[ix]!, scaleY, yDim, yOff);
|
let yPos = valToPosY(dataY[ix]!, scaleY, yDim, yOff);
|
||||||
|
|
||||||
/* eslint-disable no-multi-spaces */
|
|
||||||
let x = ori === 0 ? Math.round(lft + barWid / 2) : Math.round(yPos);
|
let x = ori === 0 ? Math.round(lft + barWid / 2) : Math.round(yPos);
|
||||||
let y = ori === 0 ? Math.round(yPos) : Math.round(lft + barWid / 2);
|
let y = ori === 0 ? Math.round(yPos) : Math.round(lft + barWid / 2);
|
||||||
|
|
||||||
u.ctx.textAlign = ori === 0 ? 'center' : dataY[ix]! >= 0 ? 'left' : 'right';
|
u.ctx.textAlign = ori === 0 ? 'center' : dataY[ix]! >= 0 ? 'left' : 'right';
|
||||||
u.ctx.textBaseline = ori === 1 ? 'middle' : dataY[ix]! >= 0 ? 'bottom' : 'top';
|
u.ctx.textBaseline = ori === 1 ? 'middle' : dataY[ix]! >= 0 ? 'bottom' : 'top';
|
||||||
/* eslint-enable */
|
|
||||||
|
|
||||||
u.ctx.fillText(formatValue(sidx, dataY[ix]), x, y);
|
u.ctx.fillText(formatValue(sidx, dataY[ix]), x, y);
|
||||||
}
|
}
|
||||||
@ -165,23 +147,15 @@ export function getConfig(opts: BarsOptions) {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
const xSplits: Axis.Splits = (u: uPlot) => {
|
||||||
const yRange: Scale.Range = (u, dataMin, dataMax) => {
|
|
||||||
// @ts-ignore
|
|
||||||
let [min, max] = uPlot.rangeNum(0, dataMax, 0.05, true);
|
|
||||||
return [0, max];
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
const xSplits: Axis.Splits = (u: uPlot, axisIdx: number) => {
|
|
||||||
const dim = ori === 0 ? u.bbox.width : u.bbox.height;
|
const dim = ori === 0 ? u.bbox.width : u.bbox.height;
|
||||||
const _dir = dir * (ori === 0 ? 1 : -1);
|
const _dir = dir * (ori === 0 ? 1 : -1);
|
||||||
|
|
||||||
let splits: number[] = [];
|
let splits: number[] = [];
|
||||||
|
|
||||||
distribute(u.data[0].length, groupWidth, groupDistr, null, (di, lftPct, widPct) => {
|
distribute(u.data[0].length, groupWidth, groupDistr, null, (di, lftPct, widPct) => {
|
||||||
let groupLftPx = (dim * lftPct) / pxRatio;
|
let groupLftPx = (dim * lftPct) / devicePixelRatio;
|
||||||
let groupWidPx = (dim * widPct) / pxRatio;
|
let groupWidPx = (dim * widPct) / devicePixelRatio;
|
||||||
|
|
||||||
let groupCenterPx = groupLftPx + groupWidPx / 2;
|
let groupCenterPx = groupLftPx + groupWidPx / 2;
|
||||||
|
|
||||||
@ -191,7 +165,6 @@ export function getConfig(opts: BarsOptions) {
|
|||||||
return _dir === 1 ? splits : splits.reverse();
|
return _dir === 1 ? splits : splits.reverse();
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const xValues: Axis.Values = (u) => u.data[0];
|
const xValues: Axis.Values = (u) => u.data[0];
|
||||||
|
|
||||||
let hovered: Rect | null = null;
|
let hovered: Rect | null = null;
|
||||||
@ -201,21 +174,6 @@ export function getConfig(opts: BarsOptions) {
|
|||||||
barMark.style.position = 'absolute';
|
barMark.style.position = 'absolute';
|
||||||
barMark.style.background = 'rgba(255,255,255,0.4)';
|
barMark.style.background = 'rgba(255,255,255,0.4)';
|
||||||
|
|
||||||
// hide crosshair cursor & hover points
|
|
||||||
const cursor: Cursor = {
|
|
||||||
x: false,
|
|
||||||
y: false,
|
|
||||||
points: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// disable selection
|
|
||||||
// uPlot types do not export the Select interface prior to 1.6.4
|
|
||||||
const select: Partial<Select> = {
|
|
||||||
show: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = (u: uPlot) => {
|
const init = (u: uPlot) => {
|
||||||
let over = u.root.querySelector('.u-over')! as HTMLElement;
|
let over = u.root.querySelector('.u-over')! as HTMLElement;
|
||||||
over.style.overflow = 'hidden';
|
over.style.overflow = 'hidden';
|
||||||
@ -235,50 +193,49 @@ export function getConfig(opts: BarsOptions) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// handle hover interaction with quadtree probing
|
// handle hover interaction with quadtree probing
|
||||||
const setCursor = (u: uPlot) => {
|
const interpolateBarChartTooltip: TooltipInterpolator = (
|
||||||
let found: Rect | null = null;
|
updateActiveSeriesIdx,
|
||||||
let cx = u.cursor.left! * pxRatio;
|
updateActiveDatapointIdx,
|
||||||
let cy = u.cursor.top! * pxRatio;
|
updateTooltipPosition
|
||||||
|
) => {
|
||||||
|
return (u: uPlot) => {
|
||||||
|
let found: Rect | null = null;
|
||||||
|
let cx = u.cursor.left! * devicePixelRatio;
|
||||||
|
let cy = u.cursor.top! * devicePixelRatio;
|
||||||
|
|
||||||
qt.get(cx, cy, 1, 1, (o) => {
|
qt.get(cx, cy, 1, 1, (o) => {
|
||||||
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) {
|
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) {
|
||||||
found = o;
|
found = o;
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
// prettier-ignore
|
|
||||||
if (found !== hovered) {
|
|
||||||
/* eslint-disable no-multi-spaces */
|
|
||||||
barMark.style.display = '';
|
|
||||||
barMark.style.left = found!.x / pxRatio + 'px';
|
|
||||||
barMark.style.top = found!.y / pxRatio + 'px';
|
|
||||||
barMark.style.width = found!.w / pxRatio + 'px';
|
|
||||||
barMark.style.height = found!.h / pxRatio + 'px';
|
|
||||||
hovered = found;
|
|
||||||
/* eslint-enable */
|
|
||||||
|
|
||||||
if (onHover != null) {
|
|
||||||
onHover(hovered!.sidx, hovered!.didx);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
} else if (hovered != null) {
|
|
||||||
if (onLeave != null) {
|
|
||||||
onLeave(hovered!.sidx, hovered!.didx);
|
|
||||||
}
|
|
||||||
|
|
||||||
hovered = null;
|
if (found) {
|
||||||
barMark.style.display = 'none';
|
// prettier-ignore
|
||||||
}
|
if (found !== hovered) {
|
||||||
|
barMark.style.display = '';
|
||||||
|
barMark.style.left = found!.x / devicePixelRatio + 'px';
|
||||||
|
barMark.style.top = found!.y / devicePixelRatio + 'px';
|
||||||
|
barMark.style.width = found!.w / devicePixelRatio + 'px';
|
||||||
|
barMark.style.height = found!.h / devicePixelRatio + 'px';
|
||||||
|
hovered = found;
|
||||||
|
updateActiveSeriesIdx(hovered!.sidx);
|
||||||
|
updateActiveDatapointIdx(hovered!.didx);
|
||||||
|
updateTooltipPosition();
|
||||||
|
}
|
||||||
|
} else if (hovered != null) {
|
||||||
|
updateActiveSeriesIdx(hovered!.sidx);
|
||||||
|
updateActiveDatapointIdx(hovered!.didx);
|
||||||
|
updateTooltipPosition();
|
||||||
|
hovered = null;
|
||||||
|
barMark.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
updateTooltipPosition(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// cursor & select opts
|
|
||||||
cursor,
|
|
||||||
select,
|
|
||||||
|
|
||||||
// scale & axis opts
|
// scale & axis opts
|
||||||
// yRange,
|
|
||||||
xValues,
|
xValues,
|
||||||
xSplits,
|
xSplits,
|
||||||
|
|
||||||
@ -289,6 +246,6 @@ export function getConfig(opts: BarsOptions) {
|
|||||||
// hooks
|
// hooks
|
||||||
init,
|
init,
|
||||||
drawClear,
|
drawClear,
|
||||||
setCursor,
|
interpolateBarChartTooltip,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { VizOrientation } from '@grafana/data';
|
import { VizOrientation } from '@grafana/data';
|
||||||
import { AxisConfig, GraphGradientMode, HideableFieldConfig, StackingMode } from '../uPlot/config';
|
import { AxisConfig, GraphGradientMode, HideableFieldConfig, StackingMode } from '../uPlot/config';
|
||||||
import { VizLegendOptions } from '../VizLegend/models.gen';
|
import { OptionsWithLegend, OptionsWithTooltip } from '../../options';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
@ -14,9 +14,8 @@ export enum BarValueVisibility {
|
|||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface BarChartOptions {
|
export interface BarChartOptions extends OptionsWithLegend, OptionsWithTooltip {
|
||||||
orientation: VizOrientation;
|
orientation: VizOrientation;
|
||||||
legend: VizLegendOptions;
|
|
||||||
stacking: StackingMode;
|
stacking: StackingMode;
|
||||||
showValue: BarValueVisibility;
|
showValue: BarValueVisibility;
|
||||||
barWidth: number;
|
barWidth: number;
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
||||||
import { createTheme, FieldConfig, FieldType, MutableDataFrame, VizOrientation } from '@grafana/data';
|
import {
|
||||||
|
createTheme,
|
||||||
|
DefaultTimeZone,
|
||||||
|
EventBusSrv,
|
||||||
|
FieldConfig,
|
||||||
|
FieldType,
|
||||||
|
getDefaultTimeRange,
|
||||||
|
MutableDataFrame,
|
||||||
|
VizOrientation,
|
||||||
|
} from '@grafana/data';
|
||||||
import { BarChartFieldConfig, BarChartOptions, BarValueVisibility } from './types';
|
import { BarChartFieldConfig, BarChartOptions, BarValueVisibility } from './types';
|
||||||
import { GraphGradientMode, StackingMode } from '../uPlot/config';
|
import { GraphGradientMode, StackingMode } from '../uPlot/config';
|
||||||
import { LegendDisplayMode } from '../VizLegend/models.gen';
|
import { LegendDisplayMode } from '../VizLegend/models.gen';
|
||||||
|
import { TooltipDisplayMode } from '../VizTooltip';
|
||||||
|
|
||||||
function mockDataFrame() {
|
function mockDataFrame() {
|
||||||
const df1 = new MutableDataFrame({
|
const df1 = new MutableDataFrame({
|
||||||
@ -59,7 +69,7 @@ jest.mock('@grafana/data', () => ({
|
|||||||
DefaultTimeZone: 'utc',
|
DefaultTimeZone: 'utc',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('GraphNG utils', () => {
|
describe('BarChart utils', () => {
|
||||||
describe('preparePlotConfigBuilder', () => {
|
describe('preparePlotConfigBuilder', () => {
|
||||||
const frame = mockDataFrame();
|
const frame = mockDataFrame();
|
||||||
|
|
||||||
@ -74,30 +84,48 @@ describe('GraphNG utils', () => {
|
|||||||
calcs: [],
|
calcs: [],
|
||||||
},
|
},
|
||||||
stacking: StackingMode.None,
|
stacking: StackingMode.None,
|
||||||
|
tooltip: {
|
||||||
|
mode: TooltipDisplayMode.None,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it.each([VizOrientation.Auto, VizOrientation.Horizontal, VizOrientation.Vertical])('orientation', (v) => {
|
it.each([VizOrientation.Auto, VizOrientation.Horizontal, VizOrientation.Vertical])('orientation', (v) => {
|
||||||
const result = preparePlotConfigBuilder(frame!, createTheme(), {
|
const result = preparePlotConfigBuilder({
|
||||||
...config,
|
...config,
|
||||||
orientation: v,
|
orientation: v,
|
||||||
|
frame: frame!,
|
||||||
|
theme: createTheme(),
|
||||||
|
timeZone: DefaultTimeZone,
|
||||||
|
getTimeRange: getDefaultTimeRange,
|
||||||
|
eventBus: new EventBusSrv(),
|
||||||
}).getConfig();
|
}).getConfig();
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([BarValueVisibility.Always, BarValueVisibility.Auto])('value visibility', (v) => {
|
it.each([BarValueVisibility.Always, BarValueVisibility.Auto])('value visibility', (v) => {
|
||||||
expect(
|
expect(
|
||||||
preparePlotConfigBuilder(frame!, createTheme(), {
|
preparePlotConfigBuilder({
|
||||||
...config,
|
...config,
|
||||||
showValue: v,
|
showValue: v,
|
||||||
|
frame: frame!,
|
||||||
|
theme: createTheme(),
|
||||||
|
timeZone: DefaultTimeZone,
|
||||||
|
getTimeRange: getDefaultTimeRange,
|
||||||
|
eventBus: new EventBusSrv(),
|
||||||
}).getConfig()
|
}).getConfig()
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([StackingMode.None, StackingMode.Percent, StackingMode.Normal])('stacking', (v) => {
|
it.each([StackingMode.None, StackingMode.Percent, StackingMode.Normal])('stacking', (v) => {
|
||||||
expect(
|
expect(
|
||||||
preparePlotConfigBuilder(frame!, createTheme(), {
|
preparePlotConfigBuilder({
|
||||||
...config,
|
...config,
|
||||||
stacking: v,
|
stacking: v,
|
||||||
|
frame: frame!,
|
||||||
|
theme: createTheme(),
|
||||||
|
timeZone: DefaultTimeZone,
|
||||||
|
getTimeRange: getDefaultTimeRange,
|
||||||
|
eventBus: new EventBusSrv(),
|
||||||
}).getConfig()
|
}).getConfig()
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
getFieldColorModeForField,
|
getFieldColorModeForField,
|
||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
getFieldSeriesColor,
|
getFieldSeriesColor,
|
||||||
GrafanaTheme2,
|
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
VizOrientation,
|
VizOrientation,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
@ -14,76 +13,79 @@ import { BarChartFieldConfig, BarChartOptions, BarValueVisibility, defaultBarCha
|
|||||||
import { AxisPlacement, ScaleDirection, ScaleOrientation } from '../uPlot/config';
|
import { AxisPlacement, ScaleDirection, ScaleOrientation } from '../uPlot/config';
|
||||||
import { BarsOptions, getConfig } from './bars';
|
import { BarsOptions, getConfig } from './bars';
|
||||||
import { FIXED_UNIT } from '../GraphNG/GraphNG';
|
import { FIXED_UNIT } from '../GraphNG/GraphNG';
|
||||||
import { Select } from 'uplot';
|
|
||||||
import { ScaleDistribution } from '../uPlot/models.gen';
|
import { ScaleDistribution } from '../uPlot/models.gen';
|
||||||
|
import { PrepConfigOpts } from '../GraphNG/utils';
|
||||||
|
|
||||||
|
type PrepConfig = (opts: PrepConfigOpts<BarChartOptions>) => UPlotConfigBuilder;
|
||||||
|
|
||||||
/** @alpha */
|
/** @alpha */
|
||||||
export function preparePlotConfigBuilder(
|
function getBarCharScaleOrientation(orientation: VizOrientation) {
|
||||||
data: DataFrame,
|
|
||||||
theme: GrafanaTheme2,
|
|
||||||
{ orientation, showValue, groupWidth, barWidth }: BarChartOptions
|
|
||||||
) {
|
|
||||||
const builder = new UPlotConfigBuilder();
|
|
||||||
|
|
||||||
// bar orientation -> x scale orientation & direction
|
|
||||||
let xOri = ScaleOrientation.Vertical;
|
|
||||||
let xDir = ScaleDirection.Down;
|
|
||||||
let yOri = ScaleOrientation.Horizontal;
|
|
||||||
let yDir = ScaleDirection.Right;
|
|
||||||
|
|
||||||
if (orientation === VizOrientation.Vertical) {
|
if (orientation === VizOrientation.Vertical) {
|
||||||
xOri = ScaleOrientation.Horizontal;
|
return {
|
||||||
xDir = ScaleDirection.Right;
|
xOri: ScaleOrientation.Horizontal,
|
||||||
yOri = ScaleOrientation.Vertical;
|
xDir: ScaleDirection.Right,
|
||||||
yDir = ScaleDirection.Up;
|
yOri: ScaleOrientation.Vertical,
|
||||||
|
yDir: ScaleDirection.Up,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatValue =
|
return {
|
||||||
showValue !== BarValueVisibility.Never
|
xOri: ScaleOrientation.Vertical,
|
||||||
? (seriesIdx: number, value: any) => formattedValueToString(data.fields[seriesIdx].display!(value))
|
xDir: ScaleDirection.Down,
|
||||||
: undefined;
|
yOri: ScaleOrientation.Horizontal,
|
||||||
|
yDir: ScaleDirection.Right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const preparePlotConfigBuilder: PrepConfig = ({
|
||||||
|
frame,
|
||||||
|
theme,
|
||||||
|
orientation,
|
||||||
|
showValue,
|
||||||
|
groupWidth,
|
||||||
|
barWidth,
|
||||||
|
}) => {
|
||||||
|
const builder = new UPlotConfigBuilder();
|
||||||
|
const defaultValueFormatter = (seriesIdx: number, value: any) =>
|
||||||
|
formattedValueToString(frame.fields[seriesIdx].display!(value));
|
||||||
|
|
||||||
|
// bar orientation -> x scale orientation & direction
|
||||||
|
const vizOrientation = getBarCharScaleOrientation(orientation);
|
||||||
|
|
||||||
|
const formatValue = showValue !== BarValueVisibility.Never ? defaultValueFormatter : undefined;
|
||||||
|
|
||||||
// Use bar width when only one field
|
// Use bar width when only one field
|
||||||
if (data.fields.length === 2) {
|
if (frame.fields.length === 2) {
|
||||||
groupWidth = barWidth;
|
groupWidth = barWidth;
|
||||||
barWidth = 1;
|
barWidth = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const opts: BarsOptions = {
|
const opts: BarsOptions = {
|
||||||
xOri,
|
xOri: vizOrientation.xOri,
|
||||||
xDir,
|
xDir: vizOrientation.xDir,
|
||||||
groupWidth,
|
groupWidth,
|
||||||
barWidth,
|
barWidth,
|
||||||
formatValue,
|
formatValue,
|
||||||
onHover: (seriesIdx: number, valueIdx: number) => {
|
|
||||||
console.log('hover', { seriesIdx, valueIdx });
|
|
||||||
},
|
|
||||||
onLeave: (seriesIdx: number, valueIdx: number) => {
|
|
||||||
console.log('leave', { seriesIdx, valueIdx });
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = getConfig(opts);
|
const config = getConfig(opts);
|
||||||
|
|
||||||
builder.addHook('init', config.init);
|
builder.addHook('init', config.init);
|
||||||
builder.addHook('drawClear', config.drawClear);
|
builder.addHook('drawClear', config.drawClear);
|
||||||
builder.addHook('setCursor', config.setCursor);
|
builder.setTooltipInterpolator(config.interpolateBarChartTooltip);
|
||||||
|
|
||||||
builder.setCursor(config.cursor);
|
|
||||||
builder.setSelect(config.select as Select);
|
|
||||||
|
|
||||||
builder.addScale({
|
builder.addScale({
|
||||||
scaleKey: 'x',
|
scaleKey: 'x',
|
||||||
isTime: false,
|
isTime: false,
|
||||||
distribution: ScaleDistribution.Ordinal,
|
distribution: ScaleDistribution.Ordinal,
|
||||||
orientation: xOri,
|
orientation: vizOrientation.xOri,
|
||||||
direction: xDir,
|
direction: vizOrientation.xDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addAxis({
|
builder.addAxis({
|
||||||
scaleKey: 'x',
|
scaleKey: 'x',
|
||||||
isTime: false,
|
isTime: false,
|
||||||
placement: xOri === 0 ? AxisPlacement.Bottom : AxisPlacement.Left,
|
placement: vizOrientation.xOri === 0 ? AxisPlacement.Bottom : AxisPlacement.Left,
|
||||||
splits: config.xSplits,
|
splits: config.xSplits,
|
||||||
values: config.xValues,
|
values: config.xValues,
|
||||||
grid: false,
|
grid: false,
|
||||||
@ -95,8 +97,8 @@ export function preparePlotConfigBuilder(
|
|||||||
let seriesIndex = 0;
|
let seriesIndex = 0;
|
||||||
|
|
||||||
// iterate the y values
|
// iterate the y values
|
||||||
for (let i = 1; i < data.fields.length; i++) {
|
for (let i = 1; i < frame.fields.length; i++) {
|
||||||
const field = data.fields[i];
|
const field = frame.fields[i];
|
||||||
|
|
||||||
field.state!.seriesIndex = seriesIndex++;
|
field.state!.seriesIndex = seriesIndex++;
|
||||||
|
|
||||||
@ -127,7 +129,7 @@ export function preparePlotConfigBuilder(
|
|||||||
fieldIndex: i,
|
fieldIndex: i,
|
||||||
frameIndex: 0,
|
frameIndex: 0,
|
||||||
},
|
},
|
||||||
fieldName: getFieldDisplayName(field, data),
|
fieldName: getFieldDisplayName(field, frame),
|
||||||
hideInLegend: customConfig.hideFrom?.legend,
|
hideInLegend: customConfig.hideFrom?.legend,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -138,8 +140,8 @@ export function preparePlotConfigBuilder(
|
|||||||
max: field.config.max,
|
max: field.config.max,
|
||||||
softMin: customConfig.axisSoftMin,
|
softMin: customConfig.axisSoftMin,
|
||||||
softMax: customConfig.axisSoftMax,
|
softMax: customConfig.axisSoftMax,
|
||||||
orientation: yOri,
|
orientation: vizOrientation.yOri,
|
||||||
direction: yDir,
|
direction: vizOrientation.yDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
||||||
@ -147,7 +149,7 @@ export function preparePlotConfigBuilder(
|
|||||||
if (!placement || placement === AxisPlacement.Auto) {
|
if (!placement || placement === AxisPlacement.Auto) {
|
||||||
placement = AxisPlacement.Left;
|
placement = AxisPlacement.Left;
|
||||||
}
|
}
|
||||||
if (xOri === 1) {
|
if (vizOrientation.xOri === 1) {
|
||||||
if (placement === AxisPlacement.Left) {
|
if (placement === AxisPlacement.Left) {
|
||||||
placement = AxisPlacement.Bottom;
|
placement = AxisPlacement.Bottom;
|
||||||
}
|
}
|
||||||
@ -168,7 +170,7 @@ export function preparePlotConfigBuilder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
};
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export function preparePlotFrame(data: DataFrame[]) {
|
export function preparePlotFrame(data: DataFrame[]) {
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
TimeRange,
|
TimeRange,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { preparePlotFrame } from './utils';
|
import { preparePlotFrame as defaultPreparePlotFrame } from './utils';
|
||||||
|
|
||||||
import { VizLegendOptions } from '../VizLegend/models.gen';
|
import { VizLegendOptions } from '../VizLegend/models.gen';
|
||||||
import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
|
import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
|
||||||
@ -38,9 +38,9 @@ export interface GraphNGProps extends Themeable2 {
|
|||||||
fields?: XYFieldMatchers; // default will assume timeseries data
|
fields?: XYFieldMatchers; // default will assume timeseries data
|
||||||
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
||||||
children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode;
|
children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode;
|
||||||
|
|
||||||
prepConfig: (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => UPlotConfigBuilder;
|
prepConfig: (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => UPlotConfigBuilder;
|
||||||
propsToDiff?: string[];
|
propsToDiff?: string[];
|
||||||
|
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame;
|
||||||
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement;
|
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,9 +84,11 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
|||||||
prepState(props: GraphNGProps, withConfig = true) {
|
prepState(props: GraphNGProps, withConfig = true) {
|
||||||
let state: GraphNGState = null as any;
|
let state: GraphNGState = null as any;
|
||||||
|
|
||||||
const { frames, fields } = props;
|
const { frames, fields, preparePlotFrame } = props;
|
||||||
|
|
||||||
const alignedFrame = preparePlotFrame(
|
const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame;
|
||||||
|
|
||||||
|
const alignedFrame = preparePlotFrameFn(
|
||||||
frames,
|
frames,
|
||||||
fields || {
|
fields || {
|
||||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { PlotConfig } from '../types';
|
import { PlotConfig, TooltipInterpolator } from '../types';
|
||||||
import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
|
import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
|
||||||
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
|
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
|
||||||
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
|
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
|
||||||
@ -23,6 +23,11 @@ export class UPlotConfigBuilder {
|
|||||||
private tz: string | undefined = undefined;
|
private tz: string | undefined = undefined;
|
||||||
// to prevent more than one threshold per scale
|
// to prevent more than one threshold per scale
|
||||||
private thresholds: Record<string, UPlotThresholdOptions> = {};
|
private thresholds: Record<string, UPlotThresholdOptions> = {};
|
||||||
|
/**
|
||||||
|
* Custom handler for closest datapoint and series lookup. Technicaly returns uPlots setCursor hook
|
||||||
|
* that sets tooltips state.
|
||||||
|
*/
|
||||||
|
tooltipInterpolator: TooltipInterpolator | undefined = undefined;
|
||||||
|
|
||||||
constructor(timeZone: TimeZone = DefaultTimeZone) {
|
constructor(timeZone: TimeZone = DefaultTimeZone) {
|
||||||
this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName;
|
this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName;
|
||||||
@ -116,6 +121,10 @@ export class UPlotConfigBuilder {
|
|||||||
this.bands.push(band);
|
this.bands.push(band);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTooltipInterpolator(interpolator: TooltipInterpolator) {
|
||||||
|
this.tooltipInterpolator = interpolator;
|
||||||
|
}
|
||||||
|
|
||||||
getConfig() {
|
getConfig() {
|
||||||
const config: PlotConfig = { series: [{}] };
|
const config: PlotConfig = { series: [{}] };
|
||||||
config.axes = this.ensureNonOverlappingAxes(Object.values(this.axes)).map((a) => a.getConfig());
|
config.axes = this.ensureNonOverlappingAxes(Object.values(this.axes)).map((a) => a.getConfig());
|
||||||
|
@ -38,7 +38,6 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
const plotCtx = usePlotContext();
|
const plotCtx = usePlotContext();
|
||||||
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
|
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
|
||||||
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
|
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
|
||||||
|
|
||||||
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
|
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
|
||||||
|
|
||||||
const pluginId = `TooltipPlugin`;
|
const pluginId = `TooltipPlugin`;
|
||||||
@ -50,27 +49,50 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
|
|
||||||
// Add uPlot hooks to the config, or re-add when the config changed
|
// Add uPlot hooks to the config, or re-add when the config changed
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
config.addHook('setCursor', (u) => {
|
if (config.tooltipInterpolator) {
|
||||||
const bbox = plotCtx.getCanvasBoundingBox();
|
// Custom toolitp positioning
|
||||||
if (!bbox) {
|
config.addHook('setCursor', (u) => {
|
||||||
return;
|
config.tooltipInterpolator!(setFocusedSeriesIdx, setFocusedPointIdx, (clear) => {
|
||||||
}
|
if (clear) {
|
||||||
|
setCoords(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { x, y } = positionTooltip(u, bbox);
|
const bbox = plotCtx.getCanvasBoundingBox();
|
||||||
|
if (!bbox) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (x !== undefined && y !== undefined) {
|
const { x, y } = positionTooltip(u, bbox);
|
||||||
setCoords({ x, y });
|
if (x !== undefined && y !== undefined) {
|
||||||
} else {
|
setCoords({ x, y });
|
||||||
setCoords(null);
|
}
|
||||||
}
|
})(u);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// default series/datapoint idx retireval
|
||||||
|
config.addHook('setCursor', (u) => {
|
||||||
|
const bbox = plotCtx.getCanvasBoundingBox();
|
||||||
|
if (!bbox) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setFocusedPointIdx(u.cursor.idx === undefined ? u.posToIdx(u.cursor.left || 0) : u.cursor.idx);
|
const { x, y } = positionTooltip(u, bbox);
|
||||||
});
|
|
||||||
|
|
||||||
config.addHook('setSeries', (_, idx) => {
|
if (x !== undefined && y !== undefined) {
|
||||||
setFocusedSeriesIdx(idx);
|
setCoords({ x, y });
|
||||||
});
|
} else {
|
||||||
}, [plotCtx, config]);
|
setCoords(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFocusedPointIdx(u.cursor.idx === undefined ? u.posToIdx(u.cursor.left || 0) : u.cursor.idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
config.addHook('setSeries', (_, idx) => {
|
||||||
|
setFocusedSeriesIdx(idx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [plotCtx, config, setFocusedPointIdx, setFocusedSeriesIdx, setCoords]);
|
||||||
|
|
||||||
const plotInstance = plotCtx.plot;
|
const plotInstance = plotCtx.plot;
|
||||||
if (!plotInstance || focusedPointIdx === null) {
|
if (!plotInstance || focusedPointIdx === null) {
|
||||||
|
@ -27,3 +27,9 @@ export abstract class PlotConfigBuilder<P, T> {
|
|||||||
constructor(public props: P) {}
|
constructor(public props: P) {}
|
||||||
abstract getConfig(): T;
|
abstract getConfig(): T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TooltipInterpolator = (
|
||||||
|
updateActiveSeriesIdx: (sIdx: number | null) => void,
|
||||||
|
updateActiveDatapointIdx: (dIdx: number | null) => void,
|
||||||
|
updateTooltipPosition: (clear?: boolean) => void
|
||||||
|
) => (u: uPlot) => void;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { FieldType, PanelProps, VizOrientation } from '@grafana/data';
|
import { FieldType, PanelProps, TimeRange, VizOrientation } from '@grafana/data';
|
||||||
import { BarChart, BarChartOptions, GraphNGLegendEvent } from '@grafana/ui';
|
import { BarChart, BarChartOptions, GraphNGLegendEvent, TooltipPlugin } from '@grafana/ui';
|
||||||
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';
|
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';
|
||||||
|
|
||||||
interface Props extends PanelProps<BarChartOptions> {}
|
interface Props extends PanelProps<BarChartOptions> {}
|
||||||
@ -14,6 +14,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fieldConfig,
|
fieldConfig,
|
||||||
|
timeZone,
|
||||||
onFieldConfigChange,
|
onFieldConfigChange,
|
||||||
}) => {
|
}) => {
|
||||||
const orientation = useMemo(() => {
|
const orientation = useMemo(() => {
|
||||||
@ -57,13 +58,19 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BarChart
|
<BarChart
|
||||||
data={data.series}
|
frames={data.series}
|
||||||
|
timeZone={timeZone}
|
||||||
|
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
|
||||||
structureRev={data.structureRev}
|
structureRev={data.structureRev}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
onLegendClick={onLegendClick}
|
onLegendClick={onLegendClick}
|
||||||
{...options}
|
{...options}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
/>
|
>
|
||||||
|
{(config, alignedFrame) => {
|
||||||
|
return <TooltipPlugin data={alignedFrame} config={config} mode={options.tooltip.mode} timeZone={timeZone} />;
|
||||||
|
}}
|
||||||
|
</BarChart>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
graphFieldOptions,
|
graphFieldOptions,
|
||||||
commonOptionsBuilder,
|
commonOptionsBuilder,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import { defaultBarChartFieldConfig } from '@grafana/ui/src/components/BarChart/types';
|
import { defaultBarChartFieldConfig } from '@grafana/ui/src/components/BarChart/types';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarChartPanel)
|
export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarChartPanel)
|
||||||
@ -119,6 +120,7 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
commonOptionsBuilder.addTooltipOptions(builder);
|
||||||
commonOptionsBuilder.addLegendOptions(builder);
|
commonOptionsBuilder.addLegendOptions(builder);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user