Compare commits

..

6 Commits

Author SHA1 Message Date
Shefali
a8e70cd831 Merge branch 'release/2.1.5' of https://github.com/nasa/openmct into release/2.1.5 2023-01-03 10:16:31 -08:00
Shefali Joshi
4958594336 Update version to 2.1.5 (#6093) 2023-01-03 09:54:26 -08:00
Shefali
a96490da22 Update version to 2.1.5 2023-01-03 09:48:20 -08:00
Jesse Mazzella
532cec1531 cherry-pick(#6067): [Notebook] Handle conflicts properly (#6087) 2022-12-29 14:22:44 -08:00
Shefali Joshi
a11a4a23e1 cherry-pick(#6082): Use the current clock's timestamp to show the now line in the timestrip (#6086) 2022-12-29 10:45:40 -08:00
Jesse Mazzella
1e4d585e9d cherry-pick(#6080): fix(imagery): Unblock 'latest' strategy requests for Related Telemetry in realtime mode (#6084)
* fix: use ephemeral timeContext for thumbnail metadata requests

* fix(TEMP): use `eval-source-map`

- **!!! REVERT THIS CHANGE BEFORE MERGE !!!**

* fix: only mutate if object supports mutation

* fix: pass identifier instead of whole domainObject

* fix: add start and end bounds to request

* Revert "fix(TEMP): use `eval-source-map`"

This reverts commit 7972d8c33a.

* docs: add comments
2022-12-28 19:29:07 +00:00
37 changed files with 408 additions and 1907 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.29.0-focal
- image: mcr.microsoft.com/playwright:v1.25.2-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps

View File

@@ -23,7 +23,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.29.0 install
- run: npx playwright@1.25.2 install
- run: npm install
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
- run: npm run test:e2e:couchdb

View File

@@ -30,7 +30,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.29.0 install
- run: npx playwright@1.25.2 install
- run: npx playwright install chrome-beta
- run: npm install
- run: npm run test:e2e:full

View File

@@ -8,7 +8,7 @@ This document is designed to capture on the What, Why, and How's of writing and
1. [Getting Started](#getting-started)
2. [Types of Testing](#types-of-e2e-testing)
3. [Architecture](#test-architecture-and-ci)
3. [Architecture](#architecture)
## Getting Started
@@ -400,23 +400,3 @@ A single e2e test in Open MCT is extended to run:
- Tests won't start because 'Error: <http://localhost:8080/># is already used...'
This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:
```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```
### Upgrading Playwright
In order to upgrade from one version of Playwright to another, the version should be updated in several places in both `openmct` and `openmct-yamcs` repos. An easy way to identify these locations is to search for the current version in all files and find/replace.
For reference, all of the locations where the version should be updated are listed below:
#### **In `openmct`:**
- `package.json`
- Both packages `@playwright/test` and `playwright-core` should be updated to the same target version.
- `.circleci/config.yml`
- `.github/workflows/e2e-couchdb.yml`
- `.github/workflows/e2e-pr.yml`
#### **In `openmct-yamcs`:**
- `package.json`
- `@playwright/test` should be updated to the target version.
- `.github/workflows/yamcs-quickstart-e2e.yml`

View File

@@ -44,6 +44,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`Entry ${iteration}`);
await page.locator(entryLocator).press('Enter');
}
return notebook;

View File

@@ -156,7 +156,7 @@ async function turnOffAutoscale(page) {
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
// uncheck autoscale
await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck();
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
// save
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();

View File

@@ -205,8 +205,7 @@ async function enableEditMode(page) {
*/
async function enableLogMode(page) {
// turn on log mode
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();
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
}
/**
@@ -214,7 +213,7 @@ async function enableLogMode(page) {
*/
async function disableLogMode(page) {
// turn off log mode
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck();
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
}
/**

View File

@@ -1,116 +0,0 @@
/*****************************************************************************
* 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"
]);
});
});

View File

@@ -1,13 +1,13 @@
{
"name": "openmct",
"version": "2.1.5-SNAPSHOT",
"version": "2.1.5",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.2",
"@percy/cli": "1.16.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.29.0",
"@playwright/test": "1.25.2",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.191",
"babel-loader": "9.1.0",
@@ -48,7 +48,7 @@
"moment-timezone": "0.5.40",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.29.0",
"playwright-core": "1.25.2",
"plotly.js-basic-dist": "2.14.0",
"plotly.js-gl2d-dist": "2.14.0",
"printj": "1.3.1",

View File

@@ -193,23 +193,27 @@ export default class ObjectAPI {
* @memberof module:openmct.ObjectProvider#
* @param {string} key the key for the domain object to load
* @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
* @param {boolean} forceRemote defaults to false. If true, will skip cached and
* dirty/in-transaction objects use and the provider.get method
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
*/
get(identifier, abortSignal) {
get(identifier, abortSignal, forceRemote = false) {
let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
if (!forceRemote) {
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = utils.parseKeyString(identifier);
identifier = utils.parseKeyString(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
if (dirtyObject) {
return Promise.resolve(dirtyObject);
if (dirtyObject) {
return Promise.resolve(dirtyObject);
}
}
}
@@ -391,7 +395,6 @@ export default class ObjectAPI {
lastPersistedTime = domainObject.persisted;
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
savedObjectPromise = provider.update(domainObject);
}
@@ -399,7 +402,7 @@ export default class ObjectAPI {
savedObjectPromise.then(response => {
savedResolve(response);
}).catch((error) => {
if (lastPersistedTime !== undefined) {
if (!isNewObject) {
this.#mutate(domainObject, 'persisted', lastPersistedTime);
}
@@ -412,11 +415,12 @@ export default class ObjectAPI {
return result.catch(async (error) => {
if (error instanceof this.errors.Conflict) {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
// Synchronized objects will resolve their own conflicts
if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
this.openmct.notifications.info(`Conflict detected while saving "${this.makeKeyString(domainObject.name)}", attempting to resolve`);
} else {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
// Synchronized objects will resolve their own conflicts, so
// bypass the refresh here and throw the error.
if (!this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
if (this.isTransactionActive()) {
this.endTransaction();
}

View File

@@ -788,7 +788,7 @@ export default {
}
},
persistVisibleLayers() {
if (this.domainObject.configuration) {
if (this.domainObject.configuration && this.openmct.objects.supportsMutation(this.domainObject.identifier)) {
this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers);
}

View File

@@ -28,6 +28,7 @@ function copyRelatedMetadata(metadata) {
return copiedMetadata;
}
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
export default class RelatedTelemetry {
constructor(openmct, domainObject, telemetryKeys) {
@@ -88,9 +89,31 @@ export default class RelatedTelemetry {
this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId);
this[key].requestLatestFor = async (datum) => {
const options = {
// We need to create a throwaway time context and pass it along
// as a request option. We do this to "trick" the Time API
// into thinking we are in fixed time mode in order to bypass this logic:
// https://github.com/akhenry/openmct-yamcs/blob/1060d42ebe43bf346dac0f6a8068cb288ade4ba4/src/providers/historical-telemetry-provider.js#L59
// Context: https://github.com/akhenry/openmct-yamcs/pull/217
const ephemeralContext = new IndependentTimeContext(
this._openmct,
this._openmct.time,
[this[key].historicalDomainObject]
);
// Stop following the global context, stop the clock,
// and set bounds.
ephemeralContext.resetContext();
const newBounds = {
start: this._openmct.time.bounds().start,
end: this._parseTime(datum),
end: this._parseTime(datum)
};
ephemeralContext.stopClock();
ephemeralContext.bounds(newBounds);
const options = {
start: newBounds.start,
end: newBounds.end,
timeContext: ephemeralContext,
strategy: 'latest'
};
let results = await this._openmct.telemetry

View File

@@ -50,7 +50,7 @@
<Sidebar
ref="sidebar"
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:class="sidebarClasses"
:default-page-id="defaultPageId"
:selected-page-id="getSelectedPageId()"
:default-section-id="defaultSectionId"
@@ -123,6 +123,7 @@
</div>
<div
v-if="selectedPage && !selectedPage.isLocked"
:class="{ 'disabled': activeTransaction }"
class="c-notebook__drag-area icon-plus"
@click="newEntry()"
@dragover="dragOver"
@@ -133,6 +134,11 @@
To start a new entry, click here or drag and drop any object
</span>
</div>
<progress-bar
v-if="savingTransaction"
class="c-telemetry-table__progress-bar"
:model="{ progressPerc: undefined }"
/>
<div
v-if="selectedPage && selectedPage.isLocked"
class="c-notebook__page-locked"
@@ -183,6 +189,7 @@ import NotebookEntry from './NotebookEntry.vue';
import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
@@ -200,7 +207,8 @@ export default {
NotebookEntry,
Search,
SearchResults,
Sidebar
Sidebar,
ProgressBar
},
inject: ['agent', 'openmct', 'snapshotContainer'],
props: {
@@ -225,7 +233,9 @@ export default {
showNav: false,
sidebarCoversEntries: false,
filteredAndSortedEntries: [],
notebookAnnotations: {}
notebookAnnotations: {},
activeTransaction: false,
savingTransaction: false
};
},
computed: {
@@ -270,6 +280,20 @@ export default {
return this.sections[0];
},
sidebarClasses() {
let sidebarClasses = [];
if (this.showNav) {
sidebarClasses.push('is-expanded');
}
if (this.sidebarCoversEntries) {
sidebarClasses.push('c-drawer--overlays');
} else {
sidebarClasses.push('c-drawer--push');
}
return sidebarClasses;
},
showLockButton() {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
@@ -297,6 +321,8 @@ export default {
this.formatSidebar();
this.setSectionAndPageFromUrl();
this.transaction = null;
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
this.filterAndSortEntries();
@@ -749,6 +775,7 @@ export default {
return section.id;
},
async newEntry(embed = null) {
this.startTransaction();
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
@@ -891,20 +918,34 @@ export default {
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.activeTransaction = true;
this.transaction = this.openmct.objects.startTransaction();
}
},
async saveTransaction() {
if (this.transaction !== undefined) {
await this.transaction.commit();
this.openmct.objects.endTransaction();
if (this.transaction !== null) {
this.savingTransaction = true;
try {
await this.transaction.commit();
} finally {
this.endTransaction();
}
}
},
async cancelTransaction() {
if (this.transaction !== undefined) {
await this.transaction.cancel();
this.openmct.objects.endTransaction();
if (this.transaction !== null) {
try {
await this.transaction.cancel();
} finally {
this.endTransaction();
}
}
},
endTransaction() {
this.openmct.objects.endTransaction();
this.transaction = null;
this.savingTransaction = false;
this.activeTransaction = false;
}
}
};

View File

@@ -74,19 +74,22 @@ async function resolveNotebookTagConflicts(localAnnotation, openmct) {
async function resolveNotebookEntryConflicts(localMutable, openmct) {
if (localMutable.configuration.entries) {
const FORCE_REMOTE = true;
const localEntries = structuredClone(localMutable.configuration.entries);
const remoteMutable = await openmct.objects.getMutable(localMutable.identifier);
applyLocalEntries(remoteMutable, localEntries, openmct);
openmct.objects.destroyMutable(remoteMutable);
const remoteObject = await openmct.objects.get(localMutable.identifier, undefined, FORCE_REMOTE);
return applyLocalEntries(remoteObject, localEntries, openmct);
}
return true;
}
function applyLocalEntries(mutable, entries, openmct) {
function applyLocalEntries(remoteObject, entries, openmct) {
let shouldSave = false;
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey];
const mergedEntries = [].concat(remoteEntries);
let shouldMutate = false;
@@ -110,8 +113,13 @@ function applyLocalEntries(mutable, entries, openmct) {
});
if (shouldMutate) {
openmct.objects.mutate(mutable, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
shouldSave = true;
openmct.objects.mutate(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
}
});
});
if (shouldSave) {
return openmct.objects.save(remoteObject);
}
}

View File

@@ -36,8 +36,8 @@ export default function () {
}
let wrappedFunction = openmct.objects.get;
openmct.objects.get = function migrate(identifier) {
return wrappedFunction.apply(openmct.objects, [identifier])
openmct.objects.get = function migrate() {
return wrappedFunction.apply(openmct.objects, [...arguments])
.then(function (object) {
if (needsMigration(object)) {
migrateObject(object)

View File

@@ -28,6 +28,7 @@
connected = false;
// stop listening for events
couchEventSource.removeEventListener('message', self.onCouchMessage);
couchEventSource.close();
console.debug('🚪 Closed couch connection 🚪');
return;

View File

@@ -96,8 +96,13 @@ class CouchObjectProvider {
let keyString = this.openmct.objects.makeKeyString(objectIdentifier);
//TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.
let observersForObject = this.observers[keyString];
let isInTransaction = false;
if (observersForObject) {
if (this.openmct.objects.isTransactionActive()) {
isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier);
}
if (observersForObject && !isInTransaction) {
observersForObject.forEach(async (observer) => {
const updatedObject = await this.get(objectIdentifier);
if (this.isSynchronizedObject(updatedObject)) {
@@ -219,7 +224,12 @@ class CouchObjectProvider {
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
} else {
console.error(error.message);
if (body?.model && isNotebookOrAnnotationType(body.model)) {
// warn since we handle conflicts for notebooks
console.warn(error.message);
} else {
console.error(error.message);
}
throw error;
}

View File

@@ -34,26 +34,23 @@
@legendHoverChanged="legendHoverChanged"
/>
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
<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>
<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
class="gl-plot-wrapper-display-area-and-x-axis"
:style="xAxisStyle"
:style="{
left: (plotWidth + 20) + 'px'
}"
>
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
@@ -72,12 +69,9 @@
/>
<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"
/>
@@ -220,7 +214,6 @@ import YAxis from "./axis/YAxis.vue";
import _ from "lodash";
const OFFSET_THRESHOLD = 10;
const AXES_PADDING = 20;
export default {
components: {
@@ -276,6 +269,7 @@ export default {
altPressed: false,
highlights: [],
lockHighlightPoint: false,
tickWidth: 0,
yKeyOptions: [],
yAxisLabel: '',
rectangles: [],
@@ -290,31 +284,12 @@ export default {
isTimeOutOfSync: false,
showLimitLineLabels: this.limitLineLabels,
isFrozenOnMouseDown: false,
hasSameRangeValue: true,
cursorGuide: this.initCursorGuide,
gridLines: this.initGridLines,
yAxes: []
gridLines: this.initGridLines
};
},
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));
@@ -337,17 +312,8 @@ export default {
return 'plot-legend-collapsed';
}
},
plotLeftTickWidth() {
let leftTickWidth = 0;
this.yAxes.forEach((yAxis) => {
if (yAxis.id > 2) {
return;
}
leftTickWidth = leftTickWidth + yAxis.tickWidth;
});
return this.plotTickWidth || leftTickWidth;
plotWidth() {
return this.plotTickWidth || this.tickWidth;
}
},
watch: {
@@ -376,20 +342,6 @@ 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);
@@ -431,10 +383,8 @@ export default {
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.followTimeContext();
},
followTimeContext() {
this.updateDisplayBounds(this.timeContext.bounds());
@@ -467,13 +417,12 @@ 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);
@@ -481,21 +430,20 @@ export default {
this.loadSeriesData(series);
}, this);
this.checkSameRangeValue();
this.loadSeriesData(series);
},
removeSeries(plotSeries, index) {
const yAxisId = plotSeries.get('yAxisId');
this.updateAxisUsageCount(yAxisId, -1);
this.seriesModels.splice(index, 1);
this.stopListening(plotSeries);
checkSameRangeValue() {
this.hasSameRangeValue = this.seriesModels.every((model) => {
return model.get('yKey') === this.seriesModels[0].get('yKey');
});
},
updateAxisUsageCount(yAxisId, updateCountBy) {
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
if (foundYAxis) {
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy;
}
removeSeries(plotSeries, index) {
this.seriesModels.splice(index, 1);
this.checkSameRangeValue();
this.stopListening(plotSeries);
},
loadSeriesData(series) {
@@ -725,7 +673,6 @@ 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;
@@ -743,9 +690,6 @@ 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) {
@@ -760,24 +704,20 @@ export default {
}
},
onTickWidthChange(data, fromDifferentObject) {
const {width, yAxisId} = data;
if (yAxisId) {
const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId);
if (fromDifferentObject) {
onTickWidthChange(width, fromDifferentObject) {
if (fromDifferentObject) {
// Always accept tick width if it comes from a different object.
this.yAxes[index].tickWidth = width;
} else {
this.tickWidth = width;
} else {
// Otherwise, only accept tick with if it's larger.
const newWidth = Math.max(width, this.yAxes[index].tickWidth);
if (newWidth !== this.yAxes[index].tickWidth) {
this.yAxes[index].tickWidth = newWidth;
}
const newWidth = Math.max(width, this.tickWidth);
if (newWidth !== this.tickWidth) {
this.tickWidth = newWidth;
}
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id);
}
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('plotTickWidth', this.tickWidth, id);
},
trackMousePosition(event) {
@@ -1168,9 +1108,8 @@ export default {
this.userViewportChangeEnd();
},
setYAxisKey(yKey, yAxisId) {
const seriesForYAxis = this.config.series.models.filter((model => model.get('yAxisId') === yAxisId));
seriesForYAxis.forEach(model => model.set('yKey', yKey));
setYAxisKey(yKey) {
this.config.series.models[0].set('yKey', yKey);
},
pause() {

View File

@@ -103,12 +103,6 @@ export default {
return 6;
}
},
axisId: {
type: Number,
default() {
return null;
}
},
position: {
required: true,
type: String,
@@ -151,15 +145,7 @@ export default {
throw new Error('config is missing');
}
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];
}
return config[this.axisType];
},
/**
* Determine whether ticks should be regenerated for a given range.
@@ -272,10 +258,7 @@ export default {
}, 0));
this.tickWidth = tickWidth;
this.$emit('plotTickWidth', {
width: tickWidth,
yAxisId: this.axisType === 'yAxis' ? this.axisId : ''
});
this.$emit('plotTickWidth', tickWidth);
this.shouldCheckWidth = false;
}
}

View File

@@ -22,8 +22,10 @@
<template>
<div
v-if="loaded"
class="gl-plot-axis-area gl-plot-y has-local-controls js-plot-y-axis"
:style="yAxisStyle"
class="gl-plot-axis-area gl-plot-y has-local-controls"
:style="{
width: (tickWidth + 20) + 'px'
}"
>
<div
@@ -50,7 +52,6 @@
</select>
<mct-ticks
:axis-id="id"
:axis-type="'yAxis'"
class="gl-plot-ticks"
:position="'top'"
@@ -62,10 +63,6 @@
<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: {
@@ -73,10 +70,22 @@ export default {
},
inject: ['openmct', 'domainObject'],
props: {
id: {
type: Number,
singleSeries: {
type: Boolean,
default() {
return 1;
return true;
}
},
hasSameRangeValue: {
type: Boolean,
default() {
return true;
}
},
seriesModel: {
type: Object,
default() {
return {};
}
},
tickWidth: {
@@ -84,132 +93,37 @@ 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,
yKeyOptions: [],
hasSameRangeValue: true,
singleSeries: true,
mainYAxisId: null,
hasAdditionalYAxes: false
loaded: 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() {
eventHelpers.extend(this);
this.initAxisAndSeriesConfig();
this.yAxis = this.getYAxisFromConfig();
this.loaded = true;
this.setUpYAxisOptions();
},
methods: {
initAxisAndSeriesConfig() {
getYAxisFromConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
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);
return config.yAxis;
}
},
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;
}
const seriesModel = this.seriesModels[0];
if (seriesModel.metadata) {
this.yKeyOptions = seriesModel.metadata
if (this.seriesModel.metadata) {
this.yKeyOptions = this.seriesModel.metadata
.valuesForHints(['range'])
.map(function (o) {
return {
@@ -221,22 +135,22 @@ export default {
// set yAxisLabel if none is set yet
if (this.yAxisLabel === 'none') {
this.yAxisLabel = this.yAxis.get('label');
let yKey = this.seriesModel.model.yKey;
let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0];
this.yAxisLabel = yKeyModel ? yKeyModel.name : '';
}
},
toggleYAxisLabel() {
let yAxisObject = this.yKeyOptions.filter(o => o.name === this.yAxisLabel)[0];
if (yAxisObject) {
this.$emit('yKeyChanged', yAxisObject.key, this.id);
this.$emit('yKeyChanged', yAxisObject.key);
this.yAxis.set('label', this.yAxisLabel);
}
},
onTickWidthChange(data) {
this.$emit('tickWidthChanged', {
width: data.width,
yAxisId: this.id
});
onTickWidthChange(width) {
this.$emit('tickWidthChanged', width);
}
}
};

View File

@@ -98,21 +98,7 @@ export default {
this.limitLines = [];
this.pointSets = [];
this.alarmSets = [];
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.offset = {};
this.seriesElements = new WeakMap();
this.seriesLimits = new WeakMap();
@@ -125,7 +111,8 @@ 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');
@@ -237,31 +224,25 @@ export default {
this.limitLines.forEach(line => line.destroy());
DrawLoader.releaseDrawAPI(this.drawAPI);
},
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) {
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) {
line.reset();
});
const limitLines = this.limitLines.filter(this.matchByYAxisId.bind(this, yAxisId));
limitLines.forEach(function (line) {
this.limitLines.forEach(function (line) {
line.reset();
});
const pointSets = this.pointSets.filter(this.matchByYAxisId.bind(this, yAxisId));
pointSets.forEach(function (pointSet) {
this.pointSets.forEach(function (pointSet) {
pointSet.reset();
});
},
setOffset(offsetPoint, index, series) {
const mainYAxisId = this.config.yAxis.get('id');
const yAxisId = series.get('yAxisId') || mainYAxisId;
if (this.offset[yAxisId].x && this.offset[yAxisId].y) {
if (this.offset.x && this.offset.y) {
return;
}
@@ -270,20 +251,19 @@ export default {
y: series.getYVal(offsetPoint)
};
this.offset[yAxisId].x = function (x) {
this.offset.x = function (x) {
return x - offsets.x;
}.bind(this);
this.offset[yAxisId].y = function (y) {
this.offset.y = function (y) {
return y - offsets.y;
}.bind(this);
this.offset[yAxisId].xVal = function (point, pSeries) {
return this.offset[yAxisId].x(pSeries.getXVal(point));
this.offset.xVal = function (point, pSeries) {
return this.offset.x(pSeries.getXVal(point));
}.bind(this);
this.offset[yAxisId].yVal = function (point, pSeries) {
return this.offset[yAxisId].y(pSeries.getYVal(point));
this.offset.yVal = function (point, pSeries) {
return this.offset.y(pSeries.getYVal(point));
}.bind(this);
},
initializeCanvas(canvas, overlay) {
this.canvas = canvas;
this.overlay = overlay;
@@ -331,15 +311,11 @@ 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,
offset
this.offset
);
}
@@ -347,45 +323,33 @@ export default {
return new MCTChartLineStepAfter(
series,
this,
offset
this.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,
offset,
this.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,
offset
this.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,
offset
this.offset
);
}
},
@@ -446,8 +410,8 @@ export default {
this.seriesLimits.delete(series);
}
},
canDraw(yAxisId) {
if (!this.offset[yAxisId] || !this.offset[yAxisId].x || !this.offset[yAxisId].y) {
canDraw() {
if (!this.offset.x || !this.offset.y) {
return false;
}
@@ -470,31 +434,16 @@ export default {
}
this.drawAPI.clear();
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(yAxisId) {
const mainYAxisId = this.config.yAxis.get('id');
const xRange = this.config.xAxis.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 (this.canDraw()) {
this.updateViewport();
this.drawSeries();
this.drawRectangles();
this.drawHighlights();
}
},
updateViewport() {
const xRange = this.config.xAxis.get('displayRange');
const yRange = this.config.yAxis.get('displayRange');
if (!xRange || !yRange) {
return;
@@ -505,10 +454,9 @@ export default {
yRange.max - yRange.min
];
let origin;
origin = [
this.offset[yAxisId].x(xRange.min),
this.offset[yAxisId].y(yRange.min)
const origin = [
this.offset.x(xRange.min),
this.offset.y(yRange.min)
];
this.drawAPI.setDimensions(
@@ -516,66 +464,38 @@ export default {
origin
);
},
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);
drawSeries() {
this.lines.forEach(this.drawLine, this);
this.pointSets.forEach(this.drawPoints, this);
this.alarmSets.forEach(this.drawAlarmPoints, this);
},
drawLimitLines() {
this.config.series.models.forEach(series => {
const yAxisId = series.get('yAxisId');
this.drawLimitLinesForSeries(yAxisId, series);
});
},
drawLimitLinesForSeries(yAxisId, series) {
if (!this.canDraw(yAxisId)) {
return;
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.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
@@ -657,25 +577,22 @@ export default {
);
},
drawLine(chartElement, disconnected) {
if (chartElement) {
this.drawAPI.drawLine(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count,
disconnected
);
}
this.drawAPI.drawLine(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count,
disconnected
);
},
drawHighlights(yAxisId) {
drawHighlights() {
if (this.highlights && this.highlights.length) {
const highlights = this.highlights.filter(this.matchByYAxisId.bind(this, yAxisId));
highlights.forEach(this.drawHighlight.bind(this, yAxisId), this);
this.highlights.forEach(this.drawHighlight, this);
}
},
drawHighlight(yAxisId, highlight) {
drawHighlight(highlight) {
const points = new Float32Array([
this.offset[yAxisId].xVal(highlight.point, highlight.series),
this.offset[yAxisId].yVal(highlight.point, highlight.series)
this.offset.xVal(highlight.point, highlight.series),
this.offset.yVal(highlight.point, highlight.series)
]);
const color = highlight.series.get('color').asRGBAArray();
@@ -684,21 +601,20 @@ export default {
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
},
drawRectangles(yAxisId) {
drawRectangles() {
if (this.rectangles) {
const rectangles = this.rectangles.filter(this.matchByYAxisId.bind(this, yAxisId));
rectangles.forEach(this.drawRectangle.bind(this, yAxisId), this);
this.rectangles.forEach(this.drawRectangle, this);
}
},
drawRectangle(yAxisId, rect) {
drawRectangle(rect) {
this.drawAPI.drawSquare(
[
this.offset[yAxisId].x(rect.start.x),
this.offset[yAxisId].y(rect.start.y)
this.offset.x(rect.start.x),
this.offset.y(rect.start.y)
],
[
this.offset[yAxisId].x(rect.end.x),
this.offset[yAxisId].y(rect.end.y)
this.offset.x(rect.end.x),
this.offset.y(rect.end.y)
],
rect.color
);

View File

@@ -27,10 +27,6 @@ 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
@@ -62,35 +58,8 @@ export default class PlotConfigurationModel extends Model {
this.yAxis = new YAxisModel({
model: options.model.yAxis,
plot: this,
openmct: options.openmct,
id: options.model.yAxis.id || MAIN_Y_AXES_ID
openmct: options.openmct
});
//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,
@@ -112,9 +81,6 @@ 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);
@@ -179,7 +145,6 @@ 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 ?? {})
};
}

View File

@@ -118,8 +118,7 @@ export default class PlotSeries extends Model {
markerShape: 'point',
markerSize: 2.0,
alarmMarkers: true,
limitLines: false,
yAxisId: options.model.yAxisId || 1
limitLines: false
};
}

View File

@@ -135,44 +135,18 @@ export default class YAxisModel extends Model {
}
}
resetStats() {
//TODO: do we need the series id here?
this.unset('stats');
this.getSeriesForYAxis(this.seriesCollection).forEach(series => {
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 {
@@ -180,10 +154,6 @@ export default class YAxisModel extends Model {
}
});
this.listenTo(series, 'change:yKey', () => {
if (series.get('yAxisId') !== this.id) {
return;
}
this.updateFromSeries(this.seriesCollection);
});
}
@@ -282,40 +252,14 @@ 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 seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection);
if (!seriesForThisYAxis.length) {
return;
}
const yAxis = this.getYAxisForId(this.id);
const label = yAxis?.label;
const sampleSeries = seriesForThisYAxis[0];
const plotModel = this.plot.get('domainObject');
const label = plotModel.configuration?.yAxis?.label;
const sampleSeries = seriesCollection.first();
if (!sampleSeries || !sampleSeries.metadata) {
if (!label) {
this.unset('label');
@@ -335,17 +279,41 @@ export default class YAxisModel extends Model {
}
this.set('values', yMetadata.values);
if (!label) {
const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name');
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);
if (labelName) {
this.set('label', labelName);
return;
}
//if the name is not available, set the units as the label
const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units');
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 (labelUnits) {
this.set('label', labelUnits);
@@ -363,8 +331,7 @@ export default class YAxisModel extends Model {
frozen: false,
autoscale: true,
logMode: options.model?.logMode ?? false,
autoscalePadding: 0.1,
id: options.id
autoscalePadding: 0.1
// 'range' is not specified here, it is undefined at first. When the
// user turns off autoscale, the current 'displayRange' is used for

View File

@@ -36,21 +36,20 @@
/>
</ul>
<div
v-if="plotSeries.length && !isStackedPlotObject"
v-if="plotSeries.length"
class="grid-properties"
>
<ul
v-for="(yAxis, index) in yAxesWithSeries"
:key="`yAxis-${index}`"
v-if="!isStackedPlotObject"
class="l-inspector-part js-yaxis-properties"
>
<h2 title="Y axis settings for this object">Y Axis {{ yAxis.id }}</h2>
<h2 title="Y axis settings for this object">Y Axis</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">{{ yAxis.label ? yAxis.label : "Not defined" }}</div>
<div class="grid-cell value">{{ label ? label : "Not defined" }}</div>
</li>
<li class="grid-row">
<div
@@ -58,7 +57,7 @@
title="Enable log mode."
>Log mode</div>
<div class="grid-cell value">
{{ yAxis.logMode ? "Enabled" : "Disabled" }}
{{ logMode ? "Enabled" : "Disabled" }}
</div>
</li>
<li class="grid-row">
@@ -67,36 +66,32 @@
title="Automatically scale the Y axis to keep all values in view."
>Auto scale</div>
<div class="grid-cell value">
{{ yAxis.autoscale ? "Enabled: " + yAxis.autoscalePadding : "Disabled" }}
{{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }}
</div>
</li>
<li
v-if="!yAxis.autoscale && yAxis.rangeMin"
v-if="!autoscale && rangeMin"
class="grid-row"
>
<div
class="grid-cell label"
title="Minimum Y axis value."
>Minimum value</div>
<div class="grid-cell value">{{ yAxis.rangeMin }}</div>
<div class="grid-cell value">{{ rangeMin }}</div>
</li>
<li
v-if="!yAxis.autoscale && yAxis.rangeMax"
v-if="!autoscale && rangeMax"
class="grid-row"
>
<div
class="grid-cell label"
title="Maximum Y axis value."
>Maximum value</div>
<div class="grid-cell value">{{ yAxis.rangeMax }}</div>
<div class="grid-cell value">{{ 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>
@@ -162,6 +157,12 @@ export default {
data() {
return {
config: {},
label: '',
autoscale: '',
logMode: false,
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
position: '',
hideLegendWhenSmall: '',
expandByDefault: '',
@@ -172,8 +173,7 @@ export default {
showMaximumWhenExpanded: '',
showUnitsWhenExpanded: '',
loaded: false,
plotSeries: [],
yAxes: []
plotSeries: []
};
},
computed: {
@@ -182,18 +182,13 @@ 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.initLegendConfiguration();
this.initConfiguration();
this.loaded = true;
},
@@ -201,38 +196,18 @@ export default {
this.stopListening();
},
methods: {
initYAxesConfiguration() {
initConfiguration() {
if (this.config) {
let range = this.config.yAxis.get('range');
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;
}
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');
@@ -254,44 +229,18 @@ 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.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');
}
}
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
}
},
addSeries(series, index) {
const yAxisId = series.get('yAxisId');
this.updateAxisUsageCount(yAxisId, 1);
this.$set(this.plotSeries, index, series);
this.setYAxisLabel(yAxisId);
this.initConfiguration();
},
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;
}
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
}
}
};

View File

@@ -40,10 +40,8 @@
</li>
</ul>
<y-axis-form
v-for="(yAxisId, index) in yAxesIds"
:id="yAxisId.id"
:key="`yAxis-${index}`"
class="grid-properties js-yaxis-grid-properties"
v-if="plotSeries.length && !isStackedPlotObject"
class="grid-properties"
:y-axis="config.yAxis"
@seriesUpdated="updateSeriesConfigForObject"
/>
@@ -78,7 +76,6 @@ export default {
data() {
return {
config: {},
yAxes: [],
plotSeries: [],
loaded: false
};
@@ -89,27 +86,11 @@ 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;
},
@@ -126,47 +107,16 @@ 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.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');
}
}
}
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
},
addSeries(series, index) {
const yAxisId = series.get('yAxisId');
this.updateAxisUsageCount(yAxisId, 1);
this.$set(this.plotSeries, index, series);
this.setYAxisLabel(yAxisId);
},
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;
}
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
},
updateSeriesConfigForObject(config) {

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="loaded">
<div>
<ul class="l-inspector-part">
<h2>Y Axis {{ id }}</h2>
<h2>Y Axis</h2>
<li class="grid-row">
<div
class="grid-cell label"
@@ -25,7 +25,6 @@
<!-- eslint-disable-next-line vue/html-self-closing -->
<input
v-model="logMode"
class="js-log-mode-input"
type="checkbox"
@change="updateForm('logMode')"
/>
@@ -104,72 +103,52 @@
<script>
import { objectPath } from "./formUtil";
import _ from "lodash";
import eventHelpers from "../../lib/eventHelpers";
import configStore from "../../configuration/ConfigStore";
export default {
inject: ['openmct', 'domainObject'],
props: {
id: {
type: Number,
required: true
yAxis: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
yAxis: null,
label: '',
autoscale: '',
logMode: false,
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
validationErrors: {},
loaded: false
validationErrors: {}
};
},
mounted() {
eventHelpers.extend(this);
this.getConfig();
this.loaded = true;
this.initFields();
this.initialize();
this.initFormValues();
},
methods: {
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()}`;
initialize: function () {
this.fields = {
label: {
objectPath: `${prefix}.label`
objectPath: 'configuration.yAxis.label'
},
autoscale: {
coerce: Boolean,
objectPath: `${prefix}.autoscale`
objectPath: 'configuration.yAxis.autoscale'
},
autoscalePadding: {
coerce: Number,
objectPath: `${prefix}.autoscalePadding`
objectPath: 'configuration.yAxis.autoscalePadding'
},
logMode: {
coerce: Boolean,
objectPath: `${prefix}.logMode`
objectPath: 'configuration.yAxis.logMode'
},
range: {
objectPath: `${prefix}.range'`,
objectPath: 'configuration.yAxis.range',
coerce: function coerceRange(range) {
const newRange = {
min: -1,
@@ -223,25 +202,6 @@ 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') {
@@ -271,42 +231,18 @@ export default {
this.yAxis.set(formKey, newVal);
// Then we mutate the domain object configuration to persist the settings
if (path) {
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
});
}
if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
this.$emit('seriesUpdated', {
identifier: this.domainObject.identifier,
path: `yAxis.${formKey}`,
value: newVal
});
} else {
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
});
}
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.yAxis),
newVal
);
}
}
}

View File

@@ -1,504 +0,0 @@
/*****************************************************************************
* 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();
});
});
});
});

View File

@@ -28,8 +28,6 @@ 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;
@@ -406,20 +404,6 @@ 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");

View File

@@ -593,8 +593,6 @@ 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; }

View File

@@ -101,7 +101,8 @@ export default {
if (nowMarker) {
nowMarker.classList.remove('hidden');
nowMarker.style.height = this.contentHeight + 'px';
const now = this.xScale(Date.now());
const nowTimeStamp = this.openmct.time.clock().currentValue();
const now = this.xScale(nowTimeStamp);
nowMarker.style.left = now + this.offset + 'px';
}
}

View File

@@ -25,7 +25,7 @@
draggable="true"
@dragstart="emitDragStartEvent"
@dragenter="onDragenter"
@dragover.prevent
@dragover="onDragover"
@dragleave="onDragleave"
@drop="emitDropEvent"
>
@@ -38,7 +38,6 @@
}"
>
<span
v-if="showGrippy"
class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"
></span>
<object-label
@@ -82,10 +81,6 @@ export default {
},
allowDrop: {
type: Boolean
},
showGrippy: {
type: Boolean,
default: true
}
},
data() {
@@ -98,8 +93,11 @@ export default {
};
},
methods: {
onDragover(event) {
event.preventDefault();
},
emitDropEvent(event) {
this.$emit('drop-custom', event);
this.$emit('drop-custom', this.index);
this.hover = false;
},
emitDragStartEvent(event) {

View File

@@ -1,101 +0,0 @@
/*****************************************************************************
* 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>

View File

@@ -65,8 +65,8 @@ import ElementItem from './ElementItem.vue';
export default {
components: {
Search,
ElementItem
'Search': Search,
'ElementItem': ElementItem
},
inject: ['openmct'],
data() {

View File

@@ -56,12 +56,7 @@
handle="before"
label="Elements"
>
<plot-elements-pool
v-if="isOverlayPlot"
/>
<elements-pool
v-else
/>
<elements-pool />
</pane>
</multipane>
<multipane
@@ -88,7 +83,6 @@
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';
@@ -105,7 +99,6 @@ export default {
multipane,
pane,
ElementsPool,
PlotElementsPool,
Properties,
ObjectName,
Location,
@@ -125,7 +118,6 @@ export default {
return {
hasComposition: false,
showStyles: false,
isOverlayPlot: false,
tabbedViews: [{
key: '__properties',
name: 'Properties'
@@ -159,7 +151,6 @@ 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) {

View File

@@ -1,330 +0,0 @@
/*****************************************************************************
* 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>

View File

@@ -21,11 +21,6 @@
flex: 0 0 auto;
}
&__group {
flex: 1 1 auto;
overflow: auto;
}
&__elements {
flex: 1 1 auto;
overflow: auto;