Add chart view model
This commit is contained in:
parent
c77ee300c7
commit
87c29a0a7c
203
js/src/chartviewmodel.js
Normal file
203
js/src/chartviewmodel.js
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
* μ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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Chartist from 'chartist'
|
||||||
|
import ViewModel from './viewmodel.js';
|
||||||
|
import ctAxisTitle from 'chartist-plugin-axistitle';
|
||||||
|
import { lang } from './initializer.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
|
||||||
|
});
|
||||||
|
this.state = state;
|
||||||
|
/** @type {PlotData} */
|
||||||
|
this.data = [];
|
||||||
|
/** @type {?Chartist.Line} */
|
||||||
|
this.chart = null;
|
||||||
|
/** @type {?NodeListOf<SVGLineElement>} */
|
||||||
|
this.chartPoints = null;
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
this.chartElement = document.querySelector('#chart');
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
this.chartContainer = this.chartElement.parentElement;
|
||||||
|
/** @type {HTMLAnchorElement} */
|
||||||
|
this.buttonElement = document.querySelector('#altitudes');
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.chartSetup();
|
||||||
|
this.setObservers();
|
||||||
|
this.bindAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
chartSetup() {
|
||||||
|
uUtils.addCss('css/chartist.min.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: `${lang.strings['altitude']} (${lang.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) => {
|
||||||
|
this.render();
|
||||||
|
this.model.buttonVisible = !!track && track.hasPlotData;
|
||||||
|
});
|
||||||
|
this.onChanged('buttonVisible', (visible) => this.renderButton(visible));
|
||||||
|
this.onChanged('chartVisible', (visible) => this.renderContainer(visible));
|
||||||
|
this.model.onChartToggle = () => {
|
||||||
|
this.model.chartVisible = !this.model.chartVisible;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} isVisible
|
||||||
|
*/
|
||||||
|
renderContainer(isVisible) {
|
||||||
|
if (isVisible) {
|
||||||
|
this.chartContainer.style.display = 'block';
|
||||||
|
this.render();
|
||||||
|
} else {
|
||||||
|
this.chartContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} isVisible
|
||||||
|
*/
|
||||||
|
renderButton(isVisible) {
|
||||||
|
if (isVisible) {
|
||||||
|
this.buttonElement.style.visibility = 'visible';
|
||||||
|
} else {
|
||||||
|
this.buttonElement.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let data = [];
|
||||||
|
if (this.state.currentTrack && this.state.currentTrack.hasPlotData && this.model.chartVisible) {
|
||||||
|
data = this.state.currentTrack.plotData;
|
||||||
|
} else {
|
||||||
|
this.model.chartVisible = false;
|
||||||
|
}
|
||||||
|
if (this.data !== data) {
|
||||||
|
console.log(`Chart 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) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
407
js/test/chartviewmodel.test.js
Normal file
407
js/test/chartviewmodel.test.js
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
/*
|
||||||
|
* μ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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ChartViewModel from '../src/chartviewmodel.js';
|
||||||
|
import Chartist from 'chartist'
|
||||||
|
import TrackFactory from './helpers/trackfactory.js';
|
||||||
|
import ViewModel from '../src/viewmodel.js';
|
||||||
|
import { lang } from '../src/initializer.js';
|
||||||
|
import uObserve from '../src/observe.js';
|
||||||
|
import uState from '../src/state.js';
|
||||||
|
import uUtils from '../src/utils.js';
|
||||||
|
|
||||||
|
describe('ChartViewModel tests', () => {
|
||||||
|
|
||||||
|
let state;
|
||||||
|
/** @type {HTMLAnchorElement} */
|
||||||
|
let buttonEl;
|
||||||
|
/** @type {HTMLAnchorElement} */
|
||||||
|
let closeEl;
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
let chartEl;
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
let chartContainerEl;
|
||||||
|
let vm;
|
||||||
|
let mockChart;
|
||||||
|
let chartFixture;
|
||||||
|
let chartData;
|
||||||
|
let chartPointNodes;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// language=XML
|
||||||
|
chartFixture = `<svg xmlns:ct="http://gionkunz.github.com/chartist-js/ct" width="100%" height="100%" class="ct-chart-line">
|
||||||
|
<g class="ct-grids"/>
|
||||||
|
<g class="ct-series ct-series-a">
|
||||||
|
<path d="1" class="ct-area"/>
|
||||||
|
<path d="1" class="ct-line"/>
|
||||||
|
<line x1="50" y1="115" x2="50" y2="115" class="ct-point" ct:value="0,130"/>
|
||||||
|
<line x1="173" y1="158" x2="173" y2="158" class="ct-point" ct:value="48,104"/>
|
||||||
|
<line x1="286" y1="23" x2="286" y2="23" class="ct-point" ct:value="92,185"/>
|
||||||
|
<line x1="400" y1="23" x2="400" y2="23" class="ct-point" ct:value="136,185"/>
|
||||||
|
<line x1="657" y1="135" x2="657" y2="135" class="ct-point" ct:value="236,118"/>
|
||||||
|
<line x1="1046" y1="135" x2="1046" y2="135" class="ct-point" ct:value="387,118"/>
|
||||||
|
</g>
|
||||||
|
<g class="ct-labels"/>
|
||||||
|
</svg>`;
|
||||||
|
const fixture = `<div id="fixture">
|
||||||
|
<div id="other" class="section">
|
||||||
|
<a id="altitudes" data-bind="onChartToggle">chart</a>
|
||||||
|
</div>
|
||||||
|
<div id="bottom">
|
||||||
|
<div id="chart"></div>
|
||||||
|
<a id="chart-close" data-bind="onChartToggle">close</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
chartData = [
|
||||||
|
{ x: 0, y: 130 },
|
||||||
|
{ x: 48, y: 104 },
|
||||||
|
{ x: 92, y: 185 },
|
||||||
|
{ x: 136, y: 185 },
|
||||||
|
{ x: 236, y: 118 },
|
||||||
|
{ x: 387, y: 118 }
|
||||||
|
];
|
||||||
|
document.body.insertAdjacentHTML('afterbegin', fixture);
|
||||||
|
chartEl = document.querySelector('#chart');
|
||||||
|
chartContainerEl = document.querySelector('#bottom');
|
||||||
|
buttonEl = document.querySelector('#altitudes');
|
||||||
|
closeEl = document.querySelector('#chart-close');
|
||||||
|
const chartRendered = uUtils.nodeFromHtml(chartFixture);
|
||||||
|
chartPointNodes = chartRendered.querySelectorAll('.ct-series .ct-point');
|
||||||
|
state = new uState();
|
||||||
|
vm = new ChartViewModel(state);
|
||||||
|
spyOn(lang, 'unit');
|
||||||
|
mockChart = jasmine.createSpyObj('mockChart', {
|
||||||
|
'on': { /* ignored */ },
|
||||||
|
'update': { /* ignored */ }
|
||||||
|
});
|
||||||
|
spyOn(Chartist, 'Line').and.returnValue(mockChart);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.removeChild(document.querySelector('#fixture'));
|
||||||
|
uObserve.unobserveAll(lang);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create instance', () => {
|
||||||
|
// then
|
||||||
|
expect(vm).toBeInstanceOf(ViewModel);
|
||||||
|
expect(vm.state).toBe(state);
|
||||||
|
expect(vm.chartElement).toBe(chartEl);
|
||||||
|
expect(vm.chartContainer).toBe(chartContainerEl);
|
||||||
|
expect(vm.chart).toBe(null);
|
||||||
|
expect(vm.data).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize chart, set and bind observers', () => {
|
||||||
|
// given
|
||||||
|
spyOn(vm, 'chartSetup');
|
||||||
|
spyOn(vm, 'setObservers');
|
||||||
|
spyOn(vm, 'bindAll');
|
||||||
|
// when
|
||||||
|
vm.init();
|
||||||
|
// then
|
||||||
|
expect(vm.chartSetup).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vm.setObservers).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vm.bindAll).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set up chart', () => {
|
||||||
|
// given
|
||||||
|
spyOn(uUtils, 'addCss');
|
||||||
|
// when
|
||||||
|
vm.chartSetup();
|
||||||
|
// then
|
||||||
|
expect(uUtils.addCss).toHaveBeenCalledWith('css/chartist.min.css', 'chartist_css');
|
||||||
|
expect(Chartist.Line).toHaveBeenCalledWith(chartEl, jasmine.any(Object), jasmine.any(Object));
|
||||||
|
expect(mockChart.on).toHaveBeenCalledWith('created', jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add click listeners to all chart points on created', () => {
|
||||||
|
// given
|
||||||
|
chartEl.insertAdjacentHTML('afterbegin', chartFixture);
|
||||||
|
vm.data = chartData;
|
||||||
|
spyOn(EventTarget.prototype, 'addEventListener');
|
||||||
|
// when
|
||||||
|
vm.onCreated();
|
||||||
|
// then
|
||||||
|
expect(EventTarget.prototype.addEventListener).toHaveBeenCalledTimes(chartData.length);
|
||||||
|
expect(EventTarget.prototype.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function));
|
||||||
|
expect(vm.chartPoints).toEqual(chartPointNodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render chart on non-empty track and show altitudes button', () => {
|
||||||
|
// given
|
||||||
|
spyOn(vm, 'render');
|
||||||
|
const positions = [
|
||||||
|
TrackFactory.getPosition({ id: 1, latitude: 2, longitude: 3, altitude: 4 }),
|
||||||
|
TrackFactory.getPosition({ id: 2, latitude: 3, longitude: 4, altitude: 5 })
|
||||||
|
];
|
||||||
|
state.currentTrack = null;
|
||||||
|
vm.model.buttonVisible = false;
|
||||||
|
buttonEl.style.visibility = 'hidden';
|
||||||
|
// when
|
||||||
|
vm.setObservers();
|
||||||
|
state.currentTrack = TrackFactory.getTrack(positions);
|
||||||
|
// then
|
||||||
|
expect(vm.render).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vm.model.buttonVisible).toBe(true);
|
||||||
|
expect(buttonEl.style.visibility).toBe('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render chart on null track and hide altitudes button', () => {
|
||||||
|
// given
|
||||||
|
spyOn(vm, 'render');
|
||||||
|
const positions = [
|
||||||
|
TrackFactory.getPosition({ id: 1, latitude: 2, longitude: 3, altitude: 4 }),
|
||||||
|
TrackFactory.getPosition({ id: 2, latitude: 3, longitude: 4, altitude: 5 })
|
||||||
|
];
|
||||||
|
state.currentTrack = TrackFactory.getTrack(positions);
|
||||||
|
vm.model.buttonVisible = true;
|
||||||
|
buttonEl.style.visibility = 'visible';
|
||||||
|
// when
|
||||||
|
vm.setObservers();
|
||||||
|
state.currentTrack = null;
|
||||||
|
// then
|
||||||
|
expect(vm.render).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vm.model.buttonVisible).toBe(false);
|
||||||
|
expect(buttonEl.style.visibility).toBe('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render chart on empty track and hide altitudes button', () => {
|
||||||
|
// given
|
||||||
|
spyOn(vm, 'render');
|
||||||
|
state.currentTrack = TrackFactory.getTrack(2);
|
||||||
|
vm.model.buttonVisible = true;
|
||||||
|
buttonEl.style.visibility = 'visible';
|
||||||
|
// when
|
||||||
|
vm.setObservers();
|
||||||
|
state.currentTrack = TrackFactory.getTrack(0);
|
||||||
|
// then
|
||||||
|
expect(vm.render).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vm.model.buttonVisible).toBe(false);
|
||||||
|
expect(buttonEl.style.visibility).toBe('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render button visible', () => {
|
||||||
|
// given
|
||||||
|
vm.model.buttonVisible = false;
|
||||||
|
buttonEl.style.visibility = 'hidden';
|
||||||
|
// when
|
||||||
|
vm.setObservers();
|
||||||
|
vm.model.buttonVisible = true;
|
||||||
|
// then
|
||||||
|
expect(buttonEl.style.visibility).toBe('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render button hidden', () => {
|
||||||
|
// given
|
||||||
|
vm.model.buttonVisible = true;
|
||||||
|
buttonEl.style.visibility = 'visible';
|
||||||
|
// when
|
||||||
|
vm.setObservers();
|
||||||
|
vm.model.buttonVisible = false;
|
||||||
|
// then
|
||||||
|
expect(buttonEl.style.visibility).toBe('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render chart container visible and render chart', () => {
|
||||||
|
// given
|
||||||
|
spyOn(vm, 'render');
|
||||||
|
vm.model.chartVisible = false;
|
||||||
|
chartContainerEl.style.display = 'none';
|
||||||
|
// when
|
||||||
|
vm.setObservers();
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
// then
|
||||||
|
expect(vm.render).toHaveBeenCalledTimes(1);
|
||||||
|
expect(chartContainerEl.style.display).toBe('block');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render chart container hidden', () => {
|
||||||
|
// given
|
||||||
|
spyOn(vm, 'render');
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
chartContainerEl.style.display = 'block';
|
||||||
|
// when
|
||||||
|
vm.setObservers();
|
||||||
|
vm.model.chartVisible = false;
|
||||||
|
// then
|
||||||
|
expect(vm.render).not.toHaveBeenCalled();
|
||||||
|
expect(chartContainerEl.style.display).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render chart on non-empty track and chart visible', () => {
|
||||||
|
// given
|
||||||
|
const positions = [
|
||||||
|
TrackFactory.getPosition({ id: 1, latitude: 2, longitude: 3, altitude: 4 }),
|
||||||
|
TrackFactory.getPosition({ id: 2, latitude: 3, longitude: 4, altitude: 5 })
|
||||||
|
];
|
||||||
|
const track = TrackFactory.getTrack(positions);
|
||||||
|
state.currentTrack = track;
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
vm.data = null;
|
||||||
|
vm.chartSetup();
|
||||||
|
// when
|
||||||
|
vm.render();
|
||||||
|
// then
|
||||||
|
expect(mockChart.update).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockChart.update.calls.mostRecent().args[0].series[0]).toBe(track.plotData);
|
||||||
|
expect(vm.data).toBe(track.plotData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render chart on same track and chart visible', () => {
|
||||||
|
// given
|
||||||
|
const positions = [
|
||||||
|
TrackFactory.getPosition({ id: 1, latitude: 2, longitude: 3, altitude: 4 }),
|
||||||
|
TrackFactory.getPosition({ id: 2, latitude: 3, longitude: 4, altitude: 5 })
|
||||||
|
];
|
||||||
|
const track = TrackFactory.getTrack(positions);
|
||||||
|
state.currentTrack = track;
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
vm.data = track.plotData;
|
||||||
|
vm.chartSetup();
|
||||||
|
// when
|
||||||
|
vm.render();
|
||||||
|
// then
|
||||||
|
expect(mockChart.update).not.toHaveBeenCalled();
|
||||||
|
expect(vm.data).toBe(track.plotData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render empty chart on empty track and hide chart container', () => {
|
||||||
|
// given
|
||||||
|
const track = TrackFactory.getTrack(0);
|
||||||
|
state.currentTrack = track;
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
vm.data = chartData;
|
||||||
|
vm.chartSetup();
|
||||||
|
// when
|
||||||
|
vm.render();
|
||||||
|
// then
|
||||||
|
expect(mockChart.update).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockChart.update.calls.mostRecent().args[0].series[0]).toEqual(track.plotData);
|
||||||
|
expect(vm.data).toEqual(track.plotData);
|
||||||
|
expect(vm.model.chartVisible).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render empty chart on null track and hide chart container', () => {
|
||||||
|
// given
|
||||||
|
state.currentTrack = null;
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
vm.data = chartData;
|
||||||
|
vm.chartSetup();
|
||||||
|
// when
|
||||||
|
vm.render();
|
||||||
|
// then
|
||||||
|
expect(mockChart.update).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockChart.update.calls.mostRecent().args[0].series[0]).toEqual([]);
|
||||||
|
expect(vm.data).toEqual([]);
|
||||||
|
expect(vm.model.chartVisible).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hilight chart point', () => {
|
||||||
|
// given
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
vm.chartPoints = chartPointNodes;
|
||||||
|
const pointId = 0;
|
||||||
|
/** @type {SVGLineElement} */
|
||||||
|
const point = chartPointNodes[pointId];
|
||||||
|
// when
|
||||||
|
vm.onPointOver(pointId);
|
||||||
|
// then
|
||||||
|
expect(point.classList.contains('ct-point-hilight')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove hilight from all points', () => {
|
||||||
|
// given
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
vm.chartPoints = chartPointNodes;
|
||||||
|
const pointId = 0;
|
||||||
|
/** @type {SVGLineElement} */
|
||||||
|
const point = chartPointNodes[pointId];
|
||||||
|
point.classList.add('ct-point-hilight');
|
||||||
|
// when
|
||||||
|
vm.onPointOut();
|
||||||
|
// then
|
||||||
|
expect(point.classList.contains('ct-point-hilight')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select chart point', () => {
|
||||||
|
// given
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
vm.chartPoints = chartPointNodes;
|
||||||
|
const pointId = 0;
|
||||||
|
/** @type {SVGLineElement} */
|
||||||
|
const point = chartPointNodes[pointId];
|
||||||
|
// when
|
||||||
|
vm.onPointSelect(pointId);
|
||||||
|
// then
|
||||||
|
expect(point.classList.contains('ct-point-selected')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove selection from all points', () => {
|
||||||
|
// given
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
vm.chartPoints = chartPointNodes;
|
||||||
|
const pointId = 0;
|
||||||
|
/** @type {SVGLineElement} */
|
||||||
|
const point = chartPointNodes[pointId];
|
||||||
|
point.classList.add('ct-point-selected');
|
||||||
|
// when
|
||||||
|
vm.onPointUnselect();
|
||||||
|
// then
|
||||||
|
expect(point.classList.contains('ct-point-selected')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show chart on button click', () => {
|
||||||
|
// given
|
||||||
|
spyOn(vm, 'renderContainer');
|
||||||
|
vm.model.chartVisible = false;
|
||||||
|
// when
|
||||||
|
vm.bindAll();
|
||||||
|
vm.setObservers();
|
||||||
|
buttonEl.click();
|
||||||
|
// then
|
||||||
|
expect(vm.model.chartVisible).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide chart on button click', () => {
|
||||||
|
// given
|
||||||
|
spyOn(vm, 'renderContainer');
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
// when
|
||||||
|
vm.bindAll();
|
||||||
|
vm.setObservers();
|
||||||
|
buttonEl.click();
|
||||||
|
// then
|
||||||
|
expect(vm.model.chartVisible).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide chart on close click', () => {
|
||||||
|
// given
|
||||||
|
spyOn(vm, 'renderContainer');
|
||||||
|
vm.model.chartVisible = true;
|
||||||
|
// when
|
||||||
|
vm.bindAll();
|
||||||
|
vm.setObservers();
|
||||||
|
closeEl.click();
|
||||||
|
// then
|
||||||
|
expect(vm.model.chartVisible).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -27,77 +27,82 @@ export default class TrackFactory {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @param {number} length
|
* @param {(number|uPosition[])=} p Positions array or number of positions to be autogenerated
|
||||||
* @param {T} type
|
* @param {T} type
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
* @return {T}
|
* @return {T}
|
||||||
*/
|
*/
|
||||||
static getSet(length = 2, type, params) {
|
static getSet(p = 2, type, params) {
|
||||||
let track;
|
let track;
|
||||||
if (type === uTrack) {
|
if (type === uTrack) {
|
||||||
track = new uTrack(params.id, params.name, params.user);
|
track = new uTrack(params.id, params.name, params.user);
|
||||||
} else {
|
} else {
|
||||||
track = new uPositionSet();
|
track = new uPositionSet();
|
||||||
}
|
}
|
||||||
if (length) {
|
if (Array.isArray(p)) {
|
||||||
const positions = [];
|
track.fromJson(p, true);
|
||||||
let lat = 21.01;
|
} else {
|
||||||
let lon = 52.23;
|
const length = p;
|
||||||
for (let i = 0; i < length; i++) {
|
if (length) {
|
||||||
positions.push(this.getPosition(i + 1, lat, lon));
|
const positions = [];
|
||||||
lat += 0.5;
|
let lat = 21.01;
|
||||||
lon += 0.5;
|
let lon = 52.23;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
positions.push(this.getPosition({ id: i + 1, latitude: lat, longitude: lon }));
|
||||||
|
lat += 0.5;
|
||||||
|
lon += 0.5;
|
||||||
|
}
|
||||||
|
track.fromJson(positions, true);
|
||||||
}
|
}
|
||||||
track.fromJson(positions, true);
|
|
||||||
}
|
}
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number=} length
|
* @param {(number|uPosition[])=} p Positions array or number of positions to be autogenerated
|
||||||
* @param {{ id: number, name: string, user: uUser }=} params
|
* @param {{ id: number, name: string, user: uUser }=} params
|
||||||
* @return {uTrack}
|
* @return {uTrack}
|
||||||
*/
|
*/
|
||||||
static getTrack(length = 2, params) {
|
static getTrack(p = 2, params) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
params.id = params.id || 1;
|
params.id = params.id || 1;
|
||||||
params.name = params.name || 'test track';
|
params.name = params.name || 'test track';
|
||||||
params.user = params.user || new uUser(1, 'testUser');
|
params.user = params.user || new uUser(1, 'testUser');
|
||||||
return this.getSet(length, uTrack, params);
|
return this.getSet(p, uTrack, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} length
|
* @param {(number|uPosition[])=} p Positions array or number of positions to be autogenerated
|
||||||
|
* @param {uPosition[]=} positions
|
||||||
* @return {uPositionSet}
|
* @return {uPositionSet}
|
||||||
*/
|
*/
|
||||||
static getPositionSet(length = 2) {
|
static getPositionSet(p = 2, positions) {
|
||||||
return this.getSet(length, uPositionSet);
|
return this.getSet(p, uPositionSet, positions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number=} id
|
* @param {Object=} params
|
||||||
* @param {number=} latitude
|
|
||||||
* @param {number=} longitude
|
|
||||||
* @return {uPosition}
|
* @return {uPosition}
|
||||||
*/
|
*/
|
||||||
static getPosition(id = 1, latitude = 52.23, longitude = 21.01) {
|
static getPosition(params) {
|
||||||
|
params = params || {};
|
||||||
const position = new uPosition();
|
const position = new uPosition();
|
||||||
position.id = id;
|
position.id = params.id || 1;
|
||||||
position.latitude = latitude;
|
position.latitude = params.latitude || 52.23;
|
||||||
position.longitude = longitude;
|
position.longitude = params.longitude || 21.01;
|
||||||
position.altitude = null;
|
position.altitude = params.altitude || null;
|
||||||
position.speed = null;
|
position.speed = params.speed || null;
|
||||||
position.bearing = null;
|
position.bearing = params.bearing || null;
|
||||||
position.timestamp = 1;
|
position.timestamp = params.timestamp || 1;
|
||||||
position.accuracy = null;
|
position.accuracy = params.accuracy || null;
|
||||||
position.provider = null;
|
position.provider = params.provider || null;
|
||||||
position.comment = null;
|
position.comment = params.comment || null;
|
||||||
position.image = null;
|
position.image = params.image || null;
|
||||||
position.username = 'testUser';
|
position.username = params.username || 'testUser';
|
||||||
position.trackid = 1;
|
position.trackid = params.trackid || 1;
|
||||||
position.trackname = 'test track';
|
position.trackname = params.trackname || 'test track';
|
||||||
position.meters = 0;
|
position.meters = params.meters || 0;
|
||||||
position.seconds = 0;
|
position.seconds = params.seconds || 0;
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user