Skip to content

Commit

Permalink
feat(spie-ui): finish plotter feature
Browse files Browse the repository at this point in the history
  • Loading branch information
robsonos committed Dec 25, 2024
1 parent 350d823 commit 0a33b8f
Show file tree
Hide file tree
Showing 13 changed files with 630 additions and 434 deletions.
168 changes: 101 additions & 67 deletions apps/spie-ui-e2e/src/e2e/plotter.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ describe('Plotter routine', () => {
cy.get('.apexcharts-canvas').should('be.visible');
});

it.only('should use sample count by default', () => {
cy.get('.apexcharts-xaxis-title-text').should(
'contain.text',
'Sample count'
);
});

it('should render series', () => {
const numberOfPoints = 2;
const mockEventData = plotOneVariable.eventData.slice(0, numberOfPoints);
Expand All @@ -51,16 +58,14 @@ describe('Plotter routine', () => {
return new Promise<void>((resolve) => {
// Mock data event with numberOfPoints point 50ms apart
mockEventData.forEach((data, index) => {
setTimeout(() => {
win.onSerialPortEventTrigger({
type: 'data',
data: data,
});
win.onSerialPortEventTrigger({
type: 'data-delimited',
data: data,
});

if (index === mockEventData.length - 1) {
resolve();
}
}, index * 50);
if (index === mockEventData.length - 1) {
resolve();
}
});
});
})
Expand All @@ -81,16 +86,14 @@ describe('Plotter routine', () => {
return new Promise<void>((resolve) => {
// Mock data event with numberOfPoints point 50ms apart
mockEventData.forEach((data, index) => {
setTimeout(() => {
win.onSerialPortEventTrigger({
type: 'data',
data: data,
});
win.onSerialPortEventTrigger({
type: 'data-delimited',
data: data,
});

if (index === mockEventData.length - 1) {
resolve();
}
}, index * 50);
if (index === mockEventData.length - 1) {
resolve();
}
});
});
})
Expand Down Expand Up @@ -119,16 +122,14 @@ describe('Plotter routine', () => {
return new Promise<void>((resolve) => {
// Mock data event with numberOfPoints point 50ms apart
mockEventData.forEach((data, index) => {
setTimeout(() => {
win.onSerialPortEventTrigger({
type: 'data',
data: data,
});
win.onSerialPortEventTrigger({
type: 'data-delimited',
data: data,
});

if (index === mockEventData.length - 1) {
resolve();
}
}, index * 50);
if (index === mockEventData.length - 1) {
resolve();
}
});
});
})
Expand Down Expand Up @@ -161,24 +162,20 @@ describe('Plotter routine', () => {
it('should render multiple variables and large series', () => {
const mockEventData = plotThreeVariables.eventData;
const mockSeries = plotThreeVariables.series;
const chartStartOffset = 89; // Estimated offset to the first tooltip
const chartEndOffset = 9; // Estimated offset to the last tooltip

cy.window()
.then((win) => {
return new Promise<void>((resolve) => {
// Mock data event with numberOfPoints point 50ms apart
mockEventData.forEach((data, index) => {
setTimeout(() => {
win.onSerialPortEventTrigger({
type: 'data',
data: data,
});
win.onSerialPortEventTrigger({
type: 'data-delimited',
data: data,
});

if (index === mockEventData.length - 1) {
resolve();
}
}, index * 50);
if (index === mockEventData.length - 1) {
resolve();
}
});
});
})
Expand All @@ -193,41 +190,47 @@ describe('Plotter routine', () => {
.should('be.below', 940)
.wait(500);

// Select the first path element and extract X coordinates
cy.get('g.apexcharts-series path.apexcharts-line')
.first()
.then(($path) => {
const pathData = $path.attr('d');

const xCoordinates = (pathData as string)
.split('L')
.map((segment, index, array) => {
const [x] = segment
.trim()
.replace('M', '')
.split(' ')
.map(Number);

// If it's the last element in the array, adjust the X coordinate
if (index === array.length - 1) {
return x + chartStartOffset - chartEndOffset;
}

// Otherwise, add the pageOffset to the X value
return x + chartStartOffset;
})
.filter((x) => !isNaN(x)); // Filter out NaN values
.invoke('attr', 'd')
.then((dAttribute) => {
// Get coordinates from the first line element
const coordinates: { x: any; y: any }[] = [];
const commands = (dAttribute as string).match(
/[ML]\s*[-\d.]+\s*[-\d.]+/g
);

if (commands) {
commands.forEach((command) => {
const [x, y] = command.slice(1).trim().split(/\s+/).map(Number);
coordinates.push({ x, y });
});
}
return coordinates;
})
.then((coordinates) => {
// Get the chart offset coordinates bases on the first vertical line
return cy.get('.apexcharts-xaxis-tick').then(($ticks) => {
const firstElement = $ticks[0];
const firstX = firstElement.getBoundingClientRect().x;
const firstY = firstElement.getBoundingClientRect().x;

return coordinates.map((point) => ({
x: point.x + firstX,
y: point.y + firstY + 15,
}));
});
})
.then((coordinates) => {
// Test labels and values
mockSeries.forEach((points, pointsIndex) => {
// // Helper for debug
// cy.get('body').then(($body) => {
// const refCircle = document.createElement('div');
// refCircle.style.position = 'absolute';
// refCircle.style.left = `${xCoordinates[pointsIndex]}px`;
// refCircle.style.top = `${150}px`;
// refCircle.style.width = '5px';
// refCircle.style.height = '5px';
// refCircle.style.left = `${coordinates[pointsIndex].x}px`;
// refCircle.style.top = `${coordinates[pointsIndex].y}px`;
// refCircle.style.width = '2px';
// refCircle.style.height = '2px';
// refCircle.style.borderRadius = '50%';
// refCircle.style.backgroundColor = 'red'; // Red for visibility
// refCircle.style.zIndex = '9999'; // High z-index to appear on top
Expand All @@ -238,8 +241,8 @@ describe('Plotter routine', () => {

// Move mouse to estimated tooltip position
cy.get('.apexcharts-canvas').trigger('mousemove', {
clientX: xCoordinates[pointsIndex],
clientY: 150,
clientX: coordinates[pointsIndex].x,
clientY: coordinates[pointsIndex].y,
});

points.forEach((point, pointIndex) => {
Expand All @@ -259,4 +262,35 @@ describe('Plotter routine', () => {
});
});
});

it('should open and close the advanced modal', () => {
cy.get('app-plotter-component ion-button [name="settings-outline"]')
.parent()
.click();
cy.get('ion-modal ion-toolbar ion-title').should(
'contain',
'Advanced Plotter Settings'
);
cy.get('ion-modal ion-toolbar ion-button').click();
cy.get('ion-modal').should('not.be.visible');
});

it('should use timestamps if it is set', () => {
cy.get('app-plotter-component ion-button [name="settings-outline"]')
.parent()
.click();
cy.get('ion-modal ion-toolbar ion-title').should(
'contain',
'Advanced Plotter Settings'
);

cy.getAdvancedModalCheckboxElement(
'plotter-advanced-modal',
'Use sample counter'
).click();

cy.get('ion-modal ion-toolbar ion-button').click();

cy.get('.apexcharts-xaxis-title-text').should('contain.text', 'Time (ms)');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<ion-modal #plotterAdvancedModal id="plotter-advanced-modal">
<ng-template>
<ion-header>
<ion-toolbar color="primary">
<ion-title>Advanced Plotter Settings</ion-title>
<ion-buttons slot="end">
<ion-button (click)="plotterAdvancedModal.dismiss()"
>Close</ion-button
>
</ion-buttons>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-list lines="full">
<ion-item>
<ion-checkbox
justify="space-between"
checked="{{ plotterOptions().useSampleCount }}"
(ionChange)="onChangeUseSampleCount($event)"
>Use sample counter<ion-icon
color="warning"
class="tooltipIcon-top"
name="help-circle-outline"
matTooltip="The system may not be able to capture the time of the data event on fast data rates. Use this option to use the sample count instead of the timestamp for the xaxis. You will still be able to see the timestamp information when the chart is paused "
></ion-icon
></ion-checkbox>
</ion-item>

<ion-item>
<ion-range
labelPlacement="start"
label="Scrollback size"
[pin]="true"
[snaps]="true"
[min]="0"
[max]="4"
[step]="1"
[pinFormatter]="pinFormatter"
[value]="
NUMBER_OF_POINTS_VALUES.indexOf(plotterOptions().numberOfPoints)
"
(ionKnobMoveEnd)="onChangeNumberOfPoints($event)"
>
</ion-range>
</ion-item>
</ion-list>
</ion-content>
</ng-template>
</ion-modal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Component, model, output, viewChild } from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import {
IonButton,
IonButtons,
IonCheckbox,
IonContent,
IonHeader,
IonItem,
IonList,
IonModal,
IonRange,
IonTitle,
IonToolbar,
} from '@ionic/angular/standalone';

import {
NUMBER_OF_POINTS_VALUES,
type PlotterOptions,
} from '../../interfaces/app.interface';
import {
type CheckboxCustomEvent,
type RangeCustomEvent,
} from '../../interfaces/ionic.interface';

@Component({
selector: 'app-plotter-advanced-modal-component',
templateUrl: 'plotter-advanced-modal.component.html',
styleUrls: ['./plotter-advanced-modal.component.scss'],
imports: [
IonButton,
IonButtons,
IonCheckbox,
IonContent,
IonHeader,
IonItem,
IonList,
IonModal,
IonRange,
IonTitle,
IonToolbar,
MatTooltipModule,
],
})
export class PlotterAdvancedComponent {
plotterOptions = model.required<PlotterOptions>();
clearChart = output<void>();

plotterAdvancedModal = viewChild.required<IonModal>('plotterAdvancedModal');

NUMBER_OF_POINTS_VALUES = NUMBER_OF_POINTS_VALUES;

onChangeUseSampleCount(event: CheckboxCustomEvent<boolean>): void {
const selectedOption = event.detail.checked;
this.plotterOptions.update((plotterOptions) => ({
...plotterOptions,
useSampleCount: selectedOption,
}));

this.clearChart.emit();
}

onChangeNumberOfPoints(event: RangeCustomEvent): void {
const index = event.detail.value as number;
const selectedOption = NUMBER_OF_POINTS_VALUES[index];
this.plotterOptions.update((plotterOptions) => ({
...plotterOptions,
numberOfPoints: selectedOption,
}));
}

pinFormatter(index: number): string {
const rangeValues = NUMBER_OF_POINTS_VALUES;
return `${rangeValues[index]}`;
}
}
Loading

0 comments on commit 0a33b8f

Please sign in to comment.