From 050929f070426f07bd38651e24146614525a8800 Mon Sep 17 00:00:00 2001 From: Roshan Kumar Date: Sat, 25 Jan 2025 20:26:15 +0530 Subject: [PATCH] [change] Switched to prettier --- .github/workflows/ci.yml | 2 +- .jshintrc | 20 - .jshintignore => .prettierignore | 0 .stylelintignore | 1 - .stylelintrc.json | 20 - docs/developer/installation.rst | 2 +- .../static/monitoring/css/percircle.min.css | 221 ++++- .../static/monitoring/js/alert-settings.js | 22 +- .../device/static/monitoring/js/device-map.js | 560 ++++++------ .../monitoring/js/wifi-session-inline.js | 54 +- .../static/monitoring/css/chart.css | 2 +- .../static/monitoring/js/chart-utils.js | 754 +++++++++------- .../monitoring/static/monitoring/js/chart.js | 840 +++++++++--------- .../static/monitoring/js/dashboard-chart.js | 11 +- run-qa-checks | 6 - 15 files changed, 1423 insertions(+), 1092 deletions(-) delete mode 100644 .jshintrc rename .jshintignore => .prettierignore (100%) delete mode 100644 .stylelintignore delete mode 100644 .stylelintrc.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7b6165e7..ea567059c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: pip install -U -I -e . pip uninstall -y django pip install -U ${{ matrix.django-version }} - sudo npm install -g jshint stylelint + sudo npm install -g prettier # start influxdb docker compose up -d influxdb diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 60d738294..000000000 --- a/.jshintrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "undef": false, - "unused": true, - "esversion": 6, - "curly": true, - "strict": "global", - "globals": { - "advancedJSONEditor": true, - "alert": true, - "django": true, - "Plotly": false, - "gettext": true, - "JSONEditor": true, - "ReconnectingWebSocket": true, - "createChart": true, - "triggerChartLoading": true, - "moment": true - }, - "browser": true -} diff --git a/.jshintignore b/.prettierignore similarity index 100% rename from .jshintignore rename to .prettierignore diff --git a/.stylelintignore b/.stylelintignore deleted file mode 100644 index fd04c4fe8..000000000 --- a/.stylelintignore +++ /dev/null @@ -1 +0,0 @@ -**/*.min.css diff --git a/.stylelintrc.json b/.stylelintrc.json deleted file mode 100644 index 45ff7011e..000000000 --- a/.stylelintrc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "rules": { - "block-no-empty": null, - "color-no-invalid-hex": true, - "comment-empty-line-before": [ - "always", - { - "ignore": ["stylelint-commands", "after-comment"] - } - ], - "rule-empty-line-before": [ - "never-multi-line", - { - "except": ["first-nested"], - "ignore": ["after-comment", "inside-block"] - } - ], - "unit-allowed-list": ["em", "rem", "%", "s", "px", "vh", "deg", "pt", "dpi"] - } -} diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index f57d8505d..ca15f10dd 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -69,7 +69,7 @@ Install development dependencies: pip install -e . pip install -r requirements-test.txt - npm install -g jshint stylelint + npm install -g prettier Install WebDriver for Chromium for your browser version from https://chromedriver.chromium.org/home and extract ``chromedriver`` to one diff --git a/openwisp_monitoring/device/static/monitoring/css/percircle.min.css b/openwisp_monitoring/device/static/monitoring/css/percircle.min.css index 36041a8e9..a8c2b75c1 100644 --- a/openwisp_monitoring/device/static/monitoring/css/percircle.min.css +++ b/openwisp_monitoring/device/static/monitoring/css/percircle.min.css @@ -1 +1,220 @@ -.percircle.dark{background-color:#777}.percircle.dark .bar,.percircle.dark .fill{border-color:#c6ff00}.percircle.dark>span{color:#777}.percircle.dark:after{background-color:#555}.percircle.dark:hover>span{color:#c6ff00}.percircle.red .bar,.percircle.red .fill{border-color:#dd5454}.percircle.red:hover>span{color:#dd5454}.percircle.red.dark .bar,.percircle.red.dark .fill{border-color:#f84a4a}.percircle.red.dark:hover>span{color:#f84a4a}.percircle.blue .bar,.percircle.blue .fill{border-color:#82cefa}.percircle.blue:hover>span{color:#82cefa}.percircle.blue.dark .bar,.percircle.blue.dark .fill{border-color:#20cceb}.percircle.blue.dark:hover>span{color:#20cceb}.percircle.green .bar,.percircle.green .fill{border-color:#8dea7b}.percircle.green:hover>span{color:#8dea7b}.percircle.green.dark .bar,.percircle.green.dark .fill{border-color:#a9ff3a}.percircle.green.dark:hover>span{color:#a9ff3a}.percircle.orange .bar,.percircle.orange .fill{border-color:#e88239}.percircle.orange:hover>span{color:#e88239}.percircle.orange.dark .bar,.percircle.orange.dark .fill{border-color:#dc5b00}.percircle.orange.dark:hover>span{color:#dc5b00}.percircle.pink .bar,.percircle.pink .fill{border-color:#ff8ed0}.percircle.pink:hover>span{color:#ff8ed0}.percircle.pink.dark .bar,.percircle.pink.dark .fill{border-color:#ff58b9}.percircle.pink.dark:hover>span{color:#ff58b9}.percircle.purple .bar,.percircle.purple .fill{border-color:#aa7eff}.percircle.purple:hover>span{color:#aa7eff}.percircle.purple.dark .bar,.percircle.purple.dark .fill{border-color:#7a38f7}.percircle.purple.dark:hover>span{color:#7a38f7}.percircle.yellow .bar,.percircle.yellow .fill{border-color:#dcbd00}.percircle.yellow:hover>span{color:#dcbd00}.percircle.yellow.dark .bar,.percircle.yellow.dark .fill{border-color:#ffdb00}.percircle.yellow.dark:hover>span{color:#ffdb00}.percircle.gt50 .slice,.rect-auto{clip:rect(auto,auto,auto,auto)}.gt50 .fill,.percircle .bar,.pie{position:absolute;border:.08em solid #307bbb;width:.84em;height:.84em;clip:rect(0,.5em,1em,0);border-radius:50%;-webkit-transform:rotate(0deg);transform:rotate(0deg)}.bar{-webkit-backface-visibility:hidden;backface-visibility:hidden}.gt50 .bar:after,.gt50 .fill,.pie-fill{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.percircle{position:relative;font-size:120px;width:1em;height:1em;border-radius:50%;float:left;margin:0 .1em .1em 0;background-color:#ccc}.percircle *,.percircle :after,.percircle :before{box-sizing:content-box}.percircle.animate:after,.percircle.animate>span{-webkit-transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out}.percircle.animate .bar{-webkit-transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out}.percircle.center{float:none;margin:0 auto}.percircle.big{font-size:240px}.percircle.small{font-size:80px}.percircle>span{position:absolute;z-index:1;width:100%;top:50%;top:calc(50% - .1em);transform:translateY(-50%);height:1em;font-size:.2em;color:#ccc;display:block;text-align:center;white-space:nowrap}.perclock>span{font-size:.175em}.percircle:after{position:absolute;top:.08em;left:.08em;display:block;content:" ";border-radius:50%;background-color:#f5f5f5;width:.84em;height:.84em}.percircle .slice{position:absolute;width:1em;height:1em;clip:rect(0,1em,1em,.5em)}.percircle:hover{cursor:default}.percircle:hover>span{-webkit-transform:scale(1.3) translateY(-50%);transform:scale(1.3) translateY(-50%);color:#307bbb}.percircle:hover:after{-webkit-transform:scale(1.1);transform:scale(1.1)} +.percircle.dark { + background-color: #777; +} +.percircle.dark .bar, +.percircle.dark .fill { + border-color: #c6ff00; +} +.percircle.dark > span { + color: #777; +} +.percircle.dark:after { + background-color: #555; +} +.percircle.dark:hover > span { + color: #c6ff00; +} +.percircle.red .bar, +.percircle.red .fill { + border-color: #dd5454; +} +.percircle.red:hover > span { + color: #dd5454; +} +.percircle.red.dark .bar, +.percircle.red.dark .fill { + border-color: #f84a4a; +} +.percircle.red.dark:hover > span { + color: #f84a4a; +} +.percircle.blue .bar, +.percircle.blue .fill { + border-color: #82cefa; +} +.percircle.blue:hover > span { + color: #82cefa; +} +.percircle.blue.dark .bar, +.percircle.blue.dark .fill { + border-color: #20cceb; +} +.percircle.blue.dark:hover > span { + color: #20cceb; +} +.percircle.green .bar, +.percircle.green .fill { + border-color: #8dea7b; +} +.percircle.green:hover > span { + color: #8dea7b; +} +.percircle.green.dark .bar, +.percircle.green.dark .fill { + border-color: #a9ff3a; +} +.percircle.green.dark:hover > span { + color: #a9ff3a; +} +.percircle.orange .bar, +.percircle.orange .fill { + border-color: #e88239; +} +.percircle.orange:hover > span { + color: #e88239; +} +.percircle.orange.dark .bar, +.percircle.orange.dark .fill { + border-color: #dc5b00; +} +.percircle.orange.dark:hover > span { + color: #dc5b00; +} +.percircle.pink .bar, +.percircle.pink .fill { + border-color: #ff8ed0; +} +.percircle.pink:hover > span { + color: #ff8ed0; +} +.percircle.pink.dark .bar, +.percircle.pink.dark .fill { + border-color: #ff58b9; +} +.percircle.pink.dark:hover > span { + color: #ff58b9; +} +.percircle.purple .bar, +.percircle.purple .fill { + border-color: #aa7eff; +} +.percircle.purple:hover > span { + color: #aa7eff; +} +.percircle.purple.dark .bar, +.percircle.purple.dark .fill { + border-color: #7a38f7; +} +.percircle.purple.dark:hover > span { + color: #7a38f7; +} +.percircle.yellow .bar, +.percircle.yellow .fill { + border-color: #dcbd00; +} +.percircle.yellow:hover > span { + color: #dcbd00; +} +.percircle.yellow.dark .bar, +.percircle.yellow.dark .fill { + border-color: #ffdb00; +} +.percircle.yellow.dark:hover > span { + color: #ffdb00; +} +.percircle.gt50 .slice, +.rect-auto { + clip: rect(auto, auto, auto, auto); +} +.gt50 .fill, +.percircle .bar, +.pie { + position: absolute; + border: 0.08em solid #307bbb; + width: 0.84em; + height: 0.84em; + clip: rect(0, 0.5em, 1em, 0); + border-radius: 50%; + -webkit-transform: rotate(0deg); + transform: rotate(0deg); +} +.bar { + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} +.gt50 .bar:after, +.gt50 .fill, +.pie-fill { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); +} +.percircle { + position: relative; + font-size: 120px; + width: 1em; + height: 1em; + border-radius: 50%; + float: left; + margin: 0 0.1em 0.1em 0; + background-color: #ccc; +} +.percircle *, +.percircle :after, +.percircle :before { + box-sizing: content-box; +} +.percircle.animate:after, +.percircle.animate > span { + -webkit-transition: -webkit-transform 0.2s ease-in-out; + transition: transform 0.2s ease-in-out; +} +.percircle.animate .bar { + -webkit-transition: -webkit-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; +} +.percircle.center { + float: none; + margin: 0 auto; +} +.percircle.big { + font-size: 240px; +} +.percircle.small { + font-size: 80px; +} +.percircle > span { + position: absolute; + z-index: 1; + width: 100%; + top: 50%; + top: calc(50% - 0.1em); + transform: translateY(-50%); + height: 1em; + font-size: 0.2em; + color: #ccc; + display: block; + text-align: center; + white-space: nowrap; +} +.perclock > span { + font-size: 0.175em; +} +.percircle:after { + position: absolute; + top: 0.08em; + left: 0.08em; + display: block; + content: " "; + border-radius: 50%; + background-color: #f5f5f5; + width: 0.84em; + height: 0.84em; +} +.percircle .slice { + position: absolute; + width: 1em; + height: 1em; + clip: rect(0, 1em, 1em, 0.5em); +} +.percircle:hover { + cursor: default; +} +.percircle:hover > span { + -webkit-transform: scale(1.3) translateY(-50%); + transform: scale(1.3) translateY(-50%); + color: #307bbb; +} +.percircle:hover:after { + -webkit-transform: scale(1.1); + transform: scale(1.1); +} diff --git a/openwisp_monitoring/device/static/monitoring/js/alert-settings.js b/openwisp_monitoring/device/static/monitoring/js/alert-settings.js index 8617436a7..07ff48cb9 100644 --- a/openwisp_monitoring/device/static/monitoring/js/alert-settings.js +++ b/openwisp_monitoring/device/static/monitoring/js/alert-settings.js @@ -1,11 +1,17 @@ (function ($) { - 'use strict'; - $(function () { - var isActiveCheckboxes = $('[id^="id_monitoring-metric-content_type-object_id"][id $="alertsettings-0-is_active"]'); - isActiveCheckboxes.each(function(i, checkbox){ - $(checkbox).click(function () { - $('#' + this.id).parent().parent().siblings().toggle(this.checked); - }); - }); + "use strict"; + $(function () { + var isActiveCheckboxes = $( + '[id^="id_monitoring-metric-content_type-object_id"][id $="alertsettings-0-is_active"]', + ); + isActiveCheckboxes.each(function (i, checkbox) { + $(checkbox).click(function () { + $("#" + this.id) + .parent() + .parent() + .siblings() + .toggle(this.checked); + }); }); + }); })(django.jQuery); diff --git a/openwisp_monitoring/device/static/monitoring/js/device-map.js b/openwisp_monitoring/device/static/monitoring/js/device-map.js index 986f4ceb2..c4338f280 100644 --- a/openwisp_monitoring/device/static/monitoring/js/device-map.js +++ b/openwisp_monitoring/device/static/monitoring/js/device-map.js @@ -2,71 +2,71 @@ /*jshint esversion: 8 */ (function ($) { - const loadingOverlay = $('#device-map-container .ow-loading-spinner'); - const localStorageKey = 'ow-map-shown'; - const mapContainer = $('#device-map-container'); - const statuses = ['critical', 'problem', 'ok', 'unknown', 'deactivated']; - const colors = { - ok: '#267126', - problem: '#ffb442', - critical: '#a72d1d', - unknown: '#353c44', - deactivated: '#0000', - }; - const getLocationDeviceUrl = function (pk) { - return window._owGeoMapConfig.locationDeviceUrl.replace('000', pk); - }; - const getColor = function (data) { - let deviceCount = data.device_count, - findResult = function (func) { - for (let i in statuses) { - let status = statuses[i], - statusCount = data[status + '_count']; - if (statusCount === 0) { - continue; - } - return func(status, statusCount); - } - }; - // if one status has absolute majority, it's the winner - let majority = findResult(function (status, statusCount) { - if (statusCount > deviceCount / 2) { - return colors[status]; - } - }); - if (majority) { - return majority; - } - // otherwise simply return the color based on the priority - return findResult(function (status, statusCount) { - // if one status has absolute majority, it's the winner - if (statusCount) { - return colors[status]; - } - }); - }; - const loadPopUpContent = function (layer, url) { - // allows reopening the last page which was opened before popup close - // defaults to the passed URL or the default URL (first page) - if (!url) { - url = layer.url || getLocationDeviceUrl(layer.feature.id); + const loadingOverlay = $("#device-map-container .ow-loading-spinner"); + const localStorageKey = "ow-map-shown"; + const mapContainer = $("#device-map-container"); + const statuses = ["critical", "problem", "ok", "unknown", "deactivated"]; + const colors = { + ok: "#267126", + problem: "#ffb442", + critical: "#a72d1d", + unknown: "#353c44", + deactivated: "#0000", + }; + const getLocationDeviceUrl = function (pk) { + return window._owGeoMapConfig.locationDeviceUrl.replace("000", pk); + }; + const getColor = function (data) { + let deviceCount = data.device_count, + findResult = function (func) { + for (let i in statuses) { + let status = statuses[i], + statusCount = data[status + "_count"]; + if (statusCount === 0) { + continue; + } + return func(status, statusCount); } - layer.url = url; + }; + // if one status has absolute majority, it's the winner + let majority = findResult(function (status, statusCount) { + if (statusCount > deviceCount / 2) { + return colors[status]; + } + }); + if (majority) { + return majority; + } + // otherwise simply return the color based on the priority + return findResult(function (status, statusCount) { + // if one status has absolute majority, it's the winner + if (statusCount) { + return colors[status]; + } + }); + }; + const loadPopUpContent = function (layer, url) { + // allows reopening the last page which was opened before popup close + // defaults to the passed URL or the default URL (first page) + if (!url) { + url = layer.url || getLocationDeviceUrl(layer.feature.id); + } + layer.url = url; - loadingOverlay.show(); + loadingOverlay.show(); - $.ajax({ - dataType: "json", - url: url, - xhrFields: { - withCredentials: true - }, - success: function (data) { - let html = '', - device; - for (let i = 0; i < data.results.length; i++) { - device = data.results[i]; - html += ` + $.ajax({ + dataType: "json", + url: url, + xhrFields: { + withCredentials: true, + }, + success: function (data) { + let html = "", + device; + for (let i = 0; i < data.results.length; i++) { + device = data.results[i]; + html += ` ${device.name} @@ -75,26 +75,30 @@ `; - } - let pagination = '', - parts = []; - if (data.previous || data.next) { - if (data.previous) { - parts.push(``); - } - if (data.next) { - parts.push(``); - } - pagination = `

${parts.join(' ')}`; - } - layer.bindPopup(` + } + let pagination = "", + parts = []; + if (data.previous || data.next) { + if (data.previous) { + parts.push( + ``, + ); + } + if (data.next) { + parts.push( + ``, + ); + } + pagination = `

${parts.join(" ")}`; + } + layer.bindPopup(`

${layer.feature.properties.name} (${data.count})

- - + + @@ -103,217 +107,227 @@
${gettext('name')}${gettext('status')}${gettext("name")}${gettext("status")}
${pagination}
`); - layer.openPopup(); - - // bind next/prev buttons - let el = $(layer.getPopup().getElement()); - el.find('.next').click(function () { - loadPopUpContent(layer, $(this).data('url')); - }); - el.find('.prev').click(function () { - loadPopUpContent(layer, $(this).data('url')); - }); - - loadingOverlay.hide(); + layer.openPopup(); - }, - error: function () { - loadingOverlay.hide(); - alert(gettext('Error while retrieving data')); - } + // bind next/prev buttons + let el = $(layer.getPopup().getElement()); + el.find(".next").click(function () { + loadPopUpContent(layer, $(this).data("url")); + }); + el.find(".prev").click(function () { + loadPopUpContent(layer, $(this).data("url")); }); - }; - const leafletConfig = JSON.parse($('#leaflet-config').text()); - const tiles = leafletConfig.TILES.map((tile) => { - let tileLayer = tile[1]; - if (tileLayer.includes('https:')) { - tileLayer = tileLayer.split('https:')[1]; - } - let options = {}; - if (typeof tile[2] === 'object') { - options = tile[2]; - } else { - options.attribution = tile[2]; - } - return { - label: tile[0], - urlTemplate: `https:${tileLayer}`, - options, - }; + loadingOverlay.hide(); + }, + error: function () { + loadingOverlay.hide(); + alert(gettext("Error while retrieving data")); + }, }); + }; + const leafletConfig = JSON.parse($("#leaflet-config").text()); + const tiles = leafletConfig.TILES.map((tile) => { + let tileLayer = tile[1]; + if (tileLayer.includes("https:")) { + tileLayer = tileLayer.split("https:")[1]; + } + let options = {}; + if (typeof tile[2] === "object") { + options = tile[2]; + } else { + options.attribution = tile[2]; + } + return { + label: tile[0], + urlTemplate: `https:${tileLayer}`, + options, + }; + }); - function onAjaxSuccess(data) { - if (!data.count) { - mapContainer.find('.no-data').fadeIn(500); - loadingOverlay.hide(); - mapContainer.find('.no-data').click(function (e) { - e.preventDefault(); - mapContainer.slideUp(); - localStorage.setItem(localStorageKey, 'false'); - }); - return; - } else { - localStorage.removeItem(localStorageKey); - mapContainer.slideDown(); - } - /* Workaround for https://github.com/openwisp/openwisp-monitoring/issues/462 + function onAjaxSuccess(data) { + if (!data.count) { + mapContainer.find(".no-data").fadeIn(500); + loadingOverlay.hide(); + mapContainer.find(".no-data").click(function (e) { + e.preventDefault(); + mapContainer.slideUp(); + localStorage.setItem(localStorageKey, "false"); + }); + return; + } else { + localStorage.removeItem(localStorageKey); + mapContainer.slideDown(); + } + /* Workaround for https://github.com/openwisp/openwisp-monitoring/issues/462 Leaflet does not support looping (wrapping) the map. Therefore, to work around abrupt automatic map panning due to bounds, we plot markers on three worlds. This allow users to view devices around the International Date Line without any weird affects. */ - /* global NetJSONGraph */ - const map = new NetJSONGraph(data, { - el: '#device-map-container', - render: 'map', - clustering: false, - // set map initial state. - mapOptions: { - center: leafletConfig.DEFAULT_CENTER, - zoom: leafletConfig.DEFAULT_ZOOM, - minZoom: leafletConfig.MIN_ZOOM || 1, - maxZoom: leafletConfig.MAX_ZOOM || 24, - fullscreenControl: true, - }, - mapTileConfig: tiles, - geoOptions: { - style: function (feature) { - return { - radius: 9, - fillColor: getColor(feature.properties), - color: 'rgba(0, 0, 0, 0.3)', - weight: 3, - opacity: 1, - fillOpacity: 0.7, - }; - }, - onEachFeature: function (feature, layer) { - const color = getColor(feature.properties); - feature.properties.status = Object.keys(colors).filter( - (key) => colors[key] === color - )[0]; - - layer.on('mouseover', function () { - layer.unbindTooltip(); - if (!layer.isPopupOpen()) { - layer.bindTooltip(feature.properties.name).openTooltip(); - } - }); - layer.on('click', function () { - layer.unbindTooltip(); - layer.unbindPopup(); - loadPopUpContent(layer); - }); - }, - }, - onReady: function () { - const map = this.leaflet; - let scale = { - imperial: false, - metric: false, - }; - if (leafletConfig.SCALE === 'metric') { - scale.metric = true; - } else if (leafletConfig.SCALE === 'imperial') { - scale.imperial = true; - } else if (leafletConfig.SCALE === 'both') { - scale.metric = true; - scale.imperial = true; - } - - if (leafletConfig.SCALE) { - /* global L */ - map.addControl(new L.control.scale(scale)); - } + /* global NetJSONGraph */ + const map = new NetJSONGraph(data, { + el: "#device-map-container", + render: "map", + clustering: false, + // set map initial state. + mapOptions: { + center: leafletConfig.DEFAULT_CENTER, + zoom: leafletConfig.DEFAULT_ZOOM, + minZoom: leafletConfig.MIN_ZOOM || 1, + maxZoom: leafletConfig.MAX_ZOOM || 24, + fullscreenControl: true, + }, + mapTileConfig: tiles, + geoOptions: { + style: function (feature) { + return { + radius: 9, + fillColor: getColor(feature.properties), + color: "rgba(0, 0, 0, 0.3)", + weight: 3, + opacity: 1, + fillOpacity: 0.7, + }; + }, + onEachFeature: function (feature, layer) { + const color = getColor(feature.properties); + feature.properties.status = Object.keys(colors).filter( + (key) => colors[key] === color, + )[0]; - if (map.geoJSON.getLayers().length === 1) { - map.setView(map.geoJSON.getBounds().getCenter(), 10); - } else { - map.fitBounds(map.geoJSON.getBounds()); - } - map.geoJSON.eachLayer(function (layer) { - layer[layer.feature.geometry.type == 'Point' ? 'bringToFront' : 'bringToBack'](); - }); + layer.on("mouseover", function () { + layer.unbindTooltip(); + if (!layer.isPopupOpen()) { + layer.bindTooltip(feature.properties.name).openTooltip(); + } + }); + layer.on("click", function () { + layer.unbindTooltip(); + layer.unbindPopup(); + loadPopUpContent(layer); + }); + }, + }, + onReady: function () { + const map = this.leaflet; + let scale = { + imperial: false, + metric: false, + }; + if (leafletConfig.SCALE === "metric") { + scale.metric = true; + } else if (leafletConfig.SCALE === "imperial") { + scale.imperial = true; + } else if (leafletConfig.SCALE === "both") { + scale.metric = true; + scale.imperial = true; + } - // Workaround for https://github.com/openwisp/openwisp-monitoring/issues/462 - map.setMaxBounds( - L.latLngBounds(L.latLng(-90, -540), L.latLng(90, 540)) - ); - map.on('moveend', event => { - let netjsonGraph = this; - let bounds = event.target.getBounds(); - if (bounds._southWest.lng < -180 && !netjsonGraph.westWorldFeaturesAppended) { - let westWorldFeatures = window.structuredClone(netjsonGraph.data); - // Exclude the features that may be added for the East world map - westWorldFeatures.features = westWorldFeatures.features.filter( - element => element.geometry.coordinates[0] <= 180 - ); - westWorldFeatures.features.forEach(element => { - if (element.geometry) { - element.geometry.coordinates[0] -= 360; - } - }); - netjsonGraph.utils.appendData(westWorldFeatures, netjsonGraph); - netjsonGraph.westWorldFeaturesAppended = true; + if (leafletConfig.SCALE) { + /* global L */ + map.addControl(new L.control.scale(scale)); + } - } - if (bounds._northEast.lng > 180 && !netjsonGraph.eastWorldFeaturesAppended) { - let eastWorldFeatures = window.structuredClone(netjsonGraph.data); - // Exclude the features that may be added for the West world map - eastWorldFeatures.features = eastWorldFeatures.features.filter( - element => element.geometry.coordinates[0] >= -180 - ); - eastWorldFeatures.features.forEach(element => { - if (element.geometry) { - element.geometry.coordinates[0] += 360; - } - }); - netjsonGraph.utils.appendData(eastWorldFeatures, netjsonGraph); - netjsonGraph.eastWorldFeaturesAppended = true; - } - }); - }, - }); - map.setUtils({ - showLoading: function () { - loadingOverlay.show(); - }, - hideLoading: function () { - loadingOverlay.hide(); - }, - paginatedDataParse: async function (JSONParam) { - let res; - let data; - try { - res = await this.utils.JSONParamParse(JSONParam); - data = res; - while (res.next && data.features.length <= this.config.maxPointsFetched) { - res = await this.utils.JSONParamParse(res.next); - res = await res.json(); - data.features = data.features.concat(res.features); - } - } catch (e) { - /* global console */ - console.error(e); - } - return data; - }, + if (map.geoJSON.getLayers().length === 1) { + map.setView(map.geoJSON.getBounds().getCenter(), 10); + } else { + map.fitBounds(map.geoJSON.getBounds()); + } + map.geoJSON.eachLayer(function (layer) { + layer[ + layer.feature.geometry.type == "Point" + ? "bringToFront" + : "bringToBack" + ](); }); - map.render(); - } - if (localStorage.getItem(localStorageKey) === 'false') { - mapContainer.slideUp(50); - } - $.ajax({ - dataType: "json", - url: window._owGeoMapConfig.geoJsonUrl, - xhrFields: { - withCredentials: true - }, - success: onAjaxSuccess, - context: window, + // Workaround for https://github.com/openwisp/openwisp-monitoring/issues/462 + map.setMaxBounds( + L.latLngBounds(L.latLng(-90, -540), L.latLng(90, 540)), + ); + map.on("moveend", (event) => { + let netjsonGraph = this; + let bounds = event.target.getBounds(); + if ( + bounds._southWest.lng < -180 && + !netjsonGraph.westWorldFeaturesAppended + ) { + let westWorldFeatures = window.structuredClone(netjsonGraph.data); + // Exclude the features that may be added for the East world map + westWorldFeatures.features = westWorldFeatures.features.filter( + (element) => element.geometry.coordinates[0] <= 180, + ); + westWorldFeatures.features.forEach((element) => { + if (element.geometry) { + element.geometry.coordinates[0] -= 360; + } + }); + netjsonGraph.utils.appendData(westWorldFeatures, netjsonGraph); + netjsonGraph.westWorldFeaturesAppended = true; + } + if ( + bounds._northEast.lng > 180 && + !netjsonGraph.eastWorldFeaturesAppended + ) { + let eastWorldFeatures = window.structuredClone(netjsonGraph.data); + // Exclude the features that may be added for the West world map + eastWorldFeatures.features = eastWorldFeatures.features.filter( + (element) => element.geometry.coordinates[0] >= -180, + ); + eastWorldFeatures.features.forEach((element) => { + if (element.geometry) { + element.geometry.coordinates[0] += 360; + } + }); + netjsonGraph.utils.appendData(eastWorldFeatures, netjsonGraph); + netjsonGraph.eastWorldFeaturesAppended = true; + } + }); + }, + }); + map.setUtils({ + showLoading: function () { + loadingOverlay.show(); + }, + hideLoading: function () { + loadingOverlay.hide(); + }, + paginatedDataParse: async function (JSONParam) { + let res; + let data; + try { + res = await this.utils.JSONParamParse(JSONParam); + data = res; + while ( + res.next && + data.features.length <= this.config.maxPointsFetched + ) { + res = await this.utils.JSONParamParse(res.next); + res = await res.json(); + data.features = data.features.concat(res.features); + } + } catch (e) { + /* global console */ + console.error(e); + } + return data; + }, }); + map.render(); + } + + if (localStorage.getItem(localStorageKey) === "false") { + mapContainer.slideUp(50); + } + $.ajax({ + dataType: "json", + url: window._owGeoMapConfig.geoJsonUrl, + xhrFields: { + withCredentials: true, + }, + success: onAjaxSuccess, + context: window, + }); })(django.jQuery); diff --git a/openwisp_monitoring/device/static/monitoring/js/wifi-session-inline.js b/openwisp_monitoring/device/static/monitoring/js/wifi-session-inline.js index add627ed5..c0e2e03dd 100644 --- a/openwisp_monitoring/device/static/monitoring/js/wifi-session-inline.js +++ b/openwisp_monitoring/device/static/monitoring/js/wifi-session-inline.js @@ -1,41 +1,41 @@ -'use strict'; +"use strict"; -if (typeof gettext === 'undefined') { - var gettext = function (word) { - return word; - }; +if (typeof gettext === "undefined") { + var gettext = function (word) { + return word; + }; } (function ($) { - $(function ($) { - function getObjectIdFromUrl() { - let objectId; - try { - objectId = /\/((\w{4,12}-?)){5}\//.exec(window.location)[0]; - } catch (error) { - try { - objectId = /\/(\d+)\//.exec(window.location)[0]; - } catch (error) { - throw error; - } - } - return objectId.replace(/\//g, ''); + $(function ($) { + function getObjectIdFromUrl() { + let objectId; + try { + objectId = /\/((\w{4,12}-?)){5}\//.exec(window.location)[0]; + } catch (error) { + try { + objectId = /\/(\d+)\//.exec(window.location)[0]; + } catch (error) { + throw error; } + } + return objectId.replace(/\//g, ""); + } - let wifiSessionGroup = $('#wifisession_set-group'), - wifiSessionUrl = $('#monitoring-wifisession-changelist-url').data('url'), - wifiSessionLinkElement; - wifiSessionUrl = `${wifiSessionUrl}?device=${getObjectIdFromUrl()}`; - wifiSessionLinkElement = ` + let wifiSessionGroup = $("#wifisession_set-group"), + wifiSessionUrl = $("#monitoring-wifisession-changelist-url").data("url"), + wifiSessionLinkElement; + wifiSessionUrl = `${wifiSessionUrl}?device=${getObjectIdFromUrl()}`; + wifiSessionLinkElement = ` `; - wifiSessionGroup.append(wifiSessionLinkElement); - }); -}(django.jQuery)); + wifiSessionGroup.append(wifiSessionLinkElement); + }); +})(django.jQuery); diff --git a/openwisp_monitoring/monitoring/static/monitoring/css/chart.css b/openwisp_monitoring/monitoring/static/monitoring/css/chart.css index 29e09f73d..d22ad9c35 100644 --- a/openwisp_monitoring/monitoring/static/monitoring/css/chart.css +++ b/openwisp_monitoring/monitoring/static/monitoring/css/chart.css @@ -22,7 +22,7 @@ top: 0; height: 27px; } -#ow-chart-utils>span { +#ow-chart-utils > span { display: flex; justify-content: space-between; width: 100%; diff --git a/openwisp_monitoring/monitoring/static/monitoring/js/chart-utils.js b/openwisp_monitoring/monitoring/static/monitoring/js/chart-utils.js index edf32a846..ea32adfb0 100644 --- a/openwisp_monitoring/monitoring/static/monitoring/js/chart-utils.js +++ b/openwisp_monitoring/monitoring/static/monitoring/js/chart-utils.js @@ -1,107 +1,146 @@ -'use strict'; -const isChartZoomed = 'ow2-chart-custom-zoom'; // true/false -const isChartZoomScroll = 'ow2-chart-custom-zoom-scroll'; // true/false -const isCustomDateRange = 'ow2-chart-custom-daterange'; // true/false -const timeRangeKey = 'ow2-chart-time-range'; // 30d -const startDayKey = 'ow2-chart-start-day'; // September 3, 2022 -const endDayKey = 'ow2-chart-end-day'; // October 3, 2022 -const startDateTimeKey = 'ow2-chart-start-datetime'; // 2022-09-03 00:00:00 -const endDateTimeKey = 'ow2-chart-end-datetime'; // 2022-09-03 00:00:00 -const pickerChosenLabelKey = 'ow2-chart-picker-label'; // 2022-09-03 00:00:00 -const zoomtimeRangeKey = 'ow2-chart-zoom-time-range'; // 30d -const zoomStartDayKey = 'ow2-chart-zoom-start-day'; // September 3, 2022 -const zoomEndDayKey = 'ow2-chart-zoom-end-day'; // October 3, 2022 -const zoomStartDateTimeKey = 'ow2-chart-zoom-start-datetime'; // 2022-09-03 00:00:00 -const zoomEndDateTimeKey = 'ow2-chart-zoom-end-datetime'; // 2022-09-03 00:00:00 -const zoomChartIdKey = 'ow2-chart-zoom-id'; // true/false +"use strict"; +const isChartZoomed = "ow2-chart-custom-zoom"; // true/false +const isChartZoomScroll = "ow2-chart-custom-zoom-scroll"; // true/false +const isCustomDateRange = "ow2-chart-custom-daterange"; // true/false +const timeRangeKey = "ow2-chart-time-range"; // 30d +const startDayKey = "ow2-chart-start-day"; // September 3, 2022 +const endDayKey = "ow2-chart-end-day"; // October 3, 2022 +const startDateTimeKey = "ow2-chart-start-datetime"; // 2022-09-03 00:00:00 +const endDateTimeKey = "ow2-chart-end-datetime"; // 2022-09-03 00:00:00 +const pickerChosenLabelKey = "ow2-chart-picker-label"; // 2022-09-03 00:00:00 +const zoomtimeRangeKey = "ow2-chart-zoom-time-range"; // 30d +const zoomStartDayKey = "ow2-chart-zoom-start-day"; // September 3, 2022 +const zoomEndDayKey = "ow2-chart-zoom-end-day"; // October 3, 2022 +const zoomStartDateTimeKey = "ow2-chart-zoom-start-datetime"; // 2022-09-03 00:00:00 +const zoomEndDateTimeKey = "ow2-chart-zoom-end-datetime"; // 2022-09-03 00:00:00 +const zoomChartIdKey = "ow2-chart-zoom-id"; // true/false -django.jQuery(function ($) { - $(document).ready(function () { - var pickerStart, pickerEnd, pickerDays, pickerChosenLabel, end = moment(); - var range = localStorage.getItem(timeRangeKey) || $('#monitoring-timeseries-default-time').data('value'); - var start = localStorage.getItem(isCustomDateRange) === 'true' ? moment() : moment().subtract(range.split('d')[0], 'days'); +django.jQuery( + (function ($) { + $(document).ready(function () { + var pickerStart, + pickerEnd, + pickerDays, + pickerChosenLabel, + end = moment(); + var range = + localStorage.getItem(timeRangeKey) || + $("#monitoring-timeseries-default-time").data("value"); + var start = + localStorage.getItem(isCustomDateRange) === "true" + ? moment() + : moment().subtract(range.split("d")[0], "days"); - // Initialize date range picker labels - var last1DayLabel = gettext('Last 1 Day'); - var last3DaysLabel = gettext('Last 3 Days'); - var last7DaysLabel = gettext('Last 7 Days'); - var last30DaysLabel = gettext('Last 30 Days'); - var last365DaysLabel = gettext('Last 365 Days'); - var customDateRangeLabel = gettext('Custom Range'); + // Initialize date range picker labels + var last1DayLabel = gettext("Last 1 Day"); + var last3DaysLabel = gettext("Last 3 Days"); + var last7DaysLabel = gettext("Last 7 Days"); + var last30DaysLabel = gettext("Last 30 Days"); + var last365DaysLabel = gettext("Last 365 Days"); + var customDateRangeLabel = gettext("Custom Range"); // Add label to daterangepicker widget function addDateRangePickerLabel(startDate, endDate) { - $('#daterangepicker-widget span').html(startDate + ' - ' + endDate); + $("#daterangepicker-widget span").html(startDate + " - " + endDate); } function initDateRangePickerWidget(start, end) { - addDateRangePickerLabel(start.format('MMMM D, YYYY'), end.format('MMMM D, YYYY')); - $(`[data-range-key='${last1DayLabel}']`).attr('data-time', '1d'); - $(`[data-range-key='${last3DaysLabel}']`).attr('data-time', '3d'); - $(`[data-range-key='${last7DaysLabel}']`).attr('data-time', '7d'); - $(`[data-range-key='${last30DaysLabel}']`).attr('data-time', '30d'); - $(`[data-range-key='${last365DaysLabel}']`).attr('data-time', '365d'); - $(`[data-range-key='${customDateRangeLabel}']`).attr('data-time', customDateRangeLabel); + addDateRangePickerLabel( + start.format("MMMM D, YYYY"), + end.format("MMMM D, YYYY"), + ); + $(`[data-range-key='${last1DayLabel}']`).attr("data-time", "1d"); + $(`[data-range-key='${last3DaysLabel}']`).attr("data-time", "3d"); + $(`[data-range-key='${last7DaysLabel}']`).attr("data-time", "7d"); + $(`[data-range-key='${last30DaysLabel}']`).attr("data-time", "30d"); + $(`[data-range-key='${last365DaysLabel}']`).attr("data-time", "365d"); + $(`[data-range-key='${customDateRangeLabel}']`).attr( + "data-time", + customDateRangeLabel, + ); } - $('#daterangepicker-widget').daterangepicker({ - startDate: start, - endDate: end, - maxDate: moment(), - maxSpan: { - "year": 1, - }, - locale: { - applyLabel: gettext('Apply'), - cancelLabel: gettext('Cancel'), - customRangeLabel: gettext(customDateRangeLabel), + $("#daterangepicker-widget").daterangepicker( + { + startDate: start, + endDate: end, + maxDate: moment(), + maxSpan: { + year: 1, + }, + locale: { + applyLabel: gettext("Apply"), + cancelLabel: gettext("Cancel"), + customRangeLabel: gettext(customDateRangeLabel), + }, + ranges: { + [`${last1DayLabel}`]: [moment().subtract(1, "days"), moment()], + [`${last3DaysLabel}`]: [moment().subtract(3, "days"), moment()], + [`${last7DaysLabel}`]: [moment().subtract(7, "days"), moment()], + [`${last30DaysLabel}`]: [moment().subtract(30, "days"), moment()], + [`${last365DaysLabel}`]: [moment().subtract(365, "days"), moment()], + }, }, - ranges: { - [`${last1DayLabel}`]: [moment().subtract(1, 'days'), moment()], - [`${last3DaysLabel}`]: [moment().subtract(3, 'days'), moment()], - [`${last7DaysLabel}`]: [moment().subtract(7, 'days'), moment()], - [`${last30DaysLabel}`]: [moment().subtract(30, 'days'), moment()], - [`${last365DaysLabel}`]: [moment().subtract(365, 'days'), moment()], - } - }, initDateRangePickerWidget); + initDateRangePickerWidget, + ); initDateRangePickerWidget(start, end); - function setDateRangePickerWidget(pickerStartDate, pickerEndDate) { - addDateRangePickerLabel(pickerStartDate.format('MMMM D, YYYY'), pickerEndDate.format('MMMM D, YYYY')); - $('#daterangepicker-widget').data('daterangepicker').setStartDate(moment(pickerStartDate.format('MMMM D, YYYY')).format('MM/DD/YYYY')); - $('#daterangepicker-widget').data('daterangepicker').setEndDate(moment(pickerEndDate.format('MMMM D, YYYY')).format('MM/DD/YYYY')); - } - function isMonitoringChartsLocation() { - // If active monitoring charts location is not #ow-chart-container and not admin - return window.location.hash === '#ow-chart-container' || window.location.pathname === '/admin/'; - } - - function handleChartZoomChange(chartsContainers) { - // Simply return if we are not at the monitoring chart location - if (!isMonitoringChartsLocation()) { - return; + function setDateRangePickerWidget(pickerStartDate, pickerEndDate) { + addDateRangePickerLabel( + pickerStartDate.format("MMMM D, YYYY"), + pickerEndDate.format("MMMM D, YYYY"), + ); + $("#daterangepicker-widget") + .data("daterangepicker") + .setStartDate( + moment(pickerStartDate.format("MMMM D, YYYY")).format("MM/DD/YYYY"), + ); + $("#daterangepicker-widget") + .data("daterangepicker") + .setEndDate( + moment(pickerEndDate.format("MMMM D, YYYY")).format("MM/DD/YYYY"), + ); } - // Handle chart zooming with custom dates - var zoomCharts = document.getElementsByClassName(chartsContainers); - // Set zoomChartId, required for scrolling after the zoom-in event - $('.js-plotly-plot').on("click dblclick mouseover mouseout", function () { - var zoomChartId = $(this).parent().prop('id'); - if (zoomChartId === 'chart-0') { - var activeChartsLocation = window.location.hash; - zoomChartId = activeChartsLocation === '#ow-chart-container' ? 'container' : 'ow-chart-inner-container'; - } - localStorage.setItem(zoomChartIdKey, zoomChartId); - }); - for (var zoomChart of zoomCharts) { - if (!zoomChart) { - localStorage.setItem(isChartZoomed, false); - localStorage.setItem(isCustomDateRange, false); + function isMonitoringChartsLocation() { + // If active monitoring charts location is not #ow-chart-container and not admin + return ( + window.location.hash === "#ow-chart-container" || + window.location.pathname === "/admin/" + ); + } + + function handleChartZoomChange(chartsContainers) { + // Simply return if we are not at the monitoring chart location + if (!isMonitoringChartsLocation()) { return; } - zoomChart.on('plotly_relayout', - function (eventdata) { // jshint ignore:line - var eventEnd = eventdata['xaxis.range[1]']; - var eventStart = eventdata['xaxis.range[0]']; + // Handle chart zooming with custom dates + var zoomCharts = document.getElementsByClassName(chartsContainers); + // Set zoomChartId, required for scrolling after the zoom-in event + $(".js-plotly-plot").on( + "click dblclick mouseover mouseout", + function () { + var zoomChartId = $(this).parent().prop("id"); + if (zoomChartId === "chart-0") { + var activeChartsLocation = window.location.hash; + zoomChartId = + activeChartsLocation === "#ow-chart-container" + ? "container" + : "ow-chart-inner-container"; + } + localStorage.setItem(zoomChartIdKey, zoomChartId); + }, + ); + for (var zoomChart of zoomCharts) { + if (!zoomChart) { + localStorage.setItem(isChartZoomed, false); + localStorage.setItem(isCustomDateRange, false); + return; + } + zoomChart.on("plotly_relayout", function (eventdata) { + // jshint ignore:line + var eventEnd = eventdata["xaxis.range[1]"]; + var eventStart = eventdata["xaxis.range[0]"]; // Simply return if we are not at the monitoring chart location if (!isMonitoringChartsLocation()) { return; @@ -114,34 +153,51 @@ django.jQuery(function ($) { localStorage.setItem(isChartZoomScroll, true); var daysBeforeZoom = localStorage.getItem(timeRangeKey); // Set custom date range labels & select custom date ranges for the widget - var initialStartLabel = localStorage.getItem(startDayKey) || moment().format('MMMM D, YYYY'); - var initialEndLabel = localStorage.getItem(endDayKey) || moment().format('MMMM D, YYYY'); - var initialStartDate = moment(initialStartLabel, 'MMMM D, YYYY'); - var initialEndDate = moment(initialEndLabel, 'MMMM D, YYYY'); + var initialStartLabel = + localStorage.getItem(startDayKey) || + moment().format("MMMM D, YYYY"); + var initialEndLabel = + localStorage.getItem(endDayKey) || + moment().format("MMMM D, YYYY"); + var initialStartDate = moment(initialStartLabel, "MMMM D, YYYY"); + var initialEndDate = moment(initialEndLabel, "MMMM D, YYYY"); // Set date range picker widget labels setDateRangePickerWidget(initialStartDate, initialEndDate); // On zoom out, load all charts to their initial zoom level loadCharts(daysBeforeZoom, true); // refresh every 2.5 minutes clearInterval(window.owChartRefresh); - window.owChartRefresh = setInterval(loadFetchedCharts, + window.owChartRefresh = setInterval( + loadFetchedCharts, 1000 * 60 * 2.5, - daysBeforeZoom + daysBeforeZoom, ); return; } // When the chart zoomed in, var pickerEndDate = moment(eventEnd); var pickerStartDate = moment(eventStart); - var pickerDays = pickerEndDate.diff(pickerStartDate, 'days') + 'd'; - if (pickerDays === '0d') { - pickerDays = '1d'; + var pickerDays = pickerEndDate.diff(pickerStartDate, "days") + "d"; + if (pickerDays === "0d") { + pickerDays = "1d"; } // Set custom date range values - localStorage.setItem(zoomStartDateTimeKey, pickerStartDate.format('YYYY-MM-DD HH:mm:ss')); - localStorage.setItem(zoomEndDateTimeKey, pickerEndDate.format('YYYY-MM-DD HH:mm:ss')); - localStorage.setItem(zoomStartDayKey, pickerStartDate.format('MMMM D, YYYY')); - localStorage.setItem(zoomEndDayKey, pickerEndDate.format('MMMM D, YYYY')); + localStorage.setItem( + zoomStartDateTimeKey, + pickerStartDate.format("YYYY-MM-DD HH:mm:ss"), + ); + localStorage.setItem( + zoomEndDateTimeKey, + pickerEndDate.format("YYYY-MM-DD HH:mm:ss"), + ); + localStorage.setItem( + zoomStartDayKey, + pickerStartDate.format("MMMM D, YYYY"), + ); + localStorage.setItem( + zoomEndDayKey, + pickerEndDate.format("MMMM D, YYYY"), + ); localStorage.setItem(isChartZoomScroll, true); localStorage.setItem(isChartZoomed, true); localStorage.setItem(isCustomDateRange, true); @@ -152,248 +208,300 @@ django.jQuery(function ($) { loadCharts(pickerDays, true); // refresh every 2.5 minutes clearInterval(window.owChartRefresh); - window.owChartRefresh = setInterval(loadFetchedCharts, + window.owChartRefresh = setInterval( + loadFetchedCharts, 1000 * 60 * 2.5, - pickerDays + pickerDays, ); - } - ); + }); + } } - } - function triggerZoomCharts (containerClassName) { - handleChartZoomChange(containerClassName); - const zoomChartContainer = document.getElementById(localStorage.getItem(zoomChartIdKey)); - // If the chart zoom scrolling is active, then scroll to the zoomed chart container - if (localStorage.getItem(isChartZoomScroll) === 'true' && zoomChartContainer) { - zoomChartContainer.scrollIntoView(); + function triggerZoomCharts(containerClassName) { + handleChartZoomChange(containerClassName); + const zoomChartContainer = document.getElementById( + localStorage.getItem(zoomChartIdKey), + ); + // If the chart zoom scrolling is active, then scroll to the zoomed chart container + if ( + localStorage.getItem(isChartZoomScroll) === "true" && + zoomChartContainer + ) { + zoomChartContainer.scrollIntoView(); + } } - } - var chartQuickLinks, chartContents = $('#ow-chart-contents'), - fallback = $('#ow-chart-fallback'), - defaultTimeRange = localStorage.getItem(timeRangeKey) || $('#monitoring-timeseries-default-time').data('value'), - apiUrl = $('#monitoring-timeseries-api-url').data('value'), - baseUrl = `${apiUrl}?time=`, - globalLoadingOverlay = $('#loading-overlay'), - localLoadingOverlay = $('#chart-loading-overlay'), - getChartFetchUrl = function (time) { - var url = baseUrl + time; - // pass pickerEndDate and pickerStartDate to url - if (localStorage.getItem(isCustomDateRange) === 'true' || localStorage.getItem(pickerChosenLabelKey) === customDateRangeLabel) { - var startDate = localStorage.getItem(startDateTimeKey); - var endDate = localStorage.getItem(endDateTimeKey); - if (localStorage.getItem(isChartZoomed) === 'true') { - endDate = localStorage.getItem(zoomEndDateTimeKey); - startDate = localStorage.getItem(zoomStartDateTimeKey); + var chartQuickLinks, + chartContents = $("#ow-chart-contents"), + fallback = $("#ow-chart-fallback"), + defaultTimeRange = + localStorage.getItem(timeRangeKey) || + $("#monitoring-timeseries-default-time").data("value"), + apiUrl = $("#monitoring-timeseries-api-url").data("value"), + baseUrl = `${apiUrl}?time=`, + globalLoadingOverlay = $("#loading-overlay"), + localLoadingOverlay = $("#chart-loading-overlay"), + getChartFetchUrl = function (time) { + var url = baseUrl + time; + // pass pickerEndDate and pickerStartDate to url + if ( + localStorage.getItem(isCustomDateRange) === "true" || + localStorage.getItem(pickerChosenLabelKey) === customDateRangeLabel + ) { + var startDate = localStorage.getItem(startDateTimeKey); + var endDate = localStorage.getItem(endDateTimeKey); + if (localStorage.getItem(isChartZoomed) === "true") { + endDate = localStorage.getItem(zoomEndDateTimeKey); + startDate = localStorage.getItem(zoomStartDateTimeKey); + } + // Ensure that the 'endDate' of zooming events + // is never greater than the 'now' date time + const now = moment().format("YYYY-MM-DD HH:mm:ss"); + const endDateTime = moment(endDate).format("YYYY-MM-DD HH:mm:ss"); + endDate = endDateTime > now ? now : endDateTime; + url = `${apiUrl}?start=${startDate}&end=${endDate}`; + var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + if (timezone) { + url = `${url}&timezone=${timezone}`; + } } - // Ensure that the 'endDate' of zooming events - // is never greater than the 'now' date time - const now = moment().format('YYYY-MM-DD HH:mm:ss'); - const endDateTime = moment(endDate).format('YYYY-MM-DD HH:mm:ss'); - endDate = endDateTime > now ? now : endDateTime; - url = `${apiUrl}?start=${startDate}&end=${endDate}`; - var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - if (timezone) { - url = `${url}&timezone=${timezone}`; + if ($("#org-selector").val()) { + var orgSlug = $("#org-selector").val(); + url = `${url}&organization_slug=${orgSlug}`; } + return url; + }, + createCharts = function (data) { + $.each(data.charts, function (i, chart) { + var htmlId = "chart-" + i, + chartDiv = $("#" + htmlId), + chartQuickLink = chartQuickLinks[chart.title]; + if (!chartDiv.length) { + chartContents.append( + '
' + + '
', + ); + } + createChart( + chart, + data.x, + htmlId, + chart.title, + chart.type, + chartQuickLink, + ); + }); + }, + addOrganizationSelector = function (data) { + var orgSelector = $("#org-selector"); + if (data.organizations === undefined) { + return; + } + if (orgSelector.data("select2-id") === "org-selector") { + return; + } + orgSelector.parent().show(); + orgSelector.select2({ + data: data.organizations, + allowClear: true, + placeholder: gettext("Organization Filter"), + }); + orgSelector.show(); + }, + loadCharts = function (time, showLoading) { + $.ajax(getChartFetchUrl(time), { + dataType: "json", + xhrFields: { + withCredentials: true, + }, + beforeSend: function () { + chartContents.hide(); + chartContents.empty(); + fallback.hide(); + if (showLoading) { + globalLoadingOverlay.show(); + } + localLoadingOverlay.show(); + }, + success: function (data) { + localLoadingOverlay.hide(); + if (data.charts.length) { + chartContents.show(); + } else { + fallback.show(); + } + createCharts(data); + addOrganizationSelector(data); + }, + error: function (response) { + var errorMessage = gettext( + "Something went wrong while loading the charts", + ); + if (response.responseJSON) { + if (response.responseJSON.constructor === Array) { + errorMessage = + errorMessage + ": " + response.responseJSON.join(" "); + } + } + alert(errorMessage); + }, + complete: function () { + triggerZoomCharts("js-plotly-plot"); + localLoadingOverlay.fadeOut(200, function () { + if (showLoading) { + globalLoadingOverlay.fadeOut(200); + } + }); + }, + }); + }; + try { + chartQuickLinks = JSON.parse($("#monitoring-chart-quick-links").html()); + } catch (error) { + chartQuickLinks = {}; + } + + window.triggerChartLoading = function () { + // Charts load with the last set time range or default time range + var range = localStorage.getItem(timeRangeKey) || defaultTimeRange; + var startLabel = + localStorage.getItem(startDayKey) || moment().format("MMMM D, YYYY"); + var endLabel = + localStorage.getItem(endDayKey) || moment().format("MMMM D, YYYY"); + + // Disable the zoom chart and scrolling when we refresh the page + localStorage.setItem(isChartZoomScroll, false); + localStorage.setItem(isChartZoomed, false); + + if (localStorage.getItem(isCustomDateRange) === "true") { + // Add label to daterangepicker widget + addDateRangePickerLabel(startLabel, endLabel); + // Set last selected custom date after page reload + var startDate = moment(startLabel, "MMMM D, YYYY"); + var endDate = moment(endLabel, "MMMM D, YYYY"); + $("#daterangepicker-widget") + .data("daterangepicker") + .setStartDate(moment(startDate).format("MM/DD/YYYY")); + $("#daterangepicker-widget") + .data("daterangepicker") + .setEndDate(moment(endDate).format("MM/DD/YYYY")); + // Then loads charts with custom ranges selected + loadCharts(range, true); + } else { + endLabel = moment().format("MMMM D, YYYY"); + startLabel = moment() + .subtract(range.split("d")[0], "days") + .format("MMMM D, YYYY"); + // Add label to daterangepicker widget + addDateRangePickerLabel(startLabel, endLabel); + // Set last selected default dates after page reload + $(".daterangepicker .ranges ul li[data-time=" + range + "]").trigger( + "click", + ); } - if ($('#org-selector').val()) { - var orgSlug = $('#org-selector').val(); - url = `${url}&organization_slug=${orgSlug}`; + }; + // try adding the browser timezone to the querystring + try { + var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + if (timezone) { + baseUrl = baseUrl.replace("time=", "timezone=" + timezone + "&time="); } - return url; - }, - createCharts = function (data){ - $.each(data.charts, function (i, chart) { - var htmlId = 'chart-' + i, - chartDiv = $('#' + htmlId), - chartQuickLink = chartQuickLinks[chart.title]; - if (!chartDiv.length) { - chartContents.append( - '
' + - '
' - ); + // ignore failures (older browsers do not support this) + } catch (e) {} + + // daterangepicker widget logic here + $("#daterangepicker-widget").on( + "apply.daterangepicker", + function (ev, picker) { + pickerChosenLabel = picker.chosenLabel; + pickerStart = moment(picker.startDate.format("YYYY-MM-DD HH:mm:ss")); + pickerEnd = moment(picker.endDate.format("YYYY-MM-DD HH:mm:ss")); + pickerDays = pickerEnd.diff(pickerStart, "days") + "d"; + // set date values required for daterangepicker labels + localStorage.setItem(pickerChosenLabelKey, pickerChosenLabel); + localStorage.setItem( + startDateTimeKey, + picker.startDate.format("YYYY-MM-DD HH:mm:ss"), + ); + localStorage.setItem( + endDateTimeKey, + picker.endDate.format("YYYY-MM-DD HH:mm:ss"), + ); + localStorage.setItem(startDayKey, pickerStart.format("MMMM D, YYYY")); + localStorage.setItem(endDayKey, pickerEnd.format("MMMM D, YYYY")); + localStorage.setItem(isChartZoomed, false); + localStorage.setItem(isChartZoomScroll, false); + localStorage.setItem(timeRangeKey, pickerDays); + localStorage.setItem( + isCustomDateRange, + pickerChosenLabel === "Custom Range", + ); + loadCharts(pickerDays, true); + // refresh charts every 2.5 minutes + clearInterval(window.owChartRefresh); + window.owChartRefresh = setInterval( + loadFetchedCharts, + 1000 * 60 * 2.5, + pickerDays, + ); + }, + ); + // bind export button + $("#ow-chart-export").click(function () { + var queryString, + queryParams = { csv: 1 }; + queryParams.time = localStorage.getItem(timeRangeKey); + // If custom or pickerChosenLabelKey is 'Custom Range', pass pickerEndDate and pickerStartDate to csv url + if ( + localStorage.getItem(isCustomDateRange) === "true" || + localStorage.getItem(pickerChosenLabelKey) === customDateRangeLabel + ) { + queryParams.start = localStorage.getItem(startDateTimeKey); + queryParams.end = localStorage.getItem(endDateTimeKey); + if (localStorage.getItem(isChartZoomed) === "true") { + queryParams.time = localStorage.getItem(zoomtimeRangeKey); + queryParams.end = localStorage.getItem(zoomEndDateTimeKey); + queryParams.start = localStorage.getItem(zoomStartDateTimeKey); + } + timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + if (timezone) { + queryParams.timezone = timezone; } - createChart(chart, data.x, htmlId, chart.title, chart.type, chartQuickLink); - }); - }, - addOrganizationSelector = function (data) { - var orgSelector = $('#org-selector'); - if (data.organizations === undefined) { - return; } - if (orgSelector.data('select2-id') === 'org-selector') { - return; + if ($("#org-selector").val()) { + queryParams.organization_slug = $("#org-selector").val(); } - orgSelector.parent().show(); - orgSelector.select2({ - data: data.organizations, - allowClear: true, - placeholder: gettext('Organization Filter') - }); - orgSelector.show(); - }, - loadCharts = function (time, showLoading) { + queryString = Object.keys(queryParams) + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`, + ) + .join("&"); + location.href = `${apiUrl}?${queryString}`; + }); + // fetch chart data and replace the old charts with the new ones + function loadFetchedCharts(time) { $.ajax(getChartFetchUrl(time), { - dataType: 'json', - xhrFields: { - withCredentials: true - }, - beforeSend: function () { - chartContents.hide(); - chartContents.empty(); - fallback.hide(); - if (showLoading) { - globalLoadingOverlay.show(); - } - localLoadingOverlay.show(); - }, + dataType: "json", success: function (data) { - localLoadingOverlay.hide(); if (data.charts.length) { - chartContents.show(); - } else { - fallback.show(); + createCharts(data); + triggerZoomCharts("js-plotly-plot"); } - createCharts(data); - addOrganizationSelector(data); }, - error: function (response) { - var errorMessage = gettext('Something went wrong while loading the charts'); - if (response.responseJSON) { - if (response.responseJSON.constructor === Array) { - errorMessage = errorMessage + ': ' + response.responseJSON.join(' '); - } - } - alert(errorMessage); + error: function () { + window.console.error("Unable to fetch chart data."); }, - complete: function () { - triggerZoomCharts('js-plotly-plot'); - localLoadingOverlay.fadeOut(200, function() { - if (showLoading) { - globalLoadingOverlay.fadeOut(200); - } - }); - } }); - }; - try { - chartQuickLinks = JSON.parse($('#monitoring-chart-quick-links').html()); - } catch (error) { - chartQuickLinks = {}; - } - - window.triggerChartLoading = function() { - // Charts load with the last set time range or default time range - var range = localStorage.getItem(timeRangeKey) || defaultTimeRange; - var startLabel = localStorage.getItem(startDayKey) || moment().format('MMMM D, YYYY'); - var endLabel = localStorage.getItem(endDayKey) || moment().format('MMMM D, YYYY'); - - // Disable the zoom chart and scrolling when we refresh the page - localStorage.setItem(isChartZoomScroll, false); - localStorage.setItem(isChartZoomed, false); - - if (localStorage.getItem(isCustomDateRange) === 'true') { - // Add label to daterangepicker widget - addDateRangePickerLabel(startLabel, endLabel); - // Set last selected custom date after page reload - var startDate = moment(startLabel, 'MMMM D, YYYY'); - var endDate = moment(endLabel, 'MMMM D, YYYY'); - $('#daterangepicker-widget').data('daterangepicker').setStartDate(moment(startDate).format('MM/DD/YYYY')); - $('#daterangepicker-widget').data('daterangepicker').setEndDate(moment(endDate).format('MM/DD/YYYY')); - // Then loads charts with custom ranges selected - loadCharts(range, true); } - else { - endLabel = moment().format('MMMM D, YYYY'); - startLabel = moment().subtract(range.split('d')[0], 'days').format('MMMM D, YYYY'); - // Add label to daterangepicker widget - addDateRangePickerLabel(startLabel, endLabel); - // Set last selected default dates after page reload - $('.daterangepicker .ranges ul li[data-time=' + range + ']').trigger('click'); - } - }; - // try adding the browser timezone to the querystring - try { - var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - if (timezone) { - baseUrl = baseUrl.replace('time=', 'timezone=' + timezone + '&time='); - } - // ignore failures (older browsers do not support this) - } catch (e) {} - // daterangepicker widget logic here - $("#daterangepicker-widget").on('apply.daterangepicker', function (ev, picker) { - pickerChosenLabel = picker.chosenLabel; - pickerStart = moment(picker.startDate.format('YYYY-MM-DD HH:mm:ss')); - pickerEnd = moment(picker.endDate.format('YYYY-MM-DD HH:mm:ss')); - pickerDays = pickerEnd.diff(pickerStart, 'days') + 'd'; - // set date values required for daterangepicker labels - localStorage.setItem(pickerChosenLabelKey, pickerChosenLabel); - localStorage.setItem(startDateTimeKey, picker.startDate.format('YYYY-MM-DD HH:mm:ss')); - localStorage.setItem(endDateTimeKey, picker.endDate.format('YYYY-MM-DD HH:mm:ss')); - localStorage.setItem(startDayKey, pickerStart.format('MMMM D, YYYY')); - localStorage.setItem(endDayKey, pickerEnd.format('MMMM D, YYYY')); - localStorage.setItem(isChartZoomed, false); - localStorage.setItem(isChartZoomScroll, false); - localStorage.setItem(timeRangeKey, pickerDays); - localStorage.setItem(isCustomDateRange, pickerChosenLabel === "Custom Range"); - loadCharts(pickerDays, true); - // refresh charts every 2.5 minutes - clearInterval(window.owChartRefresh); - window.owChartRefresh = setInterval(loadFetchedCharts, - 1000 * 60 * 2.5, - pickerDays - ); - }); - // bind export button - $('#ow-chart-export').click(function () { - var queryString, - queryParams = {'csv': 1}; - queryParams.time = localStorage.getItem(timeRangeKey); - // If custom or pickerChosenLabelKey is 'Custom Range', pass pickerEndDate and pickerStartDate to csv url - if (localStorage.getItem(isCustomDateRange) === 'true' || localStorage.getItem(pickerChosenLabelKey) === customDateRangeLabel) { - queryParams.start = localStorage.getItem(startDateTimeKey); - queryParams.end = localStorage.getItem(endDateTimeKey); - if (localStorage.getItem(isChartZoomed) === 'true') { - queryParams.time = localStorage.getItem(zoomtimeRangeKey); - queryParams.end = localStorage.getItem(zoomEndDateTimeKey); - queryParams.start = localStorage.getItem(zoomStartDateTimeKey); - } - timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - if (timezone) { - queryParams.timezone = timezone; - } - } - if ($('#org-selector').val()) { - queryParams.organization_slug = $('#org-selector').val(); - } - queryString = Object.keys(queryParams) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`) - .join('&'); - location.href = `${apiUrl}?${queryString}`; - }); - // fetch chart data and replace the old charts with the new ones - function loadFetchedCharts(time){ - $.ajax(getChartFetchUrl(time), { - dataType: 'json', - success: function (data) { - if (data.charts.length) { - createCharts(data); - triggerZoomCharts('js-plotly-plot'); - } - }, - error: function () { - window.console.error('Unable to fetch chart data.'); - }, + $("#org-selector").change(function () { + loadCharts( + localStorage.getItem(timeRangeKey) || defaultTimeRange, + true, + ); }); - } - - $('#org-selector').change(function(){ - loadCharts( - localStorage.getItem(timeRangeKey) || defaultTimeRange, - true - ); }); - }); -}(django.jQuery)); + })(django.jQuery), +); diff --git a/openwisp_monitoring/monitoring/static/monitoring/js/chart.js b/openwisp_monitoring/monitoring/static/monitoring/js/chart.js index 4f826bcfc..3503fd522 100644 --- a/openwisp_monitoring/monitoring/static/monitoring/js/chart.js +++ b/openwisp_monitoring/monitoring/static/monitoring/js/chart.js @@ -1,436 +1,466 @@ -'use strict'; +"use strict"; (function ($) { - function sortByTraceOrder(traceOrder, arr, propertyName){ - if (traceOrder === undefined) { - return arr; - } - var newArr = []; - for (var traceIndex=0; traceIndex= 1000) { - multiplier = 0.001; - unit = 'T' + unit; + var newArr = []; + for (var traceIndex = 0; traceIndex < traceOrder.length; ++traceIndex) { + for (var arrIndex = 0; arrIndex < arr.length; ++arrIndex) { + if (traceOrder[traceIndex] === arr[arrIndex][propertyName]) { + newArr.push(arr[arrIndex]); + break; } - return { - multiplier: multiplier, - unit: unit - }; + } } + return newArr; + } - function getAdaptiveBytes(value, multiplier) { - return Math.round((value * multiplier) * 100) / 100; + function getAdaptiveScale(value, multiplier, unit) { + if (value == 0) { + multiplier = 1; + unit = unit; + } else if (value < 0.001) { + multiplier = 1000000; + unit = "K" + unit; + } else if (value < 1) { + multiplier = 1000; + unit = "M" + unit; + } else if (value < 1000) { + multiplier = 1; + unit = "G" + unit; + } else if (value >= 1000) { + multiplier = 0.001; + unit = "T" + unit; } + return { + multiplier: multiplier, + unit: unit, + }; + } - function adaptiveFilterPoints(charts, layout, yRawVal, chartUnit = '') { - var y = charts[0].y, sum = 0, count = 0, shownVal, average; - for (var i=0; i < y.length; i++) { - sum += y[i]; - if (y[i]) { - count++; - } - } - average = sum / count; - var scales = getAdaptiveScale(average, 1, chartUnit); - var multiplier = scales.multiplier, - unit = scales.unit; - for (i=0; i < y.length; i++) { - for (var j=0; j < charts.length; j++) { - if (yRawVal[i] == null) { - charts[j].hovertemplate[i] = 'N/A' + ''; - continue; - } - shownVal = charts[j].y[i]; - charts[j].y[i] = getAdaptiveBytes(charts[j].y[i], multiplier); - var hoverScales = getAdaptiveScale(shownVal, 1, chartUnit); - var hoverMultiplier = hoverScales.multiplier, - hoverUnit = hoverScales.unit; - shownVal = getAdaptiveBytes(shownVal, hoverMultiplier); - charts[j].hovertemplate[i] = shownVal + ' ' + hoverUnit; - } + function getAdaptiveBytes(value, multiplier) { + return Math.round(value * multiplier * 100) / 100; + } + + function adaptiveFilterPoints(charts, layout, yRawVal, chartUnit = "") { + var y = charts[0].y, + sum = 0, + count = 0, + shownVal, + average; + for (var i = 0; i < y.length; i++) { + sum += y[i]; + if (y[i]) { + count++; + } + } + average = sum / count; + var scales = getAdaptiveScale(average, 1, chartUnit); + var multiplier = scales.multiplier, + unit = scales.unit; + for (i = 0; i < y.length; i++) { + for (var j = 0; j < charts.length; j++) { + if (yRawVal[i] == null) { + charts[j].hovertemplate[i] = "N/A" + ""; + continue; } - layout.yaxis.title = unit; + shownVal = charts[j].y[i]; + charts[j].y[i] = getAdaptiveBytes(charts[j].y[i], multiplier); + var hoverScales = getAdaptiveScale(shownVal, 1, chartUnit); + var hoverMultiplier = hoverScales.multiplier, + hoverUnit = hoverScales.unit; + shownVal = getAdaptiveBytes(shownVal, hoverMultiplier); + charts[j].hovertemplate[i] = shownVal + " " + hoverUnit; + } } + layout.yaxis.title = unit; + } - function adaptiveFilterSummary(i, percircles, value, chartUnit = '') { - var scales = getAdaptiveScale(value, 1, chartUnit), - multiplier = scales.multiplier, - unit = scales.unit; - value = getAdaptiveBytes(value, multiplier); - percircles[i].text = value + ' ' + unit; - } + function adaptiveFilterSummary(i, percircles, value, chartUnit = "") { + var scales = getAdaptiveScale(value, 1, chartUnit), + multiplier = scales.multiplier, + unit = scales.unit; + value = getAdaptiveBytes(value, multiplier); + percircles[i].text = value + " " + unit; + } - window.createChart = function (data, x, id, title, type, quickLink) { - if (data === false) { - alert(gettext('error while receiving data from server')); - return; - } - if (!x) {x = data.x;} + window.createChart = function (data, x, id, title, type, quickLink) { + if (data === false) { + alert(gettext("error while receiving data from server")); + return; + } + if (!x) { + x = data.x; + } - var xaxis = data.xaxis || {}; - var yaxis = data.yaxis || {}; - xaxis.visible = type != 'histogram'; + var xaxis = data.xaxis || {}; + var yaxis = data.yaxis || {}; + xaxis.visible = type != "histogram"; - var mode = x.length > 30 ? 'lines' : 'markers+lines', - layout = { - showlegend: true, - legend: { - orientation: 'h', - xanchor: 'center', - yanchor: 'top', - y: -0.15, - x: 0.5, - traceorder: 'normal' - }, - xaxis: xaxis, - yaxis: yaxis, - margin: { - l: 50, - r: 50, - b: 15, - t: 20, - pad: 4 - }, - height: 350, - hovermode: 'x unified' - }, - charts = [], - container = $('#' + id), - plotlyContainer = container.find('.js-plotly-plot').get(0), - notApplicable = gettext('N/A'), - unit = data.unit, - summaryLabels = [], - fixedY = false, - fixedYMax = 100, - help, tooltip, heading; - if (data.colors) { - layout.colorway = data.colors; + var mode = x.length > 30 ? "lines" : "markers+lines", + layout = { + showlegend: true, + legend: { + orientation: "h", + xanchor: "center", + yanchor: "top", + y: -0.15, + x: 0.5, + traceorder: "normal", + }, + xaxis: xaxis, + yaxis: yaxis, + margin: { + l: 50, + r: 50, + b: 15, + t: 20, + pad: 4, + }, + height: 350, + hovermode: "x unified", + }, + charts = [], + container = $("#" + id), + plotlyContainer = container.find(".js-plotly-plot").get(0), + notApplicable = gettext("N/A"), + unit = data.unit, + summaryLabels = [], + fixedY = false, + fixedYMax = 100, + help, + tooltip, + heading; + if (data.colors) { + layout.colorway = data.colors; + } + // hide yaxis in fixed value charts + if (data.colorscale && typeof data.colorscale.fixed_value !== undefined) { + layout.yaxis = { visible: false }; + // if using %, we'll make sure the minimum y axis range is 0-100 + } else if (unit === "%") { + fixedY = true; + } + if (type === "histogram") { + layout.hovermode = "closest"; + } + var map, mapped, label, fixedValue, key, chartUnit, yValues; + // given a value, returns its color and description + // according to the color map configuration of this chart + function findInColorMap(value) { + var desc, + color, + controlVal, + n, + map = data.colorscale.map; + if (!map) { + return false; + } + for (n in map) { + controlVal = map[n][0]; + if (controlVal === null || value >= controlVal) { + color = map[n][1]; + desc = map[n][2]; + break; } - // hide yaxis in fixed value charts - if (data.colorscale && typeof(data.colorscale.fixed_value) !== undefined) { - layout.yaxis = {visible: false}; - // if using %, we'll make sure the minimum y axis range is 0-100 - } else if (unit === '%') { - fixedY = true; + } + return { color: color, desc: desc }; + } + if (data.calculate_total === true) { + var total = data.traces[0][1].slice(); + for (i = 1; i < data.traces.length; ++i) { + for (var j = 0; j < data.traces[i][1].length; ++j) { + total[j] += data.traces[i][1][j]; } - if (type === 'histogram') { - layout.hovermode = 'closest'; + } + data.traces.push(["total", total]); + data.summary.total = Object.values(data.summary).reduce(function (a, b) { + return a + b; + }, 0); + } + // loop over traces to put them on the chart + for (var i = 0; i < data.traces.length; i++) { + key = data.traces[i][0]; + // label for the trace, use trace_labels if available, otherwise + // replace underscores with spaces in the key name + label = + (data.trace_labels && data.trace_labels[data.traces[i][0]]) || + data.traces[i][0].replace(/_/g, " "); + + if (data.summary_labels) { + if (data.trace_order) { + summaryLabels.push([ + key, + data.summary_labels[data.trace_order.indexOf(key)], + ]); + } else { + summaryLabels.push([key, data.summary_labels[i]]); } - var map, mapped, label, fixedValue, key, chartUnit, yValues; - // given a value, returns its color and description - // according to the color map configuration of this chart - function findInColorMap(value) { - var desc, color, controlVal, n, - map = data.colorscale.map; - if (!map) { return false; } - for (n in map) { - controlVal = map[n][0]; - if (controlVal === null || value >= controlVal) { - color = map[n][1]; - desc = map[n][2]; - break; - } - } - return {color: color, desc: desc}; + } + var options = { + name: label, + type: type, + mode: mode, + fill: data.fill || "tozeroy", + hovertemplate: [], + y: [], + // We use the "_key" field to sort the charts + // according to the order defined in "data.trace_order" + _key: key, + _connectPoints: data.connect_points || false, + }, + yValuesRaw = data.traces[i][1]; + if (type !== "histogram") { + options.x = x; + options.hoverinfo = "x+y"; + } else { + options.x = [""]; + options.histfunc = "sum"; + } + if (type.includes("stackedbar")) { + layout.barmode = "stack"; + options.type = "bar"; + if (type === "stackedbar+lines") { + if (data.trace_type[key] === "lines") { + options.type = "scatter"; + options.mode = "lines+markers"; + options.line = { shape: "hvh" }; + options.fill = data.fill; + } + if (options._connectPoints) { + options.mode = "lines"; + } } - if (data.calculate_total === true) { - var total = data.traces[0][1].slice(); - for (i = 1; i < data.traces.length; ++i) { - for (var j = 0; j < data.traces[i][1].length; ++j) { - total[j] += data.traces[i][1][j]; - } - } - data.traces.push(['total', total]); - data.summary.total = Object.values(data.summary).reduce(function (a, b) { - return a + b; - }, 0); + } + if (data.colorscale) { + var config = data.colorscale; + map = data.colorscale.map; + fixedValue = data.colorscale.fixed_value; + options.marker = { + cmax: config.max, + cmin: config.min, + colorbar: { title: config.label }, + colorscale: config.scale, + color: [], + }; + if (map) { + layout.showlegend = false; + layout.margin.b = 45; } - // loop over traces to put them on the chart - for (var i=0; i