mirror of
https://github.com/nasa/openmct.git
synced 2024-12-19 21:27:52 +00:00
Support for spectral plots via existing bar graphs (#5162)
Spectral plots support Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov> Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com> Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
parent
111b0d0d68
commit
162cc6bc77
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -20,18 +20,155 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<ul class="c-tree c-bar-graph-options">
|
||||
<h2 title="Display properties for this object">Bar Graph Series</h2>
|
||||
<li
|
||||
v-for="series in domainObject.composition"
|
||||
:key="series.key"
|
||||
>
|
||||
<series-options
|
||||
:item="series"
|
||||
:color-palette="colorPalette"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="c-bar-graph-options js-bar-plot-option">
|
||||
<ul class="c-tree">
|
||||
<h2 title="Display properties for this object">Bar Graph Series</h2>
|
||||
<li>
|
||||
<series-options
|
||||
v-for="series in plotSeries"
|
||||
:key="series.key"
|
||||
:item="series"
|
||||
:color-palette="colorPalette"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="grid-properties">
|
||||
<ul class="l-inspector-part">
|
||||
<h2 title="Y axis settings for this object">Axes</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="X axis selection."
|
||||
>X Axis</div>
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="grid-cell value"
|
||||
>
|
||||
<select
|
||||
v-model="xKey"
|
||||
@change="updateForm('xKey')"
|
||||
>
|
||||
<option
|
||||
v-for="option in xKeyOptions"
|
||||
:key="`xKey-${option.value}`"
|
||||
:value="option.value"
|
||||
:selected="option.value === xKey"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="grid-cell value"
|
||||
>{{ xKeyLabel }}</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="yKey !== ''"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Y axis selection."
|
||||
>Y Axis</div>
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="grid-cell value"
|
||||
>
|
||||
<select
|
||||
v-model="yKey"
|
||||
@change="updateForm('yKey')"
|
||||
>
|
||||
<option
|
||||
v-for="option in yKeyOptions"
|
||||
:key="`yKey-${option.value}`"
|
||||
:value="option.value"
|
||||
:selected="option.value === yKey"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="grid-cell value"
|
||||
>{{ yKeyLabel }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="grid-properties">
|
||||
<ul class="l-inspector-part">
|
||||
<h2 title="Settings for plot">Settings</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="grid-cell label"
|
||||
title="Display style for the plot"
|
||||
>Display Style</div>
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="grid-cell value"
|
||||
>
|
||||
<select
|
||||
v-model="useBar"
|
||||
@change="updateBar"
|
||||
>
|
||||
<option :value="true">Bar</option>
|
||||
<option :value="false">Line</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isEditing"
|
||||
class="grid-cell label"
|
||||
title="Display style for plot"
|
||||
>Display Style</div>
|
||||
<div
|
||||
v-if="!isEditing"
|
||||
class="grid-cell value"
|
||||
>{{ {
|
||||
'true': 'Bar',
|
||||
'false': 'Line'
|
||||
}[useBar] }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!useBar"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="grid-cell label"
|
||||
title="The rendering method to join lines for this series."
|
||||
>Line Method</div>
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="grid-cell value"
|
||||
>
|
||||
<select
|
||||
v-model="useInterpolation"
|
||||
@change="updateInterpolation"
|
||||
>
|
||||
<option value="linear">Linear interpolate</option>
|
||||
<option value="hv">Step after</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isEditing"
|
||||
class="grid-cell label"
|
||||
title="The rendering method to join lines for this series."
|
||||
>Line Method</div>
|
||||
<div
|
||||
v-if="!isEditing"
|
||||
class="grid-cell value"
|
||||
>{{ {
|
||||
'linear': 'Linear interpolation',
|
||||
'hv': 'Step After'
|
||||
}[useInterpolation] }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -45,8 +182,17 @@ export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
xKey: this.domainObject.configuration.axes.xKey,
|
||||
yKey: this.domainObject.configuration.axes.yKey,
|
||||
xKeyLabel: '',
|
||||
yKeyLabel: '',
|
||||
plotSeries: [],
|
||||
yKeyOptions: [],
|
||||
xKeyOptions: [],
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
colorPalette: this.colorPalette
|
||||
colorPalette: this.colorPalette,
|
||||
useInterpolation: this.domainObject.configuration.useInterpolation,
|
||||
useBar: this.domainObject.configuration.useBar
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -59,13 +205,187 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.registerListeners();
|
||||
this.composition.load();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
},
|
||||
registerListeners() {
|
||||
this.composition.on('add', this.addSeries);
|
||||
this.composition.on('remove', this.removeSeries);
|
||||
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setKeysAndSetupOptions);
|
||||
},
|
||||
stopListening() {
|
||||
this.composition.off('add', this.addSeries);
|
||||
this.composition.off('remove', this.removeSeries);
|
||||
if (this.unobserve) {
|
||||
this.unobserve();
|
||||
}
|
||||
},
|
||||
addSeries(series, index) {
|
||||
this.$set(this.plotSeries, this.plotSeries.length, series);
|
||||
this.setupOptions();
|
||||
},
|
||||
removeSeries(seriesIdentifier) {
|
||||
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier));
|
||||
if (index >= 0) {
|
||||
this.$delete(this.plotSeries, index);
|
||||
this.setupOptions();
|
||||
}
|
||||
},
|
||||
setKeysAndSetupOptions() {
|
||||
this.xKey = this.domainObject.configuration.axes.xKey;
|
||||
this.yKey = this.domainObject.configuration.axes.yKey;
|
||||
this.setupOptions();
|
||||
},
|
||||
setupOptions() {
|
||||
this.xKeyOptions = [];
|
||||
this.yKeyOptions = [];
|
||||
if (this.plotSeries.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let update = false;
|
||||
const series = this.plotSeries[0];
|
||||
const metadata = this.openmct.telemetry.getMetadata(series);
|
||||
const metadataRangeValues = metadata.valuesForHints(['range']).map((metaDatum) => {
|
||||
metaDatum.isArrayValue = metadata.isArrayValue(metaDatum);
|
||||
|
||||
return metaDatum;
|
||||
});
|
||||
const metadataArrayValues = metadataRangeValues.filter(metadataObj => metadataObj.isArrayValue);
|
||||
const metadataValues = metadataRangeValues.filter(metadataObj => !metadataObj.isArrayValue);
|
||||
metadataArrayValues.forEach((metadataValue) => {
|
||||
this.xKeyOptions.push({
|
||||
name: metadataValue.name || metadataValue.key,
|
||||
value: metadataValue.key,
|
||||
isArrayValue: metadataValue.isArrayValue
|
||||
});
|
||||
this.yKeyOptions.push({
|
||||
name: metadataValue.name || metadataValue.key,
|
||||
value: metadataValue.key,
|
||||
isArrayValue: metadataValue.isArrayValue
|
||||
});
|
||||
});
|
||||
|
||||
//Metadata values that are not array values will be grouped together as x-axis only option.
|
||||
// Here, the y-axis is not relevant.
|
||||
if (metadataValues.length) {
|
||||
this.xKeyOptions.push(
|
||||
metadataValues.reduce((previousValue, currentValue) => {
|
||||
return {
|
||||
name: `${previousValue.name}, ${currentValue.name}`,
|
||||
value: currentValue.key,
|
||||
isArrayValue: currentValue.isArrayValue
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let xKeyOptionIndex;
|
||||
let yKeyOptionIndex;
|
||||
|
||||
if (this.domainObject.configuration.axes.xKey) {
|
||||
xKeyOptionIndex = this.xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
|
||||
if (xKeyOptionIndex > -1) {
|
||||
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
|
||||
this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;
|
||||
}
|
||||
} else {
|
||||
if (this.xKey === undefined) {
|
||||
update = true;
|
||||
xKeyOptionIndex = 0;
|
||||
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
|
||||
this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;
|
||||
}
|
||||
}
|
||||
|
||||
if (metadataRangeValues.length > 1) {
|
||||
if (this.domainObject.configuration.axes.yKey && this.domainObject.configuration.axes.yKey !== 'none') {
|
||||
yKeyOptionIndex = this.yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
|
||||
if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {
|
||||
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
|
||||
this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;
|
||||
}
|
||||
} else {
|
||||
if (this.yKey === undefined) {
|
||||
yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex);
|
||||
if (yKeyOptionIndex > -1) {
|
||||
update = true;
|
||||
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
|
||||
this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.yKeyOptions = this.yKeyOptions.map((option, index) => {
|
||||
if (index === xKeyOptionIndex) {
|
||||
option.name = `${option.name} (swap)`;
|
||||
option.swap = yKeyOptionIndex;
|
||||
} else {
|
||||
option.name = option.name.replace(' (swap)', '');
|
||||
option.swap = undefined;
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
}
|
||||
|
||||
this.xKeyOptions = this.xKeyOptions.map((option, index) => {
|
||||
if (index === yKeyOptionIndex) {
|
||||
option.name = `${option.name} (swap)`;
|
||||
option.swap = xKeyOptionIndex;
|
||||
} else {
|
||||
option.name = option.name.replace(' (swap)', '');
|
||||
option.swap = undefined;
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
|
||||
if (update === true) {
|
||||
this.saveConfiguration();
|
||||
}
|
||||
},
|
||||
updateForm(property) {
|
||||
if (property === 'xKey') {
|
||||
const xKeyOption = this.xKeyOptions.find(option => option.value === this.xKey);
|
||||
if (xKeyOption.swap !== undefined) {
|
||||
//swap
|
||||
this.yKey = this.xKeyOptions[xKeyOption.swap].value;
|
||||
} else if (!xKeyOption.isArrayValue) {
|
||||
this.yKey = 'none';
|
||||
} else {
|
||||
this.yKey = undefined;
|
||||
}
|
||||
} else if (property === 'yKey') {
|
||||
const yKeyOption = this.yKeyOptions.find(option => option.value === this.yKey);
|
||||
if (yKeyOption.swap !== undefined) {
|
||||
//swap
|
||||
this.xKey = this.yKeyOptions[yKeyOption.swap].value;
|
||||
}
|
||||
}
|
||||
|
||||
this.saveConfiguration();
|
||||
},
|
||||
saveConfiguration() {
|
||||
this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {
|
||||
xKey: this.xKey,
|
||||
yKey: this.yKey
|
||||
});
|
||||
},
|
||||
updateInterpolation(event) {
|
||||
this.openmct.objects.mutate(this.domainObject, `configuration.useInterpolation`, this.useInterpolation);
|
||||
},
|
||||
updateBar(event) {
|
||||
this.openmct.objects.mutate(this.domainObject, `configuration.useBar`, this.useBar);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -38,16 +38,19 @@
|
||||
<div class="c-object-label__name">{{ name }}</div>
|
||||
</div>
|
||||
</li>
|
||||
<ColorSwatch
|
||||
v-if="expanded"
|
||||
:current-color="currentColor"
|
||||
title="Manually set the color for this bar graph series."
|
||||
edit-title="Manually set the color for this bar graph series"
|
||||
view-title="The color for this bar graph series."
|
||||
short-label="Color"
|
||||
class="grid-properties"
|
||||
@colorSet="setColor"
|
||||
/>
|
||||
<ul class="grid-properties">
|
||||
<li class="grid-row">
|
||||
<ColorSwatch
|
||||
v-if="expanded"
|
||||
:current-color="currentColor"
|
||||
title="Manually set the color for this bar graph series."
|
||||
edit-title="Manually set the color for this bar graph series."
|
||||
view-title="The color for this bar graph series."
|
||||
short-label="Color"
|
||||
@colorSet="setColor"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
@ -109,7 +112,6 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.key = this.openmct.objects.makeKeyString(this.item);
|
||||
this.initColorAndName();
|
||||
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
|
||||
},
|
||||
@ -120,6 +122,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
initColorAndName() {
|
||||
this.key = this.openmct.objects.makeKeyString(this.item.identifier);
|
||||
// this is called before the plot is initialized
|
||||
if (!this.domainObject.configuration.barStyles.series[this.key]) {
|
||||
const color = this.colorPalette.getNextColor().asHexString();
|
||||
|
@ -28,14 +28,17 @@ export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType(BAR_GRAPH_KEY, {
|
||||
key: BAR_GRAPH_KEY,
|
||||
name: "Bar Graph",
|
||||
name: "Graph (Bar or Line)",
|
||||
cssClass: "icon-bar-chart",
|
||||
description: "View data as a bar graph. Can be added to Display Layouts.",
|
||||
creatable: true,
|
||||
initialize: function (domainObject) {
|
||||
domainObject.composition = [];
|
||||
domainObject.configuration = {
|
||||
barStyles: { series: {} }
|
||||
barStyles: { series: {} },
|
||||
axes: {},
|
||||
useInterpolation: 'linear',
|
||||
useBar: true
|
||||
};
|
||||
},
|
||||
priority: 891
|
||||
|
@ -57,18 +57,18 @@ describe("the plugin", function () {
|
||||
const testTelemetry = [
|
||||
{
|
||||
'utc': 1,
|
||||
'some-key': 'some-value 1',
|
||||
'some-other-key': 'some-other-value 1'
|
||||
'some-key': ['1.3222'],
|
||||
'some-other-key': [1]
|
||||
},
|
||||
{
|
||||
'utc': 2,
|
||||
'some-key': 'some-value 2',
|
||||
'some-other-key': 'some-other-value 2'
|
||||
'some-key': ['2.555'],
|
||||
'some-other-key': [2]
|
||||
},
|
||||
{
|
||||
'utc': 3,
|
||||
'some-key': 'some-value 3',
|
||||
'some-other-key': 'some-other-value 3'
|
||||
'some-key': ['3.888'],
|
||||
'some-other-key': [3]
|
||||
}
|
||||
];
|
||||
|
||||
@ -123,7 +123,6 @@ describe("the plugin", function () {
|
||||
});
|
||||
|
||||
describe("The bar graph view", () => {
|
||||
let testDomainObject;
|
||||
let barGraphObject;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let component;
|
||||
@ -135,51 +134,21 @@ describe("the plugin", function () {
|
||||
namespace: "",
|
||||
key: "test-plot"
|
||||
},
|
||||
configuration: {
|
||||
barStyles: {
|
||||
series: {}
|
||||
},
|
||||
axes: {},
|
||||
useInterpolation: 'linear',
|
||||
useBar: true
|
||||
},
|
||||
type: "telemetry.plot.bar-graph",
|
||||
name: "Test Bar Graph"
|
||||
};
|
||||
|
||||
testDomainObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object"
|
||||
},
|
||||
configuration: {
|
||||
barStyles: {
|
||||
series: {}
|
||||
}
|
||||
},
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "utc",
|
||||
format: "utc",
|
||||
name: "Time",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-key",
|
||||
name: "Some attribute",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key",
|
||||
name: "Another attribute",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
mockComposition = new EventEmitter();
|
||||
mockComposition.load = () => {
|
||||
mockComposition.emit('add', testDomainObject);
|
||||
|
||||
return [testDomainObject];
|
||||
return [];
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||
@ -247,15 +216,116 @@ describe("the plugin", function () {
|
||||
|
||||
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
|
||||
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
|
||||
const barGraphView = plotViewProvider.view(testDomainObject, [testDomainObject]);
|
||||
const barGraphView = plotViewProvider.view(barGraphObject, [barGraphObject]);
|
||||
barGraphView.show(child, true);
|
||||
expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
|
||||
mockComposition.emit('add', dotFullTelemetryObject);
|
||||
expect(testDomainObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
|
||||
expect(barGraphObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
|
||||
barGraphView.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("The spectral plot view for telemetry objects with array values", () => {
|
||||
let barGraphObject;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let component;
|
||||
let mockComposition;
|
||||
|
||||
beforeEach(async () => {
|
||||
barGraphObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-plot"
|
||||
},
|
||||
configuration: {
|
||||
barStyles: {
|
||||
series: {}
|
||||
},
|
||||
axes: {
|
||||
xKey: 'some-key',
|
||||
yKey: 'some-other-key'
|
||||
},
|
||||
useInterpolation: 'linear',
|
||||
useBar: false
|
||||
},
|
||||
type: "telemetry.plot.bar-graph",
|
||||
name: "Test Bar Graph"
|
||||
};
|
||||
|
||||
mockComposition = new EventEmitter();
|
||||
mockComposition.load = () => {
|
||||
return [];
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||
|
||||
let viewContainer = document.createElement("div");
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
BarGraph
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: barGraphObject,
|
||||
composition: openmct.composition.get(barGraphObject)
|
||||
},
|
||||
template: "<BarGraph></BarGraph>"
|
||||
});
|
||||
|
||||
await Vue.nextTick();
|
||||
});
|
||||
|
||||
it("Renders spectral plots", () => {
|
||||
const dotFullTelemetryObject = {
|
||||
identifier: {
|
||||
namespace: "someNamespace",
|
||||
key: "~OpenMCT~outer.test-object.foo.bar"
|
||||
},
|
||||
type: "test-dotful-object",
|
||||
name: "A Dotful Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "utc",
|
||||
format: "utc",
|
||||
name: "Time",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-key",
|
||||
name: "Some attribute",
|
||||
formatString: '%0.2f[]',
|
||||
hints: {
|
||||
range: 1
|
||||
},
|
||||
source: 'some-key'
|
||||
}, {
|
||||
key: "some-other-key",
|
||||
name: "Another attribute",
|
||||
format: "number[]",
|
||||
hints: {
|
||||
range: 2
|
||||
},
|
||||
source: 'some-other-key'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
|
||||
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
|
||||
const barGraphView = plotViewProvider.view(barGraphObject, [barGraphObject]);
|
||||
barGraphView.show(child, true);
|
||||
mockComposition.emit('add', dotFullTelemetryObject);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
|
||||
expect(plotElement).not.toBeNull();
|
||||
barGraphView.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("the bar graph objects", () => {
|
||||
const mockObject = {
|
||||
name: 'A very nice bar graph',
|
||||
@ -412,7 +482,7 @@ describe("the plugin", function () {
|
||||
testDomainObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object"
|
||||
key: "~Some~foo.bar"
|
||||
},
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
@ -460,11 +530,16 @@ describe("the plugin", function () {
|
||||
isAlias: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
axes: {},
|
||||
useInterpolation: 'linear',
|
||||
useBar: true
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
key: '~Some~foo.bar'
|
||||
identifier: {
|
||||
key: '~Some~foo.bar'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div class="grid-row">
|
||||
<div class="grid-row grid-row--pad-label-for-button">
|
||||
<template v-if="canEdit">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
|
@ -173,12 +173,18 @@
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-editing & {
|
||||
.c-inspect-properties {
|
||||
&__value, &__label {
|
||||
line-height: 160%; // Prevent buttons/selects from overlapping when wrapping
|
||||
}
|
||||
.is-editing {
|
||||
.c-inspect-properties {
|
||||
&__value, &__label {
|
||||
line-height: 160%; // Prevent buttons/selects from overlapping when wrapping
|
||||
}
|
||||
}
|
||||
.grid-row--pad-label-for-button {
|
||||
// Add extra space at the top of the label grid cell because there's a button to the right
|
||||
[class*='label'] {
|
||||
line-height: 1.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user