MarketTrend: add new alpha panel (#40909)

pull/41388/head
Leon Sorokin 4 years ago committed by GitHub
parent af61839a26
commit f0a108afb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/grafana-schema/src/scuemata/dashboard/dist/family.cue
  2. 15
      packages/grafana-ui/src/components/GraphNG/GraphNG.tsx
  3. 2
      packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap
  4. 8
      packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx
  5. 119
      packages/grafana-ui/src/components/TimeSeries/utils.ts
  6. 32
      packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts
  7. 1
      packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts
  8. 9
      packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts
  9. 6
      packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts
  10. 1
      pkg/cmd/grafana-cli/commands/cuetsify_command.go
  11. 1
      pkg/plugins/manager/manager_integration_test.go
  12. 5
      pkg/tsdb/grafanads/testdata/list.golden.txt
  13. 2
      public/app/features/plugins/built_in_plugins.ts
  14. 1
      public/app/plugins/datasource/testdata/components/CSVFileEditor.tsx
  15. 16
      public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap
  16. 2
      public/app/plugins/panel/barchart/utils.ts
  17. 316
      public/app/plugins/panel/market-trend/MarketTrendPanel.tsx
  18. 30
      public/app/plugins/panel/market-trend/img/candlestick.svg
  19. 29
      public/app/plugins/panel/market-trend/models.cue
  20. 52
      public/app/plugins/panel/market-trend/models.gen.ts
  21. 102
      public/app/plugins/panel/market-trend/module.tsx
  22. 17
      public/app/plugins/panel/market-trend/plugin.json
  23. 181
      public/app/plugins/panel/market-trend/utils.ts
  24. 2
      public/app/plugins/panel/state-timeline/utils.ts
  25. 2102
      public/testdata/ohlc_dogecoin.csv
  26. 2
      scripts/stripnulls.sh

@ -8,6 +8,7 @@ import (
pdashlist "github.com/grafana/grafana/public/app/plugins/panel/dashlist:grafanaschema" pdashlist "github.com/grafana/grafana/public/app/plugins/panel/dashlist:grafanaschema"
pgauge "github.com/grafana/grafana/public/app/plugins/panel/gauge:grafanaschema" pgauge "github.com/grafana/grafana/public/app/plugins/panel/gauge:grafanaschema"
phistogram "github.com/grafana/grafana/public/app/plugins/panel/histogram:grafanaschema" phistogram "github.com/grafana/grafana/public/app/plugins/panel/histogram:grafanaschema"
pmt "github.com/grafana/grafana/public/app/plugins/panel/market-trend:grafanaschema"
pnews "github.com/grafana/grafana/public/app/plugins/panel/news:grafanaschema" pnews "github.com/grafana/grafana/public/app/plugins/panel/news:grafanaschema"
pstat "github.com/grafana/grafana/public/app/plugins/panel/stat:grafanaschema" pstat "github.com/grafana/grafana/public/app/plugins/panel/stat:grafanaschema"
st "github.com/grafana/grafana/public/app/plugins/panel/state-timeline:grafanaschema" st "github.com/grafana/grafana/public/app/plugins/panel/state-timeline:grafanaschema"
@ -31,6 +32,7 @@ Family: dashboard.Family & {
dashlist: pdashlist.Panel dashlist: pdashlist.Panel
gauge: pgauge.Panel gauge: pgauge.Panel
histogram: phistogram.Panel histogram: phistogram.Panel
"market-trend": pmt.Panel
news: pnews.Panel news: pnews.Panel
stat: pstat.Panel stat: pstat.Panel
"state-timeline": st.Panel "state-timeline": st.Panel

@ -18,9 +18,11 @@ import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { throttleTime } from 'rxjs/operators'; import { throttleTime } from 'rxjs/operators';
import { GraphNGLegendEvent, XYFieldMatchers } from './types'; import { GraphNGLegendEvent, XYFieldMatchers } from './types';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { Renderers, UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { VizLayout } from '../VizLayout/VizLayout'; import { VizLayout } from '../VizLayout/VizLayout';
import { UPlotChart } from '../uPlot/Plot'; import { UPlotChart } from '../uPlot/Plot';
import { ScaleProps } from '../uPlot/config/UPlotScaleBuilder';
import { AxisProps } from '../uPlot/config/UPlotAxisBuilder';
/** /**
* @internal -- not a public API * @internal -- not a public API
@ -41,12 +43,23 @@ export interface GraphNGProps extends Themeable2 {
timeZone: TimeZone; timeZone: TimeZone;
legend: VizLegendOptions; legend: VizLegendOptions;
fields?: XYFieldMatchers; // default will assume timeseries data fields?: XYFieldMatchers; // default will assume timeseries data
renderers?: Renderers;
tweakScale?: (opts: ScaleProps) => ScaleProps;
tweakAxis?: (opts: AxisProps) => AxisProps;
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, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder; prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder;
propsToDiff?: Array<string | PropDiffFn>; propsToDiff?: Array<string | PropDiffFn>;
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame; preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null; renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
/**
* needed for propsToDiff to re-init the plot & config
* this is a generic approach to plot re-init, without having to specify which panel-level options
* should cause invalidation. we can drop this in favor of something like panelOptionsRev that gets passed in
* similar to structureRev. then we can drop propsToDiff entirely.
*/
options?: Record<string, any>;
} }
function sameProps(prevProps: any, nextProps: any, propsToDiff: Array<string | PropDiffFn> = []) { function sameProps(prevProps: any, nextProps: any, propsToDiff: Array<string | PropDiffFn> = []) {

@ -4,6 +4,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {
@ -30,6 +31,7 @@ Object {
"values": [Function], "values": [Function],
}, },
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {

@ -7,8 +7,9 @@ import { LegendDisplayMode } from '@grafana/schema';
import { preparePlotConfigBuilder } from './utils'; import { preparePlotConfigBuilder } from './utils';
import { withTheme2 } from '../../themes/ThemeContext'; import { withTheme2 } from '../../themes/ThemeContext';
import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext'; import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
import { PropDiffFn } from '../../../../../packages/grafana-ui/src/components/GraphNG/GraphNG';
const propsToDiff: string[] = ['legend']; const propsToDiff: Array<string | PropDiffFn> = ['legend', 'options'];
type TimeSeriesProps = Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'>; type TimeSeriesProps = Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'>;
@ -18,7 +19,7 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
const { eventBus, sync } = this.context; const { eventBus, sync } = this.context;
const { theme, timeZone, legend } = this.props; const { theme, timeZone, legend, renderers, tweakAxis, tweakScale } = this.props;
return preparePlotConfigBuilder({ return preparePlotConfigBuilder({
frame: alignedFrame, frame: alignedFrame,
@ -29,6 +30,9 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
sync, sync,
allFrames, allFrames,
legend, legend,
renderers,
tweakScale,
tweakAxis,
}); });
}; };

@ -44,7 +44,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
eventBus, eventBus,
sync, sync,
allFrames, allFrames,
renderers,
legend, legend,
tweakScale = (opts) => opts,
tweakAxis = (opts) => opts,
}) => { }) => {
const builder = new UPlotConfigBuilder(timeZone); const builder = new UPlotConfigBuilder(timeZone);
@ -105,6 +108,9 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
}); });
} }
let customRenderedFields =
renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? [];
const stackingGroups: Map<string, number[]> = new Map(); const stackingGroups: Map<string, number[]> = new Map();
let indexByName: Map<string, number> | undefined; let indexByName: Map<string, number> | undefined;
@ -120,6 +126,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
if (field === xField || field.type !== FieldType.number) { if (field === xField || field.type !== FieldType.number) {
continue; continue;
} }
// TODO: skip this for fields with custom renderers?
field.state!.seriesIndex = seriesIndex++; field.state!.seriesIndex = seriesIndex++;
const fmt = field.display ?? defaultFormatter; const fmt = field.display ?? defaultFormatter;
@ -129,32 +137,36 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
const seriesColor = scaleColor.color; const seriesColor = scaleColor.color;
// The builder will manage unique scaleKeys and combine where appropriate // The builder will manage unique scaleKeys and combine where appropriate
builder.addScale({ builder.addScale(
scaleKey, tweakScale({
orientation: ScaleOrientation.Vertical, scaleKey,
direction: ScaleDirection.Up, orientation: ScaleOrientation.Vertical,
distribution: customConfig.scaleDistribution?.type, direction: ScaleDirection.Up,
log: customConfig.scaleDistribution?.log, distribution: customConfig.scaleDistribution?.type,
min: field.config.min, log: customConfig.scaleDistribution?.log,
max: field.config.max, min: field.config.min,
softMin: customConfig.axisSoftMin, max: field.config.max,
softMax: customConfig.axisSoftMax, softMin: customConfig.axisSoftMin,
}); softMax: customConfig.axisSoftMax,
})
);
if (!yScaleKey) { if (!yScaleKey) {
yScaleKey = scaleKey; yScaleKey = scaleKey;
} }
if (customConfig.axisPlacement !== AxisPlacement.Hidden) { if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
builder.addAxis({ builder.addAxis(
scaleKey, tweakAxis({
label: customConfig.axisLabel, scaleKey,
size: customConfig.axisWidth, label: customConfig.axisLabel,
placement: customConfig.axisPlacement ?? AxisPlacement.Auto, size: customConfig.axisWidth,
formatValue: (v) => formattedValueToString(fmt(v)), placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
theme, formatValue: (v) => formattedValueToString(fmt(v)),
grid: { show: customConfig.axisGridShow }, theme,
}); grid: { show: customConfig.axisGridShow },
})
);
} }
const showPoints = const showPoints =
@ -199,28 +211,43 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
let { fillOpacity } = customConfig; let { fillOpacity } = customConfig;
if (customConfig.fillBelowTo && field.state?.origin) { let pathBuilder: uPlot.Series.PathBuilder | null = null;
let pointsBuilder: uPlot.Series.Points.Show | null = null;
if (field.state?.origin) {
if (!indexByName) { if (!indexByName) {
indexByName = getNamesToFieldIndex(frame, allFrames); indexByName = getNamesToFieldIndex(frame, allFrames);
} }
const originFrame = allFrames[field.state.origin.frameIndex]; const originFrame = allFrames[field.state.origin.frameIndex];
const originField = originFrame.fields[field.state.origin.fieldIndex]; const originField = originFrame?.fields[field.state.origin.fieldIndex];
const t = indexByName.get(getFieldDisplayName(originField, originFrame, allFrames)); const dispName = getFieldDisplayName(originField ?? field, originFrame, allFrames);
const b = indexByName.get(customConfig.fillBelowTo);
if (isNumber(b) && isNumber(t)) { // disable default renderers
builder.addBand({ if (customRenderedFields.indexOf(dispName) >= 0) {
series: [t, b], pathBuilder = () => null;
fill: null as any, // using null will have the band use fill options from `t` pointsBuilder = () => undefined;
});
} }
if (!fillOpacity) {
fillOpacity = 35; // default from flot if (customConfig.fillBelowTo) {
const t = indexByName.get(dispName);
const b = indexByName.get(customConfig.fillBelowTo);
if (isNumber(b) && isNumber(t)) {
builder.addBand({
series: [t, b],
fill: undefined, // using null will have the band use fill options from `t`
});
}
if (!fillOpacity) {
fillOpacity = 35; // default from flot
}
} }
} }
builder.addSeries({ builder.addSeries({
pathBuilder,
pointsBuilder,
scaleKey, scaleKey,
showPoints, showPoints,
pointsFilter, pointsFilter,
@ -279,6 +306,18 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
} }
} }
// hook up custom/composite renderers
renderers?.forEach((r) => {
let fieldIndices: Record<string, number> = {};
for (let key in r.fieldMap) {
let dispName = r.fieldMap[key];
fieldIndices[key] = indexByName!.get(dispName)!;
}
r.init(builder, fieldIndices);
});
builder.scaleKeys = [xScaleKey, yScaleKey]; builder.scaleKeys = [xScaleKey, yScaleKey];
// if hovered value is null, how far we may scan left/right to hover nearest non-null // if hovered value is null, how far we may scan left/right to hover nearest non-null
@ -377,18 +416,14 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map<string, number> { export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map<string, number> {
const originNames = new Map<string, number>(); const originNames = new Map<string, number>();
for (let i = 0; i < frame.fields.length; i++) { frame.fields.forEach((field, i) => {
const origin = frame.fields[i].state?.origin; const origin = field.state?.origin;
if (origin) { if (origin) {
originNames.set( const origField = allFrames[origin.frameIndex]?.fields[origin.fieldIndex];
getFieldDisplayName( if (origField) {
allFrames[origin.frameIndex].fields[origin.fieldIndex], originNames.set(getFieldDisplayName(origField, allFrames[origin.frameIndex], allFrames), i);
allFrames[origin.frameIndex], }
allFrames
),
i
);
} }
} });
return originNames; return originNames;
} }

@ -15,7 +15,9 @@ export interface AxisProps {
valueRotation?: number; valueRotation?: number;
placement?: AxisPlacement; placement?: AxisPlacement;
grid?: Axis.Grid; grid?: Axis.Grid;
ticks?: boolean; ticks?: Axis.Ticks;
filter?: Axis.Filter;
space?: Axis.Space;
formatValue?: (v: any) => string; formatValue?: (v: any) => string;
incrs?: Axis.Incrs; incrs?: Axis.Incrs;
splits?: Axis.Splits; splits?: Axis.Splits;
@ -86,7 +88,9 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
show = true, show = true,
placement = AxisPlacement.Auto, placement = AxisPlacement.Auto,
grid = { show: true }, grid = { show: true },
ticks = true, ticks,
space,
filter,
gap = 5, gap = 5,
formatValue, formatValue,
splits, splits,
@ -127,17 +131,23 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
stroke: gridColor, stroke: gridColor,
width: 1 / devicePixelRatio, width: 1 / devicePixelRatio,
}, },
ticks: { ticks: Object.assign(
show: ticks, {
stroke: gridColor, show: true,
width: 1 / devicePixelRatio, stroke: gridColor,
size: 4, width: 1 / devicePixelRatio,
}, size: 4,
},
ticks
),
splits, splits,
values: values, values: values,
space: (self, axisIdx, scaleMin, scaleMax, plotDim) => { space:
return this.calculateSpace(self, axisIdx, scaleMin, scaleMax, plotDim); space ??
}, ((self, axisIdx, scaleMin, scaleMax, plotDim) => {
return this.calculateSpace(self, axisIdx, scaleMin, scaleMax, plotDim);
}),
filter,
}; };
if (label != null && label.length > 0) { if (label != null && label.length > 0) {

@ -349,6 +349,7 @@ describe('UPlotConfigBuilder', () => {
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {

@ -272,6 +272,12 @@ export class UPlotConfigBuilder {
} }
} }
export type Renderers = Array<{
fieldMap: Record<string, string>;
indicesOnly: string[];
init: (config: UPlotConfigBuilder, fieldIndices: Record<string, number>) => void;
}>;
/** @alpha */ /** @alpha */
type UPlotConfigPrepOpts<T extends Record<string, any> = {}> = { type UPlotConfigPrepOpts<T extends Record<string, any> = {}> = {
frame: DataFrame; frame: DataFrame;
@ -280,6 +286,9 @@ type UPlotConfigPrepOpts<T extends Record<string, any> = {}> = {
getTimeRange: () => TimeRange; getTimeRange: () => TimeRange;
eventBus: EventBus; eventBus: EventBus;
allFrames: DataFrame[]; allFrames: DataFrame[];
renderers?: Renderers;
tweakScale?: (opts: ScaleProps) => ScaleProps;
tweakAxis?: (opts: AxisProps) => AxisProps;
} & T; } & T;
/** @alpha */ /** @alpha */

@ -38,9 +38,9 @@ export interface SeriesProps extends LineConfig, BarConfig, FillConfig, PointsCo
softMax?: number | null; softMax?: number | null;
drawStyle?: GraphDrawStyle; drawStyle?: GraphDrawStyle;
pathBuilder?: Series.PathBuilder; pathBuilder?: Series.PathBuilder | null;
pointsFilter?: Series.Points.Filter; pointsFilter?: Series.Points.Filter | null;
pointsBuilder?: Series.Points.Show; pointsBuilder?: Series.Points.Show | null;
show?: boolean; show?: boolean;
dataFrameFieldIndex?: DataFrameFieldIndex; dataFrameFieldIndex?: DataFrameFieldIndex;
theme: GrafanaTheme2; theme: GrafanaTheme2;

@ -44,6 +44,7 @@ var skipPaths = []string{
"public/app/plugins/panel/gauge/models.cue", "public/app/plugins/panel/gauge/models.cue",
"public/app/plugins/panel/histogram/models.cue", "public/app/plugins/panel/histogram/models.cue",
"public/app/plugins/panel/stat/models.cue", "public/app/plugins/panel/stat/models.cue",
"public/app/plugins/panel/market-trend/models.cue",
"public/app/plugins/panel/state-timeline/models.cue", "public/app/plugins/panel/state-timeline/models.cue",
"public/app/plugins/panel/status-history/models.cue", "public/app/plugins/panel/status-history/models.cue",
"public/app/plugins/panel/table/models.cue", "public/app/plugins/panel/table/models.cue",

@ -72,6 +72,7 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
"icon": {}, "icon": {},
"live": {}, "live": {},
"logs": {}, "logs": {},
"market-trend": {},
"news": {}, "news": {},
"nodeGraph": {}, "nodeGraph": {},
"piechart": {}, "piechart": {},

@ -5,7 +5,7 @@ Frame[0] {
"pathSeparator": "/" "pathSeparator": "/"
} }
Name: Name:
Dimensions: 2 Fields by 6 Rows Dimensions: 2 Fields by 7 Rows
+--------------------------+------------------+ +--------------------------+------------------+
| Name: name | Name: media-type | | Name: name | Name: media-type |
| Labels: | Labels: | | Labels: | Labels: |
@ -15,10 +15,11 @@ Dimensions: 2 Fields by 6 Rows
| flight_info_by_state.csv | | | flight_info_by_state.csv | |
| gdp_per_capita.csv | | | gdp_per_capita.csv | |
| js_libraries.csv | | | js_libraries.csv | |
| ohlc_dogecoin.csv | |
| population_by_state.csv | | | population_by_state.csv | |
| weight_height.csv | | | weight_height.csv | |
+--------------------------+------------------+ +--------------------------+------------------+
====== TEST DATA RESPONSE (arrow base64) ====== ====== TEST DATA RESPONSE (arrow base64) ======
FRAME=QVJST1cxAAD/////uAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAKQAAAADAAAATAAAACgAAAAEAAAA0P7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADw/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAABD///8IAAAAPAAAADAAAAB7InR5cGUiOiJkaXJlY3RvcnktbGlzdGluZyIsInBhdGhTZXBhcmF0b3IiOiIvIn0AAAAABAAAAG1ldGEAAAAAAgAAAHwAAAAEAAAAnv///xQAAABAAAAAQAAAAAAAAAU8AAAAAQAAAAQAAACM////CAAAABQAAAAKAAAAbWVkaWEtdHlwZQAABAAAAG5hbWUAAAAAAAAAAIj///8KAAAAbWVkaWEtdHlwZQAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABIAAAAAAAABUQAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAG5hbWUAAAAABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAABAAAAG5hbWUAAAAA/////9gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAADAAAAAAAAAABQAAAAAAAADAwAKABgADAAIAAQACgAAABQAAAB4AAAABgAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAACAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAACAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAFwAAAC8AAABBAAAAUQAAAGgAAAB5AAAAAAAAAGJyb3dzZXJfbWFya2V0c2hhcmUuY3N2ZmxpZ2h0X2luZm9fYnlfc3RhdGUuY3N2Z2RwX3Blcl9jYXBpdGEuY3N2anNfbGlicmFyaWVzLmNzdnBvcHVsYXRpb25fYnlfc3RhdGUuY3N2d2VpZ2h0X2hlaWdodC5jc3YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAPAAAAAAAAwABAAAAyAEAAAAAAADgAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACkAAAAAwAAAEwAAAAoAAAABAAAAND+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAA8P7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAAQ////CAAAADwAAAAwAAAAeyJ0eXBlIjoiZGlyZWN0b3J5LWxpc3RpbmciLCJwYXRoU2VwYXJhdG9yIjoiLyJ9AAAAAAQAAABtZXRhAAAAAAIAAAB8AAAABAAAAJ7///8UAAAAQAAAAEAAAAAAAAAFPAAAAAEAAAAEAAAAjP///wgAAAAUAAAACgAAAG1lZGlhLXR5cGUAAAQAAABuYW1lAAAAAAAAAACI////CgAAAG1lZGlhLXR5cGUAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAASAAAAAAAAAVEAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABuYW1lAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAQAAABuYW1lAAAAAOgBAABBUlJPVzE= FRAME=QVJST1cxAAD/////uAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAKQAAAADAAAATAAAACgAAAAEAAAA0P7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADw/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAABD///8IAAAAPAAAADAAAAB7InR5cGUiOiJkaXJlY3RvcnktbGlzdGluZyIsInBhdGhTZXBhcmF0b3IiOiIvIn0AAAAABAAAAG1ldGEAAAAAAgAAAHwAAAAEAAAAnv///xQAAABAAAAAQAAAAAAAAAU8AAAAAQAAAAQAAACM////CAAAABQAAAAKAAAAbWVkaWEtdHlwZQAABAAAAG5hbWUAAAAAAAAAAIj///8KAAAAbWVkaWEtdHlwZQAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABIAAAAAAAABUQAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAG5hbWUAAAAABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAABAAAAG5hbWUAAAAA/////9gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAADQAAAAAAAAABQAAAAAAAADAwAKABgADAAIAAQACgAAABQAAAB4AAAABwAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAACQAAAAAAAAALAAAAAAAAAAAAAAAAAAAACwAAAAAAAAACAAAAAAAAAA0AAAAAAAAAAAAAAAAAAAAAAAAAACAAAABwAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAFwAAAC8AAABBAAAAUQAAAGIAAAB5AAAAigAAAGJyb3dzZXJfbWFya2V0c2hhcmUuY3N2ZmxpZ2h0X2luZm9fYnlfc3RhdGUuY3N2Z2RwX3Blcl9jYXBpdGEuY3N2anNfbGlicmFyaWVzLmNzdm9obGNfZG9nZWNvaW4uY3N2cG9wdWxhdGlvbl9ieV9zdGF0ZS5jc3Z3ZWlnaHRfaGVpZ2h0LmNzdgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAMAAQAAAMgBAAAAAAAA4AAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAApAAAAAMAAABMAAAAKAAAAAQAAADQ/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAPD+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAEP///wgAAAA8AAAAMAAAAHsidHlwZSI6ImRpcmVjdG9yeS1saXN0aW5nIiwicGF0aFNlcGFyYXRvciI6Ii8ifQAAAAAEAAAAbWV0YQAAAAACAAAAfAAAAAQAAACe////FAAAAEAAAABAAAAAAAAABTwAAAABAAAABAAAAIz///8IAAAAFAAAAAoAAABtZWRpYS10eXBlAAAEAAAAbmFtZQAAAAAAAAAAiP///woAAABtZWRpYS10eXBlAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEgAAAAAAAAFRAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAbmFtZQAAAAAEAAAAbmFtZQAAAAAAAAAABAAEAAQAAAAEAAAAbmFtZQAAAADoAQAAQVJST1cx

@ -44,6 +44,7 @@ import * as textPanel from 'app/plugins/panel/text/module';
import * as timeseriesPanel from 'app/plugins/panel/timeseries/module'; import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
import * as stateTimelinePanel from 'app/plugins/panel/state-timeline/module'; import * as stateTimelinePanel from 'app/plugins/panel/state-timeline/module';
import * as statusHistoryPanel from 'app/plugins/panel/status-history/module'; import * as statusHistoryPanel from 'app/plugins/panel/status-history/module';
import * as marketTrendPanel from 'app/plugins/panel/market-trend/module';
import * as graphPanel from 'app/plugins/panel/graph/module'; import * as graphPanel from 'app/plugins/panel/graph/module';
import * as xyChartPanel from 'app/plugins/panel/xychart/module'; import * as xyChartPanel from 'app/plugins/panel/xychart/module';
import * as dashListPanel from 'app/plugins/panel/dashlist/module'; import * as dashListPanel from 'app/plugins/panel/dashlist/module';
@ -99,6 +100,7 @@ const builtInPlugins: any = {
'app/plugins/panel/timeseries/module': timeseriesPanel, 'app/plugins/panel/timeseries/module': timeseriesPanel,
'app/plugins/panel/state-timeline/module': stateTimelinePanel, 'app/plugins/panel/state-timeline/module': stateTimelinePanel,
'app/plugins/panel/status-history/module': statusHistoryPanel, 'app/plugins/panel/status-history/module': statusHistoryPanel,
'app/plugins/panel/market-trend/module': marketTrendPanel,
'app/plugins/panel/graph/module': graphPanel, 'app/plugins/panel/graph/module': graphPanel,
'app/plugins/panel/xychart/module': xyChartPanel, 'app/plugins/panel/xychart/module': xyChartPanel,
'app/plugins/panel/geomap/module': geomapPanel, 'app/plugins/panel/geomap/module': geomapPanel,

@ -13,6 +13,7 @@ export const CSVFileEditor = ({ onChange, query }: EditorProps) => {
'population_by_state.csv', 'population_by_state.csv',
'gdp_per_capita.csv', 'gdp_per_capita.csv',
'js_libraries.csv', 'js_libraries.csv',
'ohlc_dogecoin.csv',
'weight_height.csv', 'weight_height.csv',
'browser_marketshare.csv', 'browser_marketshare.csv',
].map((name) => ({ label: name, value: name })); ].map((name) => ({ label: name, value: name }));

@ -4,6 +4,7 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 1`] = `
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 15, "gap": 15,
"grid": Object { "grid": Object {
@ -30,6 +31,7 @@ Object {
"values": [Function], "values": [Function],
}, },
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {
@ -138,6 +140,7 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 2`] = `
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 15, "gap": 15,
"grid": Object { "grid": Object {
@ -164,6 +167,7 @@ Object {
"values": [Function], "values": [Function],
}, },
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {
@ -272,6 +276,7 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 3`] = `
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 15, "gap": 15,
"grid": Object { "grid": Object {
@ -298,6 +303,7 @@ Object {
"values": [Function], "values": [Function],
}, },
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {
@ -406,6 +412,7 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 1`] = `
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 15, "gap": 15,
"grid": Object { "grid": Object {
@ -432,6 +439,7 @@ Object {
"values": [Function], "values": [Function],
}, },
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {
@ -540,6 +548,7 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 2`] = `
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 15, "gap": 15,
"grid": Object { "grid": Object {
@ -566,6 +575,7 @@ Object {
"values": [Function], "values": [Function],
}, },
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {
@ -674,6 +684,7 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 3`] = `
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 15, "gap": 15,
"grid": Object { "grid": Object {
@ -700,6 +711,7 @@ Object {
"values": [Function], "values": [Function],
}, },
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {
@ -808,6 +820,7 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 1`] = `
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 15, "gap": 15,
"grid": Object { "grid": Object {
@ -834,6 +847,7 @@ Object {
"values": [Function], "values": [Function],
}, },
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {
@ -942,6 +956,7 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 2`] = `
Object { Object {
"axes": Array [ "axes": Array [
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 15, "gap": 15,
"grid": Object { "grid": Object {
@ -968,6 +983,7 @@ Object {
"values": [Function], "values": [Function],
}, },
Object { Object {
"filter": undefined,
"font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "font": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"gap": 5, "gap": 5,
"grid": Object { "grid": Object {

@ -121,7 +121,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
splits: config.xSplits, splits: config.xSplits,
values: config.xValues, values: config.xValues,
grid: { show: false }, grid: { show: false },
ticks: false, ticks: { show: false },
gap: 15, gap: 15,
valueRotation: valueRotation * -1, valueRotation: valueRotation * -1,
theme, theme,

@ -0,0 +1,316 @@
// this file is pretty much a copy-paste of TimeSeriesPanel.tsx :(
// with some extra renderers passed to the <TimeSeries> component
import React, { useMemo } from 'react';
import { DataFrame, Field, getDisplayProcessor, PanelProps } from '@grafana/data';
import { TooltipDisplayMode } from '@grafana/schema';
import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, UPlotConfigBuilder } from '@grafana/ui';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin';
import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin';
import { prepareGraphableFields } from '../timeseries/utils';
import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin';
import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin';
import { config } from 'app/core/config';
import { drawMarkers, FieldIndices } from './utils';
import { defaultColors, MarketOptions, MarketTrendMode } from './models.gen';
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
import { findField } from 'app/features/dimensions';
interface MarketPanelProps extends PanelProps<MarketOptions> {}
function findFieldInFrames(frames?: DataFrame[], name?: string): Field | undefined {
if (frames?.length) {
for (const frame of frames) {
const f = findField(frame, name);
if (f) {
return f;
}
}
}
return undefined;
}
export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
data,
timeRange,
timeZone,
width,
height,
options,
fieldConfig,
onChangeTimeRange,
replaceVariables,
}) => {
const { sync, canAddAnnotations, onThresholdsChange, canEditThresholds, onSplitOpen } = usePanelContext();
const getFieldLinks = (field: Field, rowIndex: number) => {
return getFieldLinksForExplore({ field, rowIndex, splitOpenFn: onSplitOpen, range: timeRange });
};
const { frames, warn } = useMemo(
() => prepareGraphableFields(data?.series, config.theme2),
// eslint-disable-next-line react-hooks/exhaustive-deps
[data, options]
);
const { renderers, tweakScale, tweakAxis } = useMemo(() => {
let tweakScale = (opts: ScaleProps) => opts;
let tweakAxis = (opts: AxisProps) => opts;
let doNothing = {
renderers: [],
tweakScale,
tweakAxis,
};
if (options.fieldMap == null) {
return doNothing;
}
const { mode, priceStyle, fieldMap, colorStrategy } = options;
const colors = { ...defaultColors, ...options.colors };
let { open, high, low, close, volume } = fieldMap;
if (
open == null ||
close == null ||
findFieldInFrames(frames, open) == null ||
findFieldInFrames(frames, close) == null
) {
return doNothing;
}
let volumeAlpha = 0.5;
let volumeIdx = -1;
let shouldRenderVolume = false;
// find volume field and set overrides
if (volume != null && mode !== MarketTrendMode.Price) {
let volumeField = findFieldInFrames(frames, volume);
if (volumeField != null) {
shouldRenderVolume = true;
let { fillOpacity } = volumeField.config.custom;
if (fillOpacity) {
volumeAlpha = fillOpacity / 100;
}
// we only want to put volume on own shorter axis when rendered with price
if (mode !== MarketTrendMode.Volume) {
volumeField.config = { ...volumeField.config };
volumeField.config.unit = 'short';
volumeField.display = getDisplayProcessor({
field: volumeField,
theme: config.theme2,
});
tweakAxis = (opts: AxisProps) => {
if (opts.scaleKey === 'short') {
let filter = (u: uPlot, splits: number[]) => {
let _splits = [];
let max = u.series[volumeIdx].max as number;
for (let i = 0; i < splits.length; i++) {
_splits.push(splits[i]);
if (splits[i] > max) {
break;
}
}
return _splits;
};
opts.space = 20; // reduce tick spacing
opts.filter = filter; // hide tick labels
opts.ticks = { ...opts.ticks, filter }; // hide tick marks
}
return opts;
};
tweakScale = (opts: ScaleProps) => {
if (opts.scaleKey === 'short') {
opts.range = (u: uPlot, min: number, max: number) => [0, max * 7];
}
return opts;
};
}
}
}
let shouldRenderPrice =
mode !== MarketTrendMode.Volume &&
high != null &&
low != null &&
findFieldInFrames(frames, high) != null &&
findFieldInFrames(frames, low) != null;
if (!shouldRenderPrice && !shouldRenderVolume) {
return doNothing;
}
let fields: Record<string, string> = {};
let indicesOnly = [];
if (shouldRenderPrice) {
fields = { open, high, low, close };
// hide series from legend that are rendered as composite markers
for (let key in fields) {
let field = findFieldInFrames(frames, fields[key])!;
field.config = {
...field.config,
custom: {
...field.config.custom,
hideFrom: { legend: true, tooltip: false, viz: false },
},
};
}
} else {
// these fields should not be omitted from normal rendering if they arent rendered
// as part of price markers. they're only here so we can get back their indicies in the
// init callback below. TODO: remove this when field mapping happens in the panel instead of deep
indicesOnly.push(open, close);
}
if (shouldRenderVolume) {
fields.volume = volume;
fields.open = open;
fields.close = close;
}
return {
renderers: [
{
fieldMap: fields,
indicesOnly,
init: (builder: UPlotConfigBuilder, fieldIndices: FieldIndices) => {
volumeIdx = fieldIndices.volume!;
builder.addHook(
'drawAxes',
drawMarkers({
mode,
fields: fieldIndices,
upColor: config.theme2.visualization.getColorByName(colors.up),
downColor: config.theme2.visualization.getColorByName(colors.down),
flatColor: config.theme2.visualization.getColorByName(colors.flat),
volumeAlpha,
colorStrategy,
priceStyle,
flatAsUp: true,
})
);
},
},
],
tweakScale,
tweakAxis,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options, data.structureRev]);
if (!frames || warn) {
return (
<div className="panel-empty">
<p>{warn ?? 'No data found in response'}</p>
</div>
);
}
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
return (
<TimeSeries
frames={frames}
structureRev={data.structureRev}
timeRange={timeRange}
timeZone={timeZone}
width={width}
height={height}
legend={options.legend}
renderers={renderers}
tweakAxis={tweakAxis}
tweakScale={tweakScale}
options={options}
>
{(config, alignedDataFrame) => {
return (
<>
<ZoomPlugin config={config} onZoom={onChangeTimeRange} />
<TooltipPlugin
data={alignedDataFrame}
config={config}
mode={TooltipDisplayMode.Multi}
sync={sync}
timeZone={timeZone}
/>
{/* Renders annotation markers*/}
{data.annotations && (
<AnnotationsPlugin annotations={data.annotations} config={config} timeZone={timeZone} />
)}
{/* Enables annotations creation*/}
<AnnotationEditorPlugin data={alignedDataFrame} timeZone={timeZone} config={config}>
{({ startAnnotating }) => {
return (
<ContextMenuPlugin
data={alignedDataFrame}
config={config}
timeZone={timeZone}
replaceVariables={replaceVariables}
defaultItems={
enableAnnotationCreation
? [
{
items: [
{
label: 'Add annotation',
ariaLabel: 'Add annotation',
icon: 'comment-alt',
onClick: (e, p) => {
if (!p) {
return;
}
startAnnotating({ coords: p.coords });
},
},
],
},
]
: []
}
/>
);
}}
</AnnotationEditorPlugin>
{data.annotations && (
<ExemplarsPlugin
config={config}
exemplars={data.annotations}
timeZone={timeZone}
getFieldLinks={getFieldLinks}
/>
)}
{canEditThresholds && onThresholdsChange && (
<ThresholdControlsPlugin
config={config}
fieldConfig={fieldConfig}
onThresholdsChange={onThresholdsChange}
/>
)}
</>
);
}}
</TimeSeries>
);
};

@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<path style="fill:#4788C7;" d="M35.5,23L35.5,23c-0.275,0-0.5-0.225-0.5-0.5v-21C35,1.225,35.225,1,35.5,1l0,0 C35.775,1,36,1.225,36,1.5v21C36,22.775,35.775,23,35.5,23z"/>
<path style="fill:#4788C7;" d="M14.5,30L14.5,30c-0.275,0-0.5-0.225-0.5-0.5v-23C14,6.225,14.225,6,14.5,6h0 C14.775,6,15,6.225,15,6.5v23C15,29.775,14.775,30,14.5,30z"/>
<path style="fill:#4788C7;" d="M25.5,35L25.5,35c-0.275,0-0.5-0.225-0.5-0.5v-17c0-0.275,0.225-0.5,0.5-0.5l0,0 c0.275,0,0.5,0.225,0.5,0.5v17C26,34.775,25.775,35,25.5,35z"/>
<path style="fill:#4788C7;" d="M4.5,39L4.5,39C4.225,39,4,38.775,4,38.5v-19C4,19.225,4.225,19,4.5,19h0C4.775,19,5,19.225,5,19.5 v19C5,38.775,4.775,39,4.5,39z"/>
<g>
<rect x="32.5" y="4.5" style="fill:#98CCFD;" width="6" height="15"/>
<g>
<path style="fill:#4788C7;" d="M38,5v14h-5V5H38 M39,4h-7v16h7V4L39,4z"/>
</g>
</g>
<g>
<rect x="11.5" y="9.5" style="fill:#98CCFD;" width="6" height="17"/>
<g>
<path style="fill:#4788C7;" d="M17,10v16h-5V10H17 M18,9h-7v18h7V9L18,9z"/>
</g>
</g>
<g>
<rect x="22.5" y="20.5" style="fill:#DFF0FE;" width="6" height="11"/>
<g>
<path style="fill:#4788C7;" d="M28,21v10h-5V21H28 M29,20h-7v12h7V20L29,20z"/>
</g>
</g>
<g>
<rect x="1.5" y="22.5" style="fill:#DFF0FE;" width="6" height="13"/>
<g>
<path style="fill:#4788C7;" d="M7,23v12H2V23H7 M8,22H1v14h7V22L8,22z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,29 @@
// Copyright 2021 Grafana Labs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grafanaschema
Panel: {
lineages: [
[
{
PanelOptions: {
// anything for now
...
}
}
]
]
migrations: []
}

@ -0,0 +1,52 @@
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// NOTE: This file will be auto generated from models.cue
// It is currenty hand written but will serve as the target for cuetsy
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import { TimeSeriesOptions } from '../timeseries/types';
export const modelVersion = Object.freeze([1, 0]);
export enum MarketTrendMode {
Price = 'price',
Volume = 'volume',
PriceVolume = 'pricevolume',
}
export enum PriceStyle {
Candles = 'candles',
OHLCBars = 'ohlcbars',
}
export enum ColorStrategy {
// up/down color depends on current close vs current open
// filled always
Intra = 'intra',
// up/down color depends on current close vs prior close
// filled/hollow depends on current close vs current open
Inter = 'inter',
}
interface SemanticFieldMap {
[semanticName: string]: string;
}
export interface MarketTrendColors {
up: string;
down: string;
flat: string;
}
export const defaultColors: MarketTrendColors = {
up: 'green',
down: 'red',
flat: 'gray',
};
export interface MarketOptions extends TimeSeriesOptions {
mode: MarketTrendMode;
priceStyle: PriceStyle;
colorStrategy: ColorStrategy;
fieldMap: SemanticFieldMap;
colors: MarketTrendColors;
}

@ -0,0 +1,102 @@
import { GraphFieldConfig } from '@grafana/schema';
import { FieldConfigProperty, PanelPlugin, SelectableValue } from '@grafana/data';
import { commonOptionsBuilder } from '@grafana/ui';
import { MarketTrendPanel } from './MarketTrendPanel';
import { defaultColors, MarketOptions, MarketTrendMode, ColorStrategy, PriceStyle } from './models.gen';
import { defaultGraphConfig, getGraphFieldConfig } from '../timeseries/config';
const modeOptions = [
{ label: 'Price & Volume', value: MarketTrendMode.PriceVolume },
{ label: 'Price', value: MarketTrendMode.Price },
{ label: 'Volume', value: MarketTrendMode.Volume },
] as Array<SelectableValue<MarketTrendMode>>;
const priceStyle = [
{ label: 'Candles', value: PriceStyle.Candles },
{ label: 'OHLC Bars', value: PriceStyle.OHLCBars },
] as Array<SelectableValue<PriceStyle>>;
const colorStrategy = [
{ label: 'Since Open', value: 'intra' },
{ label: 'Since Prior Close', value: 'inter' },
] as Array<SelectableValue<ColorStrategy>>;
function getMarketFieldConfig() {
const v = getGraphFieldConfig(defaultGraphConfig);
v.standardOptions![FieldConfigProperty.Unit] = {
settings: {},
defaultValue: 'currencyUSD',
};
return v;
}
export const plugin = new PanelPlugin<MarketOptions, GraphFieldConfig>(MarketTrendPanel)
.useFieldConfig(getMarketFieldConfig())
.setPanelOptions((builder) => {
builder
.addRadio({
path: 'mode',
name: 'Mode',
description: '',
defaultValue: MarketTrendMode.PriceVolume,
settings: {
options: modeOptions,
},
})
.addRadio({
path: 'priceStyle',
name: 'Price style',
description: '',
defaultValue: PriceStyle.Candles,
settings: {
options: priceStyle,
},
showIf: (opts) => opts.mode !== MarketTrendMode.Volume,
})
.addRadio({
path: 'colorStrategy',
name: 'Color strategy',
description: '',
defaultValue: ColorStrategy.Intra,
settings: {
options: colorStrategy,
},
})
.addColorPicker({
path: 'colors.up',
name: 'Up color',
defaultValue: defaultColors.up,
})
.addColorPicker({
path: 'colors.down',
name: 'Down color',
defaultValue: defaultColors.down,
})
.addFieldNamePicker({
path: 'fieldMap.open',
name: 'Open field',
})
.addFieldNamePicker({
path: 'fieldMap.high',
name: 'High field',
showIf: (opts) => opts.mode !== MarketTrendMode.Volume,
})
.addFieldNamePicker({
path: 'fieldMap.low',
name: 'Low field',
showIf: (opts) => opts.mode !== MarketTrendMode.Volume,
})
.addFieldNamePicker({
path: 'fieldMap.close',
name: 'Close field',
})
.addFieldNamePicker({
path: 'fieldMap.volume',
name: 'Volume field',
showIf: (opts) => opts.mode !== MarketTrendMode.Price,
});
// commonOptionsBuilder.addTooltipOptions(builder);
commonOptionsBuilder.addLegendOptions(builder);
})
.setDataSupport({ annotations: true, alertStates: true });

@ -0,0 +1,17 @@
{
"type": "panel",
"name": "Market trend",
"id": "market-trend",
"state": "alpha",
"info": {
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
},
"logos": {
"small": "img/candlestick.svg",
"large": "img/candlestick.svg"
}
}
}

@ -0,0 +1,181 @@
import { MarketTrendMode, ColorStrategy, PriceStyle } from './models.gen';
import uPlot from 'uplot';
import { colorManipulator } from '@grafana/data';
const { alpha } = colorManipulator;
export type FieldIndices = Record<string, number>;
interface RendererOpts {
mode: MarketTrendMode;
priceStyle: PriceStyle;
fields: FieldIndices;
colorStrategy: ColorStrategy;
upColor: string;
downColor: string;
flatColor: string;
volumeAlpha: number;
flatAsUp: boolean;
}
export function drawMarkers(opts: RendererOpts) {
let { mode, priceStyle, fields, colorStrategy, upColor, downColor, flatColor, volumeAlpha, flatAsUp = true } = opts;
let drawPrice = mode !== MarketTrendMode.Volume && fields.high != null && fields.low != null;
let asCandles = drawPrice && priceStyle === PriceStyle.Candles;
let drawVolume = mode !== MarketTrendMode.Price && fields.volume != null;
function selectPath(priceDir: number, flatPath: Path2D, upPath: Path2D, downPath: Path2D, flatAsUp: boolean) {
return priceDir > 0 ? upPath : priceDir < 0 ? downPath : flatAsUp ? upPath : flatPath;
}
let tIdx = 0,
oIdx = fields.open,
hIdx = fields.high,
lIdx = fields.low,
cIdx = fields.close,
vIdx = fields.volume;
return (u: uPlot) => {
// split by discrete color to reduce draw calls
let downPath, upPath, flatPath;
// with adjusted reduced
let downPathVol, upPathVol, flatPathVol;
if (drawPrice) {
flatPath = new Path2D();
upPath = new Path2D();
downPath = new Path2D();
}
if (drawVolume) {
downPathVol = new Path2D();
upPathVol = new Path2D();
flatPathVol = new Path2D();
}
let hollowPath = new Path2D();
let ctx = u.ctx;
let tData = u.data[tIdx!];
let oData = u.data[oIdx!];
let cData = u.data[cIdx!];
let hData = drawPrice ? u.data[hIdx!] : null;
let lData = drawPrice ? u.data[lIdx!] : null;
let vData = drawVolume ? u.data[vIdx!] : null;
let zeroPx = vIdx != null ? Math.round(u.valToPos(0, u.series[vIdx!].scale!, true)) : null;
let [idx0, idx1] = u.series[0].idxs!;
let colWidth = u.bbox.width / (idx1 - idx0);
let barWidth = Math.round(0.6 * colWidth);
let stickWidth = 2;
let outlineWidth = 2;
if (barWidth <= 12) {
stickWidth = outlineWidth = 1;
}
let halfWidth = Math.floor(barWidth / 2);
for (let i = idx0; i <= idx1; i++) {
let tPx = Math.round(u.valToPos(tData[i]!, 'x', true));
// current close vs prior close
let interDir = i === idx0 ? 0 : Math.sign(cData[i]! - cData[i - 1]!);
// current close vs current open
let intraDir = Math.sign(cData[i]! - oData[i]!);
// volume
if (drawVolume) {
let outerPath = selectPath(
colorStrategy === ColorStrategy.Inter ? interDir : intraDir,
flatPathVol as Path2D,
upPathVol as Path2D,
downPathVol as Path2D,
i === idx0 && ColorStrategy.Inter ? false : flatAsUp
);
let vPx = Math.round(u.valToPos(vData![i]!, u.series[vIdx!].scale!, true));
outerPath.rect(tPx - halfWidth, vPx, barWidth, zeroPx! - vPx);
}
if (drawPrice) {
let outerPath = selectPath(
colorStrategy === ColorStrategy.Inter ? interDir : intraDir,
flatPath as Path2D,
upPath as Path2D,
downPath as Path2D,
i === idx0 && ColorStrategy.Inter ? false : flatAsUp
);
// stick
let hPx = Math.round(u.valToPos(hData![i]!, u.series[hIdx!].scale!, true));
let lPx = Math.round(u.valToPos(lData![i]!, u.series[lIdx!].scale!, true));
outerPath.rect(tPx - Math.floor(stickWidth / 2), hPx, stickWidth, lPx - hPx);
let oPx = Math.round(u.valToPos(oData[i]!, u.series[oIdx!].scale!, true));
let cPx = Math.round(u.valToPos(cData[i]!, u.series[cIdx!].scale!, true));
if (asCandles) {
// rect
let top = Math.min(oPx, cPx);
let btm = Math.max(oPx, cPx);
let hgt = Math.max(1, btm - top);
outerPath.rect(tPx - halfWidth, top, barWidth, hgt);
if (colorStrategy === ColorStrategy.Inter) {
if (intraDir >= 0 && hgt > outlineWidth * 2) {
hollowPath.rect(
tPx - halfWidth + outlineWidth,
top + outlineWidth,
barWidth - outlineWidth * 2,
hgt - outlineWidth * 2
);
}
}
} else {
outerPath.rect(tPx - halfWidth, oPx, halfWidth, stickWidth);
outerPath.rect(tPx, cPx, halfWidth, stickWidth);
}
}
}
ctx.save();
ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
ctx.clip();
if (drawVolume) {
ctx.fillStyle = alpha(upColor, volumeAlpha);
ctx.fill(upPathVol as Path2D);
ctx.fillStyle = alpha(downColor, volumeAlpha);
ctx.fill(downPathVol as Path2D);
ctx.fillStyle = alpha(flatColor, volumeAlpha);
ctx.fill(flatPathVol as Path2D);
}
if (drawPrice) {
ctx.fillStyle = upColor;
ctx.fill(upPath as Path2D);
ctx.fillStyle = downColor;
ctx.fill(downPath as Path2D);
ctx.fillStyle = flatColor;
ctx.fill(flatPath as Path2D);
ctx.globalCompositeOperation = 'destination-out';
ctx.fill(hollowPath);
}
ctx.restore();
};
}

@ -196,7 +196,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
splits: coreConfig.ySplits, splits: coreConfig.ySplits,
values: coreConfig.yValues, values: coreConfig.yValues,
grid: { show: false }, grid: { show: false },
ticks: false, ticks: { show: false },
gap: 16, gap: 16,
theme, theme,
}); });

File diff suppressed because it is too large Load Diff

@ -7,7 +7,7 @@
SED=$(command -v gsed) SED=$(command -v gsed)
SED=${SED:-"sed"} SED=${SED:-"sed"}
FILES=$(grep -rl '"schemaVersion": 3[01]' devenv) FILES=$(grep -rl '"schemaVersion": 3[0123]' devenv)
set -e set -e
set -x set -x
for DASH in ${FILES}; do echo "${DASH}"; grep -v 'null,$' "${DASH}" > "${DASH}-nulless"; mv "${DASH}-nulless" "${DASH}"; done for DASH in ${FILES}; do echo "${DASH}"; grep -v 'null,$' "${DASH}" > "${DASH}-nulless"; mv "${DASH}-nulless" "${DASH}"; done

Loading…
Cancel
Save