Browse Source

pass data in chartwerk

merge-requests/3/merge
vargburz 3 years ago
parent
commit
3813f27692
  1. 4
      package.json
  2. 55
      src/SimplePanel.tsx
  3. 54
      src/components/Panel.tsx
  4. 194
      src/grafana/data_processor.ts
  5. 129
      src/grafana/null_insert.ts
  6. 1
      src/img/logo.svg
  7. 12
      yarn.lock

4
package.json

@ -14,11 +14,13 @@
"author": "CorpGlory",
"license": "GPL V3",
"devDependencies": {
"@chartwerk/gauge-pod": "@chartwerk/gauge-pod",
"@chartwerk/gauge-pod": "0.4.1",
"@grafana/data": "latest",
"@grafana/toolkit": "latest",
"@grafana/ui": "latest",
"@types/grafana": "github:CorpGlory/types-grafana#8c55ade5212f089748f6955f73e8f753fff9f278",
"emotion": "10.0.27",
"lodash": "^4.17.21",
"prettier": "^2.6.2"
},
"engines": {

55
src/SimplePanel.tsx

@ -1,55 +0,0 @@
import React from 'react';
import { PanelProps } from '@grafana/data';
import { PanelOptions } from 'types';
import { css, cx } from 'emotion';
import { stylesFactory, useTheme } from '@grafana/ui';
interface Props extends PanelProps<PanelOptions> {}
export const SimplePanel: React.FC<Props> = ({ options, data, width, height }) => {
const theme = useTheme();
const styles = getStyles();
return (
<div
className={cx(
styles.wrapper,
css`
width: ${width}px;
height: ${height}px;
`
)}
>
<svg
className={styles.svg}
width={width}
height={height}
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox={`-${width / 2} -${height / 2} ${width} ${height}`}
>
<g>
<circle style={{ fill: `${theme.isLight ? theme.palette.greenBase : theme.palette.blue95}` }} r={100} />
</g>
</svg>
</div>
);
};
const getStyles = stylesFactory(() => {
return {
wrapper: css`
position: relative;
`,
svg: css`
position: absolute;
top: 0;
left: 0;
`,
textBox: css`
position: absolute;
bottom: 0;
left: 0;
padding: 10px;
`,
};
});

54
src/components/Panel.tsx

@ -1,3 +1,5 @@
import { DataProcessor } from '../grafana/data_processor';
import { PanelOptions } from '../types';
import { ChartwerkGaugePod } from '@chartwerk/gauge-pod';
@ -6,10 +8,20 @@ import { PanelProps } from '@grafana/data';
import React, { useRef } from 'react';
import { css } from 'emotion';
import * as _ from 'lodash';
interface Props extends PanelProps<PanelOptions> {}
export function Panel({ options, data, width, height, timeZone, timeRange, onChangeTimeRange }: Props) {
console.log('options', options);
console.log('data', data);
const processor = new DataProcessor({});
const seriesList = processor.getSeriesList({
dataList: data.series,
range: timeRange,
});
const chwSeriesList = updateSeriesListWithChartwerkParams(seriesList);
console.log('seriesList', chwSeriesList);
let chartContainer = useRef(null);
// TODO: use models to parse options and series
@ -18,25 +30,24 @@ export function Panel({ options, data, width, height, timeZone, timeRange, onCha
// TODO: pass datapoints
// TODO: pass options
// TODO: switch / case pod type
const pod = new ChartwerkGaugePod(
(chartContainer as any).current,
[
const pod = new ChartwerkGaugePod((chartContainer as any).current, chwSeriesList, {
maxValue: options.gauge.max,
minValue: options.gauge.min,
valueFormatter: (val) => val.toFixed(2),
defaultColor: 'green',
stops: [
{
target: 'test',
datapoints: [
[5, 5],
[0, 10],
[30, 15],
[50, 20],
[17, 25],
],
color: 'green',
value: 100,
},
],
{
maxValue: options.gauge.max,
minValue: options.gauge.min,
}
);
color: 'orange',
value: 140,
},
],
// @ts-ignore
icons: [{ src: 'https://cityhost.ua/upload_img/blog5ef308ea5529c_trash2-01.jpg', position: 'middle', size: 30 }],
});
pod.render();
});
return (
@ -49,3 +60,14 @@ export function Panel({ options, data, width, height, timeZone, timeRange, onCha
></div>
);
}
function updateSeriesListWithChartwerkParams(series: any[]): any[] {
return _.map(series, (serie: any, idx: number) => {
return {
target: serie.alias,
color: serie.color,
datapoints: _.map(serie.datapoints, (row) => _.reverse(row)),
alias: serie.label,
};
});
}

194
src/grafana/data_processor.ts

@ -0,0 +1,194 @@
import { applyNullInsertThreshold } from './null_insert';
import { find } from 'lodash';
import {
DataFrame,
dateTime,
Field,
FieldType,
getColorForTheme,
getFieldDisplayName,
getTimeField,
TimeRange,
} from '@grafana/data';
import { colors } from '@grafana/ui';
import config from 'grafana/app/core/config';
import TimeSeries from 'grafana/app/core/time_series2';
type Options = {
dataList: DataFrame[];
range?: TimeRange;
};
export class DataProcessor {
constructor(private panel: any) {}
getSeriesList(options: Options): TimeSeries[] {
const list: TimeSeries[] = [];
const { dataList, range } = options;
if (!dataList || !dataList.length) {
return list;
}
for (let i = 0; i < dataList.length; i++) {
let series = dataList[i];
let { timeField } = getTimeField(series);
if (!timeField) {
continue;
}
series = applyNullInsertThreshold(series, timeField.name);
timeField = getTimeField(series).timeField!; // use updated length
for (let j = 0; j < series.fields.length; j++) {
const field = series.fields[j];
if (field.type !== FieldType.number) {
continue;
}
const name = getFieldDisplayName(field, series, dataList);
const datapoints = [];
for (let r = 0; r < series.length; r++) {
datapoints.push([field.values.get(r), dateTime(timeField.values.get(r)).valueOf()]);
}
list.push(this.toTimeSeries(field, name, i, j, datapoints, list.length, range));
}
}
// Merge all the rows if we want to show a histogram
if (this.panel?.xaxis?.mode === 'histogram' && !this.panel?.stack && list.length > 1) {
const first = list[0];
// @ts-ignore
first.alias = first.aliasEscaped = 'Count';
for (let i = 1; i < list.length; i++) {
first.datapoints = first.datapoints.concat(list[i].datapoints);
}
return [first];
}
return list;
}
private toTimeSeries(
field: Field,
alias: string,
dataFrameIndex: number,
fieldIndex: number,
datapoints: any[][],
index: number,
range?: TimeRange
) {
const colorIndex = index % colors.length;
const color = colors[colorIndex];
const series = new TimeSeries({
datapoints: datapoints || [],
alias: alias,
color: getColorForTheme(color, config.theme),
unit: field.config ? field.config.unit : undefined,
dataFrameIndex,
fieldIndex,
});
if (datapoints && datapoints.length > 0 && range) {
const last = datapoints[datapoints.length - 1][1];
const from = range.from;
if (last - from.valueOf() < -10000) {
// If the data is in reverse order
const first = datapoints[0][1];
if (first - from.valueOf() < -10000) {
series.isOutsideRange = true;
}
}
}
return series;
}
setPanelDefaultsForNewXAxisMode() {
switch (this.panel.xaxis.mode) {
case 'time': {
this.panel.bars = false;
this.panel.lines = true;
this.panel.points = false;
this.panel.legend.show = true;
this.panel.tooltip.shared = true;
this.panel.xaxis.values = [];
break;
}
case 'series': {
this.panel.bars = true;
this.panel.lines = false;
this.panel.points = false;
this.panel.stack = false;
this.panel.legend.show = false;
this.panel.tooltip.shared = false;
this.panel.xaxis.values = ['total'];
break;
}
case 'histogram': {
this.panel.bars = true;
this.panel.lines = false;
this.panel.points = false;
this.panel.stack = false;
this.panel.legend.show = false;
this.panel.tooltip.shared = false;
break;
}
}
}
validateXAxisSeriesValue() {
switch (this.panel.xaxis.mode) {
case 'series': {
if (this.panel.xaxis.values.length === 0) {
this.panel.xaxis.values = ['total'];
return;
}
const validOptions = this.getXAxisValueOptions({});
const found: any = find(validOptions, { value: this.panel.xaxis.values[0] });
if (!found) {
this.panel.xaxis.values = ['total'];
}
return;
}
}
}
getXAxisValueOptions(options: any) {
switch (this.panel.xaxis.mode) {
case 'series': {
return [
{ text: 'Avg', value: 'avg' },
{ text: 'Min', value: 'min' },
{ text: 'Max', value: 'max' },
{ text: 'Total', value: 'total' },
{ text: 'Count', value: 'count' },
];
}
}
return [];
}
pluckDeep(obj: any, property: string) {
const propertyParts = property.split('.');
let value = obj;
for (let i = 0; i < propertyParts.length; ++i) {
if (value[propertyParts[i]]) {
value = value[propertyParts[i]];
} else {
return undefined;
}
}
return value;
}
}

129
src/grafana/null_insert.ts

@ -0,0 +1,129 @@
// file is coppied from: grafana-ui\src\components\GraphNG\nullInsertThreshold.ts
import { ArrayVector, DataFrame, FieldType } from '@grafana/data';
type InsertMode = (prev: number, next: number, threshold: number) => number;
const INSERT_MODES = {
threshold: (prev: number, next: number, threshold: number) => prev + threshold,
midpoint: (prev: number, next: number, threshold: number) => (prev + next) / 2,
// previous time + 1ms to prevent StateTimeline from forward-interpolating prior state
plusone: (prev: number, next: number, threshold: number) => prev + 1,
};
export function applyNullInsertThreshold(
frame: DataFrame,
refFieldName?: string | null,
refFieldPseudoMax: number | null = null,
insertMode: InsertMode = INSERT_MODES.threshold
): DataFrame {
if (frame.length === 0) {
return frame;
}
const refField = frame.fields.find((field) => {
// note: getFieldDisplayName() would require full DF[]
return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time;
});
if (refField == null) {
return frame;
}
const thresholds = frame.fields.map((field) => field.config.custom?.insertNulls ?? refField.config.interval ?? null);
const uniqueThresholds = new Set<number>(thresholds);
uniqueThresholds.delete(null as any);
if (uniqueThresholds.size === 0) {
return frame;
}
if (uniqueThresholds.size === 1) {
const threshold = uniqueThresholds.values().next().value;
if (threshold <= 0) {
return frame;
}
const refValues = refField.values.toArray();
const frameValues = frame.fields.map((field) => field.values.toArray());
const filledFieldValues = nullInsertThreshold(refValues, frameValues, threshold, refFieldPseudoMax, insertMode);
if (filledFieldValues === frameValues) {
return frame;
}
return {
...frame,
length: filledFieldValues[0].length,
fields: frame.fields.map((field, i) => ({
...field,
values: new ArrayVector(filledFieldValues[i]),
})),
};
}
// TODO: unique threshold-per-field (via overrides) is unimplemented
// should be done by processing each (refField + thresholdA-field1 + thresholdA-field2...)
// as a separate nullInsertThreshold() dataset, then re-join into single dataset via join()
return frame;
}
function nullInsertThreshold(
refValues: number[],
frameValues: any[][],
threshold: number,
// will insert a trailing null when refFieldPseudoMax > last datapoint + threshold
refFieldPseudoMax: number | null = null,
getInsertValue: InsertMode
) {
const len = refValues.length;
let prevValue: number = refValues[0];
const refValuesNew: number[] = [prevValue];
for (let i = 1; i < len; i++) {
const curValue = refValues[i];
if (curValue - prevValue > threshold) {
refValuesNew.push(getInsertValue(prevValue, curValue, threshold));
}
refValuesNew.push(curValue);
prevValue = curValue;
}
if (refFieldPseudoMax != null && prevValue + threshold <= refFieldPseudoMax) {
refValuesNew.push(getInsertValue(prevValue, refFieldPseudoMax, threshold));
}
const filledLen = refValuesNew.length;
if (filledLen === len) {
return frameValues;
}
const filledFieldValues: any[][] = [];
for (let fieldValues of frameValues) {
let filledValues;
if (fieldValues !== refValues) {
filledValues = Array(filledLen);
for (let i = 0, j = 0; i < filledLen; i++) {
filledValues[i] = refValues[j] === refValuesNew[i] ? fieldValues[j++] : null;
}
} else {
filledValues = refValuesNew;
}
filledFieldValues.push(filledValues);
}
return filledFieldValues;
}

1
src/img/logo.svg

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 81.9 71.52"><defs><style>.cls-1{fill:#84aff1;}.cls-2{fill:#3865ab;}.cls-3{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="42.95" y1="16.88" x2="81.9" y2="16.88" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M55.46,62.43A2,2,0,0,1,54.07,59l4.72-4.54a2,2,0,0,1,2.2-.39l3.65,1.63,3.68-3.64a2,2,0,1,1,2.81,2.84l-4.64,4.6a2,2,0,0,1-2.22.41L60.6,58.26l-3.76,3.61A2,2,0,0,1,55.46,62.43Z"/><path class="cls-2" d="M37,0H2A2,2,0,0,0,0,2V31.76a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V2A2,2,0,0,0,37,0ZM4,29.76V8.84H35V29.76Z"/><path class="cls-3" d="M79.9,0H45a2,2,0,0,0-2,2V31.76a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V2A2,2,0,0,0,79.9,0ZM47,29.76V8.84h31V29.76Z"/><path class="cls-2" d="M37,37.76H2a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V39.76A2,2,0,0,0,37,37.76ZM4,67.52V46.6H35V67.52Z"/><path class="cls-2" d="M79.9,37.76H45a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V39.76A2,2,0,0,0,79.9,37.76ZM47,67.52V46.6h31V67.52Z"/><rect class="cls-1" x="10.48" y="56.95" width="4" height="5.79"/><rect class="cls-1" x="17.43" y="53.95" width="4" height="8.79"/><rect class="cls-1" x="24.47" y="50.95" width="4" height="11.79"/><path class="cls-1" d="M19.47,25.8a6.93,6.93,0,1,1,6.93-6.92A6.93,6.93,0,0,1,19.47,25.8Zm0-9.85a2.93,2.93,0,1,0,2.93,2.93A2.93,2.93,0,0,0,19.47,16Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

12
yarn.lock

@ -906,10 +906,10 @@
version "0.1.0"
resolved "https://codeload.github.com/chartwerk/core/tar.gz/a30ca83842247c79969deaaacfc7fb444a60cefb"
"@chartwerk/gauge-pod@@chartwerk/gauge-pod":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@chartwerk/gauge-pod/-/gauge-pod-0.4.0.tgz#affe5354ce4a5d069a1ddc97d9bc71c8fd968d12"
integrity sha512-avOwJEeqWYshCN7XCPtzFrTYVFNpWNdFfDjrt+4i3zKHuCSiV0rTvHPAoKPvJc2ue50CTOdcT+3mOXJp6NtPZQ==
"@chartwerk/gauge-pod@0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@chartwerk/gauge-pod/-/gauge-pod-0.4.1.tgz#ac346d777f72ec855e51f5f7c8c01e12a1e1cb5c"
integrity sha512-Ik6Dr4AJP/L+7YjZVJ9W19ujXXB5/b5A3Qxboi491hrXYlqMrfAx/LoyfDAtSEmGNhK5qpT8XLluzuHVcgTY4g==
dependencies:
"@chartwerk/core" "github:chartwerk/core#a30ca83842247c79969deaaacfc7fb444a60cefb"
@ -2276,6 +2276,10 @@
dependencies:
"@types/node" "*"
"@types/grafana@github:CorpGlory/types-grafana#8c55ade5212f089748f6955f73e8f753fff9f278":
version "4.6.3"
resolved "https://codeload.github.com/CorpGlory/types-grafana/tar.gz/8c55ade5212f089748f6955f73e8f753fff9f278"
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"

Loading…
Cancel
Save