Compare commits
	
		
			7 Commits
		
	
	
		
			omm-r5.2.0
			...
			hide-gripp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | b117890f27 | ||
|   | afc37209d2 | ||
|   | e45b58e2a8 | ||
|   | 8b3487bdbe | ||
|   | 50719b1383 | ||
|   | cfda067794 | ||
|   | 4847cc8931 | 
| @@ -156,7 +156,7 @@ async function turnOffAutoscale(page) { | ||||
|     await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click(); | ||||
|  | ||||
|     // uncheck autoscale | ||||
|     await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck(); | ||||
|     await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck(); | ||||
|  | ||||
|     // save | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|   | ||||
| @@ -205,7 +205,8 @@ async function enableEditMode(page) { | ||||
|  */ | ||||
| async function enableLogMode(page) { | ||||
|     // turn on log mode | ||||
|     await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check(); | ||||
|     await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check(); | ||||
|     // await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check(); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -213,7 +214,7 @@ async function enableLogMode(page) { | ||||
|  */ | ||||
| async function disableLogMode(page) { | ||||
|     // turn off log mode | ||||
|     await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck(); | ||||
|     await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
							
								
								
									
										116
									
								
								e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Tests to verify log plot functionality. Note this test suite if very much under active development and should not | ||||
| necessarily be used for reference when writing new tests in this area. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Overlay Plot', () => { | ||||
|     test('Plot legend color is in sync with plot series color', async ({ page }) => { | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|         const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Overlay Plot" | ||||
|         }); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|  | ||||
|         await page.goto(overlayPlot.url); | ||||
|  | ||||
|         // navigate to plot series color palette | ||||
|         await page.click('.l-browse-bar__actions__edit'); | ||||
|         await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click(); | ||||
|         await page.locator('.c-click-swatch--menu').click(); | ||||
|         await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click(); | ||||
|  | ||||
|         // gets color for swatch located in legend | ||||
|         const element = await page.waitForSelector('.plot-series-color-swatch'); | ||||
|         const color = await element.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('background-color'); | ||||
|         }); | ||||
|  | ||||
|         expect(color).toBe('rgb(255, 166, 61)'); | ||||
|     }); | ||||
|     test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => { | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|         const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Overlay Plot" | ||||
|         }); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             name: 'swg a', | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             name: 'swg b', | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             name: 'swg c', | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             name: 'swg d', | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             name: 'swg e', | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|  | ||||
|         await page.goto(overlayPlot.url); | ||||
|         await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         // Expand the elements pool vertically | ||||
|         await page.locator('.l-pane__handle').nth(2).hover({ trial: true }); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(0, 100); | ||||
|         await page.mouse.up(); | ||||
|  | ||||
|         await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1)); | ||||
|         await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1)); | ||||
|         await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1)); | ||||
|         await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group"]').nth(2)); | ||||
|         const elementsTree = await page.locator('#inspector-elements-tree').allInnerTexts(); | ||||
|         expect(elementsTree.join('').split('\n')).toEqual([ | ||||
|             "Y Axis 1", | ||||
|             "swg d", | ||||
|             "Y Axis 2", | ||||
|             "swg e", | ||||
|             "swg c", | ||||
|             "swg a", | ||||
|             "Y Axis 3", | ||||
|             "swg b" | ||||
|         ]); | ||||
|     }); | ||||
| }); | ||||
| @@ -34,23 +34,26 @@ | ||||
|         @legendHoverChanged="legendHoverChanged" | ||||
|     /> | ||||
|     <div class="plot-wrapper-axis-and-display-area flex-elem grows"> | ||||
|         <y-axis | ||||
|             v-if="seriesModels.length > 0" | ||||
|             :tick-width="tickWidth" | ||||
|             :single-series="seriesModels.length === 1" | ||||
|             :has-same-range-value="hasSameRangeValue" | ||||
|             :series-model="seriesModels[0]" | ||||
|             :style="{ | ||||
|                 left: (plotWidth - tickWidth) + 'px' | ||||
|             }" | ||||
|             @yKeyChanged="setYAxisKey" | ||||
|             @tickWidthChanged="onTickWidthChange" | ||||
|         /> | ||||
|         <div | ||||
|             v-if="seriesModels.length" | ||||
|             class="u-contents" | ||||
|         > | ||||
|             <y-axis | ||||
|                 v-for="(yAxis, index) in yAxesIds" | ||||
|                 :id="yAxis.id" | ||||
|                 :key="`yAxis-${index}`" | ||||
|                 :multiple-left-axes="multipleLeftAxes" | ||||
|                 :position="yAxis.id > 2 ? 'right' : 'left'" | ||||
|                 :class="{'plot-yaxis-right': yAxis.id > 2}" | ||||
|                 :tick-width="yAxis.tickWidth" | ||||
|                 :plot-left-tick-width="yAxis.id > 2 ? yAxis.tickWidth: plotLeftTickWidth" | ||||
|                 @yKeyChanged="setYAxisKey" | ||||
|                 @tickWidthChanged="onTickWidthChange" | ||||
|             /> | ||||
|         </div> | ||||
|         <div | ||||
|             class="gl-plot-wrapper-display-area-and-x-axis" | ||||
|             :style="{ | ||||
|                 left: (plotWidth + 20) + 'px' | ||||
|             }" | ||||
|             :style="xAxisStyle" | ||||
|         > | ||||
|  | ||||
|             <div class="gl-plot-display-area has-local-controls has-cursor-guides"> | ||||
| @@ -69,9 +72,12 @@ | ||||
|                 /> | ||||
|  | ||||
|                 <mct-ticks | ||||
|                     v-for="(yAxis, index) in yAxesIds" | ||||
|                     v-show="gridLines" | ||||
|                     :key="`yAxis-gridlines-${index}`" | ||||
|                     :axis-type="'yAxis'" | ||||
|                     :position="'bottom'" | ||||
|                     :axis-id="yAxis.id" | ||||
|                     @plotTickWidth="onTickWidthChange" | ||||
|                 /> | ||||
|  | ||||
| @@ -214,6 +220,7 @@ import YAxis from "./axis/YAxis.vue"; | ||||
| import _ from "lodash"; | ||||
|  | ||||
| const OFFSET_THRESHOLD = 10; | ||||
| const AXES_PADDING = 20; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
| @@ -269,7 +276,6 @@ export default { | ||||
|             altPressed: false, | ||||
|             highlights: [], | ||||
|             lockHighlightPoint: false, | ||||
|             tickWidth: 0, | ||||
|             yKeyOptions: [], | ||||
|             yAxisLabel: '', | ||||
|             rectangles: [], | ||||
| @@ -284,12 +290,31 @@ export default { | ||||
|             isTimeOutOfSync: false, | ||||
|             showLimitLineLabels: this.limitLineLabels, | ||||
|             isFrozenOnMouseDown: false, | ||||
|             hasSameRangeValue: true, | ||||
|             cursorGuide: this.initCursorGuide, | ||||
|             gridLines: this.initGridLines | ||||
|             gridLines: this.initGridLines, | ||||
|             yAxes: [] | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         xAxisStyle() { | ||||
|             const rightAxis = this.yAxesIds.find(yAxis => yAxis.id > 2); | ||||
|             const leftOffset = this.multipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING; | ||||
|             let style = { | ||||
|                 left: `${this.plotLeftTickWidth + leftOffset}px` | ||||
|             }; | ||||
|  | ||||
|             if (rightAxis) { | ||||
|                 style.right = `${rightAxis.tickWidth + AXES_PADDING}px`; | ||||
|             } | ||||
|  | ||||
|             return style; | ||||
|         }, | ||||
|         yAxesIds() { | ||||
|             return this.yAxes.filter(yAxis => yAxis.seriesCount > 0); | ||||
|         }, | ||||
|         multipleLeftAxes() { | ||||
|             return this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1; | ||||
|         }, | ||||
|         isNestedWithinAStackedPlot() { | ||||
|             const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path)); | ||||
|  | ||||
| @@ -312,8 +337,17 @@ export default { | ||||
|                 return 'plot-legend-collapsed'; | ||||
|             } | ||||
|         }, | ||||
|         plotWidth() { | ||||
|             return this.plotTickWidth || this.tickWidth; | ||||
|         plotLeftTickWidth() { | ||||
|             let leftTickWidth = 0; | ||||
|             this.yAxes.forEach((yAxis) => { | ||||
|                 if (yAxis.id > 2) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 leftTickWidth = leftTickWidth + yAxis.tickWidth; | ||||
|             }); | ||||
|  | ||||
|             return this.plotTickWidth || leftTickWidth; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
| @@ -342,6 +376,20 @@ export default { | ||||
|  | ||||
|         this.config = this.getConfig(); | ||||
|         this.legend = this.config.legend; | ||||
|         this.yAxes = [{ | ||||
|             id: this.config.yAxis.id, | ||||
|             seriesCount: 0, | ||||
|             tickWidth: 0 | ||||
|         }]; | ||||
|         if (this.config.additionalYAxes) { | ||||
|             this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => { | ||||
|                 return { | ||||
|                     id: yAxis.id, | ||||
|                     seriesCount: 0, | ||||
|                     tickWidth: 0 | ||||
|                 }; | ||||
|             })); | ||||
|         } | ||||
|  | ||||
|         if (this.isNestedWithinAStackedPlot) { | ||||
|             const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
| @@ -383,8 +431,10 @@ export default { | ||||
|         }, | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTimeContext(); | ||||
|  | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.path); | ||||
|             this.followTimeContext(); | ||||
|  | ||||
|         }, | ||||
|         followTimeContext() { | ||||
|             this.updateDisplayBounds(this.timeContext.bounds()); | ||||
| @@ -417,12 +467,13 @@ export default { | ||||
|             return config; | ||||
|         }, | ||||
|         addSeries(series, index) { | ||||
|             const yAxisId = series.get('yAxisId'); | ||||
|             this.updateAxisUsageCount(yAxisId, 1); | ||||
|             this.$set(this.seriesModels, index, series); | ||||
|             this.listenTo(series, 'change:xKey', (xKey) => { | ||||
|                 this.setDisplayRange(series, xKey); | ||||
|             }, this); | ||||
|             this.listenTo(series, 'change:yKey', () => { | ||||
|                 this.checkSameRangeValue(); | ||||
|                 this.loadSeriesData(series); | ||||
|             }, this); | ||||
|  | ||||
| @@ -430,20 +481,21 @@ export default { | ||||
|                 this.loadSeriesData(series); | ||||
|             }, this); | ||||
|  | ||||
|             this.checkSameRangeValue(); | ||||
|             this.loadSeriesData(series); | ||||
|         }, | ||||
|  | ||||
|         checkSameRangeValue() { | ||||
|             this.hasSameRangeValue = this.seriesModels.every((model) => { | ||||
|                 return model.get('yKey') === this.seriesModels[0].get('yKey'); | ||||
|             }); | ||||
|         removeSeries(plotSeries, index) { | ||||
|             const yAxisId = plotSeries.get('yAxisId'); | ||||
|             this.updateAxisUsageCount(yAxisId, -1); | ||||
|             this.seriesModels.splice(index, 1); | ||||
|             this.stopListening(plotSeries); | ||||
|         }, | ||||
|  | ||||
|         removeSeries(plotSeries, index) { | ||||
|             this.seriesModels.splice(index, 1); | ||||
|             this.checkSameRangeValue(); | ||||
|             this.stopListening(plotSeries); | ||||
|         updateAxisUsageCount(yAxisId, updateCountBy) { | ||||
|             const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId); | ||||
|             if (foundYAxis) { | ||||
|                 foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         loadSeriesData(series) { | ||||
| @@ -673,6 +725,7 @@ export default { | ||||
|  | ||||
|             // Setup canvas etc. | ||||
|             this.xScale = new LinearScale(this.config.xAxis.get('displayRange')); | ||||
|             //TODO: handle yScale, zoom/pan for all yAxes | ||||
|             this.yScale = new LinearScale(this.config.yAxis.get('displayRange')); | ||||
|  | ||||
|             this.pan = undefined; | ||||
| @@ -690,6 +743,9 @@ export default { | ||||
|  | ||||
|             this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this); | ||||
|             this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this); | ||||
|             this.config.additionalYAxes.forEach(yAxis => { | ||||
|                 this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange, this); | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         onXAxisChange(displayBounds) { | ||||
| @@ -704,20 +760,24 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         onTickWidthChange(width, fromDifferentObject) { | ||||
|             if (fromDifferentObject) { | ||||
|         onTickWidthChange(data, fromDifferentObject) { | ||||
|             const {width, yAxisId} = data; | ||||
|             if (yAxisId) { | ||||
|                 const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId); | ||||
|                 if (fromDifferentObject) { | ||||
|                 // Always accept tick width if it comes from a different object. | ||||
|                 this.tickWidth = width; | ||||
|             } else { | ||||
|                     this.yAxes[index].tickWidth = width; | ||||
|                 } else { | ||||
|                 // Otherwise, only accept tick with if it's larger. | ||||
|                 const newWidth = Math.max(width, this.tickWidth); | ||||
|                 if (newWidth !== this.tickWidth) { | ||||
|                     this.tickWidth = newWidth; | ||||
|                     const newWidth = Math.max(width, this.yAxes[index].tickWidth); | ||||
|                     if (newWidth !== this.yAxes[index].tickWidth) { | ||||
|                         this.yAxes[index].tickWidth = newWidth; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const id = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             this.$emit('plotTickWidth', this.tickWidth, id); | ||||
|                 const id = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|                 this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         trackMousePosition(event) { | ||||
| @@ -1108,8 +1168,9 @@ export default { | ||||
|             this.userViewportChangeEnd(); | ||||
|         }, | ||||
|  | ||||
|         setYAxisKey(yKey) { | ||||
|             this.config.series.models[0].set('yKey', yKey); | ||||
|         setYAxisKey(yKey, yAxisId) { | ||||
|             const seriesForYAxis = this.config.series.models.filter((model => model.get('yAxisId') === yAxisId)); | ||||
|             seriesForYAxis.forEach(model => model.set('yKey', yKey)); | ||||
|         }, | ||||
|  | ||||
|         pause() { | ||||
|   | ||||
| @@ -103,6 +103,12 @@ export default { | ||||
|                 return 6; | ||||
|             } | ||||
|         }, | ||||
|         axisId: { | ||||
|             type: Number, | ||||
|             default() { | ||||
|                 return null; | ||||
|             } | ||||
|         }, | ||||
|         position: { | ||||
|             required: true, | ||||
|             type: String, | ||||
| @@ -145,7 +151,15 @@ export default { | ||||
|                 throw new Error('config is missing'); | ||||
|             } | ||||
|  | ||||
|             return config[this.axisType]; | ||||
|             if (this.axisType === 'yAxis') { | ||||
|                 if (this.axisId && this.axisId !== config.yAxis.id) { | ||||
|                     return config.additionalYAxes.find(axis => axis.id === this.axisId); | ||||
|                 } else { | ||||
|                     return config.yAxis; | ||||
|                 } | ||||
|             } else { | ||||
|                 return config[this.axisType]; | ||||
|             } | ||||
|         }, | ||||
|         /** | ||||
|        * Determine whether ticks should be regenerated for a given range. | ||||
| @@ -258,7 +272,10 @@ export default { | ||||
|                     }, 0)); | ||||
|  | ||||
|                     this.tickWidth = tickWidth; | ||||
|                     this.$emit('plotTickWidth', tickWidth); | ||||
|                     this.$emit('plotTickWidth', { | ||||
|                         width: tickWidth, | ||||
|                         yAxisId: this.axisType === 'yAxis' ? this.axisId : '' | ||||
|                     }); | ||||
|                     this.shouldCheckWidth = false; | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -22,10 +22,8 @@ | ||||
| <template> | ||||
| <div | ||||
|     v-if="loaded" | ||||
|     class="gl-plot-axis-area gl-plot-y has-local-controls" | ||||
|     :style="{ | ||||
|         width: (tickWidth + 20) + 'px' | ||||
|     }" | ||||
|     class="gl-plot-axis-area gl-plot-y has-local-controls js-plot-y-axis" | ||||
|     :style="yAxisStyle" | ||||
| > | ||||
|  | ||||
|     <div | ||||
| @@ -52,6 +50,7 @@ | ||||
|     </select> | ||||
|  | ||||
|     <mct-ticks | ||||
|         :axis-id="id" | ||||
|         :axis-type="'yAxis'" | ||||
|         class="gl-plot-ticks" | ||||
|         :position="'top'" | ||||
| @@ -63,6 +62,10 @@ | ||||
| <script> | ||||
| import MctTicks from "../MctTicks.vue"; | ||||
| import configStore from "../configuration/ConfigStore"; | ||||
| import eventHelpers from "../lib/eventHelpers"; | ||||
|  | ||||
| const AXIS_PADDING = 20; | ||||
| const AXIS_OFFSET = 5; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
| @@ -70,22 +73,10 @@ export default { | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         singleSeries: { | ||||
|             type: Boolean, | ||||
|         id: { | ||||
|             type: Number, | ||||
|             default() { | ||||
|                 return true; | ||||
|             } | ||||
|         }, | ||||
|         hasSameRangeValue: { | ||||
|             type: Boolean, | ||||
|             default() { | ||||
|                 return true; | ||||
|             } | ||||
|         }, | ||||
|         seriesModel: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|                 return 1; | ||||
|             } | ||||
|         }, | ||||
|         tickWidth: { | ||||
| @@ -93,37 +84,132 @@ export default { | ||||
|             default() { | ||||
|                 return 0; | ||||
|             } | ||||
|         }, | ||||
|         plotLeftTickWidth: { | ||||
|             type: Number, | ||||
|             default() { | ||||
|                 return 0; | ||||
|             } | ||||
|         }, | ||||
|         multipleLeftAxes: { | ||||
|             type: Boolean, | ||||
|             default() { | ||||
|                 return false; | ||||
|             } | ||||
|         }, | ||||
|         position: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return 'left'; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         this.seriesModels = []; | ||||
|  | ||||
|         return { | ||||
|             yAxisLabel: 'none', | ||||
|             loaded: false | ||||
|             loaded: false, | ||||
|             yKeyOptions: [], | ||||
|             hasSameRangeValue: true, | ||||
|             singleSeries: true, | ||||
|             mainYAxisId: null, | ||||
|             hasAdditionalYAxes: false | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         canShowYAxisLabel() { | ||||
|             return this.singleSeries === true || this.hasSameRangeValue === true; | ||||
|         }, | ||||
|         yAxisStyle() { | ||||
|             let style = { | ||||
|                 width: `${this.tickWidth + AXIS_PADDING}px` | ||||
|             }; | ||||
|             const multipleAxesPadding = this.multipleLeftAxes ? AXIS_PADDING : 0; | ||||
|  | ||||
|             if (this.position === 'right') { | ||||
|                 style.left = `-${this.tickWidth + AXIS_PADDING}px`; | ||||
|             } else { | ||||
|                 const thisIsTheSecondLeftAxis = (this.id - 1) > 0; | ||||
|                 if (this.multipleLeftAxes && thisIsTheSecondLeftAxis) { | ||||
|                     style.left = `${ this.plotLeftTickWidth - this.tickWidth - multipleAxesPadding - AXIS_OFFSET }px`; | ||||
|                     style['border-right'] = `1px solid`; | ||||
|                 } else { | ||||
|                     style.left = `${this.plotLeftTickWidth - this.tickWidth + multipleAxesPadding}px`; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return style; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.yAxis = this.getYAxisFromConfig(); | ||||
|         eventHelpers.extend(this); | ||||
|         this.initAxisAndSeriesConfig(); | ||||
|         this.loaded = true; | ||||
|         this.setUpYAxisOptions(); | ||||
|     }, | ||||
|     methods: { | ||||
|         getYAxisFromConfig() { | ||||
|         initAxisAndSeriesConfig() { | ||||
|             const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             let config = configStore.get(configId); | ||||
|             if (config) { | ||||
|                 return config.yAxis; | ||||
|                 this.mainYAxisId = config.yAxis.id; | ||||
|                 this.hasAdditionalYAxes = config?.additionalYAxes.length; | ||||
|                 if (this.id && this.id !== this.mainYAxisId) { | ||||
|                     this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id); | ||||
|                 } else { | ||||
|                     this.yAxis = config.yAxis; | ||||
|                 } | ||||
|  | ||||
|                 this.config = config; | ||||
|                 this.listenTo(this.config.series, 'add', this.addSeries, this); | ||||
|                 this.listenTo(this.config.series, 'remove', this.removeSeries, this); | ||||
|                 this.listenTo(this.config.series, 'reorder', this.addOrRemoveSeries, this); | ||||
|  | ||||
|                 this.config.series.models.forEach(this.addSeries, this); | ||||
|             } | ||||
|         }, | ||||
|         addOrRemoveSeries(series) { | ||||
|             const yAxisId = this.series.get('yAxisId'); | ||||
|             if (yAxisId === this.id) { | ||||
|                 this.addSeries(series); | ||||
|             } else { | ||||
|                 this.removeSeries(series); | ||||
|             } | ||||
|         }, | ||||
|         addSeries(series, index) { | ||||
|             const yAxisId = series.get('yAxisId'); | ||||
|             const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), series.get('identifier'))); | ||||
|  | ||||
|             if (yAxisId === this.id && seriesIndex < 0) { | ||||
|                 this.seriesModels.push(series); | ||||
|                 this.checkRangeValueAndSingleSeries(); | ||||
|                 this.setUpYAxisOptions(); | ||||
|             } | ||||
|         }, | ||||
|         removeSeries(plotSeries) { | ||||
|             const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), plotSeries.get('identifier'))); | ||||
|             if (seriesIndex > -1) { | ||||
|                 this.seriesModels.splice(seriesIndex, 1); | ||||
|                 this.checkRangeValueAndSingleSeries(); | ||||
|                 this.setUpYAxisOptions(); | ||||
|             } | ||||
|         }, | ||||
|         checkRangeValueAndSingleSeries() { | ||||
|             this.hasSameRangeValue = this.seriesModels.every((model) => { | ||||
|                 return model.get('yKey') === this.seriesModels[0].get('yKey'); | ||||
|             }); | ||||
|             this.singleSeries = this.seriesModels.length === 1; | ||||
|         }, | ||||
|         setUpYAxisOptions() { | ||||
|             this.yKeyOptions = []; | ||||
|             if (!this.seriesModels.length) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.seriesModel.metadata) { | ||||
|                 this.yKeyOptions = this.seriesModel.metadata | ||||
|             const seriesModel = this.seriesModels[0]; | ||||
|             if (seriesModel.metadata) { | ||||
|                 this.yKeyOptions = seriesModel.metadata | ||||
|                     .valuesForHints(['range']) | ||||
|                     .map(function (o) { | ||||
|                         return { | ||||
| @@ -135,22 +221,22 @@ export default { | ||||
|  | ||||
|             //  set yAxisLabel if none is set yet | ||||
|             if (this.yAxisLabel === 'none') { | ||||
|                 let yKey = this.seriesModel.model.yKey; | ||||
|                 let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0]; | ||||
|  | ||||
|                 this.yAxisLabel = yKeyModel ? yKeyModel.name : ''; | ||||
|                 this.yAxisLabel = this.yAxis.get('label'); | ||||
|             } | ||||
|         }, | ||||
|         toggleYAxisLabel() { | ||||
|             let yAxisObject = this.yKeyOptions.filter(o => o.name === this.yAxisLabel)[0]; | ||||
|  | ||||
|             if (yAxisObject) { | ||||
|                 this.$emit('yKeyChanged', yAxisObject.key); | ||||
|                 this.$emit('yKeyChanged', yAxisObject.key, this.id); | ||||
|                 this.yAxis.set('label', this.yAxisLabel); | ||||
|             } | ||||
|         }, | ||||
|         onTickWidthChange(width) { | ||||
|             this.$emit('tickWidthChanged', width); | ||||
|         onTickWidthChange(data) { | ||||
|             this.$emit('tickWidthChanged', { | ||||
|                 width: data.width, | ||||
|                 yAxisId: this.id | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -98,7 +98,21 @@ export default { | ||||
|         this.limitLines = []; | ||||
|         this.pointSets = []; | ||||
|         this.alarmSets = []; | ||||
|         this.offset = {}; | ||||
|         const yAxisId = this.config.yAxis.get('id'); | ||||
|         this.offset = { | ||||
|             [yAxisId]: {} | ||||
|         }; | ||||
|         this.listenTo(this.config.yAxis, 'change:key', this.resetOffsetAndSeriesDataForYAxis.bind(this, yAxisId), this); | ||||
|         this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw); | ||||
|         if (this.config.additionalYAxes.length) { | ||||
|             this.config.additionalYAxes.forEach(yAxis => { | ||||
|                 const id = yAxis.get('id'); | ||||
|                 this.offset[id] = {}; | ||||
|                 this.listenTo(yAxis, 'change', this.updateLimitsAndDraw); | ||||
|                 this.listenTo(this.config.yAxis, 'change:key', this.resetOffsetAndSeriesDataForYAxis.bind(this, id), this); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         this.seriesElements = new WeakMap(); | ||||
|         this.seriesLimits = new WeakMap(); | ||||
|  | ||||
| @@ -111,8 +125,7 @@ export default { | ||||
|  | ||||
|         this.listenTo(this.config.series, 'add', this.onSeriesAdd, this); | ||||
|         this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this); | ||||
|         this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this); | ||||
|         this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw); | ||||
|  | ||||
|         this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw); | ||||
|         this.config.series.forEach(this.onSeriesAdd, this); | ||||
|         this.$emit('chartLoaded'); | ||||
| @@ -224,25 +237,31 @@ export default { | ||||
|             this.limitLines.forEach(line => line.destroy()); | ||||
|             DrawLoader.releaseDrawAPI(this.drawAPI); | ||||
|         }, | ||||
|         clearOffset() { | ||||
|             delete this.offset.x; | ||||
|             delete this.offset.y; | ||||
|             delete this.offset.xVal; | ||||
|             delete this.offset.yVal; | ||||
|             delete this.offset.xKey; | ||||
|             delete this.offset.yKey; | ||||
|             this.lines.forEach(function (line) { | ||||
|         resetOffsetAndSeriesDataForYAxis(yAxisId) { | ||||
|             delete this.offset[yAxisId].x; | ||||
|             delete this.offset[yAxisId].y; | ||||
|             delete this.offset[yAxisId].xVal; | ||||
|             delete this.offset[yAxisId].yVal; | ||||
|             delete this.offset[yAxisId].xKey; | ||||
|             delete this.offset[yAxisId].yKey; | ||||
|  | ||||
|             const lines = this.lines.filter(this.matchByYAxisId.bind(this, yAxisId)); | ||||
|             lines.forEach(function (line) { | ||||
|                 line.reset(); | ||||
|             }); | ||||
|             this.limitLines.forEach(function (line) { | ||||
|             const limitLines = this.limitLines.filter(this.matchByYAxisId.bind(this, yAxisId)); | ||||
|             limitLines.forEach(function (line) { | ||||
|                 line.reset(); | ||||
|             }); | ||||
|             this.pointSets.forEach(function (pointSet) { | ||||
|             const pointSets = this.pointSets.filter(this.matchByYAxisId.bind(this, yAxisId)); | ||||
|             pointSets.forEach(function (pointSet) { | ||||
|                 pointSet.reset(); | ||||
|             }); | ||||
|         }, | ||||
|         setOffset(offsetPoint, index, series) { | ||||
|             if (this.offset.x && this.offset.y) { | ||||
|             const mainYAxisId = this.config.yAxis.get('id'); | ||||
|             const yAxisId = series.get('yAxisId') || mainYAxisId; | ||||
|             if (this.offset[yAxisId].x && this.offset[yAxisId].y) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @@ -251,19 +270,20 @@ export default { | ||||
|                 y: series.getYVal(offsetPoint) | ||||
|             }; | ||||
|  | ||||
|             this.offset.x = function (x) { | ||||
|             this.offset[yAxisId].x = function (x) { | ||||
|                 return x - offsets.x; | ||||
|             }.bind(this); | ||||
|             this.offset.y = function (y) { | ||||
|             this.offset[yAxisId].y = function (y) { | ||||
|                 return y - offsets.y; | ||||
|             }.bind(this); | ||||
|             this.offset.xVal = function (point, pSeries) { | ||||
|                 return this.offset.x(pSeries.getXVal(point)); | ||||
|             this.offset[yAxisId].xVal = function (point, pSeries) { | ||||
|                 return this.offset[yAxisId].x(pSeries.getXVal(point)); | ||||
|             }.bind(this); | ||||
|             this.offset.yVal = function (point, pSeries) { | ||||
|                 return this.offset.y(pSeries.getYVal(point)); | ||||
|             this.offset[yAxisId].yVal = function (point, pSeries) { | ||||
|                 return this.offset[yAxisId].y(pSeries.getYVal(point)); | ||||
|             }.bind(this); | ||||
|         }, | ||||
|  | ||||
|         initializeCanvas(canvas, overlay) { | ||||
|             this.canvas = canvas; | ||||
|             this.overlay = overlay; | ||||
| @@ -311,11 +331,15 @@ export default { | ||||
|             this.clearLimitLines(series); | ||||
|         }, | ||||
|         lineForSeries(series) { | ||||
|             const mainYAxisId = this.config.yAxis.get('id'); | ||||
|             const yAxisId = series.get('yAxisId') || mainYAxisId; | ||||
|             let offset = this.offset[yAxisId]; | ||||
|  | ||||
|             if (series.get('interpolate') === 'linear') { | ||||
|                 return new MCTChartLineLinear( | ||||
|                     series, | ||||
|                     this, | ||||
|                     this.offset | ||||
|                     offset | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
| @@ -323,33 +347,45 @@ export default { | ||||
|                 return new MCTChartLineStepAfter( | ||||
|                     series, | ||||
|                     this, | ||||
|                     this.offset | ||||
|                     offset | ||||
|                 ); | ||||
|             } | ||||
|         }, | ||||
|         limitLineForSeries(series) { | ||||
|             const mainYAxisId = this.config.yAxis.get('id'); | ||||
|             const yAxisId = series.get('yAxisId') || mainYAxisId; | ||||
|             let offset = this.offset[yAxisId]; | ||||
|  | ||||
|             return new MCTChartAlarmLineSet( | ||||
|                 series, | ||||
|                 this, | ||||
|                 this.offset, | ||||
|                 offset, | ||||
|                 this.openmct.time.bounds() | ||||
|             ); | ||||
|         }, | ||||
|         pointSetForSeries(series) { | ||||
|             const mainYAxisId = this.config.yAxis.get('id'); | ||||
|             const yAxisId = series.get('yAxisId') || mainYAxisId; | ||||
|             let offset = this.offset[yAxisId]; | ||||
|  | ||||
|             if (series.get('markers')) { | ||||
|                 return new MCTChartPointSet( | ||||
|                     series, | ||||
|                     this, | ||||
|                     this.offset | ||||
|                     offset | ||||
|                 ); | ||||
|             } | ||||
|         }, | ||||
|         alarmPointSetForSeries(series) { | ||||
|             const mainYAxisId = this.config.yAxis.get('id'); | ||||
|             const yAxisId = series.get('yAxisId') || mainYAxisId; | ||||
|             let offset = this.offset[yAxisId]; | ||||
|  | ||||
|             if (series.get('alarmMarkers')) { | ||||
|                 return new MCTChartAlarmPointSet( | ||||
|                     series, | ||||
|                     this, | ||||
|                     this.offset | ||||
|                     offset | ||||
|                 ); | ||||
|             } | ||||
|         }, | ||||
| @@ -410,8 +446,8 @@ export default { | ||||
|                 this.seriesLimits.delete(series); | ||||
|             } | ||||
|         }, | ||||
|         canDraw() { | ||||
|             if (!this.offset.x || !this.offset.y) { | ||||
|         canDraw(yAxisId) { | ||||
|             if (!this.offset[yAxisId] || !this.offset[yAxisId].x || !this.offset[yAxisId].y) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
| @@ -434,16 +470,31 @@ export default { | ||||
|             } | ||||
|  | ||||
|             this.drawAPI.clear(); | ||||
|             if (this.canDraw()) { | ||||
|                 this.updateViewport(); | ||||
|                 this.drawSeries(); | ||||
|                 this.drawRectangles(); | ||||
|                 this.drawHighlights(); | ||||
|             } | ||||
|             const mainYAxisId = this.config.yAxis.get('id'); | ||||
|             //There has to be at least one yAxis | ||||
|             const yAxisIds = [mainYAxisId].concat(this.config.additionalYAxes.map(yAxis => yAxis.get('id'))); | ||||
|             // Repeat drawing for all yAxes | ||||
|             yAxisIds.forEach((id) => { | ||||
|                 if (this.canDraw(id)) { | ||||
|                     this.updateViewport(id); | ||||
|                     this.drawSeries(id); | ||||
|                     this.drawRectangles(id); | ||||
|                     this.drawHighlights(id); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         updateViewport() { | ||||
|         updateViewport(yAxisId) { | ||||
|             const mainYAxisId = this.config.yAxis.get('id'); | ||||
|             const xRange = this.config.xAxis.get('displayRange'); | ||||
|             const yRange = this.config.yAxis.get('displayRange'); | ||||
|             let yRange; | ||||
|             if (yAxisId === mainYAxisId) { | ||||
|                 yRange = this.config.yAxis.get('displayRange'); | ||||
|             } else { | ||||
|                 if (this.config.additionalYAxes.length) { | ||||
|                     const yAxisForId = this.config.additionalYAxes.find(yAxis => yAxis.get('id') === yAxisId); | ||||
|                     yRange = yAxisForId.get('displayRange'); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (!xRange || !yRange) { | ||||
|                 return; | ||||
| @@ -454,9 +505,10 @@ export default { | ||||
|                 yRange.max - yRange.min | ||||
|             ]; | ||||
|  | ||||
|             const origin = [ | ||||
|                 this.offset.x(xRange.min), | ||||
|                 this.offset.y(yRange.min) | ||||
|             let origin; | ||||
|             origin = [ | ||||
|                 this.offset[yAxisId].x(xRange.min), | ||||
|                 this.offset[yAxisId].y(yRange.min) | ||||
|             ]; | ||||
|  | ||||
|             this.drawAPI.setDimensions( | ||||
| @@ -464,38 +516,66 @@ export default { | ||||
|                 origin | ||||
|             ); | ||||
|         }, | ||||
|         drawSeries() { | ||||
|             this.lines.forEach(this.drawLine, this); | ||||
|             this.pointSets.forEach(this.drawPoints, this); | ||||
|             this.alarmSets.forEach(this.drawAlarmPoints, this); | ||||
|         matchByYAxisId(id, item) { | ||||
|             const mainYAxisId = this.config.yAxis.get('id'); | ||||
|             let matchesId = false; | ||||
|  | ||||
|             const series = item.series; | ||||
|             if (series) { | ||||
|                 const seriesYAxisId = series.get('yAxisId') || mainYAxisId; | ||||
|  | ||||
|                 matchesId = seriesYAxisId === id; | ||||
|             } | ||||
|  | ||||
|             return matchesId; | ||||
|         }, | ||||
|         drawSeries(id) { | ||||
|             const lines = this.lines.filter(this.matchByYAxisId.bind(this, id)); | ||||
|             lines.forEach(this.drawLine, this); | ||||
|             const pointSets = this.pointSets.filter(this.matchByYAxisId.bind(this, id)); | ||||
|             pointSets.forEach(this.drawPoints, this); | ||||
|             const alarmSets = this.alarmSets.filter(this.matchByYAxisId.bind(this, id)); | ||||
|             alarmSets.forEach(this.drawAlarmPoints, this); | ||||
|         }, | ||||
|         drawLimitLines() { | ||||
|             if (this.canDraw()) { | ||||
|                 this.updateViewport(); | ||||
|  | ||||
|                 if (!this.drawAPI.origin) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 Array.from(this.$refs.limitArea.children).forEach((el) => el.remove()); | ||||
|                 let limitPointOverlap = []; | ||||
|                 this.limitLines.forEach((limitLine) => { | ||||
|                     let limitContainerEl = this.$refs.limitArea; | ||||
|                     limitLine.limits.forEach((limit) => { | ||||
|                         const showLabels = this.showLabels(limit.seriesKey); | ||||
|                         if (showLabels) { | ||||
|                             const overlap = this.getLimitOverlap(limit, limitPointOverlap); | ||||
|                             limitPointOverlap.push(overlap); | ||||
|                             let limitLabelEl = this.getLimitLabel(limit, overlap); | ||||
|                             limitContainerEl.appendChild(limitLabelEl); | ||||
|                         } | ||||
|  | ||||
|                         let limitEl = this.getLimitElement(limit); | ||||
|                         limitContainerEl.appendChild(limitEl); | ||||
|  | ||||
|                     }, this); | ||||
|                 }); | ||||
|             this.config.series.models.forEach(series => { | ||||
|                 const yAxisId = series.get('yAxisId'); | ||||
|                 this.drawLimitLinesForSeries(yAxisId, series); | ||||
|             }); | ||||
|         }, | ||||
|         drawLimitLinesForSeries(yAxisId, series) { | ||||
|             if (!this.canDraw(yAxisId)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.updateViewport(yAxisId); | ||||
|  | ||||
|             if (!this.drawAPI.origin) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             Array.from(this.$refs.limitArea.children).forEach((el) => el.remove()); | ||||
|             let limitPointOverlap = []; | ||||
|             this.limitLines.forEach((limitLine) => { | ||||
|                 let limitContainerEl = this.$refs.limitArea; | ||||
|                 limitLine.limits.forEach((limit) => { | ||||
|                     if (!series.includes(limit.seriesKey)) { | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     const showLabels = this.showLabels(limit.seriesKey); | ||||
|                     if (showLabels) { | ||||
|                         const overlap = this.getLimitOverlap(limit, limitPointOverlap); | ||||
|                         limitPointOverlap.push(overlap); | ||||
|                         let limitLabelEl = this.getLimitLabel(limit, overlap); | ||||
|                         limitContainerEl.appendChild(limitLabelEl); | ||||
|                     } | ||||
|  | ||||
|                     let limitEl = this.getLimitElement(limit); | ||||
|                     limitContainerEl.appendChild(limitEl); | ||||
|  | ||||
|                 }, this); | ||||
|             }); | ||||
|         }, | ||||
|         showLabels(seriesKey) { | ||||
|             return this.showLimitLineLabels.seriesKey | ||||
| @@ -577,22 +657,25 @@ export default { | ||||
|             ); | ||||
|         }, | ||||
|         drawLine(chartElement, disconnected) { | ||||
|             this.drawAPI.drawLine( | ||||
|                 chartElement.getBuffer(), | ||||
|                 chartElement.color().asRGBAArray(), | ||||
|                 chartElement.count, | ||||
|                 disconnected | ||||
|             ); | ||||
|         }, | ||||
|         drawHighlights() { | ||||
|             if (this.highlights && this.highlights.length) { | ||||
|                 this.highlights.forEach(this.drawHighlight, this); | ||||
|             if (chartElement) { | ||||
|                 this.drawAPI.drawLine( | ||||
|                     chartElement.getBuffer(), | ||||
|                     chartElement.color().asRGBAArray(), | ||||
|                     chartElement.count, | ||||
|                     disconnected | ||||
|                 ); | ||||
|             } | ||||
|         }, | ||||
|         drawHighlight(highlight) { | ||||
|         drawHighlights(yAxisId) { | ||||
|             if (this.highlights && this.highlights.length) { | ||||
|                 const highlights = this.highlights.filter(this.matchByYAxisId.bind(this, yAxisId)); | ||||
|                 highlights.forEach(this.drawHighlight.bind(this, yAxisId), this); | ||||
|             } | ||||
|         }, | ||||
|         drawHighlight(yAxisId, highlight) { | ||||
|             const points = new Float32Array([ | ||||
|                 this.offset.xVal(highlight.point, highlight.series), | ||||
|                 this.offset.yVal(highlight.point, highlight.series) | ||||
|                 this.offset[yAxisId].xVal(highlight.point, highlight.series), | ||||
|                 this.offset[yAxisId].yVal(highlight.point, highlight.series) | ||||
|             ]); | ||||
|  | ||||
|             const color = highlight.series.get('color').asRGBAArray(); | ||||
| @@ -601,20 +684,21 @@ export default { | ||||
|  | ||||
|             this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape); | ||||
|         }, | ||||
|         drawRectangles() { | ||||
|         drawRectangles(yAxisId) { | ||||
|             if (this.rectangles) { | ||||
|                 this.rectangles.forEach(this.drawRectangle, this); | ||||
|                 const rectangles = this.rectangles.filter(this.matchByYAxisId.bind(this, yAxisId)); | ||||
|                 rectangles.forEach(this.drawRectangle.bind(this, yAxisId), this); | ||||
|             } | ||||
|         }, | ||||
|         drawRectangle(rect) { | ||||
|         drawRectangle(yAxisId, rect) { | ||||
|             this.drawAPI.drawSquare( | ||||
|                 [ | ||||
|                     this.offset.x(rect.start.x), | ||||
|                     this.offset.y(rect.start.y) | ||||
|                     this.offset[yAxisId].x(rect.start.x), | ||||
|                     this.offset[yAxisId].y(rect.start.y) | ||||
|                 ], | ||||
|                 [ | ||||
|                     this.offset.x(rect.end.x), | ||||
|                     this.offset.y(rect.end.y) | ||||
|                     this.offset[yAxisId].x(rect.end.x), | ||||
|                     this.offset[yAxisId].y(rect.end.y) | ||||
|                 ], | ||||
|                 rect.color | ||||
|             ); | ||||
|   | ||||
| @@ -27,6 +27,10 @@ import XAxisModel from "./XAxisModel"; | ||||
| import YAxisModel from "./YAxisModel"; | ||||
| import LegendModel from "./LegendModel"; | ||||
|  | ||||
| const MAX_Y_AXES = 3; | ||||
| const MAIN_Y_AXES_ID = 1; | ||||
| const MAX_ADDITIONAL_AXES = MAX_Y_AXES - 1; | ||||
|  | ||||
| /** | ||||
|  * PlotConfiguration model stores the configuration of a plot and some | ||||
|  * limited state.  The indiidual parts of the plot configuration model | ||||
| @@ -58,8 +62,35 @@ export default class PlotConfigurationModel extends Model { | ||||
|         this.yAxis = new YAxisModel({ | ||||
|             model: options.model.yAxis, | ||||
|             plot: this, | ||||
|             openmct: options.openmct | ||||
|             openmct: options.openmct, | ||||
|             id: options.model.yAxis.id || MAIN_Y_AXES_ID | ||||
|         }); | ||||
|         //Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis | ||||
|         //Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES | ||||
|         this.additionalYAxes = []; | ||||
|         if (Array.isArray(options.model.additionalYAxes)) { | ||||
|             const maxLength = Math.min(MAX_ADDITIONAL_AXES, options.model.additionalYAxes.length); | ||||
|             for (let yAxisCount = 0; yAxisCount < maxLength; yAxisCount++) { | ||||
|                 const yAxis = options.model.additionalYAxes[yAxisCount]; | ||||
|                 this.additionalYAxes.push(new YAxisModel({ | ||||
|                     model: yAxis, | ||||
|                     plot: this, | ||||
|                     openmct: options.openmct, | ||||
|                     id: yAxis.id || (MAIN_Y_AXES_ID + yAxisCount + 1) | ||||
|                 })); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // If the saved options config doesn't include information about all the additional axes, we initialize the remaining here | ||||
|         for (let axesCount = this.additionalYAxes.length; axesCount < MAX_ADDITIONAL_AXES; axesCount++) { | ||||
|             this.additionalYAxes.push(new YAxisModel({ | ||||
|                 plot: this, | ||||
|                 openmct: options.openmct, | ||||
|                 id: MAIN_Y_AXES_ID + axesCount + 1 | ||||
|             })); | ||||
|         } | ||||
|         // end add additional axes | ||||
|  | ||||
|         this.legend = new LegendModel({ | ||||
|             model: options.model.legend, | ||||
|             plot: this, | ||||
| @@ -81,6 +112,9 @@ export default class PlotConfigurationModel extends Model { | ||||
|         } | ||||
|  | ||||
|         this.yAxis.listenToSeriesCollection(this.series); | ||||
|         this.additionalYAxes.forEach(yAxis => { | ||||
|             yAxis.listenToSeriesCollection(this.series); | ||||
|         }); | ||||
|         this.legend.listenToSeriesCollection(this.series); | ||||
|  | ||||
|         this.listenTo(this, 'destroy', this.onDestroy, this); | ||||
| @@ -145,6 +179,7 @@ export default class PlotConfigurationModel extends Model { | ||||
|             domainObject: options.domainObject, | ||||
|             xAxis: {}, | ||||
|             yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}), | ||||
|             additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []), | ||||
|             legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {}) | ||||
|         }; | ||||
|     } | ||||
|   | ||||
| @@ -118,7 +118,8 @@ export default class PlotSeries extends Model { | ||||
|             markerShape: 'point', | ||||
|             markerSize: 2.0, | ||||
|             alarmMarkers: true, | ||||
|             limitLines: false | ||||
|             limitLines: false, | ||||
|             yAxisId: options.model.yAxisId || 1 | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -135,18 +135,44 @@ export default class YAxisModel extends Model { | ||||
|         } | ||||
|     } | ||||
|     resetStats() { | ||||
|         //TODO: do we need the series id here? | ||||
|         this.unset('stats'); | ||||
|         this.seriesCollection.forEach(series => { | ||||
|         this.getSeriesForYAxis(this.seriesCollection).forEach(series => { | ||||
|             if (series.has('stats')) { | ||||
|                 this.updateStats(series.get('stats')); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     getSeriesForYAxis(seriesCollection) { | ||||
|         return seriesCollection.filter(series => { | ||||
|             const seriesYAxisId = series.get('yAxisId') || 1; | ||||
|  | ||||
|             return seriesYAxisId === this.id; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getYAxisForId(id) { | ||||
|         const plotModel = this.plot.get('domainObject'); | ||||
|         let yAxis; | ||||
|         if (this.id === 1) { | ||||
|             yAxis = plotModel.configuration?.yAxis; | ||||
|         } else { | ||||
|             if (plotModel.configuration?.additionalYAxes) { | ||||
|                 yAxis = plotModel.configuration.additionalYAxes.find(additionalYAxis => additionalYAxis.id === id); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return yAxis; | ||||
|     } | ||||
|     /** | ||||
|      * @param {import('./PlotSeries').default} series | ||||
|      */ | ||||
|     trackSeries(series) { | ||||
|         this.listenTo(series, 'change:stats', seriesStats => { | ||||
|             if (series.get('yAxisId') !== this.id) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (!seriesStats) { | ||||
|                 this.resetStats(); | ||||
|             } else { | ||||
| @@ -154,6 +180,10 @@ export default class YAxisModel extends Model { | ||||
|             } | ||||
|         }); | ||||
|         this.listenTo(series, 'change:yKey', () => { | ||||
|             if (series.get('yAxisId') !== this.id) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.updateFromSeries(this.seriesCollection); | ||||
|         }); | ||||
|     } | ||||
| @@ -252,14 +282,40 @@ export default class YAxisModel extends Model { | ||||
|         // Update the series collection labels and formatting | ||||
|         this.updateFromSeries(this.seriesCollection); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * For a given series collection, get the metadata of the current yKey for each series. | ||||
|      * Then return first available value of the given property from the metadata. | ||||
|      * @param {import('./SeriesCollection').default} series | ||||
|      * @param {String} property | ||||
|      */ | ||||
|     getMetadataValueByProperty(series, property) { | ||||
|         return series.map(s => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : '')) | ||||
|             .reduce((a, b) => { | ||||
|                 if (a === undefined) { | ||||
|                     return b; | ||||
|                 } | ||||
|  | ||||
|                 if (a === b) { | ||||
|                     return a; | ||||
|                 } | ||||
|  | ||||
|                 return ''; | ||||
|             }, undefined); | ||||
|     } | ||||
|     /** | ||||
|      * Update yAxis format, values, and label from known series. | ||||
|      * @param {import('./SeriesCollection').default} seriesCollection | ||||
|      */ | ||||
|     updateFromSeries(seriesCollection) { | ||||
|         const plotModel = this.plot.get('domainObject'); | ||||
|         const label = plotModel.configuration?.yAxis?.label; | ||||
|         const sampleSeries = seriesCollection.first(); | ||||
|         const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection); | ||||
|         if (!seriesForThisYAxis.length) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const yAxis = this.getYAxisForId(this.id); | ||||
|         const label = yAxis?.label; | ||||
|         const sampleSeries = seriesForThisYAxis[0]; | ||||
|         if (!sampleSeries || !sampleSeries.metadata) { | ||||
|             if (!label) { | ||||
|                 this.unset('label'); | ||||
| @@ -279,41 +335,17 @@ export default class YAxisModel extends Model { | ||||
|         } | ||||
|  | ||||
|         this.set('values', yMetadata.values); | ||||
|  | ||||
|         if (!label) { | ||||
|             const labelName = seriesCollection | ||||
|                 .map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : '')) | ||||
|                 .reduce((a, b) => { | ||||
|                     if (a === undefined) { | ||||
|                         return b; | ||||
|                     } | ||||
|  | ||||
|                     if (a === b) { | ||||
|                         return a; | ||||
|                     } | ||||
|  | ||||
|                     return ''; | ||||
|                 }, undefined); | ||||
|  | ||||
|             const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name'); | ||||
|             if (labelName) { | ||||
|                 this.set('label', labelName); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const labelUnits = seriesCollection | ||||
|                 .map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : '')) | ||||
|                 .reduce((a, b) => { | ||||
|                     if (a === undefined) { | ||||
|                         return b; | ||||
|                     } | ||||
|  | ||||
|                     if (a === b) { | ||||
|                         return a; | ||||
|                     } | ||||
|  | ||||
|                     return ''; | ||||
|                 }, undefined); | ||||
|  | ||||
|             //if the name is not available, set the units as the label | ||||
|             const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units'); | ||||
|             if (labelUnits) { | ||||
|                 this.set('label', labelUnits); | ||||
|  | ||||
| @@ -331,7 +363,8 @@ export default class YAxisModel extends Model { | ||||
|             frozen: false, | ||||
|             autoscale: true, | ||||
|             logMode: options.model?.logMode ?? false, | ||||
|             autoscalePadding: 0.1 | ||||
|             autoscalePadding: 0.1, | ||||
|             id: options.id | ||||
|  | ||||
|             // 'range' is not specified here, it is undefined at first. When the | ||||
|             // user turns off autoscale, the current 'displayRange' is used for | ||||
|   | ||||
| @@ -36,20 +36,21 @@ | ||||
|         /> | ||||
|     </ul> | ||||
|     <div | ||||
|         v-if="plotSeries.length" | ||||
|         v-if="plotSeries.length && !isStackedPlotObject" | ||||
|         class="grid-properties" | ||||
|     > | ||||
|         <ul | ||||
|             v-if="!isStackedPlotObject" | ||||
|             v-for="(yAxis, index) in yAxesWithSeries" | ||||
|             :key="`yAxis-${index}`" | ||||
|             class="l-inspector-part js-yaxis-properties" | ||||
|         > | ||||
|             <h2 title="Y axis settings for this object">Y Axis</h2> | ||||
|             <h2 title="Y axis settings for this object">Y Axis {{ yAxis.id }}</h2> | ||||
|             <li class="grid-row"> | ||||
|                 <div | ||||
|                     class="grid-cell label" | ||||
|                     title="Manually override how the Y axis is labeled." | ||||
|                 >Label</div> | ||||
|                 <div class="grid-cell value">{{ label ? label : "Not defined" }}</div> | ||||
|                 <div class="grid-cell value">{{ yAxis.label ? yAxis.label : "Not defined" }}</div> | ||||
|             </li> | ||||
|             <li class="grid-row"> | ||||
|                 <div | ||||
| @@ -57,7 +58,7 @@ | ||||
|                     title="Enable log mode." | ||||
|                 >Log mode</div> | ||||
|                 <div class="grid-cell value"> | ||||
|                     {{ logMode ? "Enabled" : "Disabled" }} | ||||
|                     {{ yAxis.logMode ? "Enabled" : "Disabled" }} | ||||
|                 </div> | ||||
|             </li> | ||||
|             <li class="grid-row"> | ||||
| @@ -66,32 +67,36 @@ | ||||
|                     title="Automatically scale the Y axis to keep all values in view." | ||||
|                 >Auto scale</div> | ||||
|                 <div class="grid-cell value"> | ||||
|                     {{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }} | ||||
|                     {{ yAxis.autoscale ? "Enabled: " + yAxis.autoscalePadding : "Disabled" }} | ||||
|                 </div> | ||||
|             </li> | ||||
|             <li | ||||
|                 v-if="!autoscale && rangeMin" | ||||
|                 v-if="!yAxis.autoscale && yAxis.rangeMin" | ||||
|                 class="grid-row" | ||||
|             > | ||||
|                 <div | ||||
|                     class="grid-cell label" | ||||
|                     title="Minimum Y axis value." | ||||
|                 >Minimum value</div> | ||||
|                 <div class="grid-cell value">{{ rangeMin }}</div> | ||||
|                 <div class="grid-cell value">{{ yAxis.rangeMin }}</div> | ||||
|             </li> | ||||
|             <li | ||||
|                 v-if="!autoscale && rangeMax" | ||||
|                 v-if="!yAxis.autoscale && yAxis.rangeMax" | ||||
|                 class="grid-row" | ||||
|             > | ||||
|                 <div | ||||
|                     class="grid-cell label" | ||||
|                     title="Maximum Y axis value." | ||||
|                 >Maximum value</div> | ||||
|                 <div class="grid-cell value">{{ rangeMax }}</div> | ||||
|                 <div class="grid-cell value">{{ yAxis.rangeMax }}</div> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
|     <div | ||||
|         v-if="plotSeries.length && (isStackedPlotObject || !isNestedWithinAStackedPlot)" | ||||
|         class="grid-properties" | ||||
|     > | ||||
|         <ul | ||||
|             v-if="isStackedPlotObject || !isNestedWithinAStackedPlot" | ||||
|             class="l-inspector-part js-legend-properties" | ||||
|         > | ||||
|             <h2 title="Legend settings for this object">Legend</h2> | ||||
| @@ -157,12 +162,6 @@ export default { | ||||
|     data() { | ||||
|         return { | ||||
|             config: {}, | ||||
|             label: '', | ||||
|             autoscale: '', | ||||
|             logMode: false, | ||||
|             autoscalePadding: '', | ||||
|             rangeMin: '', | ||||
|             rangeMax: '', | ||||
|             position: '', | ||||
|             hideLegendWhenSmall: '', | ||||
|             expandByDefault: '', | ||||
| @@ -173,7 +172,8 @@ export default { | ||||
|             showMaximumWhenExpanded: '', | ||||
|             showUnitsWhenExpanded: '', | ||||
|             loaded: false, | ||||
|             plotSeries: [] | ||||
|             plotSeries: [], | ||||
|             yAxes: [] | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -182,13 +182,18 @@ export default { | ||||
|         }, | ||||
|         isStackedPlotObject() { | ||||
|             return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked'); | ||||
|         }, | ||||
|         yAxesWithSeries() { | ||||
|             return this.yAxes.filter(yAxis => yAxis.seriesCount > 0); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         eventHelpers.extend(this); | ||||
|         this.config = this.getConfig(); | ||||
|         this.initYAxesConfiguration(); | ||||
|  | ||||
|         this.registerListeners(); | ||||
|         this.initConfiguration(); | ||||
|         this.initLegendConfiguration(); | ||||
|         this.loaded = true; | ||||
|  | ||||
|     }, | ||||
| @@ -196,18 +201,38 @@ export default { | ||||
|         this.stopListening(); | ||||
|     }, | ||||
|     methods: { | ||||
|         initConfiguration() { | ||||
|         initYAxesConfiguration() { | ||||
|             if (this.config) { | ||||
|                 this.label = this.config.yAxis.get('label'); | ||||
|                 this.autoscale = this.config.yAxis.get('autoscale'); | ||||
|                 this.logMode = this.config.yAxis.get('logMode'); | ||||
|                 this.autoscalePadding = this.config.yAxis.get('autoscalePadding'); | ||||
|                 const range = this.config.yAxis.get('range'); | ||||
|                 if (range) { | ||||
|                     this.rangeMin = range.min; | ||||
|                     this.rangeMax = range.max; | ||||
|                 } | ||||
|                 let range = this.config.yAxis.get('range'); | ||||
|  | ||||
|                 this.yAxes.push({ | ||||
|                     id: this.config.yAxis.id, | ||||
|                     seriesCount: 0, | ||||
|                     label: this.config.yAxis.get('label'), | ||||
|                     autoscale: this.config.yAxis.get('autoscale'), | ||||
|                     logMode: this.config.yAxis.get('logMode'), | ||||
|                     autoscalePadding: this.config.yAxis.get('autoscalePadding'), | ||||
|                     rangeMin: range ? range.min : '', | ||||
|                     rangeMax: range ? range.max : '' | ||||
|                 }); | ||||
|                 this.config.additionalYAxes.forEach(yAxis => { | ||||
|                     range = yAxis.get('range'); | ||||
|  | ||||
|                     this.yAxes.push({ | ||||
|                         id: yAxis.id, | ||||
|                         seriesCount: 0, | ||||
|                         label: yAxis.get('label'), | ||||
|                         autoscale: yAxis.get('autoscale'), | ||||
|                         logMode: yAxis.get('logMode'), | ||||
|                         autoscalePadding: yAxis.get('autoscalePadding'), | ||||
|                         rangeMin: range ? range.min : '', | ||||
|                         rangeMax: range ? range.max : '' | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         initLegendConfiguration() { | ||||
|             if (this.config) { | ||||
|                 this.position = this.config.legend.get('position'); | ||||
|                 this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall'); | ||||
|                 this.expandByDefault = this.config.legend.get('expandByDefault'); | ||||
| @@ -229,18 +254,44 @@ export default { | ||||
|                 this.config.series.forEach(this.addSeries, this); | ||||
|  | ||||
|                 this.listenTo(this.config.series, 'add', this.addSeries, this); | ||||
|                 this.listenTo(this.config.series, 'remove', this.resetAllSeries, this); | ||||
|                 this.listenTo(this.config.series, 'remove', this.removeSeries, this); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         setYAxisLabel(yAxisId) { | ||||
|             const found = this.yAxes.find(yAxis => yAxis.id === yAxisId); | ||||
|             if (found && found.seriesCount > 0) { | ||||
|                 const mainYAxisId = this.config.yAxis.id; | ||||
|                 if (mainYAxisId === yAxisId) { | ||||
|                     found.label = this.config.yAxis.get('label'); | ||||
|                 } else { | ||||
|                     const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId); | ||||
|                     if (additionalYAxis) { | ||||
|                         found.label = additionalYAxis.get('label'); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         addSeries(series, index) { | ||||
|             const yAxisId = series.get('yAxisId'); | ||||
|             this.updateAxisUsageCount(yAxisId, 1); | ||||
|             this.$set(this.plotSeries, index, series); | ||||
|             this.initConfiguration(); | ||||
|             this.setYAxisLabel(yAxisId); | ||||
|         }, | ||||
|  | ||||
|         resetAllSeries() { | ||||
|             this.plotSeries = []; | ||||
|             this.config.series.forEach(this.addSeries, this); | ||||
|         removeSeries(plotSeries, index) { | ||||
|             const yAxisId = plotSeries.get('yAxisId'); | ||||
|             this.updateAxisUsageCount(yAxisId, -1); | ||||
|             this.plotSeries.splice(index, 1); | ||||
|             this.setYAxisLabel(yAxisId); | ||||
|         }, | ||||
|  | ||||
|         updateAxisUsageCount(yAxisId, updateCount) { | ||||
|             const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId); | ||||
|             if (foundYAxis) { | ||||
|                 foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -40,8 +40,10 @@ | ||||
|         </li> | ||||
|     </ul> | ||||
|     <y-axis-form | ||||
|         v-if="plotSeries.length && !isStackedPlotObject" | ||||
|         class="grid-properties" | ||||
|         v-for="(yAxisId, index) in yAxesIds" | ||||
|         :id="yAxisId.id" | ||||
|         :key="`yAxis-${index}`" | ||||
|         class="grid-properties js-yaxis-grid-properties" | ||||
|         :y-axis="config.yAxis" | ||||
|         @seriesUpdated="updateSeriesConfigForObject" | ||||
|     /> | ||||
| @@ -76,6 +78,7 @@ export default { | ||||
|     data() { | ||||
|         return { | ||||
|             config: {}, | ||||
|             yAxes: [], | ||||
|             plotSeries: [], | ||||
|             loaded: false | ||||
|         }; | ||||
| @@ -86,11 +89,27 @@ export default { | ||||
|         }, | ||||
|         isStackedPlotObject() { | ||||
|             return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked'); | ||||
|         }, | ||||
|         yAxesIds() { | ||||
|             return !this.isStackedPlotObject && this.yAxes.filter(yAxis => yAxis.seriesCount > 0); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         eventHelpers.extend(this); | ||||
|         this.config = this.getConfig(); | ||||
|         this.yAxes = [{ | ||||
|             id: this.config.yAxis.id, | ||||
|             seriesCount: 0 | ||||
|         }]; | ||||
|         if (this.config.additionalYAxes) { | ||||
|             this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => { | ||||
|                 return { | ||||
|                     id: yAxis.id, | ||||
|                     seriesCount: 0 | ||||
|                 }; | ||||
|             })); | ||||
|         } | ||||
|  | ||||
|         this.registerListeners(); | ||||
|         this.loaded = true; | ||||
|     }, | ||||
| @@ -107,16 +126,47 @@ export default { | ||||
|             this.config.series.forEach(this.addSeries, this); | ||||
|  | ||||
|             this.listenTo(this.config.series, 'add', this.addSeries, this); | ||||
|             this.listenTo(this.config.series, 'remove', this.resetAllSeries, this); | ||||
|             this.listenTo(this.config.series, 'remove', this.removeSeries, this); | ||||
|         }, | ||||
|  | ||||
|         findYAxisForId(yAxisId) { | ||||
|             return this.yAxes.find(yAxis => yAxis.id === yAxisId); | ||||
|         }, | ||||
|  | ||||
|         setYAxisLabel(yAxisId) { | ||||
|             const found = this.findYAxisForId(yAxisId); | ||||
|             if (found && found.seriesCount > 0) { | ||||
|                 const mainYAxisId = this.config.yAxis.id; | ||||
|                 if (mainYAxisId === yAxisId) { | ||||
|                     found.label = this.config.yAxis.get('label'); | ||||
|                 } else { | ||||
|                     const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId); | ||||
|                     if (additionalYAxis) { | ||||
|                         found.label = additionalYAxis.get('label'); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         addSeries(series, index) { | ||||
|             const yAxisId = series.get('yAxisId'); | ||||
|             this.updateAxisUsageCount(yAxisId, 1); | ||||
|             this.$set(this.plotSeries, index, series); | ||||
|             this.setYAxisLabel(yAxisId); | ||||
|         }, | ||||
|  | ||||
|         resetAllSeries() { | ||||
|             this.plotSeries = []; | ||||
|             this.config.series.forEach(this.addSeries, this); | ||||
|         removeSeries(series, index) { | ||||
|             const yAxisId = series.get('yAxisId'); | ||||
|             this.updateAxisUsageCount(yAxisId, -1); | ||||
|             this.plotSeries.splice(index, 1); | ||||
|             this.setYAxisLabel(yAxisId); | ||||
|         }, | ||||
|  | ||||
|         updateAxisUsageCount(yAxisId, updateCount) { | ||||
|             const foundYAxis = this.findYAxisForId(yAxisId); | ||||
|             if (foundYAxis) { | ||||
|                 foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         updateSeriesConfigForObject(config) { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div> | ||||
| <div v-if="loaded"> | ||||
|     <ul class="l-inspector-part"> | ||||
|         <h2>Y Axis</h2> | ||||
|         <h2>Y Axis {{ id }}</h2> | ||||
|         <li class="grid-row"> | ||||
|             <div | ||||
|                 class="grid-cell label" | ||||
| @@ -25,6 +25,7 @@ | ||||
|                 <!-- eslint-disable-next-line vue/html-self-closing --> | ||||
|                 <input | ||||
|                     v-model="logMode" | ||||
|                     class="js-log-mode-input" | ||||
|                     type="checkbox" | ||||
|                     @change="updateForm('logMode')" | ||||
|                 /> | ||||
| @@ -103,52 +104,72 @@ | ||||
| <script> | ||||
| import { objectPath } from "./formUtil"; | ||||
| import _ from "lodash"; | ||||
| import eventHelpers from "../../lib/eventHelpers"; | ||||
| import configStore from "../../configuration/ConfigStore"; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         yAxis: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         id: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             yAxis: null, | ||||
|             label: '', | ||||
|             autoscale: '', | ||||
|             logMode: false, | ||||
|             autoscalePadding: '', | ||||
|             rangeMin: '', | ||||
|             rangeMax: '', | ||||
|             validationErrors: {} | ||||
|             validationErrors: {}, | ||||
|             loaded: false | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.initialize(); | ||||
|         eventHelpers.extend(this); | ||||
|         this.getConfig(); | ||||
|         this.loaded = true; | ||||
|         this.initFields(); | ||||
|         this.initFormValues(); | ||||
|     }, | ||||
|     methods: { | ||||
|         initialize: function () { | ||||
|         getConfig() { | ||||
|             const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             const config = configStore.get(configId); | ||||
|             if (config) { | ||||
|                 const mainYAxisId = config.yAxis.id; | ||||
|                 this.isAdditionalYAxis = this.id !== mainYAxisId; | ||||
|                 if (this.isAdditionalYAxis) { | ||||
|                     this.additionalYAxes = config.additionalYAxes; | ||||
|                     this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id); | ||||
|                 } else { | ||||
|                     this.yAxis = config.yAxis; | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         initFields() { | ||||
|             const prefix = `configuration.${this.getPrefix()}`; | ||||
|             this.fields = { | ||||
|                 label: { | ||||
|                     objectPath: 'configuration.yAxis.label' | ||||
|                     objectPath: `${prefix}.label` | ||||
|                 }, | ||||
|                 autoscale: { | ||||
|                     coerce: Boolean, | ||||
|                     objectPath: 'configuration.yAxis.autoscale' | ||||
|                     objectPath: `${prefix}.autoscale` | ||||
|                 }, | ||||
|                 autoscalePadding: { | ||||
|                     coerce: Number, | ||||
|                     objectPath: 'configuration.yAxis.autoscalePadding' | ||||
|                     objectPath: `${prefix}.autoscalePadding` | ||||
|                 }, | ||||
|                 logMode: { | ||||
|                     coerce: Boolean, | ||||
|                     objectPath: 'configuration.yAxis.logMode' | ||||
|                     objectPath: `${prefix}.logMode` | ||||
|                 }, | ||||
|                 range: { | ||||
|                     objectPath: 'configuration.yAxis.range', | ||||
|                     objectPath: `${prefix}.range'`, | ||||
|                     coerce: function coerceRange(range) { | ||||
|                         const newRange = { | ||||
|                             min: -1, | ||||
| @@ -202,6 +223,25 @@ export default { | ||||
|             this.rangeMin = range?.min; | ||||
|             this.rangeMax = range?.max; | ||||
|         }, | ||||
|         getPrefix() { | ||||
|             let prefix = 'yAxis'; | ||||
|             if (this.isAdditionalYAxis) { | ||||
|                 let index = -1; | ||||
|                 if (this.additionalYAxes) { | ||||
|                     index = this.additionalYAxes.findIndex((yAxis) => { | ||||
|                         return yAxis.id === this.id; | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 if (index < 0) { | ||||
|                     index = 0; | ||||
|                 } | ||||
|  | ||||
|                 prefix = `additionalYAxes[${index}]`; | ||||
|             } | ||||
|  | ||||
|             return prefix; | ||||
|         }, | ||||
|         updateForm(formKey) { | ||||
|             let newVal; | ||||
|             if (formKey === 'range') { | ||||
| @@ -231,18 +271,42 @@ export default { | ||||
|                 this.yAxis.set(formKey, newVal); | ||||
|                 // Then we mutate the domain object configuration to persist the settings | ||||
|                 if (path) { | ||||
|                     if (!this.domainObject.configuration || !this.domainObject.configuration.series) { | ||||
|                         this.$emit('seriesUpdated', { | ||||
|                             identifier: this.domainObject.identifier, | ||||
|                             path: `yAxis.${formKey}`, | ||||
|                             value: newVal | ||||
|                         }); | ||||
|                     if (this.isAdditionalYAxis) { | ||||
|                         if (this.domainObject.configuration && this.domainObject.configuration.series) { | ||||
|                             //update the id | ||||
|                             this.openmct.objects.mutate( | ||||
|                                 this.domainObject, | ||||
|                                 `configuration.${this.getPrefix()}.id`, | ||||
|                                 this.id | ||||
|                             ); | ||||
|                             //update the yAxes values | ||||
|                             this.openmct.objects.mutate( | ||||
|                                 this.domainObject, | ||||
|                                 path(this.domainObject, this.yAxis), | ||||
|                                 newVal | ||||
|                             ); | ||||
|                         } else { | ||||
|                             this.$emit('seriesUpdated', { | ||||
|                                 identifier: this.domainObject.identifier, | ||||
|                                 path: `${this.getPrefix()}.${formKey}`, | ||||
|                                 id: this.id, | ||||
|                                 value: newVal | ||||
|                             }); | ||||
|                         } | ||||
|                     } else { | ||||
|                         this.openmct.objects.mutate( | ||||
|                             this.domainObject, | ||||
|                             path(this.domainObject, this.yAxis), | ||||
|                             newVal | ||||
|                         ); | ||||
|                         if (this.domainObject.configuration && this.domainObject.configuration.series) { | ||||
|                             this.openmct.objects.mutate( | ||||
|                                 this.domainObject, | ||||
|                                 path(this.domainObject, this.yAxis), | ||||
|                                 newVal | ||||
|                             ); | ||||
|                         } else { | ||||
|                             this.$emit('seriesUpdated', { | ||||
|                                 identifier: this.domainObject.identifier, | ||||
|                                 path: `${this.getPrefix()}.${formKey}`, | ||||
|                                 value: newVal | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
							
								
								
									
										504
									
								
								src/plugins/plot/overlayPlot/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										504
									
								
								src/plugins/plot/overlayPlot/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,504 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing"; | ||||
| import PlotVuePlugin from "../plugin"; | ||||
| import Vue from "vue"; | ||||
| import Plot from "../Plot.vue"; | ||||
| import configStore from "../configuration/ConfigStore"; | ||||
| import EventEmitter from "EventEmitter"; | ||||
| import PlotOptions from "../inspector/PlotOptions.vue"; | ||||
|  | ||||
| describe("the plugin", function () { | ||||
|     let element; | ||||
|     let child; | ||||
|     let openmct; | ||||
|     let telemetryPromise; | ||||
|     let telemetryPromiseResolve; | ||||
|     let mockObjectPath; | ||||
|     let overlayPlotObject = { | ||||
|         identifier: { | ||||
|             namespace: "", | ||||
|             key: "test-plot" | ||||
|         }, | ||||
|         type: "telemetry.plot.overlay", | ||||
|         name: "Test Overlay Plot", | ||||
|         composition: [], | ||||
|         configuration: { | ||||
|             series: [] | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         mockObjectPath = [ | ||||
|             { | ||||
|                 name: 'mock folder', | ||||
|                 type: 'fake-folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 name: 'mock parent folder', | ||||
|                 type: 'time-strip', | ||||
|                 identifier: { | ||||
|                     key: 'mock-parent-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         const testTelemetry = [ | ||||
|             { | ||||
|                 'utc': 1, | ||||
|                 'some-key': 'some-value 1', | ||||
|                 'some-other-key': 'some-other-value 1', | ||||
|                 'some-key2': 'some-value2 1', | ||||
|                 'some-other-key2': 'some-other-value2 1' | ||||
|             }, | ||||
|             { | ||||
|                 'utc': 2, | ||||
|                 'some-key': 'some-value 2', | ||||
|                 'some-other-key': 'some-other-value 2', | ||||
|                 'some-key2': 'some-value2 2', | ||||
|                 'some-other-key2': 'some-other-value2 2' | ||||
|             }, | ||||
|             { | ||||
|                 'utc': 3, | ||||
|                 'some-key': 'some-value 3', | ||||
|                 'some-other-key': 'some-other-value 3', | ||||
|                 'some-key2': 'some-value2 2', | ||||
|                 'some-other-key2': 'some-other-value2 2' | ||||
|             } | ||||
|         ]; | ||||
|  | ||||
|         const timeSystem = { | ||||
|             timeSystemKey: 'utc', | ||||
|             bounds: { | ||||
|                 start: 0, | ||||
|                 end: 4 | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct = createOpenMct(timeSystem); | ||||
|  | ||||
|         telemetryPromise = new Promise((resolve) => { | ||||
|             telemetryPromiseResolve = resolve; | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.callFake(() => { | ||||
|             telemetryPromiseResolve(testTelemetry); | ||||
|  | ||||
|             return telemetryPromise; | ||||
|         }); | ||||
|  | ||||
|         openmct.install(new PlotVuePlugin()); | ||||
|  | ||||
|         element = document.createElement("div"); | ||||
|         element.style.width = "640px"; | ||||
|         element.style.height = "480px"; | ||||
|         child = document.createElement("div"); | ||||
|         child.style.width = "640px"; | ||||
|         child.style.height = "480px"; | ||||
|         element.appendChild(child); | ||||
|         document.body.appendChild(element); | ||||
|  | ||||
|         spyOn(window, 'ResizeObserver').and.returnValue({ | ||||
|             observe() {}, | ||||
|             unobserve() {}, | ||||
|             disconnect() {} | ||||
|         }); | ||||
|  | ||||
|         openmct.types.addType("test-object", { | ||||
|             creatable: true | ||||
|         }); | ||||
|  | ||||
|         spyOnBuiltins(["requestAnimationFrame"]); | ||||
|         window.requestAnimationFrame.and.callFake((callBack) => { | ||||
|             callBack(); | ||||
|         }); | ||||
|  | ||||
|         openmct.router.path = [overlayPlotObject]; | ||||
|         openmct.on("start", done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|  | ||||
|     afterEach((done) => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|         configStore.deleteAll(); | ||||
|         resetApplicationState(openmct).then(done).catch(done); | ||||
|     }); | ||||
|  | ||||
|     afterAll(() => { | ||||
|         openmct.router.path = null; | ||||
|     }); | ||||
|  | ||||
|     describe("the plot views", () => { | ||||
|         it("provides an overlay plot view for objects with telemetry", () => { | ||||
|             const testTelemetryObject = { | ||||
|                 id: "test-object", | ||||
|                 type: "telemetry.plot.overlay", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "some-key" | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); | ||||
|             let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay"); | ||||
|             expect(plotView).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     describe("The overlay plot view with multiple axes", () => { | ||||
|         let testTelemetryObject; | ||||
|         let testTelemetryObject2; | ||||
|         let config; | ||||
|         let component; | ||||
|         let mockComposition; | ||||
|  | ||||
|         afterAll(() => { | ||||
|             component.$destroy(); | ||||
|             openmct.router.path = null; | ||||
|         }); | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             testTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 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 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             testTelemetryObject2 = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object2" | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object2", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "utc", | ||||
|                         format: "utc", | ||||
|                         name: "Time", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-key2", | ||||
|                         name: "Some attribute2", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key2", | ||||
|                         name: "Another attribute2", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|             overlayPlotObject.composition = [ | ||||
|                 { | ||||
|                     identifier: testTelemetryObject.identifier | ||||
|                 }, | ||||
|                 { | ||||
|                     identifier: testTelemetryObject2.identifier | ||||
|                 } | ||||
|             ]; | ||||
|             overlayPlotObject.configuration.series = [ | ||||
|                 { | ||||
|                     identifier: testTelemetryObject.identifier, | ||||
|                     yAxisId: 1 | ||||
|                 }, | ||||
|                 { | ||||
|                     identifier: testTelemetryObject2.identifier, | ||||
|                     yAxisId: 3 | ||||
|                 } | ||||
|             ]; | ||||
|             overlayPlotObject.configuration.additionalYAxes = [ | ||||
|                 { | ||||
|                     label: 'Test Object Label', | ||||
|                     id: 2 | ||||
|                 }, | ||||
|                 { | ||||
|                     label: 'Test Object 2 Label', | ||||
|                     id: 3 | ||||
|                 } | ||||
|             ]; | ||||
|             mockComposition = new EventEmitter(); | ||||
|             mockComposition.load = () => { | ||||
|                 mockComposition.emit('add', testTelemetryObject); | ||||
|                 mockComposition.emit('add', testTelemetryObject2); | ||||
|  | ||||
|                 return [testTelemetryObject, testTelemetryObject2]; | ||||
|             }; | ||||
|  | ||||
|             spyOn(openmct.composition, 'get').and.returnValue(mockComposition); | ||||
|  | ||||
|             let viewContainer = document.createElement("div"); | ||||
|             child.append(viewContainer); | ||||
|             component = new Vue({ | ||||
|                 el: viewContainer, | ||||
|                 components: { | ||||
|                     Plot | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct: openmct, | ||||
|                     domainObject: overlayPlotObject, | ||||
|                     composition: openmct.composition.get(overlayPlotObject), | ||||
|                     path: [overlayPlotObject] | ||||
|                 }, | ||||
|                 template: '<plot ref="plotComponent"></plot>' | ||||
|             }); | ||||
|  | ||||
|             return telemetryPromise | ||||
|                 .then(Vue.nextTick()) | ||||
|                 .then(() => { | ||||
|                     const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); | ||||
|                     config = configStore.get(configId); | ||||
|                 }); | ||||
|         }); | ||||
|  | ||||
|         it("Renders multiple Y-axis for the telemetry objects", (done) => { | ||||
|             config.yAxis.set('displayRange', { | ||||
|                 min: 10, | ||||
|                 max: 20 | ||||
|             }); | ||||
|             Vue.nextTick(() => { | ||||
|                 let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper"); | ||||
|                 expect(yAxisElement.length).toBe(2); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('the inspector view', () => { | ||||
|             let inspectorComponent; | ||||
|             let viewComponentObject; | ||||
|             let selection; | ||||
|             beforeEach((done) => { | ||||
|                 selection = [ | ||||
|                     [ | ||||
|                         { | ||||
|                             context: { | ||||
|                                 item: { | ||||
|                                     id: overlayPlotObject.identifier.key, | ||||
|                                     identifier: overlayPlotObject.identifier, | ||||
|                                     type: overlayPlotObject.type, | ||||
|                                     configuration: overlayPlotObject.configuration, | ||||
|                                     composition: overlayPlotObject.composition | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     ] | ||||
|                 ]; | ||||
|  | ||||
|                 let viewContainer = document.createElement('div'); | ||||
|                 child.append(viewContainer); | ||||
|                 inspectorComponent = new Vue({ | ||||
|                     el: viewContainer, | ||||
|                     components: { | ||||
|                         PlotOptions | ||||
|                     }, | ||||
|                     provide: { | ||||
|                         openmct: openmct, | ||||
|                         domainObject: selection[0][0].context.item, | ||||
|                         path: [selection[0][0].context.item] | ||||
|                     }, | ||||
|                     template: '<plot-options/>' | ||||
|                 }); | ||||
|  | ||||
|                 Vue.nextTick(() => { | ||||
|                     viewComponentObject = inspectorComponent.$root.$children[0]; | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             afterEach(() => { | ||||
|                 openmct.router.path = null; | ||||
|             }); | ||||
|  | ||||
|             describe('in edit mode', () => { | ||||
|                 let editOptionsEl; | ||||
|  | ||||
|                 beforeEach((done) => { | ||||
|                     viewComponentObject.setEditState(true); | ||||
|                     Vue.nextTick(() => { | ||||
|                         editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); | ||||
|                         done(); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it('shows multiple yAxis options', () => { | ||||
|                     const yAxisProperties = editOptionsEl.querySelectorAll(".js-yaxis-grid-properties .l-inspector-part h2"); | ||||
|                     expect(yAxisProperties.length).toEqual(2); | ||||
|                 }); | ||||
|  | ||||
|                 it('saves yAxis options', () => { | ||||
|                     //toggle log mode and save | ||||
|                     config.additionalYAxes[1].set('displayRange', { | ||||
|                         min: 10, | ||||
|                         max: 20 | ||||
|                     }); | ||||
|                     const yAxisProperties = editOptionsEl.querySelectorAll(".js-log-mode-input"); | ||||
|                     const clickEvent = createMouseEvent("click"); | ||||
|                     yAxisProperties[1].dispatchEvent(clickEvent); | ||||
|  | ||||
|                     expect(config.additionalYAxes[1].get('logMode')).toEqual(true); | ||||
|  | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     describe("The overlay plot view with single axes", () => { | ||||
|         let testTelemetryObject; | ||||
|         let config; | ||||
|         let component; | ||||
|         let mockComposition; | ||||
|  | ||||
|         afterAll(() => { | ||||
|             component.$destroy(); | ||||
|             openmct.router.path = null; | ||||
|         }); | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             testTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 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 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             overlayPlotObject.composition = [ | ||||
|                 { | ||||
|                     identifier: testTelemetryObject.identifier | ||||
|                 } | ||||
|             ]; | ||||
|             overlayPlotObject.configuration.series = [ | ||||
|                 { | ||||
|                     identifier: testTelemetryObject.identifier | ||||
|                 } | ||||
|             ]; | ||||
|             mockComposition = new EventEmitter(); | ||||
|             mockComposition.load = () => { | ||||
|                 mockComposition.emit('add', testTelemetryObject); | ||||
|  | ||||
|                 return [testTelemetryObject]; | ||||
|             }; | ||||
|  | ||||
|             spyOn(openmct.composition, 'get').and.returnValue(mockComposition); | ||||
|  | ||||
|             let viewContainer = document.createElement("div"); | ||||
|             child.append(viewContainer); | ||||
|             component = new Vue({ | ||||
|                 el: viewContainer, | ||||
|                 components: { | ||||
|                     Plot | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct: openmct, | ||||
|                     domainObject: overlayPlotObject, | ||||
|                     composition: openmct.composition.get(overlayPlotObject), | ||||
|                     path: [overlayPlotObject] | ||||
|                 }, | ||||
|                 template: '<plot ref="plotComponent"></plot>' | ||||
|             }); | ||||
|  | ||||
|             return telemetryPromise | ||||
|                 .then(Vue.nextTick()) | ||||
|                 .then(() => { | ||||
|                     const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); | ||||
|                     config = configStore.get(configId); | ||||
|                 }); | ||||
|         }); | ||||
|  | ||||
|         it("Renders single Y-axis for the telemetry object", (done) => { | ||||
|             config.yAxis.set('displayRange', { | ||||
|                 min: 10, | ||||
|                 max: 20 | ||||
|             }); | ||||
|             Vue.nextTick(() => { | ||||
|                 let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper"); | ||||
|                 expect(yAxisElement.length).toBe(1); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -28,6 +28,8 @@ import EventEmitter from "EventEmitter"; | ||||
| import PlotOptions from "./inspector/PlotOptions.vue"; | ||||
| import PlotConfigurationModel from "./configuration/PlotConfigurationModel"; | ||||
|  | ||||
| const TEST_KEY_ID = 'test-key'; | ||||
|  | ||||
| describe("the plugin", function () { | ||||
|     let element; | ||||
|     let child; | ||||
| @@ -404,6 +406,20 @@ describe("the plugin", function () { | ||||
|             expect(options[1].value).toBe("Another attribute"); | ||||
|         }); | ||||
|  | ||||
|         it("Updates the Y-axis label when changed", () => { | ||||
|             const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); | ||||
|             const config = configStore.get(configId); | ||||
|             const yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y")[0].__vue__; | ||||
|             config.yAxis.seriesCollection.models.forEach((plotSeries) => { | ||||
|                 expect(plotSeries.model.yKey).toBe('some-key'); | ||||
|             }); | ||||
|  | ||||
|             yAxisElement.$emit('yKeyChanged', TEST_KEY_ID, 1); | ||||
|             config.yAxis.seriesCollection.models.forEach((plotSeries) => { | ||||
|                 expect(plotSeries.model.yKey).toBe(TEST_KEY_ID); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('hides the pause and play controls', () => { | ||||
|             let pauseEl = element.querySelectorAll(".c-button-set .icon-pause"); | ||||
|             let playEl = element.querySelectorAll(".c-button-set .icon-arrow-right"); | ||||
|   | ||||
| @@ -593,6 +593,8 @@ mct-plot { | ||||
| .plot-legend-left .gl-plot-legend { margin-right: $interiorMargin; } | ||||
| .plot-legend-right .gl-plot-legend { margin-left: $interiorMargin; } | ||||
|  | ||||
| .gl-plot .plot-yaxis-right.gl-plot-y { margin-left: 100%; } | ||||
|  | ||||
| .gl-plot, | ||||
| .c-plot { | ||||
|     &.plot-legend-collapsed .plot-wrapper-expanded-legend { display: none; } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
|     draggable="true" | ||||
|     @dragstart="emitDragStartEvent" | ||||
|     @dragenter="onDragenter" | ||||
|     @dragover="onDragover" | ||||
|     @dragover.prevent | ||||
|     @dragleave="onDragleave" | ||||
|     @drop="emitDropEvent" | ||||
| > | ||||
| @@ -38,6 +38,7 @@ | ||||
|         }" | ||||
|     > | ||||
|         <span | ||||
|             v-if="showGrippy" | ||||
|             class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag" | ||||
|         ></span> | ||||
|         <object-label | ||||
| @@ -81,6 +82,10 @@ export default { | ||||
|         }, | ||||
|         allowDrop: { | ||||
|             type: Boolean | ||||
|         }, | ||||
|         showGrippy: { | ||||
|             type: Boolean, | ||||
|             default: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
| @@ -93,11 +98,8 @@ export default { | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         onDragover(event) { | ||||
|             event.preventDefault(); | ||||
|         }, | ||||
|         emitDropEvent(event) { | ||||
|             this.$emit('drop-custom', this.index); | ||||
|             this.$emit('drop-custom', event); | ||||
|             this.hover = false; | ||||
|         }, | ||||
|         emitDragStartEvent(event) { | ||||
|   | ||||
							
								
								
									
										101
									
								
								src/ui/inspector/ElementItemGroup.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/ui/inspector/ElementItemGroup.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     class="c-elements-pool__group" | ||||
|     :class="{ | ||||
|         'hover': hover | ||||
|     }" | ||||
|     :allow-drop="allowDrop" | ||||
|     @dragover.prevent | ||||
|     @dragenter="onDragEnter" | ||||
|     @dragleave.stop="onDragLeave" | ||||
|     @drop="emitDrop" | ||||
| > | ||||
|     <ul> | ||||
|         <div> | ||||
|             <span class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"></span> | ||||
|             <div | ||||
|                 class="c-tree__item__type-icon c-object-label__type-icon" | ||||
|             > | ||||
|                 <span | ||||
|                     class="is-status__indicator" | ||||
|                 ></span> | ||||
|             </div> | ||||
|             <div | ||||
|                 class="c-tree__item__name c-object-label__name" | ||||
|                 aria-label="Element Item Group" | ||||
|             > | ||||
|                 {{ label }} | ||||
|             </div> | ||||
|         </div> | ||||
|         <slot></slot> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     props: { | ||||
|         parentObject: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|             default: () => { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         label: { | ||||
|             type: String, | ||||
|             required: true, | ||||
|             default: () => { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         allowDrop: { | ||||
|             type: Boolean | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             dragCounter: 0 | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         hover() { | ||||
|             return this.dragCounter > 0; | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         emitDrop(event) { | ||||
|             this.dragCounter = 0; | ||||
|             this.$emit('drop-group', event); | ||||
|         }, | ||||
|         onDragEnter(event) { | ||||
|             this.dragCounter++; | ||||
|         }, | ||||
|         onDragLeave(event) { | ||||
|             this.dragCounter--; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -65,8 +65,8 @@ import ElementItem from './ElementItem.vue'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         'Search': Search, | ||||
|         'ElementItem': ElementItem | ||||
|         Search, | ||||
|         ElementItem | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     data() { | ||||
|   | ||||
| @@ -56,7 +56,12 @@ | ||||
|                 handle="before" | ||||
|                 label="Elements" | ||||
|             > | ||||
|                 <elements-pool /> | ||||
|                 <plot-elements-pool | ||||
|                     v-if="isOverlayPlot" | ||||
|                 /> | ||||
|                 <elements-pool | ||||
|                     v-else | ||||
|                 /> | ||||
|             </pane> | ||||
|         </multipane> | ||||
|         <multipane | ||||
| @@ -83,6 +88,7 @@ | ||||
| import multipane from '../layout/multipane.vue'; | ||||
| import pane from '../layout/pane.vue'; | ||||
| import ElementsPool from './ElementsPool.vue'; | ||||
| import PlotElementsPool from './PlotElementsPool.vue'; | ||||
| import Location from './Location.vue'; | ||||
| import Properties from './details/Properties.vue'; | ||||
| import ObjectName from './ObjectName.vue'; | ||||
| @@ -99,6 +105,7 @@ export default { | ||||
|         multipane, | ||||
|         pane, | ||||
|         ElementsPool, | ||||
|         PlotElementsPool, | ||||
|         Properties, | ||||
|         ObjectName, | ||||
|         Location, | ||||
| @@ -118,6 +125,7 @@ export default { | ||||
|         return { | ||||
|             hasComposition: false, | ||||
|             showStyles: false, | ||||
|             isOverlayPlot: false, | ||||
|             tabbedViews: [{ | ||||
|                 key: '__properties', | ||||
|                 name: 'Properties' | ||||
| @@ -151,6 +159,7 @@ export default { | ||||
|                 let parentObject = selection[0][0].context.item; | ||||
|  | ||||
|                 this.hasComposition = Boolean(parentObject && this.openmct.composition.get(parentObject)); | ||||
|                 this.isOverlayPlot = selection[0][0].context.item.type === 'telemetry.plot.overlay'; | ||||
|             } | ||||
|         }, | ||||
|         refreshTabs(selection) { | ||||
|   | ||||
							
								
								
									
										330
									
								
								src/ui/inspector/PlotElementsPool.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								src/ui/inspector/PlotElementsPool.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,330 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-elements-pool"> | ||||
|     <Search | ||||
|         class="c-elements-pool__search" | ||||
|         :value="currentSearch" | ||||
|         @input="applySearch" | ||||
|         @clear="applySearch" | ||||
|     /> | ||||
|     <div | ||||
|         class="c-elements-pool__elements" | ||||
|     > | ||||
|         <ul | ||||
|             v-if="hasElements" | ||||
|             id="inspector-elements-tree" | ||||
|             class="c-tree c-elements-pool__tree" | ||||
|         > | ||||
|             <element-item-group | ||||
|                 v-for="(yAxis, index) in yAxes" | ||||
|                 :key="`element-group-yaxis-${yAxis.id}`" | ||||
|                 :parent-object="parentObject" | ||||
|                 :allow-drop="allowDrop" | ||||
|                 :label="`Y Axis ${yAxis.id}`" | ||||
|                 @drop-group="moveTo($event, 0, yAxis.id)" | ||||
|             > | ||||
|                 <li | ||||
|                     class="js-first-place" | ||||
|                     @drop="moveTo($event, 0, yAxis.id)" | ||||
|                 ></li> | ||||
|                 <element-item | ||||
|                     v-for="(element, elemIndex) in yAxis.elements" | ||||
|                     :key="element.identifier.key" | ||||
|                     :index="elemIndex" | ||||
|                     :element-object="element" | ||||
|                     :parent-object="parentObject" | ||||
|                     :allow-drop="allowDrop" | ||||
|                     :show-grippy="false" | ||||
|                     @dragstart-custom="moveFrom($event, yAxis.id)" | ||||
|                     @drop-custom="moveTo($event, index, yAxis.id)" | ||||
|                 /> | ||||
|                 <li | ||||
|                     v-if="yAxis.elements.length > 0" | ||||
|                     class="js-last-place" | ||||
|                     @drop="moveTo($event, yAxis.elements.length, yAxis.id)" | ||||
|                 ></li> | ||||
|             </element-item-group> | ||||
|         </ul> | ||||
|         <div | ||||
|             v-if="!hasElements" | ||||
|         > | ||||
|             No contained elements | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import _ from 'lodash'; | ||||
| import Search from '../components/search.vue'; | ||||
| import ElementItem from './ElementItem.vue'; | ||||
| import ElementItemGroup from './ElementItemGroup.vue'; | ||||
| import configStore from '../../plugins/plot/configuration/ConfigStore'; | ||||
|  | ||||
| const Y_AXIS_1 = 1; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Search, | ||||
|         ElementItemGroup, | ||||
|         ElementItem | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     data() { | ||||
|         return { | ||||
|             yAxes: [], | ||||
|             isEditing: this.openmct.editor.isEditing(), | ||||
|             parentObject: undefined, | ||||
|             currentSearch: '', | ||||
|             selection: [], | ||||
|             contextClickTracker: {}, | ||||
|             allowDrop: false | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         hasElements() { | ||||
|             for (const yAxis of this.yAxes) { | ||||
|                 if (yAxis.elements.length > 0) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         const selection = this.openmct.selection.get(); | ||||
|         if (selection && selection.length > 0) { | ||||
|             this.showSelection(selection); | ||||
|         } | ||||
|  | ||||
|         this.openmct.selection.on('change', this.showSelection); | ||||
|         this.openmct.editor.on('isEditing', this.setEditState); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.openmct.editor.off('isEditing', this.setEditState); | ||||
|         this.openmct.selection.off('change', this.showSelection); | ||||
|  | ||||
|         this.unlistenComposition(); | ||||
|     }, | ||||
|     methods: { | ||||
|         setEditState(isEditing) { | ||||
|             this.isEditing = isEditing; | ||||
|             this.showSelection(this.openmct.selection.get()); | ||||
|         }, | ||||
|         showSelection(selection) { | ||||
|             if (_.isEqual(this.selection, selection)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.selection = selection; | ||||
|             this.elementsCache = {}; | ||||
|             this.listeners = []; | ||||
|             this.parentObject = selection && selection[0] && selection[0][0].context.item; | ||||
|  | ||||
|             this.unlistenComposition(); | ||||
|  | ||||
|             if (this.parentObject) { | ||||
|                 this.setYAxisIds(); | ||||
|                 this.composition = this.openmct.composition.get(this.parentObject); | ||||
|  | ||||
|                 if (this.composition) { | ||||
|                     this.composition.load(); | ||||
|                     this.registerCompositionListeners(); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         unlistenComposition() { | ||||
|             if (this.compositionUnlistener) { | ||||
|                 this.compositionUnlistener(); | ||||
|             } | ||||
|         }, | ||||
|         registerCompositionListeners() { | ||||
|             this.composition.on('add', this.addElement); | ||||
|             this.composition.on('remove', this.removeElement); | ||||
|             this.composition.on('reorder', this.reorderElements); | ||||
|  | ||||
|             this.compositionUnlistener = () => { | ||||
|                 this.composition.off('add', this.addElement); | ||||
|                 this.composition.off('remove', this.removeElement); | ||||
|                 this.composition.off('reorder', this.reorderElements); | ||||
|                 delete this.compositionUnlistener; | ||||
|             }; | ||||
|         }, | ||||
|         setYAxisIds() { | ||||
|             const configId = this.openmct.objects.makeKeyString(this.parentObject.identifier); | ||||
|             this.config = configStore.get(configId); | ||||
|             this.yAxes.push({ | ||||
|                 id: this.config.yAxis.id, | ||||
|                 elements: this.parentObject.configuration.series.filter( | ||||
|                     series => series.yAxisId === this.config.yAxis.id | ||||
|                 ) | ||||
|             }); | ||||
|             if (this.config.additionalYAxes) { | ||||
|                 this.config.additionalYAxes.forEach(yAxis => { | ||||
|                     this.yAxes.push({ | ||||
|                         id: yAxis.id, | ||||
|                         elements: this.parentObject.configuration.series.filter( | ||||
|                             series => series.yAxisId === yAxis.id | ||||
|                         ) | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         addElement(element) { | ||||
|             // Get the index of the corresponding element in the series list | ||||
|             const seriesIndex = this.parentObject.configuration.series.findIndex( | ||||
|                 series => this.openmct.objects.areIdsEqual(series.identifier, element.identifier) | ||||
|             ); | ||||
|             const keyString = this.openmct.objects.makeKeyString(element.identifier); | ||||
|  | ||||
|             const wasDraggedOntoPlot = this.parentObject.configuration.series[seriesIndex].yAxisId === undefined; | ||||
|             const yAxisId = wasDraggedOntoPlot | ||||
|                 ? Y_AXIS_1 | ||||
|                 : this.parentObject.configuration.series[seriesIndex].yAxisId; | ||||
|  | ||||
|             if (wasDraggedOntoPlot) { | ||||
|                 const insertIndex = this.yAxes[0].elements.length; | ||||
|                 // Insert the element at the end of the first YAxis bucket | ||||
|                 this.composition.reorder(seriesIndex, insertIndex); | ||||
|             } | ||||
|  | ||||
|             // Store the element in the cache and set its yAxisId | ||||
|             this.elementsCache[keyString] = JSON.parse(JSON.stringify(element)); | ||||
|             if (this.elementsCache[keyString].yAxisId !== yAxisId) { | ||||
|                 // Mutate the YAxisId on the domainObject itself | ||||
|                 this.updateCacheAndMutate(element, yAxisId); | ||||
|             } | ||||
|  | ||||
|             this.applySearch(this.currentSearch); | ||||
|         }, | ||||
|         reorderElements() { | ||||
|             this.applySearch(this.currentSearch); | ||||
|         }, | ||||
|         removeElement(identifier) { | ||||
|             const keyString = this.openmct.objects.makeKeyString(identifier); | ||||
|             delete this.elementsCache[keyString]; | ||||
|             this.applySearch(this.currentSearch); | ||||
|         }, | ||||
|         applySearch(input) { | ||||
|             this.currentSearch = input; | ||||
|             this.yAxes.forEach(yAxis => { | ||||
|                 yAxis.elements = this.filterForSearchAndAxis(input, yAxis.id); | ||||
|             }); | ||||
|         }, | ||||
|         filterForSearchAndAxis(input, yAxisId) { | ||||
|             return this.parentObject.composition.map((id) => | ||||
|                 this.elementsCache[this.openmct.objects.makeKeyString(id)] | ||||
|             ).filter((element) => { | ||||
|                 return element !== undefined | ||||
|                     && element.name.toLowerCase().search(input) !== -1 | ||||
|                     && element.yAxisId === yAxisId; | ||||
|             }); | ||||
|         }, | ||||
|         moveFrom(elementIndex, groupIndex) { | ||||
|             this.allowDrop = true; | ||||
|             this.moveFromIndex = elementIndex; | ||||
|             this.moveFromYAxisId = groupIndex; | ||||
|         }, | ||||
|         moveTo(event, moveToIndex, moveToYAxisId) { | ||||
|             // FIXME: If the user starts the drag by clicking outside of the <object-label/> element, | ||||
|             // domain object information will not be set on the dataTransfer data. To prevent errors, | ||||
|             // we simply short-circuit here if the data is not set. | ||||
|             const serializedDomainObject = event.dataTransfer.getData('openmct/composable-domain-object'); | ||||
|             if (!serializedDomainObject) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const domainObject = JSON.parse(serializedDomainObject); | ||||
|             this.updateCacheAndMutate(domainObject, moveToYAxisId); | ||||
|  | ||||
|             const moveFromIndex = this.moveFromIndex; | ||||
|  | ||||
|             this.moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId); | ||||
|         }, | ||||
|         updateCacheAndMutate(domainObject, yAxisId) { | ||||
|             const keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|             const index = this.parentObject.configuration.series.findIndex( | ||||
|                 series => series.identifier.key === domainObject.identifier.key | ||||
|             ); | ||||
|  | ||||
|             // Handle the case of dragging an element directly into the Elements Pool | ||||
|             if (!this.elementsCache[keyString]) { | ||||
|                 // Update the series list locally so our CompositionAdd handler can | ||||
|                 // take care of the rest. | ||||
|                 this.parentObject.configuration.series.push({ | ||||
|                     identifier: domainObject.identifier, | ||||
|                     yAxisId | ||||
|                 }); | ||||
|                 this.composition.add(domainObject); | ||||
|                 this.elementsCache[keyString] = JSON.parse(JSON.stringify(domainObject)); | ||||
|             } | ||||
|  | ||||
|             this.elementsCache[keyString].yAxisId = yAxisId; | ||||
|             const shouldMutate = this.parentObject.configuration.series?.[index]?.yAxisId !== yAxisId; | ||||
|             if (shouldMutate) { | ||||
|                 this.openmct.objects.mutate( | ||||
|                     this.parentObject, | ||||
|                     `configuration.series[${index}].yAxisId`, | ||||
|                     yAxisId | ||||
|                 ); | ||||
|             } | ||||
|         }, | ||||
|         moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId) { | ||||
|             if (!this.allowDrop) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Find the corresponding indexes of the from/to yAxes in the yAxes list | ||||
|             const moveFromYAxisIndex = this.yAxes.findIndex(yAxis => yAxis.id === this.moveFromYAxisId); | ||||
|             const moveToYAxisIndex = this.yAxes.findIndex(yAxis => yAxis.id === moveToYAxisId); | ||||
|  | ||||
|             // Calculate the actual indexes of the elements in the composition array | ||||
|             // based on which bucket and index they are being moved from/to. | ||||
|             // Then, trigger a composition reorder. | ||||
|             for (let yAxisId = 0; yAxisId < moveFromYAxisIndex; yAxisId++) { | ||||
|                 const lesserYAxisBucketLength = this.yAxes[yAxisId].elements.length; | ||||
|                 // Add the lengths of preceding buckets to calculate the actual 'from' index | ||||
|                 moveFromIndex = moveFromIndex + lesserYAxisBucketLength; | ||||
|             } | ||||
|  | ||||
|             for (let yAxisId = 0; yAxisId < moveToYAxisIndex; yAxisId++) { | ||||
|                 const greaterYAxisBucketLength = this.yAxes[yAxisId].elements.length; | ||||
|                 // Add the lengths of subsequent buckets to calculate the actual 'to' index | ||||
|                 moveToIndex = moveToIndex + greaterYAxisBucketLength; | ||||
|             } | ||||
|  | ||||
|             // Adjust the index by 1 if we're moving from one bucket to another | ||||
|             if (this.moveFromYAxisId !== moveToYAxisId && moveToIndex > 0) { | ||||
|                 moveToIndex--; | ||||
|             } | ||||
|  | ||||
|             // Reorder the composition array according to the calculated indexes | ||||
|             this.composition.reorder(moveFromIndex, moveToIndex); | ||||
|  | ||||
|             this.allowDrop = false; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -21,6 +21,11 @@ | ||||
|         flex: 0 0 auto; | ||||
|     } | ||||
|  | ||||
|     &__group { | ||||
|         flex: 1 1 auto; | ||||
|         overflow: auto; | ||||
|     } | ||||
|  | ||||
|     &__elements { | ||||
|         flex: 1 1 auto; | ||||
|         overflow: auto; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user