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:
Shefali Joshi 2022-06-03 19:32:32 -07:00 committed by GitHub
parent 111b0d0d68
commit 162cc6bc77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 685 additions and 141 deletions

View File

@ -29,12 +29,12 @@ define([
} }
}, },
{ {
key: "cos", key: "wavelengths",
name: "Cosine", name: "Wavelength",
unit: "deg", unit: "nm",
formatString: '%0.2f', format: 'string[]',
hints: { hints: {
domain: 3 range: 4
} }
}, },
// Need to enable "LocalTimeSystem" plugin to make use of this // Need to enable "LocalTimeSystem" plugin to make use of this
@ -64,6 +64,14 @@ define([
hints: { hints: {
range: 2 range: 2
} }
},
{
key: "intensities",
name: "Intensities",
format: 'number[]',
hints: {
range: 3
}
} }
] ]
}, },

View File

@ -77,7 +77,8 @@
utc: nextStep, utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000, yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), 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) cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
} }
}); });
@ -126,7 +127,8 @@
utc: nextStep, utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000, yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness), sin: sin(nextStep, period, amplitude, offset, phase, randomness),
wavelength: wavelength(start, nextStep), wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(nextStep, period, amplitude, offset, phase, randomness) 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; * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
} }
function wavelength(start, nextStep) { function wavelengths() {
return (nextStep - start) / 10; 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) { function sendError(error, message) {

View File

@ -121,6 +121,18 @@ define([
return _.sortBy(matchingMetadata, ...iteratees); 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 () { TelemetryMetadataManager.prototype.getFilterableValues = function () {
return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0); return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0);
}; };

View File

@ -43,9 +43,23 @@ define([
}; };
this.valueMetadata = valueMetadata; 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.formatter = {};
this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) { this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) {
vm.byValue[e.value] = e.string; vm.byValue[e.value] = e.string;
@ -77,13 +91,13 @@ define([
// Check for formatString support once instead of per format call. // Check for formatString support once instead of per format call.
if (valueMetadata.formatString) { if (valueMetadata.formatString) {
const baseFormat = this.formatter.format; const baseFormat = this.formatter.format;
const formatString = valueMetadata.formatString; const formatString = getNonArrayValue(valueMetadata.formatString);
this.formatter.format = function (value) { this.formatter.format = function (value) {
return printj.sprintf(formatString, baseFormat.call(this, value)); return printj.sprintf(formatString, baseFormat.call(this, value));
}; };
} }
if (valueMetadata.format === 'string') { if (valueMetadataFormat === 'string') {
this.formatter.parse = function (value) { this.formatter.parse = function (value) {
if (value === undefined) { if (value === undefined) {
return ''; return '';
@ -108,7 +122,14 @@ define([
TelemetryValueFormatter.prototype.parse = function (datum) { TelemetryValueFormatter.prototype.parse = function (datum) {
if (_.isObject(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); return this.formatter.parse(datum);
@ -116,7 +137,14 @@ define([
TelemetryValueFormatter.prototype.format = function (datum) { TelemetryValueFormatter.prototype.format = function (datum) {
if (_.isObject(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); return this.formatter.format(datum);

View File

@ -40,14 +40,6 @@ export default {
BarGraph BarGraph
}, },
inject: ['openmct', 'domainObject', 'path'], inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
default() {
return {};
}
}
},
data() { data() {
this.telemetryObjects = {}; this.telemetryObjects = {};
this.telemetryObjectFormats = {}; this.telemetryObjectFormats = {};
@ -75,7 +67,9 @@ export default {
this.setTimeContext(); this.setTimeContext();
this.loadComposition(); 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() { beforeDestroy() {
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
@ -86,8 +80,19 @@ export default {
return; return;
} }
this.composition.off('add', this.addTelemetryObject); this.composition.off('add', this.addToComposition);
this.composition.off('remove', this.removeTelemetryObject); this.composition.off('remove', this.removeTelemetryObject);
if (this.unobserveAxes) {
this.unobserveAxes();
}
if (this.unobserveInterpolation) {
this.unobserveInterpolation();
}
if (this.unobserveBar) {
this.unobserveBar();
}
}, },
methods: { methods: {
setTimeContext() { setTimeContext() {
@ -105,6 +110,42 @@ export default {
this.timeContext.off('bounds', this.refreshData); 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) { addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object // grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
@ -165,7 +206,12 @@ export default {
const yAxisMetadata = metadata.valuesForHints(['range'])[0]; const yAxisMetadata = metadata.valuesForHints(['range'])[0];
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only //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 { return {
xAxisMetadata, xAxisMetadata,
@ -183,13 +229,7 @@ export default {
loadComposition() { loadComposition() {
this.composition = this.openmct.composition.get(this.domainObject); this.composition = this.openmct.composition.get(this.domainObject);
if (!this.composition) { this.composition.on('add', this.addToComposition);
this.addTelemetryObject(this.domainObject);
return;
}
this.composition.on('add', this.addTelemetryObject);
this.composition.on('remove', this.removeTelemetryObject); this.composition.on('remove', this.removeTelemetryObject);
this.composition.load(); this.composition.load();
}, },
@ -212,7 +252,10 @@ export default {
}, },
removeTelemetryObject(identifier) { removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(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]) { if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.telemetryObjectFormats[key]; delete this.telemetryObjectFormats[key];
} }
@ -237,49 +280,72 @@ export default {
this.openmct.notifications.alert(data.message); 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; return;
} }
let xValues = []; let xValues = [];
let yValues = []; let yValues = [];
let xAxisMetadata = axisMetadata.xAxisMetadata.find(metadata => metadata.key === this.domainObject.configuration.axes.xKey);
//populate X and Y values for plotly if (xAxisMetadata && xAxisMetadata.isArrayValue) {
axisMetadata.xAxisMetadata.forEach((metadata) => { //populate x and y values
xValues.push(metadata.name); let metadataKey = this.domainObject.configuration.axes.xKey;
if (data[metadata.key]) { if (data[metadataKey] !== undefined) {
const formattedValue = this.format(key, metadata.key, data); xValues = this.parse(key, metadataKey, data);
yValues.push(formattedValue);
} else {
yValues.push(null);
} }
});
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 = { let trace = {
key, key,
name: telemetryObject.name, name: telemetryObject.name,
x: xValues, x: xValues,
y: yValues, y: yValues,
text: yValues.map(String), xAxisMetadata: xAxisMetadata,
xAxisMetadata: axisMetadata.xAxisMetadata,
yAxisMetadata: axisMetadata.yAxisMetadata, 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: { marker: {
color: this.domainObject.configuration.barStyles.series[key].color 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); this.addTrace(trace, key);
}, },
isDataInTimeRange(datum, key) { isDataInTimeRange(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key; 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; return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp;
}, },
@ -299,7 +365,8 @@ export default {
}, },
requestDataFor(telemetryObject) { requestDataFor(telemetryObject) {
const axisMetadata = this.getAxisMetadata(telemetryObject); const axisMetadata = this.getAxisMetadata(telemetryObject);
this.openmct.telemetry.request(telemetryObject) const options = this.getOptions();
this.openmct.telemetry.request(telemetryObject, options)
.then(data => { .then(data => {
data.forEach((datum) => { data.forEach((datum) => {
this.addDataToGraph(telemetryObject, datum, axisMetadata); this.addDataToGraph(telemetryObject, datum, axisMetadata);

View File

@ -20,18 +20,155 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<ul class="c-tree c-bar-graph-options"> <div class="c-bar-graph-options js-bar-plot-option">
<h2 title="Display properties for this object">Bar Graph Series</h2> <ul class="c-tree">
<li <h2 title="Display properties for this object">Bar Graph Series</h2>
v-for="series in domainObject.composition" <li>
:key="series.key" <series-options
> v-for="series in plotSeries"
<series-options :key="series.key"
:item="series" :item="series"
:color-palette="colorPalette" :color-palette="colorPalette"
/> />
</li> </li>
</ul> </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> </template>
<script> <script>
@ -45,8 +182,17 @@ export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data() { data() {
return { return {
xKey: this.domainObject.configuration.axes.xKey,
yKey: this.domainObject.configuration.axes.yKey,
xKeyLabel: '',
yKeyLabel: '',
plotSeries: [],
yKeyOptions: [],
xKeyOptions: [],
isEditing: this.openmct.editor.isEditing(), isEditing: this.openmct.editor.isEditing(),
colorPalette: this.colorPalette colorPalette: this.colorPalette,
useInterpolation: this.domainObject.configuration.useInterpolation,
useBar: this.domainObject.configuration.useBar
}; };
}, },
computed: { computed: {
@ -59,13 +205,187 @@ export default {
}, },
mounted() { mounted() {
this.openmct.editor.on('isEditing', this.setEditState); this.openmct.editor.on('isEditing', this.setEditState);
this.composition = this.openmct.composition.get(this.domainObject);
this.registerListeners();
this.composition.load();
}, },
beforeDestroy() { beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState); this.openmct.editor.off('isEditing', this.setEditState);
this.stopListening();
}, },
methods: { methods: {
setEditState(isEditing) { setEditState(isEditing) {
this.isEditing = 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);
} }
} }
}; };

View File

@ -38,16 +38,19 @@
<div class="c-object-label__name">{{ name }}</div> <div class="c-object-label__name">{{ name }}</div>
</div> </div>
</li> </li>
<ColorSwatch <ul class="grid-properties">
v-if="expanded" <li class="grid-row">
:current-color="currentColor" <ColorSwatch
title="Manually set the color for this bar graph series." v-if="expanded"
edit-title="Manually set the color for this bar graph series" :current-color="currentColor"
view-title="The color for this bar graph series." title="Manually set the color for this bar graph series."
short-label="Color" edit-title="Manually set the color for this bar graph series."
class="grid-properties" view-title="The color for this bar graph series."
@colorSet="setColor" short-label="Color"
/> @colorSet="setColor"
/>
</li>
</ul>
</ul> </ul>
</template> </template>
@ -109,7 +112,6 @@ export default {
} }
}, },
mounted() { mounted() {
this.key = this.openmct.objects.makeKeyString(this.item);
this.initColorAndName(); this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName); this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
}, },
@ -120,6 +122,7 @@ export default {
}, },
methods: { methods: {
initColorAndName() { initColorAndName() {
this.key = this.openmct.objects.makeKeyString(this.item.identifier);
// this is called before the plot is initialized // this is called before the plot is initialized
if (!this.domainObject.configuration.barStyles.series[this.key]) { if (!this.domainObject.configuration.barStyles.series[this.key]) {
const color = this.colorPalette.getNextColor().asHexString(); const color = this.colorPalette.getNextColor().asHexString();

View File

@ -28,14 +28,17 @@ export default function () {
return function install(openmct) { return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, { openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY, key: BAR_GRAPH_KEY,
name: "Bar Graph", name: "Graph (Bar or Line)",
cssClass: "icon-bar-chart", cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.", description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true, creatable: true,
initialize: function (domainObject) { initialize: function (domainObject) {
domainObject.composition = []; domainObject.composition = [];
domainObject.configuration = { domainObject.configuration = {
barStyles: { series: {} } barStyles: { series: {} },
axes: {},
useInterpolation: 'linear',
useBar: true
}; };
}, },
priority: 891 priority: 891

View File

@ -57,18 +57,18 @@ describe("the plugin", function () {
const testTelemetry = [ const testTelemetry = [
{ {
'utc': 1, 'utc': 1,
'some-key': 'some-value 1', 'some-key': ['1.3222'],
'some-other-key': 'some-other-value 1' 'some-other-key': [1]
}, },
{ {
'utc': 2, 'utc': 2,
'some-key': 'some-value 2', 'some-key': ['2.555'],
'some-other-key': 'some-other-value 2' 'some-other-key': [2]
}, },
{ {
'utc': 3, 'utc': 3,
'some-key': 'some-value 3', 'some-key': ['3.888'],
'some-other-key': 'some-other-value 3' 'some-other-key': [3]
} }
]; ];
@ -123,7 +123,6 @@ describe("the plugin", function () {
}); });
describe("The bar graph view", () => { describe("The bar graph view", () => {
let testDomainObject;
let barGraphObject; let barGraphObject;
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let component; let component;
@ -135,51 +134,21 @@ describe("the plugin", function () {
namespace: "", namespace: "",
key: "test-plot" key: "test-plot"
}, },
configuration: {
barStyles: {
series: {}
},
axes: {},
useInterpolation: 'linear',
useBar: true
},
type: "telemetry.plot.bar-graph", type: "telemetry.plot.bar-graph",
name: "Test 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 = new EventEmitter();
mockComposition.load = () => { mockComposition.load = () => {
mockComposition.emit('add', testDomainObject); return [];
return [testDomainObject];
}; };
spyOn(openmct.composition, 'get').and.returnValue(mockComposition); spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
@ -247,15 +216,116 @@ describe("the plugin", function () {
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath); const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW); 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); barGraphView.show(child, true);
expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
mockComposition.emit('add', dotFullTelemetryObject); 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(); 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", () => { describe("the bar graph objects", () => {
const mockObject = { const mockObject = {
name: 'A very nice bar graph', name: 'A very nice bar graph',
@ -412,7 +482,7 @@ describe("the plugin", function () {
testDomainObject = { testDomainObject = {
identifier: { identifier: {
namespace: "", namespace: "",
key: "test-object" key: "~Some~foo.bar"
}, },
type: "test-object", type: "test-object",
name: "Test Object", name: "Test Object",
@ -460,11 +530,16 @@ describe("the plugin", function () {
isAlias: true isAlias: true
} }
} }
} },
axes: {},
useInterpolation: 'linear',
useBar: true
}, },
composition: [ composition: [
{ {
key: '~Some~foo.bar' identifier: {
key: '~Some~foo.bar'
}
} }
] ]
} }

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div class="grid-row"> <div class="grid-row grid-row--pad-label-for-button">
<template v-if="canEdit"> <template v-if="canEdit">
<div <div
class="grid-cell label" class="grid-cell label"

View File

@ -173,12 +173,18 @@
grid-column: 1 / 3; grid-column: 1 / 3;
} }
} }
}
.is-editing & { .is-editing {
.c-inspect-properties { .c-inspect-properties {
&__value, &__label { &__value, &__label {
line-height: 160%; // Prevent buttons/selects from overlapping when wrapping 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;
} }
} }
} }