From 162cc6bc77d5f68c15d99e32bac7cb1fb21b5da8 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Fri, 3 Jun 2022 19:32:32 -0700 Subject: [PATCH] Support for spectral plots via existing bar graphs (#5162) Spectral plots support Co-authored-by: Nikhil Co-authored-by: Charles Hacskaylo Co-authored-by: Andrew Henry --- .../generator/GeneratorMetadataProvider.js | 18 +- example/generator/generatorWorker.js | 30 +- src/api/telemetry/TelemetryMetadataManager.js | 12 + src/api/telemetry/TelemetryValueFormatter.js | 40 +- src/plugins/charts/bar/BarGraphView.vue | 151 +++++--- .../charts/bar/inspector/BarGraphOptions.vue | 346 +++++++++++++++++- .../charts/bar/inspector/SeriesOptions.vue | 25 +- src/plugins/charts/bar/plugin.js | 7 +- src/plugins/charts/bar/pluginSpec.js | 179 ++++++--- src/ui/color/ColorSwatch.vue | 2 +- src/ui/inspector/inspector.scss | 16 +- 11 files changed, 685 insertions(+), 141 deletions(-) diff --git a/example/generator/GeneratorMetadataProvider.js b/example/generator/GeneratorMetadataProvider.js index 7a8cd9832a..f274d2d53d 100644 --- a/example/generator/GeneratorMetadataProvider.js +++ b/example/generator/GeneratorMetadataProvider.js @@ -29,12 +29,12 @@ define([ } }, { - key: "cos", - name: "Cosine", - unit: "deg", - formatString: '%0.2f', + key: "wavelengths", + name: "Wavelength", + unit: "nm", + format: 'string[]', hints: { - domain: 3 + range: 4 } }, // Need to enable "LocalTimeSystem" plugin to make use of this @@ -64,6 +64,14 @@ define([ hints: { range: 2 } + }, + { + key: "intensities", + name: "Intensities", + format: 'number[]', + hints: { + range: 3 + } } ] }, diff --git a/example/generator/generatorWorker.js b/example/generator/generatorWorker.js index 6cf8730634..02807e06f2 100644 --- a/example/generator/generatorWorker.js +++ b/example/generator/generatorWorker.js @@ -77,7 +77,8 @@ utc: nextStep, yesterday: nextStep - 60 * 60 * 24 * 1000, sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), - wavelength: wavelength(start, nextStep), + wavelengths: wavelengths(), + intensities: intensities(), cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) } }); @@ -126,7 +127,8 @@ utc: nextStep, yesterday: nextStep - 60 * 60 * 24 * 1000, sin: sin(nextStep, period, amplitude, offset, phase, randomness), - wavelength: wavelength(start, nextStep), + wavelengths: wavelengths(), + intensities: intensities(), cos: cos(nextStep, period, amplitude, offset, phase, randomness) }); } @@ -154,8 +156,28 @@ * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; } - function wavelength(start, nextStep) { - return (nextStep - start) / 10; + function wavelengths() { + let values = []; + while (values.length < 5) { + const randomValue = Math.random() * 100; + if (!values.includes(randomValue)) { + values.push(String(randomValue)); + } + } + + return values; + } + + function intensities() { + let values = []; + while (values.length < 5) { + const randomValue = Math.random() * 10; + if (!values.includes(randomValue)) { + values.push(String(randomValue)); + } + } + + return values; } function sendError(error, message) { diff --git a/src/api/telemetry/TelemetryMetadataManager.js b/src/api/telemetry/TelemetryMetadataManager.js index 0e21ad0797..4d9761e3f7 100644 --- a/src/api/telemetry/TelemetryMetadataManager.js +++ b/src/api/telemetry/TelemetryMetadataManager.js @@ -121,6 +121,18 @@ define([ return _.sortBy(matchingMetadata, ...iteratees); }; + /** + * check out of a given metadata has array values + */ + TelemetryMetadataManager.prototype.isArrayValue = function (metadata) { + const regex = /\[\]$/g; + if (!metadata.format && !metadata.formatString) { + return false; + } + + return (metadata.format || metadata.formatString).match(regex) !== null; + }; + TelemetryMetadataManager.prototype.getFilterableValues = function () { return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0); }; diff --git a/src/api/telemetry/TelemetryValueFormatter.js b/src/api/telemetry/TelemetryValueFormatter.js index eb97fd0625..3e8c0b62c3 100644 --- a/src/api/telemetry/TelemetryValueFormatter.js +++ b/src/api/telemetry/TelemetryValueFormatter.js @@ -43,9 +43,23 @@ define([ }; this.valueMetadata = valueMetadata; - this.formatter = formatMap.get(valueMetadata.format) || numberFormatter; - if (valueMetadata.format === 'enum') { + function getNonArrayValue(value) { + //metadata format could have array formats ex. string[]/number[] + const arrayRegex = /\[\]$/g; + if (value && value.match(arrayRegex)) { + return value.replace(arrayRegex, ''); + } + + return value; + } + + let valueMetadataFormat = getNonArrayValue(valueMetadata.format); + + //Is there an existing formatter for the format specified? If not, default to number format + this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter; + + if (valueMetadataFormat === 'enum') { this.formatter = {}; this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) { vm.byValue[e.value] = e.string; @@ -77,13 +91,13 @@ define([ // Check for formatString support once instead of per format call. if (valueMetadata.formatString) { const baseFormat = this.formatter.format; - const formatString = valueMetadata.formatString; + const formatString = getNonArrayValue(valueMetadata.formatString); this.formatter.format = function (value) { return printj.sprintf(formatString, baseFormat.call(this, value)); }; } - if (valueMetadata.format === 'string') { + if (valueMetadataFormat === 'string') { this.formatter.parse = function (value) { if (value === undefined) { return ''; @@ -108,7 +122,14 @@ define([ TelemetryValueFormatter.prototype.parse = function (datum) { if (_.isObject(datum)) { - return this.formatter.parse(datum[this.valueMetadata.source]); + const objectDatum = datum[this.valueMetadata.source]; + if (Array.isArray(objectDatum)) { + return objectDatum.map((item) => { + return this.formatter.parse(item); + }); + } else { + return this.formatter.parse(objectDatum); + } } return this.formatter.parse(datum); @@ -116,7 +137,14 @@ define([ TelemetryValueFormatter.prototype.format = function (datum) { if (_.isObject(datum)) { - return this.formatter.format(datum[this.valueMetadata.source]); + const objectDatum = datum[this.valueMetadata.source]; + if (Array.isArray(objectDatum)) { + return objectDatum.map((item) => { + return this.formatter.format(item); + }); + } else { + return this.formatter.format(objectDatum); + } } return this.formatter.format(datum); diff --git a/src/plugins/charts/bar/BarGraphView.vue b/src/plugins/charts/bar/BarGraphView.vue index 02ec8f2055..da3ef199cd 100644 --- a/src/plugins/charts/bar/BarGraphView.vue +++ b/src/plugins/charts/bar/BarGraphView.vue @@ -40,14 +40,6 @@ export default { BarGraph }, inject: ['openmct', 'domainObject', 'path'], - props: { - options: { - type: Object, - default() { - return {}; - } - } - }, data() { this.telemetryObjects = {}; this.telemetryObjectFormats = {}; @@ -75,7 +67,9 @@ export default { this.setTimeContext(); this.loadComposition(); - + this.unobserveAxes = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.refreshData); + this.unobserveInterpolation = this.openmct.objects.observe(this.domainObject, 'configuration.useInterpolation', this.refreshData); + this.unobserveBar = this.openmct.objects.observe(this.domainObject, 'configuration.useBar', this.refreshData); }, beforeDestroy() { this.stopFollowingTimeContext(); @@ -86,8 +80,19 @@ export default { return; } - this.composition.off('add', this.addTelemetryObject); + this.composition.off('add', this.addToComposition); this.composition.off('remove', this.removeTelemetryObject); + if (this.unobserveAxes) { + this.unobserveAxes(); + } + + if (this.unobserveInterpolation) { + this.unobserveInterpolation(); + } + + if (this.unobserveBar) { + this.unobserveBar(); + } }, methods: { setTimeContext() { @@ -105,6 +110,42 @@ export default { this.timeContext.off('bounds', this.refreshData); } }, + addToComposition(telemetryObject) { + if (Object.values(this.telemetryObjects).length > 0) { + this.confirmRemoval(telemetryObject); + } else { + this.addTelemetryObject(telemetryObject); + } + }, + confirmRemoval(telemetryObject) { + const dialog = this.openmct.overlays.dialog({ + iconClass: 'alert', + message: 'This action will replace the current telemetry source. Do you want to continue?', + buttons: [ + { + label: 'Ok', + emphasis: true, + callback: () => { + const oldTelemetryObject = Object.values(this.telemetryObjects)[0]; + this.removeFromComposition(oldTelemetryObject); + this.removeTelemetryObject(oldTelemetryObject.identifier); + this.addTelemetryObject(telemetryObject); + dialog.dismiss(); + } + }, + { + label: 'Cancel', + callback: () => { + this.removeFromComposition(telemetryObject); + dialog.dismiss(); + } + } + ] + }); + }, + removeFromComposition(telemetryObject) { + this.composition.remove(telemetryObject); + }, addTelemetryObject(telemetryObject) { // grab information we need from the added telmetry object const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); @@ -165,7 +206,12 @@ export default { const yAxisMetadata = metadata.valuesForHints(['range'])[0]; //Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only - const xAxisMetadata = metadata.valuesForHints(['range']); + const xAxisMetadata = metadata.valuesForHints(['range']) + .map((metaDatum) => { + metaDatum.isArrayValue = metadata.isArrayValue(metaDatum); + + return metaDatum; + }); return { xAxisMetadata, @@ -183,13 +229,7 @@ export default { loadComposition() { this.composition = this.openmct.composition.get(this.domainObject); - if (!this.composition) { - this.addTelemetryObject(this.domainObject); - - return; - } - - this.composition.on('add', this.addTelemetryObject); + this.composition.on('add', this.addToComposition); this.composition.on('remove', this.removeTelemetryObject); this.composition.load(); }, @@ -212,7 +252,10 @@ export default { }, removeTelemetryObject(identifier) { const key = this.openmct.objects.makeKeyString(identifier); - delete this.telemetryObjects[key]; + if (this.telemetryObjects[key]) { + delete this.telemetryObjects[key]; + } + if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) { delete this.telemetryObjectFormats[key]; } @@ -237,49 +280,72 @@ export default { this.openmct.notifications.alert(data.message); } - if (!this.isDataInTimeRange(data, key)) { + if (!this.isDataInTimeRange(data, key, telemetryObject)) { + return; + } + + if (this.domainObject.configuration.axes.xKey === undefined || this.domainObject.configuration.axes.yKey === undefined) { return; } let xValues = []; let yValues = []; - - //populate X and Y values for plotly - axisMetadata.xAxisMetadata.forEach((metadata) => { - xValues.push(metadata.name); - if (data[metadata.key]) { - const formattedValue = this.format(key, metadata.key, data); - yValues.push(formattedValue); - } else { - yValues.push(null); + let xAxisMetadata = axisMetadata.xAxisMetadata.find(metadata => metadata.key === this.domainObject.configuration.axes.xKey); + if (xAxisMetadata && xAxisMetadata.isArrayValue) { + //populate x and y values + let metadataKey = this.domainObject.configuration.axes.xKey; + if (data[metadataKey] !== undefined) { + xValues = this.parse(key, metadataKey, data); } - }); + + metadataKey = this.domainObject.configuration.axes.yKey; + if (data[metadataKey] !== undefined) { + yValues = this.parse(key, metadataKey, data); + } + } else { + //populate X and Y values for plotly + axisMetadata.xAxisMetadata.filter(metadataObj => !metadataObj.isArrayValue).forEach((metadata) => { + if (!xAxisMetadata) { + //Assign the first metadata to use for any formatting + xAxisMetadata = metadata; + } + + xValues.push(metadata.name); + if (data[metadata.key]) { + const parsedValue = this.parse(key, metadata.key, data); + yValues.push(parsedValue); + } else { + yValues.push(null); + } + }); + } let trace = { key, name: telemetryObject.name, x: xValues, y: yValues, - text: yValues.map(String), - xAxisMetadata: axisMetadata.xAxisMetadata, + xAxisMetadata: xAxisMetadata, yAxisMetadata: axisMetadata.yAxisMetadata, - type: this.options.type ? this.options.type : 'bar', + type: this.domainObject.configuration.useBar ? 'bar' : 'scatter', + mode: 'lines', + line: { + shape: this.domainObject.configuration.useInterpolation + }, marker: { color: this.domainObject.configuration.barStyles.series[key].color }, - hoverinfo: 'skip' + hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y' }; - if (this.options.type) { - trace.mode = 'markers'; - trace.hoverinfo = 'x+y'; - } - this.addTrace(trace, key); }, - isDataInTimeRange(datum, key) { + isDataInTimeRange(datum, key, telemetryObject) { const timeSystemKey = this.timeContext.timeSystem().key; - let currentTimestamp = this.parse(key, timeSystemKey, datum); + const metadata = this.openmct.telemetry.getMetadata(telemetryObject); + let metadataValue = metadata.value(timeSystemKey) || { key: timeSystemKey }; + + let currentTimestamp = this.parse(key, metadataValue.key, datum); return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp; }, @@ -299,7 +365,8 @@ export default { }, requestDataFor(telemetryObject) { const axisMetadata = this.getAxisMetadata(telemetryObject); - this.openmct.telemetry.request(telemetryObject) + const options = this.getOptions(); + this.openmct.telemetry.request(telemetryObject, options) .then(data => { data.forEach((datum) => { this.addDataToGraph(telemetryObject, datum, axisMetadata); diff --git a/src/plugins/charts/bar/inspector/BarGraphOptions.vue b/src/plugins/charts/bar/inspector/BarGraphOptions.vue index a17fbc28bf..2a6ffcab83 100644 --- a/src/plugins/charts/bar/inspector/BarGraphOptions.vue +++ b/src/plugins/charts/bar/inspector/BarGraphOptions.vue @@ -20,18 +20,155 @@ at runtime from the About dialog for additional information. -->