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",
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
}
}
]
},

View File

@ -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) {

View File

@ -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);
};

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
}
};

View File

@ -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();

View File

@ -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

View File

@ -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'
}
}
]
}

View File

@ -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"

View File

@ -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;
}
}
}