diff --git a/.gitignore b/.gitignore index 315cf68351..dcba4db7f7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ target/ /WebContent/assets/.sass-cache/ /WebContent/images/oldicons /WebContent/resources/app/bower_components/ +/WebContent/resources/js-ui/ # OS Files # .DS_Store *.class diff --git a/.travis.yml b/.travis.yml index 55f4bd7676..8972767972 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,9 +20,7 @@ before_script: - cd ../../.. - cd ./scadalts-ui - npm install -- cd ../WebContent/resources/npm/ -- npm install -- cd ../../.. +- cd ../ language: java jdk: - openjdk8 @@ -38,6 +36,7 @@ branches: - "/^feature/#1009_History_of_pointvalue_No_annotation.*$/" - "/^feature_s/#1094_.*$/" - "/^feature_s/#1107_.*$/" + - "/^feature_s/#1115.*$/" - "/^feature/#1130.*$/" - "/^feature_s/#1128_.*$/" notifications: @@ -81,4 +80,4 @@ after_script: - export IMAGE_NAME=scadalts/scadalts - docker build -t $IMAGE_NAME:$TRAVIS_COMMIT . - docker tag $IMAGE_NAME:$TRAVIS_COMMIT $IMAGE_NAME:$TAG -- docker push $IMAGE_NAME:$TAG \ No newline at end of file +- docker push $IMAGE_NAME:$TAG diff --git a/WebContent/WEB-INF/classes/messages_de.properties b/WebContent/WEB-INF/classes/messages_de.properties index eaf117176e..80cec7980e 100644 --- a/WebContent/WEB-INF/classes/messages_de.properties +++ b/WebContent/WEB-INF/classes/messages_de.properties @@ -3163,6 +3163,4 @@ event.reactivation.sleep=Data source has been sleeped event.ds.describe={1} ds.state.sleep=Data source has been sleeped after several attempted connections had failed ds.state.startSleep=Data source has been started after sleeped - - - +header.watchlistModern=Moderne Ueberwachungsliste \ No newline at end of file diff --git a/WebContent/WEB-INF/classes/messages_en.properties b/WebContent/WEB-INF/classes/messages_en.properties index 4cf859e46f..4e2e4d4d46 100644 --- a/WebContent/WEB-INF/classes/messages_en.properties +++ b/WebContent/WEB-INF/classes/messages_en.properties @@ -3167,4 +3167,5 @@ dsList.statusDescribe=Status description event.reactivation.sleep=Data source has been sleeped event.ds.describe={1} ds.state.startSleep=Data source has been started after sleeped -ds.state.sleep=Data source has been sleeped after several attempted connections had failed \ No newline at end of file +ds.state.sleep=Data source has been sleeped after several attempted connections had failed +header.watchlistModern=Modern Watch List \ No newline at end of file diff --git a/WebContent/WEB-INF/classes/messages_pl.properties b/WebContent/WEB-INF/classes/messages_pl.properties index 05aa4c5283..256b258d78 100644 --- a/WebContent/WEB-INF/classes/messages_pl.properties +++ b/WebContent/WEB-INF/classes/messages_pl.properties @@ -3028,4 +3028,5 @@ dsList.statusDescribe=Status description event.reactivation.sleep=Data source has been sleeped event.ds.describe={1} ds.state.startSleep=Data source has been started after sleeped -ds.state.sleep=Data source has been sleeped after several attempted connections had failed \ No newline at end of file +ds.state.sleep=Data source has been sleeped after several attempted connections had failed +header.watchlistModern=Modern Watch List \ No newline at end of file diff --git a/WebContent/WEB-INF/dox/en/editingModernCharts.htm b/WebContent/WEB-INF/dox/en/editingModernCharts.htm new file mode 100644 index 0000000000..4e534fb2a4 --- /dev/null +++ b/WebContent/WEB-INF/dox/en/editingModernCharts.htm @@ -0,0 +1,137 @@ +

Modern Charts Components +

+

February 2020 - Version 1.0.2

+

ScadaLTS modern charts components it is a set of new VueJS v2.0 components designed for GraphicalView in ScadaLTS. It + is based on + am4chart library. It generates charts using JavaScript from user-side which + is a new approach to charts in Scada (they were generated via server-side scripts and libraries). It is more browser + load than it was before, but server application becomes lighter and gains performance.

+

Types of charts:

+ +

Usage:

+

New charts could be added to ScadaLTS Graphical View by adding a new HTML component with specific content. Each chart + has to be + initialized by using this listed above Extended HTML Tags. Each of this tag take a specific properties required to + set up specific chart. + Chart is generated inside this tag which has default size 750x500px.

+
+

Quick start:

+

Create simple line chart for specific [ numeric | multistate | binary ] data point.

+
<div id="chart-line-0" point-id="[dataPointID]"/>
+
+

or

+
<div id="chart-line-0" point-xid="[dataPointExportID]"/>
+
+

That’s it!
+ It has rendered line chart for specific point from last hour with default parameters. So if you want to monitor the + state of the point from last hour it is the simplest way how to do it. This chart could be zoomed in and out using + scrollbar at the bottom of the component. Values of data point in time are represented by white dots on the chart. +

+

But it is still just a chart like this old ones… What + if we really want to monitor status of this point in real-time? No problem just add next + properties.

+
+

Live Data

+
<div id="chart-line-0" point-id="[dataPointID]" refresh-rate="10000"/>
+
+

Now we’ve got live chart!
+ It is refreshed every 10s (10000 ms) and when a data point will change state to different value this new one will be + added to chart and the oldest one will be deleted from out chart. Now we can monitor state of datapoint in real-time + with chosen by us refresh rate. For critical data, we can monitor the status of the point with a high frequency of + queries to server (more real-time data but more resource consuming) and for non-critical data we can refresh chart + after a few seconds.

+

But what if we want to display chart for multiple data points?

+
+

Multiple points

+

Just add next data point after comma in ‘point-id’ property.

+
<div id="chart-step-line-0" point-id="[dataPointID],[anotherDataPointID],[andNextDataPointID"],[fourthDataPointID"]/>
+
+


+ Now we have chart for 3 data points with values from last 1 hour. This components do not have limitations for a + count of points displayed on the one chart, but I hope that you have an intuition that 30 point on a single chart is + not a wise move.

+

Can we display older values than last one hour?

+
+

Specified time period

+

Yes! Just add a new property to our tag.

+
<div id="chart-line-0" point-xid="[dataPointExportID]" refresh-rate="10000" start-date="1-day"/>
+
+

As you can see it’s a piece of cake. Just type inside + ‘start-date’ property, time period from which you want to see the data. You can use a every combination of numbers + with specific time period [ hour(s) | day(s) | weak(s) | month(s) ]. (eg. ‘2-days’, ‘1-week’, + ‘3-months’ etc.) But it is not everything! It is dynamic calculated time from now but we can also use a specific + date. If we want see data from beginning of the previous year just type in date (eg. ‘2019/02/01’ to see data + beginning from 1-st February 2019). It could be useful to limit displayed data.

+

To display values from specified period just add ‘end-date’ parameter.

+
<div id="chart-line-0" point-xid="[dataPointExportID]" start-date="2019/02/01" end-date="2019/03/01"/>
+
+

And it still works with multiple data points. It’s great! Isn’t it?
+ But what if I want to add a horizontal line to chart to create for example warning level, which of it is exceeded it + could be dangerous?

+
+

Level range line

+

Ok let’s consider this one:

+
<div id="chart-line-0" point-id="[dataPointID]" range-value="100" range-color="#FF0000" range-label="boiling"/>
+
+

Now we have created horizontal line for our chart which + indicates boiling level for water. Thanks to that we can quickly observe that temperature of water inside tank is + boiling. It is useful even inside ScadaLTS.

+

Wait a moment! We decided which color this horizontal line would have. Could we do the same with chart lines?

+
+

Chart Colors

+

For example we have got 3 sensors. This default green colors are too similar. Can we set up a different color set for our charts. Just add this parameter:

+
<div id="chart-line-0" point-id="[dpID],[dpID_2],[dpID_3]" color="#FFFC19, #0971B3, #B31212"/>
+
+

Now we have got defined 3 custom colors for our charts. + We can give just a one color value and the rest will be retrieved from this default values. What is the most + important… USE HEXADECIMAL COLOR CODE VALUES
+ Pretty colorful Modern Charts. But we still have the same size for them… Yes, yes it also could be changed.

+
+

Chart Size

+
<div id="chart-step-line-0" point-id="[dpID]" width="1080" height="720"/>
+
+

HD Chart? Why not! Values for attributes are given in Pixels (px). That is useful when we have defined a multiple chart instances on one GraphicalView. We can easily + calculate the position of the next chart.

+

Labels

+
<div id="chart-step-line-0" point-id="[dpID]" label="Mid season temperature"/>
+
+

That would be enough from the basics… It is time for more complex tasks.

+
+

Multiple charts

+

To generate multiple charts on View page just use unique identifiers.

+
<div id="chart-step-line-0" point-id="[dpID]" label="Outdoor temperature"/>
+
+<div id="chart-step-line-1" point-id="[dpID]" label="Outdoor humidity"/>
+
+<div id="chart-step-line-2" point-id="[dpID]" label="Indoor pressure"/>
+
+

+

Modern Chart documentation:

+

Available properties in one place for all chart types. Charts (excluding Gauge Charts) could be exported to external file + in graphical or text way. You can export to *.png, *.jpg, *.csv, *.json files.

+

Properties properties for Step Line, Line charts

+ +

Author

+ \ No newline at end of file diff --git a/WebContent/WEB-INF/dox/manifest.xml b/WebContent/WEB-INF/dox/manifest.xml index 2e6b3366dd..7ac0695ae4 100644 --- a/WebContent/WEB-INF/dox/manifest.xml +++ b/WebContent/WEB-INF/dox/manifest.xml @@ -229,8 +229,11 @@ + + + diff --git a/WebContent/WEB-INF/jsp/include/vue/vue-charts.js.jsp b/WebContent/WEB-INF/jsp/include/vue/vue-charts.js.jsp new file mode 100644 index 0000000000..fdbae4964e --- /dev/null +++ b/WebContent/WEB-INF/jsp/include/vue/vue-charts.js.jsp @@ -0,0 +1,2 @@ + + diff --git a/WebContent/WEB-INF/jsp/include/vue/vue-modern-wl.js.jsp b/WebContent/WEB-INF/jsp/include/vue/vue-modern-wl.js.jsp new file mode 100644 index 0000000000..c92cf0b43a --- /dev/null +++ b/WebContent/WEB-INF/jsp/include/vue/vue-modern-wl.js.jsp @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/WebContent/WEB-INF/jsp/views.jsp b/WebContent/WEB-INF/jsp/views.jsp index 104fb286c7..fee234025c 100644 --- a/WebContent/WEB-INF/jsp/views.jsp +++ b/WebContent/WEB-INF/jsp/views.jsp @@ -282,4 +282,5 @@ <%@ include file="/WEB-INF/jsp/include/vue/vue-app.js.jsp"%> -<%@ include file="/WEB-INF/jsp/include/vue/vue-view.js.jsp"%> \ No newline at end of file +<%@ include file="/WEB-INF/jsp/include/vue/vue-view.js.jsp"%> +<%@ include file="/WEB-INF/jsp/include/vue/vue-charts.js.jsp"%> \ No newline at end of file diff --git a/WebContent/WEB-INF/jsp/watchListModern.jsp b/WebContent/WEB-INF/jsp/watchListModern.jsp new file mode 100644 index 0000000000..63e0b2a493 --- /dev/null +++ b/WebContent/WEB-INF/jsp/watchListModern.jsp @@ -0,0 +1,764 @@ +<%-- + Mango - Open Source M2M - http://mango.serotoninsoftware.com + Copyright (C) 2006-2011 Serotonin Software Technologies Inc. + @author Matthew Lohbihler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/. +--%> +<%@ include file="/WEB-INF/jsp/include/tech.jsp" %> +<%@page import="com.serotonin.mango.Common"%> +<%@page import="com.serotonin.mango.view.ShareUser"%> + + + + + + + + + + + + + + + + +
+
+
+
+ + + +
+
+ + + + + +
+ + + ${sst:escapeLessThan(wl.value)} + + + +
+ + +
+ +
+ + +
+ + + +
+
+ + + + + + + + + + + + +
+ + + + + +
+
+ "/> + +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+<%@ include file="/WEB-INF/jsp/include/vue/vue-app.js.jsp"%> +<%@ include file="/WEB-INF/jsp/include/vue/vue-modern-wl.js.jsp"%> diff --git a/WebContent/WEB-INF/springDispatcher-servlet.xml b/WebContent/WEB-INF/springDispatcher-servlet.xml index 4313b77195..abd988766e 100644 --- a/WebContent/WEB-INF/springDispatcher-servlet.xml +++ b/WebContent/WEB-INF/springDispatcher-servlet.xml @@ -54,6 +54,7 @@ usersProfilesController viewsController watchListController + modernWatchListController webcamLiveFeedController projectExporterController projectImporterController @@ -180,6 +181,10 @@ watchList + + + watchListModern + webcamLiveFeed diff --git a/WebContent/WEB-INF/tags/page.tag b/WebContent/WEB-INF/tags/page.tag index b4a595643c..63420e84db 100644 --- a/WebContent/WEB-INF/tags/page.tag +++ b/WebContent/WEB-INF/tags/page.tag @@ -164,6 +164,7 @@ + diff --git a/WebContent/images/watch_list.png b/WebContent/images/watch_list.png new file mode 100644 index 0000000000..44ce68ee88 Binary files /dev/null and b/WebContent/images/watch_list.png differ diff --git a/build.xml b/build.xml index 34d211de87..23f91e938e 100644 --- a/build.xml +++ b/build.xml @@ -271,6 +271,8 @@ + + @@ -284,6 +286,9 @@ + + + @@ -335,6 +340,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/scadalts-ui/.gitignore b/scadalts-ui/.gitignore index 724c3febcd..1998a95939 100644 --- a/scadalts-ui/.gitignore +++ b/scadalts-ui/.gitignore @@ -4,6 +4,8 @@ node_modules /tests/e2e/videos/ /tests/e2e/screenshots/ +/cypress/videos +/cypress/screenshots # local env files .env.local diff --git a/scadalts-ui/cypress.json b/scadalts-ui/cypress.json index 470c720199..5c635794bd 100644 --- a/scadalts-ui/cypress.json +++ b/scadalts-ui/cypress.json @@ -1,3 +1,3 @@ { - "pluginsFile": "tests/e2e/plugins/index.js" + "baseUrl": "http://localhost:8080/ScadaBR" } diff --git a/scadalts-ui/cypress/fixtures/example.json b/scadalts-ui/cypress/fixtures/example.json new file mode 100644 index 0000000000..da18d9352a --- /dev/null +++ b/scadalts-ui/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/scadalts-ui/cypress/integration/ScadaLTS_Views/datasource_creation.spec.js b/scadalts-ui/cypress/integration/ScadaLTS_Views/datasource_creation.spec.js new file mode 100644 index 0000000000..6adc371162 --- /dev/null +++ b/scadalts-ui/cypress/integration/ScadaLTS_Views/datasource_creation.spec.js @@ -0,0 +1,71 @@ +context('Create Datasource', () => { + + function createVirtualDataSource(name) { + cy.visit('data_sources.shtm'); + cy.get("#dataSourceTypes").select('Virtual Data Source').should('have.value', '1'); + cy.get('img.ptr[src="images/icon_ds_add.png"]').first().click(); + cy.get('.smallTitle').should('contain', 'Virtual data source properties') + cy.get('#dataSourceName').type(name) + cy.get('#updatePeriods').type("{backspace}1").should('have.value', '1'); + cy.get('img.ptr[src="images/save.png"]').first().click(); + cy.get('#dataSourceMessage').should('contain', 'Data source has been saved') + // cy.debug('DataSource created!') + } + + /** + * + * @param {*} name DataPoint name + * @param {*} type [Multistate = 2| Numeric = 3] + */ + function addVirtualDataPoint(name, type) { + cy.get('img.ptr[src="images/icon_comp_add.png"]').click(); + cy.get('input#name').type(name) + cy.get('input#settable').click() + cy.get('select#dataTypeId').select(type) + if (type === '3') { + cy.wait(500) + cy.get('select#changeTypeId').select('Random') + cy.get('#divCH6').children().first().next().find('input').type("{backspace}20") + cy.get('#divCH6').children().first().next().next().find('input').type("2") + } else if (type === '2') { + cy.get('select#changeTypeId').select('8') + cy.get('input#randomMultistate').type("1") + for (let x = 1; x <= 5; x = x + 1) { + cy.get('img.ptr[src="images/add.png"]').click() + } + cy.get('select#randomMultistateChange.startValue').select('1') + } + cy.get('img.ptr#pointSaveImg').click() + cy.get('#pointMessage').should('contain', 'Point details saved') + // cy.debug(`DataPoint ${name} created!`) + } + + function login(username, password) { + cy.visit('/login.htm') + cy.get('input#username').type(username) + cy.get('input#password').type(password) + cy.get('.login-button > input').click() + } + + before(() => { + login("admin", "admin") + }) + + describe("Create Test Datasource", function () { + + it('Create datasource with 10 numeric datapoints', function () { + const count = 10; + cy.visit('/data_sources.shtm') + createVirtualDataSource(`Test-${new Date().toISOString()}`) + for (let i = 0; i < count; i = i + 1) { + addVirtualDataPoint(`0${i}-Test`, '3') + } + cy.get('#pointsList').children().should('have.length', count) + cy.get('img.ptr#enableAllImg').click(); + cy.get('#pointsList').find('img[src="images/brick_go.png"]').should('have.length', count) + cy.get('img.ptr#dsStatusImg').click(); + }) + + }) + +}) \ No newline at end of file diff --git a/scadalts-ui/cypress/integration/ScadaLTS_Views/modern_charts.spec.js b/scadalts-ui/cypress/integration/ScadaLTS_Views/modern_charts.spec.js new file mode 100644 index 0000000000..d27cc3c2da --- /dev/null +++ b/scadalts-ui/cypress/integration/ScadaLTS_Views/modern_charts.spec.js @@ -0,0 +1,161 @@ +context('Verify Modern Watch List Page and Modern Charts', () => { + + before(() => { + cy.clearCookies() + cy.visit('/login.htm') + cy.get('input#username').type("admin").should('have.value', 'admin') + cy.get('input#password').type("admin").should('have.value', 'admin') + cy.get('.login-button > input').click() + cy.location('pathname').should('include', 'watch_list') + }) + + describe("Modern Watch List", function () { + + it('Open Modern Watch List Page', function () { + cy.visit('/modern_watch_list.shtm') + cy.location('pathname').should('include', 'modern_watch_list.shtm') + }) + it("Validate Modern Charts Vue component exist", function () { + cy.get(".smallTitle").should('contain', 'Modern Chart') + }) + }) + + describe("Chart with 1 datapoint", function () { + + it('Create chart', function () { + cy.get('#watchListSelect').select("Test_WL_1"); + cy.get('.scada-widget .settings').find('button').first().click() + cy.get('.hello').find('svg') + }) + it('Modify chart - set to Line Chart', function () { + cy.get('.settings-btn[src="/ScadaBR/images/cog.png"]').click() + cy.get('.radio-button[value="line"]').click() + cy.get('.settings-btn[src="/ScadaBR/images/accept.png"]').click() + }) + it('Modify chart - set to Step Line Chart', function () { + cy.get('.settings-btn[src="/ScadaBR/images/cog.png"]').click() + cy.get('.radio-button[value="stepLine"]').click() + cy.get('.settings-btn[src="/ScadaBR/images/accept.png"]').click() + }) + it('Modify chart - change Chart Color', function () { + cy.get('.settings-btn[src="/ScadaBR/images/cog.png"]').click() + cy.get('.farbtastic-overlay').click(50, 2) + cy.get('.farbtastic-solid').should('have.css', 'background-color').and('eq', 'rgb(255, 3, 0)') + cy.get('.settings-btn[src="/ScadaBR/images/accept.png"]').click() + }) + it('Modify chart - change to Static Chart', function () { + cy.get('.settings-btn[src="/ScadaBR/images/cog.png"]').click() + cy.get('.radio-button[value="live"]').click() + cy.get('.settings-btn[src="/ScadaBR/images/accept.png"]').click() + }) + it('Modify chart - change values from last: 6 hours', function () { + cy.get('.settings-btn[src="/ScadaBR/images/cog.png"]').click() + cy.get('#live-sd').type('{backspace}6') + cy.get('#live-sd').should('have.value', '6') + cy.get('#live-sd').next().select('hour') + cy.get('#live-sd').next().should('have.value', 'hour') + cy.get('.settings-btn[src="/ScadaBR/images/accept.png"]').click() + }) + it('Modify chart - change values from last: 1 day', function () { + cy.get('.settings-btn[src="/ScadaBR/images/cog.png"]').click() + cy.get('#live-sd').type('{backspace}1') + cy.get('#live-sd').should('have.value', '1') + cy.get('#live-sd').next().select('day') + cy.get('#live-sd').next().should('have.value', 'day') + cy.get('.settings-btn[src="/ScadaBR/images/accept.png"]').click() + }) + it('Modify chart - change to Static Chart', function () { + cy.get('.settings-btn[src="/ScadaBR/images/cog.png"]').click() + cy.get('.radio-button[value="static"]').click() + }) + it('Modify chart - cancel', function () { + cy.get('.settings-btn[src="/ScadaBR/images/cross.png"]').click() + cy.get('.settings-btn[src="/ScadaBR/images/cog.png"]').click() + cy.get('.radio-button[value="live"]').should('not.be.checked') + cy.get('.settings-btn[src="/ScadaBR/images/cross.png"]').click() + }) + it('Delete chart', function () { + cy.get('.settings-btn[src="/ScadaBR/images/delete.png"]').click() + cy.get('.chart-container horizontal').should('not.contain', 'svg') + }) + }) + + describe('Chart with multiple datapoints', function () { + it('Create chart with 5 numeric datapoints', function () { + cy.get('#watchListSelect').select("Test_WL_5"); + cy.get('.scada-widget .settings').find('button').first().click() + cy.get('.hello').find('svg') + cy.get('.settings-btn[src="/ScadaBR/images/delete.png"]').click() + }) + it('Create chart with 10 numeric datapoints', function () { + cy.get('#watchListSelect').select("Test_WL_10"); + cy.get('.scada-widget .settings').find('button').first().click() + cy.get('.hello').find('svg') + cy.get('.settings-btn[src="/ScadaBR/images/delete.png"]').click() + }) + }) + + describe("Chart memory tests", function () { + it('Modify chart and save. Validate cookie', function () { + cy.get('#watchListSelect').select("Test_WL_1"); + cy.get('.scada-widget .settings').find('button').first().click() + cy.get('.settings-btn[src="/ScadaBR/images/cog.png"]').click() + cy.get('#live-sd').type('{backspace}3') + cy.get('#live-sd').should('have.value', '3') + cy.get('.settings-btn[src="/ScadaBR/images/accept.png"]').click() + cy.getCookie('WatchListChartDashboard_admin').should('exist') + cy.get('.settings-btn[src="/ScadaBR/images/delete.png"]').click() + }) + }) + + describe("Additional test - multiple charts", function () { + it('Create 2 same charts', function () { + cy.get('#watchListSelect').select("Test_WL_1"); + cy.get('.scada-widget .settings').find('button').first().click() + cy.get('.scada-widget .settings').find('button').first().click() + cy.get('.hello').should('have.length', 2) + cy.get('.settings-btn[src="/ScadaBR/images/delete.png"]').click({ multiple: true }) + cy.get('.hello').should('not.exist') + }) + it('Create 2 different charts', function () { + cy.get('#watchListSelect').select("Test_WL_5"); + let pointList = cy.get(".dojoTreeNodeLabelTitle"); + let count = 5; + pointList.get('img[src="images/bullet_go.png"]').each(($el, index, $list) => { + if (count > 0) { + cy.wrap($el).click() + count = count - 1 + } + }) + cy.get('#watchListTable').get('input[type="checkbox"]').click({ multiple: true, force: true }); + cy.get('#watchListTable').get('input[type="checkbox"]').first().click({ force: true }); + cy.get('.scada-widget .settings').find('button').first().click() + cy.get('#watchListTable').get('input[type="checkbox"]').first().click({ force: true }); + cy.get('#watchListTable').get('input[type="checkbox"]').eq(1).click({ force: true }); + cy.get('.scada-widget .settings').find('button').first().click() + cy.get('.hello').should('have.length', 2) + cy.get('.settings-btn[src="/ScadaBR/images/delete.png"]').click({ multiple: true }) + cy.get('.hello').should('not.exist') + }) + it('Create 4 different charts', function () { + cy.get('#watchListSelect').select("Test_WL_10"); + let pointList = cy.get(".dojoTreeNodeLabelTitle"); + let count = 5; + pointList.get('img[src="images/bullet_go.png"]').each(($el, index, $list) => { + if (count > 0) { + cy.wrap($el).click() + count = count - 1 + } + }) + cy.get('#watchListTable').get('input[type="checkbox"]').click({ multiple: true, force: true }); + for (let x = 0; x < 4; x = x + 1) { + cy.get('#watchListTable').get('input[type="checkbox"]').eq(x).click({ force: true }); + cy.get('.scada-widget .settings').find('button').first().click() + cy.get('#watchListTable').get('input[type="checkbox"]').eq(x).click({ force: true }); + } + cy.get('.hello').should('have.length', 4) + }) + + }) + +}) \ No newline at end of file diff --git a/scadalts-ui/cypress/integration/ScadaLTS_Views/watch_list.spec.js b/scadalts-ui/cypress/integration/ScadaLTS_Views/watch_list.spec.js new file mode 100644 index 0000000000..b6077f5608 --- /dev/null +++ b/scadalts-ui/cypress/integration/ScadaLTS_Views/watch_list.spec.js @@ -0,0 +1,43 @@ +context('Create WatchList', () => { + + function createWatchList(name, count) { + cy.visit('/watch_list.shtm'); + cy.get('img.ptr[src="images/add.png"]').last().click(); + cy.get('#watchListSelect').select("(unnamed)"); + cy.get('#wlEditImg').trigger('mouseover').get('#newWatchListName').type(`{selectall}${name}`); + cy.get('#saveWatchListNameLink').click(); + + let pointList = cy.get(".dojoTreeNodeLabelTitle"); + pointList.get('img[src="images/bullet_go.png"]').each(($el, index, $list) => { + if (count > 0) { + cy.wrap($el).click() + count = count - 1 + } + }) + + } + + function login(username, password) { + cy.visit('/login.htm') + cy.get('input#username').type(username) + cy.get('input#password').type(password) + cy.get('.login-button > input').click() + } + + beforeEach(() => { + login("admin", "admin") + }) + + describe("Create WatchList", function () { + + let watchListState = [1, 5, 10]; + watchListState.forEach(element => { + it(`Create Watch List with ${element} numeric datapoints`, function() { + createWatchList(`Test_WL_${element}`, element); + cy.get('#watchListTable').children().should('have.length', element) + }) + }); + + }) + +}) \ No newline at end of file diff --git a/scadalts-ui/cypress/plugins/index.js b/scadalts-ui/cypress/plugins/index.js new file mode 100644 index 0000000000..aa9918d215 --- /dev/null +++ b/scadalts-ui/cypress/plugins/index.js @@ -0,0 +1,21 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/scadalts-ui/cypress/support/commands.js b/scadalts-ui/cypress/support/commands.js new file mode 100644 index 0000000000..ca4d256f3e --- /dev/null +++ b/scadalts-ui/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/scadalts-ui/cypress/support/index.js b/scadalts-ui/cypress/support/index.js new file mode 100644 index 0000000000..d68db96df2 --- /dev/null +++ b/scadalts-ui/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/scadalts-ui/package.json b/scadalts-ui/package.json index e7684ff8d3..2c16b1366b 100644 --- a/scadalts-ui/package.json +++ b/scadalts-ui/package.json @@ -10,6 +10,7 @@ "test:unit": "vue-cli-service test:unit" }, "dependencies": { + "@amcharts/amcharts4": "4.8.9", "ag-grid": "18.1.2", "ag-grid-community": "21.2.2", "ag-grid-vue": "21.2.2", @@ -21,12 +22,15 @@ "moment": "2.22.2", "uiv": "0.27.0", "vue": "2.6.10", + "vue-color-picker-wheel": "^0.4.3", + "vue-cookie": "^1.1.4", "vue-jsoneditor": "1.0.13", "vue-loader": "15.7.1", "vue-lodash": "2.0.2", "vue-property-decorator": "7.3.0", "vue-router": "3.0.1", "vue-underscore": "0.1.4", + "vuejs-datepicker": "^1.6.2", "vuejs-logger": "1.5.3", "vuex": "3.0.1" }, @@ -39,8 +43,10 @@ "@vue/test-utils": "1.0.0-beta.29", "babel-eslint": "10.0.1", "chai": "4.1.2", + "cypress": "^4.0.1", "eslint": "5.16.0", "eslint-plugin-vue": "5.0.0", + "http-proxy-middleware": "^0.20.0", "sass": "1.18.0", "sass-loader": "7.1.0", "vue-template-compiler": "2.6.10" diff --git a/scadalts-ui/src/assets/doc/watch_list/MWL_4-DataPoints.gif b/scadalts-ui/src/assets/doc/watch_list/MWL_4-DataPoints.gif new file mode 100644 index 0000000000..8eed603371 Binary files /dev/null and b/scadalts-ui/src/assets/doc/watch_list/MWL_4-DataPoints.gif differ diff --git a/scadalts-ui/src/assets/doc/watch_list/MWL_AddChart.gif b/scadalts-ui/src/assets/doc/watch_list/MWL_AddChart.gif new file mode 100644 index 0000000000..acc607deae Binary files /dev/null and b/scadalts-ui/src/assets/doc/watch_list/MWL_AddChart.gif differ diff --git a/scadalts-ui/src/assets/doc/watch_list/MWL_CompareCharts.gif b/scadalts-ui/src/assets/doc/watch_list/MWL_CompareCharts.gif new file mode 100644 index 0000000000..3aa191e907 Binary files /dev/null and b/scadalts-ui/src/assets/doc/watch_list/MWL_CompareCharts.gif differ diff --git a/scadalts-ui/src/assets/doc/watch_list/MWL_Muilistate.gif b/scadalts-ui/src/assets/doc/watch_list/MWL_Muilistate.gif new file mode 100644 index 0000000000..348a42a817 Binary files /dev/null and b/scadalts-ui/src/assets/doc/watch_list/MWL_Muilistate.gif differ diff --git a/scadalts-ui/src/assets/doc/watch_list/MWL_MultistateNumeric.gif b/scadalts-ui/src/assets/doc/watch_list/MWL_MultistateNumeric.gif new file mode 100644 index 0000000000..bb792a2dfa Binary files /dev/null and b/scadalts-ui/src/assets/doc/watch_list/MWL_MultistateNumeric.gif differ diff --git a/scadalts-ui/src/assets/doc/watch_list/MWL_Navigate.gif b/scadalts-ui/src/assets/doc/watch_list/MWL_Navigate.gif new file mode 100644 index 0000000000..740772e8b7 Binary files /dev/null and b/scadalts-ui/src/assets/doc/watch_list/MWL_Navigate.gif differ diff --git a/scadalts-ui/src/assets/doc/watch_list/MWL_StepLine.gif b/scadalts-ui/src/assets/doc/watch_list/MWL_StepLine.gif new file mode 100644 index 0000000000..66a8394f06 Binary files /dev/null and b/scadalts-ui/src/assets/doc/watch_list/MWL_StepLine.gif differ diff --git a/scadalts-ui/src/components/amcharts/BaseChart.js b/scadalts-ui/src/components/amcharts/BaseChart.js new file mode 100644 index 0000000000..2e19f97a90 --- /dev/null +++ b/scadalts-ui/src/components/amcharts/BaseChart.js @@ -0,0 +1,571 @@ +/** + * @fileoverview BaseChart Class Definition. + * @author Radoslaw Jajko + * @version 1.0.0 + * + * @requires am4core + * @requires am4charts + * @requires Axios + */ +import * as am4core from "@amcharts/amcharts4/core"; +import * as am4charts from "@amcharts/amcharts4/charts"; +import Axios from "axios"; + +/** + * BaseChart class allows to create many of am4chart chart types. Base chart is based on line chart + * but it could be extended by child classes to handle more complex chart definitions. + * @class + */ +export default class BaseChart { + + //xcopy .\dist\static C:\services\tomcat7.0\webapps\ScadaLTS\resources\new-ui\ /E /K /Y + /** + * + * @param {any} chartReference Id of DOM element where this char will be initialized + * @param {String} chartType [ XYChart | PieChart | GaugeChart] available chart types + * @param {String} colors Hex value of base chart color. + * @param {String} [domain] Protocol, domain and the address of the API interface + */ + constructor(chartReference, chartType, colors, domain = '.') { + + if (chartType === "XYChart") { + this.chart = am4core.create(chartReference, am4charts.XYChart) + } else if (chartType === "PieChart") { + this.chart = am4core.create(chartReference, am4charts.PieChart); + } else if (chartType === "GaugeChart") { + this.chart = am4core.create(chartReference, am4charts.GaugeChart); + } + this.pointPastValues = new Map(); + this.pointCurrentValue = new Map(); + this.liveUpdatePointValues = new Map(); + this.lastUpdate = 0; + this.lastTimestamp = new Date().getTime(); + this.liveUpdateInterval = 5000; + this.domain = domain; + let colorPallete = [ + am4core.color("#39B54A"), + am4core.color("#69FF7D"), + am4core.color("#166921"), + am4core.color("#690C24"), + am4core.color("#B53859"), + am4core.color("#734FC1"), + am4core.color("#824F1B"), + am4core.color("#69421B"), + ]; + if (colors !== undefined && colors !== null) { + colors = colors.split(","); + if (colors.length > 0) { + for (let i = colors.length - 1; i >= 0; i--) { + colorPallete.unshift(am4core.color(colors[i].trim())); + } + } + } + this.chart.colors.list = colorPallete; + this.yAxesCount = 0; + } + + /** + * Main method to display chart + * Before launch: Load data from API + */ + showChart() { + this.setupChart() + } + + /** + * Download from API DataPoint values from specific time period. + * Override this method in children classes to prepare data for displaying in charts + * + * @param {Number} pointId Data Point ID number + * @param {Number} [startTimestamp] Default get values from 1 hour ago + * @param {Number} [endTimestamp] Default get values till now + */ + loadData(pointId, startTimestamp, endTimestamp, exportId) { + if (startTimestamp === undefined || startTimestamp === null) { + startTimestamp = new Date().getTime() - 3600000; + endTimestamp = new Date().getTime(); + } else if ((startTimestamp !== undefined && startTimestamp !== null) && (endTimestamp === undefined || endTimestamp === null)) { + startTimestamp = BaseChart.convertDate(startTimestamp); + endTimestamp = new Date().getTime(); + if (isNaN(startTimestamp)) { + console.warn("Parameter start-date is not valid!\nConnecting to API with default values"); + startTimestamp = new Date().getTime() - 3600000; + } + } else if ((startTimestamp !== undefined && startTimestamp !== null) && (endTimestamp !== undefined && endTimestamp !== null)) { + startTimestamp = new Date(startTimestamp).getTime(); + endTimestamp = new Date(endTimestamp).getTime(); + if (isNaN(startTimestamp) || isNaN(endTimestamp)) { + console.warn("Parameter [start-date | end-date] are not valid!\nConnecting to API with default values") + startTimestamp = new Date().getTime() - 3600000; + endTimestamp = new Date().getTime(); + } + } + let url; + if (exportId) { + url = `${this.domain}/api/point_value/getValuesFromTimePeriod/xid/${pointId}/${startTimestamp}/${endTimestamp}` + } else { + url = `${this.domain}/api/point_value/getValuesFromTimePeriod/${pointId}/${startTimestamp}/${endTimestamp}` + } + return new Promise((resolve, reject) => { + try { + Axios.get(url, { timeout: 5000, useCredentails: true, credentials: 'same-origin' }).then(response => { + resolve(response.data); + }).catch(webError => { + reject(webError) + }) + } catch (error) { + reject(error) + } + }) + } + + /** + * Start LiveChart Interval + * + * @param {Number} refreshRate How often send request for a new data. + */ + startLiveUpdate(refreshRate, exportId) { + this.liveUpdateInterval = setInterval(() => { + // this.loadLiveData() + this.refreshPointValues(exportId); + }, refreshRate); + } + + /** + * Connect with API and parse new data for chart + * @deprecated + */ + loadLiveData() { + for (let [k, v] of this.pointCurrentValue) { + Axios.get(`${this.domain}/api/point_value/getValue/id/${k}`, { timeout: 5000, useCredentails: true, credentials: 'same-origin' }).then(response => { + if (isNaN(response.data.value)) { + response.data.value == "true" ? response.data.value = 1 : response.data.value = 0; + } + let point = { "name": response.data.name, "value": response.data.value }; + if (this.liveUpdatePointValues.get(response.data.ts) == undefined) { + this.liveUpdatePointValues.set(response.data.ts, [point]); + } else { + this.liveUpdatePointValues.get(response.data.ts).push(point); + } + }) + } + this.chart.addData(BaseChart.prepareChartData(BaseChart.sortMapKeys(this.liveUpdatePointValues))) + if (this.liveUpdatePointValues != undefined) { + this.liveUpdatePointValues.clear(); + } + } + + /** + * Get data point data from REST API. + * + * @param {Number} pointId ID of data point. + */ + getPointValue(pointId) { + return new Promise((resolve, reject) => { + try { + Axios.get(`${this.domain}/api/point_value/getValue/id/${pointId}`, { timeout: 5000, useCredentails: true, credentials: 'same-origin' }).then(resp => { + resolve(resp.data); + }).catch(webError => { + reject(webError) + }); + } catch (error) { + reject(error) + } + }) + } + + /** + * Get data point data from REST API. + * + * @param {Number} pointXid Export ID of data point. + */ + getPointValueXid(pointXid) { + return new Promise((resolve, reject) => { + try { + Axios.get(`${this.domain}/api/point_value/getValue/${pointXid}`, { timeout: 5000, useCredentails: true, credentials: 'same-origin' }).then(resp => { + resolve(resp.data); + }).catch(webError => { + reject(webError) + }); + } catch (error) { + reject(error) + } + }) + } + + /** + * + * @param {String} pointId PointExportID or PointID + * @param {Number} startTimestamp LastUpdateTime + * @param {Boolean} exportId Using XID or ID + */ + getPeriodicUpdate(pointId, startTimestamp, exportId) { + let endTimestamp = new Date().getTime(); + let url; + if (exportId) { + url = `${this.domain}/api/point_value/getValuesFromTimePeriod/xid/${pointId}/${startTimestamp}/${endTimestamp}` + } else { + url = `${this.domain}/api/point_value/getValuesFromTimePeriod/${pointId}/${startTimestamp}/${endTimestamp}` + } + return new Promise((resolve, reject) => { + try { + Axios.get(url, { timeout: 5000, useCredentails: true, credentials: 'same-origin' }).then(response => { + resolve(response.data); + }).catch(webError => { + reject(webError) + }) + } catch (error) { + reject(error) + } + }) + } + + /** + * Stategy how to parse data received from API server. + * + * @param {Object} pointValue Object of recived point data value. + * @param {String} pointName Name of datapoint. + * @param {Map} referencedArray Map of values to which we want to save data. [pointPastValues or liveUpdatePoints] + */ + addValue(pointValue, pointName, referencedArray) { + if (isNaN(pointValue.value)) { + pointValue.value == "true" ? pointValue.value = 1 : pointValue.value = 0; + } + let point = { "name": pointName, "value": pointValue.value }; + if (referencedArray.get(pointValue.ts) == undefined) { + referencedArray.set(pointValue.ts, [point]); + } else { + referencedArray.get(pointValue.ts).push(point); + } + } + + /** + * For each defined point get current value and update chart when all request has been recived. + */ + refreshPointValues(exportId) { + let pointData = []; + for (let [k, v] of this.pointCurrentValue) { + if (exportId) { + pointData.push(this.getPeriodicUpdate(k, this.lastTimestamp, exportId).then(data => { + data.values.forEach(e => { + this.addValue(e, data.name, this.liveUpdatePointValues) + }) + })) + } + } + Promise.all(pointData).then(() => { + let lastData = BaseChart.prepareChartData(this.liveUpdatePointValues); + if (lastData.length > 0) { + if (lastData[lastData.length - 1].date > this.lastUpdate) { + this.chart.addData(lastData, 1); + this.lastTimestamp = new Date().getTime(); + this.lastUpdate = lastData[lastData.length - 1].date; + this.liveUpdatePointValues.clear(); + if (lastData != undefined) { + // console.debug(lastData); + // lastData.clear(); + } + } + } + }); + } + + /** + * Clear all data associated with this chart. + */ + stopLiveUpdate() { + clearInterval(this.liveUpdateInterval); + this.pointCurrentValue.clear(); + this.pointPastValues.clear(); + this.liveUpdatePointValues.clear(); + } + + /** + * When all point data has been downloaded, add this to chart data. + */ + setupChart() { + this.chart.data = BaseChart.prepareChartData(BaseChart.sortMapKeys(this.pointPastValues)); + this.createAxisX("DateAxis", null); + this.createAxisY(); + this.createScrollBarsAndLegend(); + this.createExportMenu(); + for (let [k, v] of this.pointCurrentValue) { + this.createSeries("StepLine", "date", v.name, v.name) + } + } + + + /** + * Prepare series for chart. + * + * @param {String} seriesType [ Column | Pie | Line | StepLine ] types + * @param {String} seriesValueX Category name or "date" field in data. + * @param {String} seriesValueY Which values disply in series + * @param {String} seriesName Name of this series. + * @param {string} suffix Additional suffix for series units. [square meteters etc.] + */ + createSeries(seriesType, seriesValueX, seriesValueY, seriesName, suffix = "") { + let series; + if (seriesType === "Column") { + series = this.chart.series.push(new am4charts.ColumnSeries()); + } else if (seriesType === "Line") { + series = this.chart.series.push(new am4charts.LineSeries()); + } else if (seriesType === "StepLine") { + series = this.chart.series.push(new am4charts.StepLineSeries()); + series.startLocation = 0.5; + } else if (seriesType === "Pie") { + series = this.chart.series.push(new am4charts.PieSeries()); + } + + if (seriesType === "Column") { + series.dataFields.categoryX = seriesValueX; + series.dataFields.valueY = seriesValueY; + series.columns.template.tooltipText = "{valueY.value}"; + series.columns.template.tooltipY = 0; + series.columns.template.strokeOpacity = 0; + } else if (seriesType === "Pie") { + series.dataFields.value = seriesValueY; + series.dataFields.category = seriesValueX; + series.slices.template.strokeWidth = 2; + series.slices.template.strokeOpacity = 1; + } else { + series.dataFields.dateX = seriesValueX; + series.dataFields.valueY = seriesValueY; + if (suffix.trim().startsWith("[")) { + suffix = `[${suffix}]`; + } + series.tooltipText = "{name}: [bold]{valueY}[/] " + suffix; + series.tooltip.background.cornerRadius = 20; + series.tooltip.background.strokeOpacity = 0; + series.tooltip.pointerOrientation = "vertical"; + series.tooltip.label.minWidth = 40; + series.tooltip.label.minHeight = 40; + series.tooltip.label.textAlign = "middle"; + series.tooltip.label.textValign = "middle"; + series.strokeWidth = 3; + series.fillOpacity = 0.3; + series.minBulletDistance = 15; + let bullet = series.bullets.push(new am4charts.CircleBullet()); + bullet.circle.strokeWidth = 2; + bullet.circle.radius = 5; + bullet.circle.fill = am4core.color("#fff"); + } + + series.name = seriesName; + + if (this.chart.scrollbarX) { + this.chart.scrollbarX.series.push(series); + } + + return series; + + } + + /** + * Create X data Axis for chart + * @param {String} axisType [ValueAxis | DateAxis | CategoryAxis] Specified types for different chart axes + * @param {String} category When Category Axis has been chosen set the name of category to display. + */ + createAxisX(axisType, category) { + let axis; + if (axisType === "ValueAxis") { + axis = this.chart.xAxes.push(new am4charts.ValueAxis()); + axis.min = 0; + axis.max = 100; + axis.renderer.grid.template.strokeOpacity = 0.3; + } else if (axisType === "DateAxis") { + axis = this.chart.xAxes.push(new am4charts.DateAxis()); + axis.dateFormats.setKey("second", "HH:mm:ss") + axis.dateFormats.setKey("minute", "HH:mm:ss") + axis.dateFormats.setKey("hour", "HH:mm") + axis.dateFormats.setKey("day", "MMM dd") + axis.periodChangeDateFormats.setKey("hour", "[bold]dd MMM HH:mm[/]"); + axis.periodChangeDateFormats.setKey("day", "[bold]MMM[/] dd"); + axis.periodChangeDateFormats.setKey("month", "[bold]yyyy[/] MMM"); + } else if (axisType === "CategoryAxis") { + axis = this.chart.xAxes.push(new am4charts.CategoryAxis()); + axis.dataFields.category = category; + axis.renderer.grid.template.location = 0; + axis.renderer.minGridDistance = 30; + axis.renderer.labels.template.horizontalCenter = "right"; + axis.renderer.labels.template.verticalCenter = "middle"; + axis.renderer.labels.template.rotation = 315; + axis.tooltip.disabled = true; + axis.renderer.minHeight = 110; + } + } + + /** + * Create Y Value Axis for chart + * @param {Map} textLabels Map with key value pair. In keys are values to be converted into label text + * @return yAxis definition. + */ + createAxisY(textLabels) { + let axis; + axis = this.chart.yAxes.push(new am4charts.ValueAxis()); + axis.tooltip.disabled = false; + axis.renderer.opposite = Boolean(this.yAxesCount % 2); + + //TextRender + if (textLabels !== undefined) { + if (textLabels.size > 0) { + axis.renderer.labels.template.adapter.add("text", function (text) { + if (textLabels.get(text) !== undefined) { + return textLabels.get(text); + } else { + return ""; + } + }) + } + } + this.yAxesCount = this.yAxesCount + 1; + return axis; + } + + /** + * Add single line range gudided by label to chart. + * @param value Value of line for yAxis + * @param color Color of this line + * @param label Label for this line (eg. 'average count') + */ + addRangeValue(value, color, label) { + if (color === undefined || color === "") { + color = "#FF150A"; + } + if (label === undefined) { + label = ""; + } + let range = this.chart.yAxes.getIndex(0).axisRanges.create(); + range.value = value; + range.grid.stroke = am4core.color(color); + range.grid.strokeWidth = 2; + range.grid.strokeOpacity = 1; + range.label.inside = true; + range.label.text = label; + range.label.fill = range.grid.stroke; + range.label.verticalCenter = "bottom"; + } + + /** + * Create specific elements of chart. + * + * @param {Boolean} [scrollbarX=true] Show scrollbar for xAxes + * @param {Boolean} [scrollbarY=false] Show scrollbar for yAxes + * @param {Boolean} [legend=true] Show chart legend + * @param {Boolean} [cursor=true] Show cursor over the chart + */ + createScrollBarsAndLegend(scrollbarX = true, scrollbarY = false, legend = true, cursor = true) { + if (scrollbarX) { + this.chart.scrollbarX = new am4charts.XYChartScrollbar(); + this.chart.scrollbarX.parent = this.chart.bottomAxesContainer; + } + if (scrollbarY) { + this.chart.scrollbarY = new am4core.Scrollbar(); + this.chart.scrollbarY.parent = this.chart.leftAxesContainer; + } + if (legend) { + this.chart.legend = new am4charts.Legend(); + } + if (cursor) { + this.chart.cursor = new am4charts.XYCursor(); + this.chart.cursor.behavior = "panXY"; + } + } + + /** + * Add export possibility to chart. Save chart as an image or export chart data to *.csv or *.xlsx format. + * + * @param {Boolean} [enabled = true] is Export menu enabled in this chart. + * @param {String} [filePrefix = "Scada_Chart"] File name to which save exported chart data. + */ + createExportMenu(enabled = true, filePrefix = "Scada_Chart") { + if (enabled) { + this.chart.exporting.menu = new am4core.ExportMenu(); + this.chart.exporting.menu.align = "right" + this.chart.exporting.menu.vetricalAlign = "top" + this.chart.exporting.filePrefix = filePrefix + "_" + String(new Date().getTime()); + } + } + + /** + * Improving perfromance of chart. Display only a points every "step" pixels omitting this between. + * It is useful for charts presenting huge amount of data. For example charts displaying values from one month period. + * + * @param {Number} step - Ommit all line point if they are closer than "step" pixels to the last point drawn + */ + static setPolylineStep(step) { + am4core.options.minPolylineStep = step; + } + + /** + * Order values stored inside Map by keys (key == timestamp) + * + * @param {Map} map Scada Data Point Values Map + */ + static sortMapKeys(map) { + var sortByKeys = (a, b) => a[0] > b[0] ? 1 : -1 + return new Map([...map].sort(sortByKeys)) + } + + /** + * + * @param {Map} map Sorted keys in chronological order TimeValueMap + */ + static prepareChartData(map) { + let data = []; // [{date: