fgp-graph
fgp-graph bases on dygraphs and uses typescript as development langulage. It allows future-grid customer to explore their timeseries aggregations data sets.
Here's how the fgp-graphs works:
- Typescript
let graphDiv: HTMLDivElement = document.getElementById("graphArea") as HTMLDivElement;
let fgpGraph = new FgpGraph(graphDiv, [vsConfig]);
fgpGraph.initGraph();
```- React
let fgpGraph = new FgpGraph(document.getElementById('graphArea'), [
vdConfig,
vsConfig
]);
fgpGraph.initGraph();
```- Angular2
coming soon~
``` This graph is interactive as same as dygraphs, but some changes are made to meet the needs of futuregrid customer requirements. You can scroll up and down on both left and right axis to scale the y-ranges. You can also zoom-in & zoom-out on graph in the same way. Panning on those area has same effect.
This graph is highly configurable. You can put mulitple "VIEW"(datasource) to this graph and swap them with the "dropdown" easily. You can download data and save current graph as a image. There are "callbacks" can be used to monitoring the date window and highlighting(selection) events.
Geeting Started
Once you`ve got ready to put it in your project, please check the demos or check out our github wiki.If you're using npm and a bundler like webpack, browserify or rollup, you can install fgp-graph via:
npm install --save @future-grid/fgp-graph
and use it via:
import FgpGraph from '@future-grid/fgp-graph';
//...... your code ......//
let fgpGraph = new FgpGraph(document.getElementById('graphArea'), [
vdConfig,
vsConfig
]);
fgpGraph.initGraph();
//...... your code ......//
Check out the React demo for more details.
Options Reference
If you are a dygrpahs user, please don't put dygraphs options in fgp-graph. fgp-graph has its own options and different formats.Usage
const vdConfig: ViewConfig = {
name: "device view",
connectSeparatedPoints: true,
graphConfig: {
hideHeader: {views: false, intervals: false, toolbar: false, series: false},
// hideHeader: false,
features: {
zoom: true,
scroll: true,
rangeBar: {show: true, format: 'DD MMM YYYY h:mm a'},
legend: this.formatters.legendForAllSeries,
exports: [GraphExports.Data, GraphExports.Image],
rangeLocked: true // lock or unlock range bar
},
entities: [
{id: "substation1", type: "substation", name: "substation1"},
],
rangeEntity: {id: "substation1", type: "substation", name: "substation1"},
rangeCollection: {
label: 'substation_day',
name: 'substation_interval_day',
interval: 86400000,
series: [
{label: "Avg", type: 'line', exp: "data.avgConsumptionVah"}
]
},
collections: [
{
label: 'substation_raw',
name: 'substation_interval',
interval: 3600000,
markLines: [{value: 256, label: '256', color: '#FF0000'}, {
value: 248,
label: '248',
color: '#FF0000'
}],
series: [
{
label: "Avg",
type: 'line',
exp: "data.avgConsumptionVah",
yIndex: 'left',
color: '#058902',
visibility: false
},
{
label: "Max",
type: 'line',
exp: "data.maxConsumptionVah",
yIndex: 'left',
color: '#d80808'
},
{
label: "Min",
type: 'line',
exp: "data.minConsumptionVah",
yIndex: 'left',
color: '#210aa8',
extraConfig: {name: "helloword"}
}
],
threshold: {min: 0, max: (1000 * 60 * 60 * 24 * 10)}, // 0 ~ 10 days
yLabel: 'voltage',
y2Label: 'voltage',
initScales: {left: {min: 245, max: 260}},
fill: false
}, {
label: 'substation_day',
name: 'substation_interval_day',
interval: 86400000,
// markLines: [{value: 255, label: '255', color: '#FF0000'}, {value: 235, label: '235', color: '#FF0000'}],
series: [
{label: "Avg", type: 'line', exp: "data.avgConsumptionVah", yIndex: 'left'},
{
label: "Max",
type: 'line',
exp: "data.maxConsumptionVah",
yIndex: 'left',
color: '#ff0000'
},
// {
// label: "Min",
// type: 'dots',
// exp: "data.minConsumptionVah",
// yIndex: 'left',
// extraConfig: {any: "anything"}
// }
],
threshold: {min: (1000 * 60 * 60 * 24 * 10), max: (1000 * 60 * 60 * 24 * 7 * 52 * 10)}, // 7 days ~ 3 weeks
yLabel: 'voltage',
y2Label: 'voltage',
initScales: {left: {min: 230, max: 260}},
fill: false
}
],
filters: {
"buttons": [
{
label: "All"
, func: () => {
return ["Min", "Max", "Avg"];
}
},
{
label: "Min"
, func: (): Array<string> => {
return ["Min"];
}
},
{
label: "Max"
, func: () => {
return ["Max"];
}
},
{
label: "Avg"
, func: () => {
return ["Avg"];
}
},
{
label: "Colors",
type: FilterType.COLORS,
func: (labels?: Array<string>) => {
let colors: Array<string> = [];
// generate colors
if (labels) {
labels.forEach(element => {
colors.push("#FF0000");
});
}
return colors;
}
},
{
label: "reset Colors",
type: FilterType.COLORS,
func: (labels?: Array<string>) => {
return [];
}
}
]
}
},
dataService: this.dataService,
show: true,
ranges: [
{name: "10 mins", value: 1000 * 60 * 10},
{name: "half an hour", value: 1000 * 60 * 30},
{name: "1 hours", value: 1000 * 60 * 60},
{name: "2 hours", value: 1000 * 60 * 60 * 2},
{name: "1 day", value: 1000 * 60 * 60 * 24},
{name: "7 days", value: 604800000, show: true},
{name: "1 month", value: 2592000000}
],
initRange: {
start: moment("2019-11-01").add(0, 'days').startOf('day').valueOf(),
end: moment("2019-12-01").subtract(0, 'days').endOf('day').valueOf()
},
interaction: {
callback: {
highlightCallback: (datetime, series, points) => {
// console.debug("selected series: ", series);
},
syncDateWindow: (dateWindow) => {
// console.debug(moment(dateWindow[0]), moment(dateWindow[1]));
},
dbClickCallback: (series) => {
// console.debug("dbl callback");
}
}
},
timezone: 'Australia/Perth',
highlightSeriesBackgroundAlpha: 1
// timezone: 'Pacific/Auckland'
};
let fgpGraph = new FgpGraph(document.getElementById('graphArea'), [
vdConfig
]);
ViewConfig
Display
name
All views configuration has a name and you will find it in the "views" dropdownd list. It's a key of view and should be exclusive. type: string default: nonetimezone
You should let the graphs know what timezone that data blongs to. You can find the right timezone for your data by googlingtype: string default: none
Australia/Melbourne Pacific/Auckland
show
fgp-graph will find the last view config with "show: true", then show it first.type: boolean default: false
ranges
array of configuration to let you change datewindow easier.[{ name: "7 days", value: 604800000, show: true },{ name: "1 month", value: 2592000000 }]
- name
type: string default: none
- value
type: number default: none
- show
type: boolean default: false
initRange
this is another way to tell graph what timewindow you want to show first. graph will ignore "ranges" configuration with this attribute.{start: moment().subtract(10, 'days').startOf('day').valueOf(),end: moment().add(1, 'days').valueOf()}
- start
- end
Graph
features
- zoom
type: boolean default: false
- scroll
type: boolean default: false
- rangebar
type: boolean default: false
- legend
type: function(data):htmlstr default: none provider: legendForAllSeries(device view) legendForSingleSeries(children view)
- exports
type: Array default: none provider: GraphExports.Data and GraphExports.Image
Entities
array of devices- id
- type
- name
sample:
[{ id: "meter1", type: "meter", name: "meter1" },{ id: "meter2", type: "meter", name: "meter2" }]
RangeEntity
single device and should always be the parent of "entities"- id
- type
- name
sample:
{ id: "substation1", type: "substation", name: "substation1" }
RangeCollection
this "collection" is a futuregrid interval configuration. Graph will call "dataservice" to get the frist and last record and render the rangebar with that timewindow. But most time insted of last record with current datetime.- label
type: string default: none
- name
type: string default: none
- interval
type: numnber default: none
- series
+ label
show this label when hover the line
+ type"line", "bar" or any other types supported by graph. Right now only "line" worked.
+ expexpression for data calculation.
```
"data.avgConsumptionVah * 10"
```
"data." is a prefix for the attribute from data
Collection
this "collection" is similar to RangeCollection, but shown on main graph. you can put multiple collections there and graph will swap them base on "threshold"- label
type: string default: none
- name
type: string default: none
- interval
type: numnber default: none
- series
```json { label: "Voltage", type: 'line', exp: "data.voltage", yIndex: 'left' } ``` + label
show this label when hover the line
+ type"line", "bar" or any other types supported by graph. Right now only "line" worked.
+ expexpression for data calculation.
```
"data.avgConsumptionVah * 10"
```
"data." is a prefix for the attribute from data
+ yIndexleft(0) or right(1) axes
- threshold
```json { min: (1000 60 60 24 10), max: (1000 60 60 24 7 52 10) } ``` + min
type: number(timestamp)
default: none
+ maxtype: number(timestamp)
default: none
- initScales
+ **min**
type: number
default: none
+ **max**
type: number
default: none
+ right + **min**
type: number
default: none
+ **max**
type: number
default: none
- yLabel
type: string default: none
Datasource
You need to provide a datasource base on "DataHandler" interface. There are two methods with "Promise" return value.- fetchFirstNLast
- fetchdata
All date that you provided should have the attributes in series coniguration and "timestamp" attribute as timeseries field.
check the react demo for more details
Interactions
Every time you change the datetime range or highlighting series on graph, the graph will call "callback" that you provided. You can do what you want, just like highlight something on map or redirect to another page.- callback
```js
(datetime, series, points) => {
// console.debug("selected series: ", series);
}
```
+ **datetime**
timestamp of the points
+ **series**
series name
+ **points**
points value
+ clickCallback```js
(series) => {
// console.debug("choosed series: ", series);
}
```
+ dbClickCallbacksame as clickCallback, just in case sometimes we need 2 different event to highlight on map and pop window for another page
```js
(series) => {
// console.debug("choosed series: ", series);
}
```
Event Listeners
There are 2 listeners let you know when "ViewConfig" & "Interval" changed. Such as you can use "viewChangeListener" to create a child graph.- viewChangeListener
```js
onViewChange = (g: FgpGraph, view: ViewConfig): void => {
console.log("view changed!", view.name);
const mainGraph = g;
if("device view" === view.name){
// add new child graph
this.setState({
childrenGraph: []
});
} else {
// add new child graph
this.setState({
childrenGraph: [{
id: '' + Math.random() * 1000,
viewConfigs: this.childViewConfigs,
onReady: (div: HTMLDivElement, g: FgpGraph) => {
mainGraph.setChildren([g]);
}
}]
});
}
};
```
Graph instance and viewConfig object will send back as parameters.
- intervalChangeListener
```js
onIntervalChange = (g: FgpGraph, interval: { name: string; value: number; show?: boolean }): void => {
console.log('interval changed!', interval);
};
````