/*
* μlogger
*
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
*
* This 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 .
*/
import { lang as $ } from './initializer.js';
import Chartist from 'chartist'
import ViewModel from './viewmodel.js';
import ctAxisTitle from 'chartist-plugin-axistitle';
import uObserve from './observe.js';
import uUtils from './utils.js';
/**
* @typedef {Object} PlotPoint
* @property {number} x
* @property {number} y
*/
/**
* @typedef {PlotPoint[]} PlotData
*/
// FIXME: Chartist is not suitable for large data sets
const LARGE_DATA = 1000;
export default class ChartViewModel extends ViewModel {
/**
* @param {uState} state
*/
constructor(state) {
super({
pointSelected: null,
chartVisible: false,
buttonVisible: false,
onChartToggle: null,
onMenuToggle: null
});
this.state = state;
/** @type {PlotData} */
this.data = [];
/** @type {?Chartist.Line} */
this.chart = null;
/** @type {?NodeListOf} */
this.chartPoints = null;
/** @type {HTMLDivElement} */
this.chartElement = document.querySelector('#chart');
/** @type {HTMLDivElement} */
this.chartContainer = this.chartElement.parentElement;
/** @type {HTMLAnchorElement} */
this.buttonElement = document.querySelector('#altitudes');
}
/**
* @return {ChartViewModel}
*/
init() {
this.chartSetup();
this.setObservers();
this.bindAll();
return this;
}
chartSetup() {
uUtils.addCss('css/dist/chartist.css', 'chartist_css');
this.chart = new Chartist.Line(this.chartElement, {
series: [ this.data ]
}, {
lineSmooth: true,
showArea: true,
axisX: {
type: Chartist.AutoScaleAxis,
onlyInteger: true,
showLabel: false
},
plugins: [
ctAxisTitle({
axisY: {
axisTitle: `${$._('altitude')} (${$.unit('unitDistance')})`,
axisClass: 'ct-axis-title',
offset: {
x: 0,
y: 11
},
textAnchor: 'middle',
flipTitle: true
}
})
]
});
this.chart.on('created', () => this.onCreated());
}
onCreated() {
if (this.data.length && this.data.length <= LARGE_DATA) {
this.chartPoints = document.querySelectorAll('.ct-series .ct-point');
const len = this.chartPoints.length;
for (let id = 0; id < len; id++) {
this.chartPoints[id].addEventListener('click', () => {
this.model.pointSelected = id;
});
}
}
}
setObservers() {
this.state.onChanged('currentTrack', (track) => {
if (track) {
uObserve.observe(track, 'positions', () => {
this.onTrackUpdate(track, true);
});
}
this.onTrackUpdate(track);
});
this.onChanged('buttonVisible', (visible) => this.renderButton(visible));
this.onChanged('chartVisible', (visible) => this.renderContainer(visible));
this.model.onChartToggle = () => {
this.model.chartVisible = !this.model.chartVisible;
};
this.model.onMenuToggle = () => {
if (this.model.chartVisible) {
this.chart.update();
}
};
}
/**
* @param {?uTrack} track
* @param {boolean=} update
*/
onTrackUpdate(track, update = false) {
this.render(track, update);
this.model.buttonVisible = !!track && track.hasPlotData;
}
/**
* @param {boolean} isVisible
*/
renderContainer(isVisible) {
if (isVisible) {
this.chartContainer.style.display = 'block';
this.render(this.state.currentTrack);
} else {
this.chartContainer.style.display = 'none';
}
}
/**
* @param {boolean} isVisible
*/
renderButton(isVisible) {
if (isVisible) {
this.buttonElement.style.visibility = 'visible';
} else {
this.buttonElement.style.visibility = 'hidden';
}
}
/**
* @param {?uTrack} track
* @param {boolean=} update
*/
render(track, update = false) {
let data = [];
if (track && track.hasPlotData && this.model.chartVisible) {
data = track.plotData;
} else {
this.model.chartVisible = false;
}
if (update || this.data !== data) {
console.log(`Chart${update ? ' forced' : ''} update (${data.length})`);
this.data = data;
const options = {
lineSmooth: (data.length <= LARGE_DATA)
};
this.chart.update({ series: [ data ] }, options, true);
}
}
/**
* @param {number} pointId
* @param {string} $className
*/
pointAddClass(pointId, $className) {
if (this.model.chartVisible && this.chartPoints.length > pointId) {
const point = this.chartPoints[pointId];
point.classList.add($className);
}
}
/**
* @param {string} $className
*/
pointsRemoveClass($className) {
if (this.model.chartVisible && this.chartPoints) {
this.chartPoints.forEach((el) => el.classList.remove($className));
}
}
/**
* @param {number} pointId
*/
onPointOver(pointId) {
this.pointAddClass(pointId, 'ct-point-hilight');
}
onPointOut() {
this.pointsRemoveClass('ct-point-hilight');
}
/**
* @param {number} pointId
*/
onPointSelect(pointId) {
this.pointAddClass(pointId, 'ct-point-selected');
}
onPointUnselect() {
this.pointsRemoveClass('ct-point-selected');
}
}