+ `,
+
+ methods: {
+ /**
+ * Add new chart
+ */
+ addChart: function (type) {
+ this.chartInstances.push({'id': this.counter, 'type': type})
+ this.counter += 1;
+ },
+ /**
+ * Remove chart with the given id
+ */
+ deleteChart: function (id) {
+ const index = this.chartInstances.findIndex(f => f.id === id);
+ this.chartInstances.splice(index,1);
+ },
+ }
+});
+
+/**
+ * Main chart component
+ */
+Vue.component("chart-display", {
+ template: `
+
+
+
+
+
+
+
+ {{ channelName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enable zoom by clicking on a chart
+ Zoom in and out by using mouse wheel
+ Drag horizontally by right click and hold
+ Change size by dragging bottom right of the chart box
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ props: ["id"],
+ data: function () {
+ return {
+ channels: _datastore.channels,
+ channelNames: [],
+ selected: null,
+ oldSelected: null,
+ channelLoaded: false,
+
+ isCollapsed: false,
+ isHelpActive: false,
+
+ chartObj: null,
+ channelId: null,
+ channelName: "",
+ channelTimestamp: null,
+ channelValue: null,
+ showControlBtns: false,
+
+ // https://nagix.github.io/chartjs-plugin-streaming/2.0.0/guide/options.html
+ duration: 60000, // (1 min) Duration of the chart in milliseconds (how much time of data it will show).
+ ttl: 1800000, // (30 min) // Duration of the data to be kept in milliseconds.
+ delay: 2000, // (2 sec) Delay added to the chart in milliseconds so that upcoming values are known before lines are plotted.
+ refresh: 1000, // (1 sec) Refresh interval of data in milliseconds. onRefresh callback function will be called at this interval.
+ frameRate: 30, // Frequency at which the chart is drawn on a display
+ pause: false, // If set to true, scrolling stops. Note that onRefresh callback is called even when this is set to true.
+ reverse: false, // If true moves from left to right
+ animation: true,
+ responsive: true,
+ maintainAspectRatio: false,
+ intersect: false,
+ minDelay: 2000, // (2 sec) Min value of the delay option
+ maxDelay: 1800000, // (30 min) Max value of the delay option
+ minDuration: 2000, // (2 sec) Min value of the duration option
+ maxDuration: 1800000, // (30 min) Max value of the duration option
+
+ config: {},
+ };
+ },
+
+ methods: {
+
+ /**
+ * Extract channel name from channel object
+ */
+ setChannelNames() {
+ if (this.channelLoaded || this.channels === undefined) {
+ return;
+ }
+ let ch_keys = Object.keys(this.channels);
+ if (ch_keys.length === 0) {
+ return;
+ }
+ this.channelNames = []; // reset channel names to avoid duplicates
+ for (let i = 0; i < ch_keys.length; i++) {
+ let ch = this.channels[ch_keys[i]];
+ this.channelNames.push({
+ option: ch.template.full_name,
+ id: ch.id,
+ });
+ this.channelLoaded = true;
+ }
+ },
+
+ /**
+ * Function to update the chart with new data
+ */
+ onRefresh(){
+ this.chartObj.data.datasets[0].data.push({
+ x: Date.now(),
+ y: this.channelValue,
+ });
+ },
+
+ /**
+ * returns current status (enable/disable) of zooming with mouse wheel
+ */
+ zoomStatus() {
+ if (this.chartObj) {
+ return 'Zoom: ' + (this.chartObj.options.plugins.zoom.zoom.wheel.enabled ? 'enabled' : 'disabled');
+ } else {
+ return 'Zoom: ' + 'disabled';
+ }
+ },
+
+ /**
+ * Allow user to pause the chart stream
+ */
+ toggleStreamFlow() {
+ const realtimeOpts = this.chartObj.options.scales.x.realtime;
+ realtimeOpts.pause = !realtimeOpts.pause;
+ this.pause = !this.pause;
+ this.chartObj.update("none");
+ },
+
+ /**
+ * Set chart configuration
+ */
+ setConfig() {
+ this.config = {
+ type: "line",
+ data: {
+ datasets: [
+ {
+ label: this.channelName,
+ backgroundColor: "rgba(54, 162, 235, 0.5)",
+ borderColor: "rgb(54, 162, 235)",
+ cubicInterpolationMode: "monotone",
+ data: [],
+ },
+ ],
+ },
+ options: {
+ animation: this.animation,
+ responsive: this.responsive,
+ maintainAspectRatio: this.maintainAspectRatio,
+ interaction: {
+ intersect: this.intersect
+ },
+ onClick(e) {
+ const chart = e.chart;
+ chart.options.plugins.zoom.zoom.wheel.enabled = !chart.options.plugins.zoom.zoom.wheel.enabled;
+ chart.options.plugins.zoom.zoom.pinch.enabled = !chart.options.plugins.zoom.zoom.pinch.enabled;
+ chart.update();
+ },
+ scales: {
+ x: {
+ type: "realtime",
+ realtime: {
+ duration: this.duration,
+ ttl: this.ttl,
+ delay: this.delay,
+ refresh: this.refresh,
+ frameRate: this.frameRate,
+ pause: this.pause,
+ onRefresh: this.onRefresh
+ },
+ reverse: this.reverse
+ },
+ y: {
+ title: {
+ display: true,
+ text: "Value"
+ }
+ }
+ },
+ plugins: {
+ zoom: {
+ // Assume x axis has the realtime scale
+ pan: {
+ enabled: true, // Enable panning
+ mode: "x", // Allow panning in the x direction
+ },
+ zoom: {
+ pinch: {
+ enabled: false, // Enable pinch zooming
+ },
+ wheel: {
+ enabled: false, // Enable wheel zooming
+ },
+ mode: "x", // Allow zooming in the x direction
+ },
+ limits: {
+ x: {
+ minDelay: this.minDelay,
+ maxDelay: this.maxDelay,
+ minDuration: this.minDuration,
+ maxDuration: this.maxDuration,
+ },
+ },
+ },
+ title: {
+ display: true,
+ position: 'bottom',
+ text: this.zoomStatus // keep track of zoom enable status
+ },
+ },
+ },
+ plugins:[
+ // Highlight chart border when user clicks on the chart area
+ {
+ id: 'chartAreaBorder',
+ beforeDraw(chart, args, options) {
+ const {ctx, chartArea: {left, top, width, height}} = chart;
+ if (chart.options.plugins.zoom.zoom.wheel.enabled) {
+ ctx.save();
+ ctx.strokeStyle = '#f5c6cb';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(left, top, width, height);
+ ctx.restore();
+ }
+ }
+ }
+ ],
+ }
+ },
+
+ /**
+ * Register a new chart object
+ */
+ registerChart() {
+ // If there is a chart object destroy it to reset the chart
+ if (this.chartObj !== null) {
+ this.chartObj.data.datasets.forEach((dataset) => {
+ dataset.data = [];
+ });
+ this.chartObj.destroy();
+ this.showControlBtns = false;
+ }
+
+ // If the selected channel does not have any value do not register the chart
+ let id = this.selected.id;
+ if (this.isChannelOff(id)) {
+ return;
+ }
+
+ this.channelName = this.getChannelName(id);
+ this.setConfig();
+ this.showControlBtns = true;
+ try {
+ this.chartObj = new Chart(
+ this.$el.querySelector("#ds-line-chart"),
+ this.config
+ );
+ } catch(err) {
+ // Todo. This currently suppresses the following bug error
+ // See ChartJs bug report https://github.com/chartjs/Chart.js/issues/9368
+ }
+ },
+
+ /**
+ * Check whether there is any data in the channel
+ */
+ isChannelOff(id) {
+ return this.channels[id].str === undefined;
+ },
+
+ getChannelName(id) {
+ return this.channels[id].template.full_name;
+ },
+
+ /**
+ * Reset chart zoom back to default
+ */
+ resetZoom() {
+ this.chartObj.resetZoom("none");
+ },
+
+ /**
+ * Allow user to collapse or open chart display box
+ */
+ toggleCollapseChart() {
+ this.isCollapsed = !this.isCollapsed;
+ },
+
+ /**
+ * Show or remove alert box when user click on the help button
+ */
+ toggleShowHelp() {
+ this.isHelpActive = !this.isHelpActive;
+ },
+
+ /**
+ * Remove alert box when user click on the close button of help alert
+ */
+ dismissHelp () {
+ this.isHelpActive = false;
+ },
+
+ /**
+ * 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) {
+ if (this.chartObj) {
+ this.chartObj.destroy();
+ }
+ this.$emit('delete-chart', id);
+ },
+ },
+
+ mounted: function () {
+ this.setChannelNames();
+ },
+
+ computed: {
+ updateData: function () {
+ this.setChannelNames();
+
+ if (this.selected === null) {
+ return;
+ }
+ let id = this.selected.id;
+ if (this.isChannelOff(id)) {
+ return;
+ } else {
+ this.channelId = this.channels[id].id;
+ this.channelName = this.channels[id].template.full_name;
+ this.channelTimestamp = this.channels[id].time.seconds;
+ this.channelValue = this.channels[id].val;
+ }
+ },
+ },
+
+ /**
+ * 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();
+ }
+ },
+ }
+});
diff --git a/src/fprime_gds/flask/static/css/fpstyle.css b/src/fprime_gds/flask/static/css/fpstyle.css
index c3e1f724..30c81bee 100644
--- a/src/fprime_gds/flask/static/css/fpstyle.css
+++ b/src/fprime_gds/flask/static/css/fpstyle.css
@@ -173,3 +173,34 @@ table td:last-child {
background-color: #28a745 !important;
color: #fff !important;
}
+
+/**
+* Style related to Charts page
+**/
+.fp-chart-btn-icon {
+ font-size: 1.5rem;
+ font-weight: 700;
+ line-height: 1;
+ color: #fff;
+}
+
+.fp-chart-btn-text {
+ font-size: 1rem;
+ font-weight: 700;
+ line-height: 2;
+ color: #fff;
+}
+
+.fp-resize-box {
+ resize: vertical;
+ overflow: auto;
+ padding: 1em;
+}
+
+.fade-enter-active, .fade-leave-active {
+ transition: opacity .5s;
+}
+
+.fade-enter, .fade-leave-to {
+ opacity: 0;
+}
\ No newline at end of file
diff --git a/src/fprime_gds/flask/static/index.html b/src/fprime_gds/flask/static/index.html
index d6ddff66..4bc6a5cc 100644
--- a/src/fprime_gds/flask/static/index.html
+++ b/src/fprime_gds/flask/static/index.html
@@ -30,6 +30,7 @@
+
@@ -91,10 +92,17 @@
+
+
+
+
+