Skip to content

Commit

Permalink
lestarch: final pre-look at realtime high-rate charting
Browse files Browse the repository at this point in the history
  • Loading branch information
LeStarch committed Jul 29, 2021
1 parent 0a374a4 commit f8c3627
Show file tree
Hide file tree
Showing 16 changed files with 2,385 additions and 487 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@


export let chart_wrapper_template = `
<div class="fp-flex-repeater">
<div class="row mt-2">
<div class="col-md-10">
<button class="btn btn-sm btn-secondary" v-on:click="addChart">
<span class="fp-chart-btn-icon">&plus;</span><span class="fp-chart-btn-text">Add Chart</span>
</button>
<button class="btn btn-sm" :class="{'btn-secondary': !this.siblings.in_sync, 'btn-success': siblings.in_sync}" v-on:click="siblings.in_sync = !siblings.in_sync">
<span class="fp-chart-btn-text">Lock Timescales</span>
</button>
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-secondary float-right" v-on:click="isHelpActive = !isHelpActive">
<span class="fp-chart-btn-text">Help</span>
</button>
</div>
</div>
<transition name="fade">
<div v-if="isHelpActive">
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<div class="row">
<div class="col-6">
<strong>Zoom in and out</strong> by holding <strong>ALT</strong> and using mouse wheel to scroll while hovering over an axis <br/>
<strong>Zoom in</strong> by holding <strong>ALT</strong> and clicking and dragging a selection on the chart
</div>
<div class="col-6">
<strong>Pan</strong> by holding <strong>SHIFT</strong> and clicking and dragging the chart <br/>
<strong>Change size</strong> by clicking and dragging the icon at the bottom right of the chart box
</div>
<p>
<button type="button" class="close">
<span v-on:click="isHelpActive = !isHelpActive">&times;</span>
</button>
</div>
</div>
</transition>
<component v-for="(chartInst, index) in wrappers" is="chart-display" :key="chartInst.id"
:id="chartInst.id" :siblings="siblings" v-on:delete-chart="deleteChart">
</component>
</div>
`;

export let chart_display_template = `
<div class="mt-3">
<div class="card">
<div class="card-header">
<button type="button" class="close ml-2">
<span v-on:click="emitDeleteChart(id)">&times;</span>
</button>
<button type="button" class="close ml-2" v-on:click="isCollapsed = !isCollapsed">
<span v-if="!isCollapsed">&minus;</span>
<span v-if="isCollapsed">&#9744;</span>
</button>
<span class="card-subtitle text-muted">{{ selected }} </span>
</div>
<div class="card-body" v-bind:class="{'collapse': isCollapsed}">
<div class="row">
<div class="col-md-4">
<v-select placeholder="Select a Channel" id="channelList" label="option" style="flex: 1 1 auto;"
:clearable="false" :searchable="true" :filterable="true" :options="channelNames"
v-model="selected">
</v-select>
</div>
</div>
<div class="row justify-content-between">
<div class="col-md-4 mt-2">
<button type="button" class="btn" v-bind:class="{'btn-warning': !pause, 'btn-success': pause}"
v-on:click="toggleStreamFlow()" v-if="chart != null">
<span v-if="!pause">&#10074;&#10074;</span>
<span v-if="pause">&#9654;</span>
</button>
<button type="button" class="btn btn-warning" v-on:click="resetZoom()" v-if="chart != null">
Reset Zoom
</button>
</div>
</div>
<div class="row">
<div class="col-md-12 mt-2 fp-resize-box">
<canvas id="ds-line-chart" style="min-width: 50%"></canvas>
</div>
</div>
</div>
</div>
</div>
`;
166 changes: 166 additions & 0 deletions src/fprime_gds/flask/static/addons/chart-display/addon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* addons/chart-display.js:
*
* Visualize selected telemetry channels using time series charts
*
* @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 './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';


function timeToDate(time) {
let date = new Date((time.seconds * 1000) + (time.microseconds/1000));
return date;
}

/**
* Wrapper component to allow user add multiple charts to the same page
*/
Vue.component("chart-wrapper", {
data: function () {
return {
locked: false,
isHelpActive: true,
wrappers: [{"id": 0}],
siblings: new SiblingSet()
};
},
template: chart_wrapper_template,
methods: {
/**
* Add new chart
*/
addChart(type) {
this.wrappers.push({'id': this.counter});
this.counter += 1;
},
/**
* Remove chart with the given id
*/
deleteChart(id) {
const index = this.wrappers.findIndex(f => f.id === id);
this.wrappers.splice(index,1);
},
}
});

/**
* Main chart component
*/
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});

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

isCollapsed: false,
pause: false,

chart: null,
};
},
methods: {
/**
* Allow user to pause the chart stream
*/
toggleStreamFlow() {
const realtimeOpts = this.chart.options.scales.x.realtime;
realtimeOpts.pause = !realtimeOpts.pause;
this.pause = realtimeOpts.pause;
this.siblings.pause(realtimeOpts.pause);
},
/**
* Register a new chart object
*/
registerChart() {
// If there is a chart object destroy it to reset the chart
this.destroy();
_datastore.registerChannelConsumer(this);
let config = generate_chart_config(this.selected);
config.options.plugins.zoom.zoom.onZoom = this.siblings.syncToAll;
config.options.plugins.zoom.pan.onPan = this.siblings.syncToAll;
// Category IV magic: do not alter
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) {
// Todo. This currently suppresses the following bug error
// See ChartJs bug report https://github.com/chartjs/Chart.js/issues/9368
}
this.siblings.add(this.chart);
},
/**
* Reset chart zoom back to default
*/
resetZoom() {
this.chart.resetZoom("none");
this.siblings.reset();
},
destroy() {
// Guard against destroying that which is destroyed
if (this.chart == null) {
return;
}
_datastore.deregisterChannelConsumer(this);
this.chart.data.datasets.forEach((dataset) => {dataset.data = [];});
this.chart.destroy();
this.siblings.remove(this.chart);
this.chart = null;
},

/**
* sending message up to the parent to remove this chart with this id
* @param {int} id of current chart instance known to the parent
*/
emitDeleteChart(id) {
this.destroy();
this.$emit('delete-chart', id);
},

sendChannels(channels) {
if (this.selected == null || this.chart == null) {
return;
}
let name = this.selected;
let new_channels = channels.filter((channel) => {
return channel.template.full_name == name
});
new_channels = new_channels.map(
(channel) => {
return {x: timeToDate(channel.time), y: channel.val}
}
);

this.chart.data.datasets[0].data.push(...new_channels);
this.chart.update('quiet');
}
},
/**
* Watch for new selection of channel and re-register the chart
*/
watch: {
selected: function() {
if (this.selected !== this.oldSelected) {
this.oldSelected = this.selected;
this.registerChart();
}
},
}
});
Loading

0 comments on commit f8c3627

Please sign in to comment.