-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
lestarch: final pre-look at realtime high-rate charting
- Loading branch information
Showing
16 changed files
with
2,385 additions
and
487 deletions.
There are no files selected for viewing
96 changes: 96 additions & 0 deletions
96
src/fprime_gds/flask/static/addons/chart-display/addon-templates.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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">+</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">×</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)">×</span> | ||
</button> | ||
<button type="button" class="close ml-2" v-on:click="isCollapsed = !isCollapsed"> | ||
<span v-if="!isCollapsed">−</span> | ||
<span v-if="isCollapsed">☐</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">❚❚</span> | ||
<span v-if="pause">▶</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
166
src/fprime_gds/flask/static/addons/chart-display/addon.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
}, | ||
} | ||
}); |
Oops, something went wrong.