Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to graph nested serializable types #31

Merged
merged 3 commits into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export let chart_wrapper_template = `
<div class="col-md-6 mb-1">
<button class="btn btn-block" :class="{'btn-secondary': !this.siblings.in_sync, 'btn-success': siblings.in_sync}" v-on:click="siblings.in_sync = !siblings.in_sync">
<i class="fas fa-thumbtack"></i>
<span class="d-md-none d-lg-inline">Lock Timescales</span>
<span class="d-md-none d-lg-inline">Lock Scale</span>
</button>
</div>
</div>
Expand Down
112 changes: 77 additions & 35 deletions src/fprime_gds/flask/static/addons/chart-display/addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@
*
* Visualize selected telemetry channels using time series charts. This is done in realtime. Time-shifted signals
* will need to be panned into focus.
*
*
* @author saba-ja
*/
import {generate_chart_config} from "./config.js";
import {chart_wrapper_template, chart_display_template} from "./addon-templates.js";
import { _datastore } from '../../js/datastore.js';
import {_loader} from "../../js/loader.js";
import {SiblingSet} from './sibling.js';
import {timeToDate} from "../../js/vue-support/utils.js"
import { generate_chart_config } from "./config.js";
import {
chart_wrapper_template,
chart_display_template,
} from "./addon-templates.js";
import { _datastore } from "../../js/datastore.js";
import { _loader } from "../../js/loader.js";
import { SiblingSet } from "./sibling.js";
import { timeToDate } from "../../js/vue-support/utils.js";

import "./vendor/chart.js";
import "./vendor/chartjs-adapter-luxon.min.js";
import "./vendor/hammer.min.js";
import { flatten } from "./modified-vendor/flat.js";

import './vendor/chart.js';
import './vendor/chartjs-adapter-luxon.min.js';
import './vendor/hammer.min.js';
// Note: these are modified versions of the original plugin files
import './modified-vendor/chartjs-plugin-zoom.js';
import './modified-vendor/chartjs-plugin-streaming.js';
import "./modified-vendor/chartjs-plugin-zoom.js";
import "./modified-vendor/chartjs-plugin-streaming.js";

/**
* Wrapper component to allow user add multiple charts to the same page. This component handles the functions for
Expand All @@ -30,8 +35,8 @@ Vue.component("chart-wrapper", {
counter: 1,
locked: false,
isHelpActive: false,
wrappers: [{"id": 0}], // Starts with a single chart
siblings: new SiblingSet()
wrappers: [{ id: 0 }], // Starts with a single chart
siblings: new SiblingSet(),
};
},
template: chart_wrapper_template,
Expand All @@ -40,17 +45,17 @@ Vue.component("chart-wrapper", {
* Add new chart handling the Chart+ button.
*/
addChart(type) {
this.wrappers.push({'id': this.counter});
this.wrappers.push({ id: this.counter });
this.counter += 1;
},
/**
* Remove chart with the given id for handling the X button on a chart wrapper
*/
deleteChart(id) {
const index = this.wrappers.findIndex(f => f.id === id);
this.wrappers.splice(index,1);
const index = this.wrappers.findIndex((f) => f.id === id);
this.wrappers.splice(index, 1);
},
}
},
});

/**
Expand All @@ -60,15 +65,25 @@ Vue.component("chart-display", {
template: chart_display_template,
props: ["id", "siblings"],
data: function () {
let names = Object.values(_loader.endpoints["channel-dict"].data).map((value) => {return value.full_name});
const names_list = Object.values(
_loader.endpoints["channel-dict"].data
).map((value) => {
return flatten(value.type_obj, {
maxDepth: 20,
prefix: value.full_name,
});
});

const names = names_list.flat();

return {
channelNames: names,
selected: null,
oldSelected: null,

isCollapsed: false,
pause: false,

chart: null,
};
},
Expand Down Expand Up @@ -96,8 +111,11 @@ Vue.component("chart-display", {
config.options.scales.x.realtime.onRefresh = this.siblings.sync;
this.showControlBtns = true;
try {
this.chart = new Chart(this.$el.querySelector("#ds-line-chart"), config);
} catch(err) {
this.chart = new Chart(
this.$el.querySelector("#ds-line-chart"),
config
);
} catch (err) {
// Todo. This currently suppresses the following bug error
// See ChartJs bug report https://github.com/chartjs/Chart.js/issues/9368
}
Expand All @@ -119,7 +137,9 @@ Vue.component("chart-display", {
return;
}
_datastore.deregisterChannelConsumer(this);
this.chart.data.datasets.forEach((dataset) => {dataset.data = [];});
this.chart.data.datasets.forEach((dataset) => {
dataset.data = [];
});
this.chart.destroy();
this.siblings.remove(this.chart);
this.chart = null;
Expand All @@ -131,7 +151,7 @@ Vue.component("chart-display", {
*/
emitDeleteChart(id) {
this.destroy();
this.$emit('delete-chart', id);
this.$emit("delete-chart", id);
},
/**
* Callback to handle new channels being pushed at this object.
Expand All @@ -141,32 +161,54 @@ Vue.component("chart-display", {
if (this.selected == null || this.chart == null) {
return;
}
let name = this.selected;
// Get channel name assuming the string is in component.channel format.
let channel_full_name = this.selected
.split(".")
.slice(0, 2)
.join(".");
let serial_path = this.selected.split(".").slice(2).join(".");

// Filter channels down to the graphed channel
let new_channels = channels.filter((channel) => {
return channel.template.full_name === name
return channel.template.full_name === channel_full_name;
});
// Convert to chart JS format
new_channels = new_channels.map(
(channel) => {
return {x: timeToDate(channel.time), y: channel.val}

// Get channel value
function getValue(ch_obj, path_str) {
// If serializable path exist parse and return its value
if (path_str) {
let keys = "val_obj." + serial_path + ".value";
return keys
.split(".")
.reduce((o, k) => (o || {})[k], ch_obj);
} else {
// otherwise assume simple object
return ch_obj.val;
}
);
}

// Convert to chart JS format
new_channels = new_channels.map((channel) => {
return {
x: timeToDate(channel.time),
y: getValue(channel, serial_path),
};
});

// Graph and update
this.chart.data.datasets[0].data.push(...new_channels);
this.chart.update('quiet');
}
this.chart.update("quiet");
},
},
/**
* Watch for new selection of channel and re-register the chart
*/
watch: {
selected: function() {
selected: function () {
if (this.selected !== this.oldSelected) {
this.oldSelected = this.selected;
this.registerChart();
}
},
}
},
});
4 changes: 2 additions & 2 deletions src/fprime_gds/flask/static/addons/chart-display/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export let ticks_config = {
export let realtime_config = {
// Initial display width (ms): 1 min
duration: 60000,
// Total data history (ms): 4 min
ttl: 4 * 60 * 1000,
// Total data history (ms): 60 min
ttl: 60 * 60 * 1000,
// Initial chart delay (ms): 0
delay: 0,
// Drawing framerate (ms): 30 Hz
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* BSD 3-Clause "New" or "Revised" License
* Source: https://github.com/hughsk/flat
* Copyright (c) 2014, Hugh Kennedy

The module has been modified to support F Prime GDS tasks
*/

function isBuffer(obj) {
return (
obj &&
obj.constructor &&
typeof obj.constructor.isBuffer === "function" &&
obj.constructor.isBuffer(obj)
);
}

function keyIdentity(key) {
return key;
}

/**
*
* @param {json} target : channel object
* @param {json} opts : config options
* @returns list of flatten paths
*/
function flatten(target, opts) {
opts = opts || {};
// Default supported F Prime types
const supportedTypes =
["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] ||
opts.supportedTypes;

const prefix = opts.prefix || "";
const delimiter = opts.delimiter || ".";
const maxDepth = opts.maxDepth;
const transformKey = opts.transformKey || keyIdentity;
const output = {};

function step(object, prev, currentDepth) {
currentDepth = currentDepth || 1;

Object.keys(object).forEach(function (key) {
const value = object[key];
const isarray = opts.safe && Array.isArray(value);
const type = Object.prototype.toString.call(value);
const isbuffer = isBuffer(value);
const isobject =
type === "[object Object]" || type === "[object Array]";

const newKey = prev
? prev + delimiter + transformKey(key)
: transformKey(key);

if (
!isarray &&
!isbuffer &&
isobject &&
Object.keys(value).length &&
(!opts.maxDepth || currentDepth < maxDepth)
) {
return step(value, newKey, currentDepth + 1);
}

if (supportedTypes.indexOf(value) !== -1) {
let keyId = "";
if (prefix) {
keyId = keyId.concat(prefix, delimiter, newKey);
} else {
keyId = newKey;
}
output[keyId] = value;
}
});
}

step(target);

// Get unique parent of each path
const output_set = new Set();
for (const [key, value] of Object.entries(output)) {
output_set.add(key.split(delimiter).slice(0, -1).join(delimiter));
}

// Sort and return a list of flatten json paths
let output_list = Array.from(output_set).sort();
return output_list;
}

export { flatten };