define(['./web-chart.xml'], function(pageXml) { function isInteger(value) { return (typeof value === 'number') && isFinite(value) && (Math.floor(value) === value); } function isFloat(value) { return (typeof value === 'number') && isFinite(value) && (Math.floor(value) !== value); } function countDigits(value) { var s = String(value); var i = s.indexOf('.'); return i > 0 ? s.length - i - 1 : 0; } function createNumberChartDataSets(dataPointSet, datasets, prefix) { datasets = datasets || []; prefix = prefix || ''; var avgSerie = []; var minSerie = []; var maxSerie = []; var maxDigits = 0; dataPointSet.forEach(function(item) { var digits = countDigits(item.min); if (digits > maxDigits) { maxDigits = digits; } minSerie.push(item.min); maxSerie.push(item.max); }); dataPointSet.forEach(function(item) { var avg = item.average; if (isFloat(avg)) { avg = avg.toFixed(maxDigits); } avgSerie.push(avg); }); var hue = (Math.abs(hashString(prefix)) % 240) / 240; var maxColor = hsvToRgb(hue, 0.7, 0.8); var avgColor = hsvToRgb(hue, 0.5, 0.8); var minColor = hsvToRgb(hue, 0.3, 0.8); datasets.push({ label: prefix + 'Average', backgroundColor: Chart.helpers.color(avgColor).alpha(0.5).rgbString(), borderColor: avgColor, fill: false, data: avgSerie }, { label: prefix + 'Max', hidden: true, backgroundColor: Chart.helpers.color(maxColor).alpha(0.5).rgbString(), borderColor: maxColor, fill: false, data: maxSerie }, { label: prefix + 'Min', hidden: true, backgroundColor: Chart.helpers.color(minColor).alpha(0.5).rgbString(), borderColor: minColor, fill: false, data: minSerie }); return datasets; } function createMappedChartDataSets(dataPointSet, datasets, prefix, map) { datasets = datasets || []; prefix = prefix || ''; var chgSerie = []; var valSerie = []; dataPointSet.forEach(function(item) { chgSerie.push(item.changes); valSerie.push(item.index); }); var hue = (Math.abs(hashString(prefix) + 120) % 240) / 240; var hue2 = (Math.abs(hashString(prefix) + 180) % 240) / 240; var valColor = hsvToRgb(hue, 0.5, 0.8); var chgColor = hsvToRgb(hue2, 0.5, 0.8); datasets.push({ label: prefix + 'Values', backgroundColor: Chart.helpers.color(valColor).alpha(0.5).rgbString(), borderColor: valColor, fill: false, steppedLine: 'after', pointStyle: 'rectRot', //pointRadius: 3, //yAxisID: 'map', map: map, data: valSerie }); datasets.push({ label: prefix + 'Changes', hidden: true, backgroundColor: Chart.helpers.color(chgColor).alpha(0.5).rgbString(), borderColor: chgColor, fill: false, steppedLine: 'after', pointStyle: 'triangle', data: chgSerie }); return datasets; } function createChangesChartDataSets(dataPointSet, datasets, prefix) { datasets = datasets || []; prefix = prefix || ''; var chgSerie = []; dataPointSet.forEach(function(item) { chgSerie.push(item.changes); }); var hue = (Math.abs(hashString(prefix) + 60) % 240) / 240; var chgColor = hsvToRgb(hue, 0.5, 0.8); datasets.push({ label: prefix + 'Changes', backgroundColor: Chart.helpers.color(chgColor).alpha(0.5).rgbString(), borderColor: chgColor, fill: true, data: chgSerie }); return datasets; } function listDates(dataPointSet) { var dates = []; var labels = []; dataPointSet.forEach(function(item) { var date = new Date(item.time * 1000); dates.push(date); labels.push(date.toISOString().substring(11, 16)); }); return dates; } function createChartDataSets(dataPointSet, datasets, prefix) { //console.log('createChartDataSets()', dataPointSet, datasets, prefix); if (dataPointSet.length === 0) { return []; } var firstItem = dataPointSet[0]; if ('map' in firstItem) { return createMappedChartDataSets(dataPointSet, datasets, prefix, firstItem.map); } if ((firstItem.type === 'number') || (firstItem.type === 'integer')) { return createNumberChartDataSets(dataPointSet, datasets, prefix); } // TODO Remove var lastItem = dataPointSet[dataPointSet.length - 1]; if ('average' in lastItem) { return createNumberChartDataSets(dataPointSet, datasets, prefix); } if ('changes' in lastItem) { return createChangesChartDataSets(dataPointSet, datasets, prefix); } return []; } function loadChart() { if (typeof Chart === 'undefined') { return loadScripts('static/moment.js', 'static/chart/Chart.min.js'); } return Promise.resolve(); } /************************************************************ * Chart ************************************************************/ var vue = new Vue({ template: pageXml, data: { chartType: 'line', chartTension: 0, chartBeginAtZero: true, path: '', paths: [], toDays: 0, things: [], duration: 43200, period: 0, chart: null }, methods: { getHistoricalDataHeaders: function() { var duration = parseInt(this.duration, 10); var period = parseInt(this.period, 10); if (period <= 0) { period = duration / 120; // how many data points period = Math.floor(period / 300) * 300; // round to 5 min period = Math.max(900, Math.min(period, SEC_IN_DAY)); // between 15 min to one day //console.log('auto period is ' + period); } var toDays = parseInt(this.toDays, 10); var toDate = new Date(Date.now() - (toDays * SEC_IN_DAY * 1000)); var nextHour = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate(), toDate.getHours() + 1); var tomorrow = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate() + 1); var toTime = dateToSec(duration > SEC_IN_DAY ? tomorrow : nextHour); var fromTime = toTime - duration; return { "X-FROM-TIME": fromTime, "X-TO-TIME": toTime, "X-PERIOD": period }; }, loadMultiHistoricalData: function(paths) { this.paths = paths; var headers = this.getHistoricalDataHeaders(); headers["X-PATHS"] = paths.join(','); var self = this; fetch('/engine/historicalData/', { method: 'GET', headers: headers }).then(getResponseJson).then(function(dataPointSets) { //console.log('fetch()', response); var dates = null; var datasets = []; dataPointSets.forEach(function(dataPointSet, i) { if (dates === null) { dates = listDates(dataPointSet); } createChartDataSets(dataPointSet, datasets, '' + (i + 1) + ' '); }); self.createChart(dates, datasets); }); }, loadHistoricalData: function(path) { this.path = path; var self = this; loadChart().then(function() { return fetch('/engine/historicalData/' + path, { method: 'GET', headers: self.getHistoricalDataHeaders() }); }).then(getResponseJson).then(function(dataPointSet) { //console.log('fetch()', response); self.createChart(listDates(dataPointSet), createChartDataSets(dataPointSet)); }); }, openPath: function() { //console.info('openPath() ' + this.path); if (this.path) { app.toPage('data-chart', this.path); } }, onShow: function(path) { if (path) { this.loadHistoricalData(path); } //console.log('onShow(' + path + ') path = "' + this.path + '"'); var self = this; app.getThings().then(function(things) { self.things = things; }); }, cleanMultiHistoricalData: function() { this.paths = []; }, pushMultiHistoricalData: function() { if (this.path) { this.paths.push(this.path); this.path = ''; this.reloadHistoricalData(); } }, reloadHistoricalData: function() { if (this.path) { this.loadHistoricalData(this.path); } else if (this.paths.length > 0) { this.loadMultiHistoricalData(this.paths); } }, createChart: function(dates, datasets) { if (this.chart) { this.chart.destroy(); } //var lastIndex = title.lastIndexOf('/'); //if (lastIndex > 0) { title = title.substring(lastIndex + 1); } var chartTension = parseFloat(this.chartTension); var chartBeginAtZero = parseBoolean(this.chartBeginAtZero); var yAxes = [{ scaleLabel: { labelString: 'Value', //display: true }, //display: true, //position: 'left', ticks: { //suggestedMin: 10, //callback: ticksCallback, //suggestedMax: suggestedMax, beginAtZero: chartBeginAtZero } }]; datasets.forEach(function(dataset, index) { if ('map' in dataset) { // https://www.chartjs.org/samples/latest/scales/non-numeric-y.html var map = dataset.map; delete dataset.map; dataset.yAxisID = 'map'; yAxes.push({ id: dataset.yAxisID, //type: 'category', //display: true, position: 'right', ticks: { beginAtZero: true, callback: function(value) { if (isInteger(value) && (value >= 1) && (value <= map.length)) { return map[value - 1]; } return undefined; }, suggestedMax: 3 } }); } }); //console.info('chart', datasets, yAxes); this.chart = new Chart('chart-data-view-canvas', { type: this.chartType, data: { labels: dates, datasets: datasets }, options: { //responsive: true, elements: { line: { tension: chartTension }, rectangle: { borderWidth: 1 } }, legend: { display: true, position: 'bottom' }, scales: { xAxes: [{ type: 'time', time: { parser: 'MM/DD/YYYY HH:mm', tooltipFormat: 'll HH:mm' }, scaleLabel: { display: true, labelString: 'Date' } }], yAxes: yAxes } } }); } } }); addPageComponent(vue, 'chart-line', true); });