covid19-dashboard
Version:
Dashboard App displaying COVID-19 numbers by country
467 lines (395 loc) • 13.6 kB
JavaScript
import ComponentController from '../../../node_modules/neo.mjs/src/controller/Component.mjs';
import NeoArray from '../../../node_modules/neo.mjs/src/util/Array.mjs';
import Util from '../Util.mjs';
/**
* @class Covid.view.MainContainerController
* @extends Neo.controller.Component
*/
class MainContainerController extends ComponentController {
static getConfig() {return {
/**
* @member {String} className='Covid.view.MainContainerController'
* @protected
*/
className: 'Covid.view.MainContainerController',
/**
* @member {String} ntype='maincontainer-controller'
* @protected
*/
ntype: 'maincontainer-controller',
/**
* @member {Number} activeMainTabIndex=0
*/
activeMainTabIndex: 0,
/**
* @member {String} apiSummaryUrl='https://disease.sh/v3/covid-19/all'
*/
apiSummaryUrl: 'https://disease.sh/v3/covid-19/all',
/**
* @member {String} apiUrl='https://disease.sh/v3/covid-19/countries'
*/
apiUrl: 'https://disease.sh/v3/covid-19/countries',
/**
* @member {Object|null} countryRecord=null
*/
countryRecord: null,
/**
* @member {Object[]|null} data=null
*/
data: null,
/**
* @member {String[]} mainTabs=['table', 'mapboxglmap', 'worldmap', 'gallery', 'helix', 'attribution']
* @protected
*/
mainTabs: ['table', 'mapboxglmap', 'worldmap', 'gallery', 'helix', 'attribution'],
/**
* Flag to only load the map once onHashChange, but always on reload button click
* @member {Boolean} mapboxglMapHasData=false
* @protected
*/
mapboxglMapHasData: false,
/**
* @member {Object} summaryData=null
*/
summaryData: null,
/**
* Flag to only load the map once onHashChange, but always on reload button click
* @member {Boolean} worldMapHasData=false
* @protected
*/
worldMapHasData: false
}}
/**
* @param {Object[]} data
*/
addStoreItems(data) {
let me = this,
countryField = me.getReference('country-field'),
countryStore = countryField.store,
reference = me.mainTabs[me.activeMainTabIndex],
activeTab = me.getReference(reference);
data.forEach(item => {
if (item.country.includes('"')) {
item.country = item.country.replace('"', "\'");
}
item.casesPerOneMillion = item.casesPerOneMillion > item.cases ? 'N/A' : item.casesPerOneMillion || 0;
item.infected = item.casesPerOneMillion;
});
me.data = data;
if (countryStore.getCount() < 1) {
countryStore.data = data;
me.onCountryFieldChange({
component: countryField,
value : countryField.value
});
}
if (['gallery', 'helix', 'table'].includes(reference)) {
if (activeTab) {
activeTab.store.data = data;
}
}
else if (reference === 'mapboxglmap') {
me.getReference('mapboxglmap').chartData = data;
me.mapboxglMapHasData = true;
}
else if (reference === 'worldmap') {
activeTab.loadData(data);
me.worldMapHasData = true;
}
}
/**
* @param {Object} data
* @param {Number} data.active
* @param {Number} data.cases
* @param {Number} data.deaths
* @param {Number} data.recovered
* @param {Number} data.updated // timestamp
*/
applySummaryData(data) {
let me = this,
container = me.getReference('total-stats'),
vdom = container.vdom;
me.summaryData = data;
vdom.cn[0].cn[1].html = Util.formatNumber({value: data.cases});
vdom.cn[1].cn[1].html = Util.formatNumber({value: data.active});
vdom.cn[2].cn[1].html = Util.formatNumber({value: data.recovered});
vdom.cn[3].cn[1].html = Util.formatNumber({value: data.deaths});
container.vdom = vdom;
container = me.getReference('last-update');
vdom = container.vdom;
vdom.html = 'Last Update: ' + new Intl.DateTimeFormat('default', {
hour : 'numeric',
minute: 'numeric',
second: 'numeric'
}).format(new Date(data.updated));
container.vdom = vdom;
}
/**
* @param {Object} hashObject
* @param {String} hashObject.mainview
* @returns {Number}
*/
getTabIndex(hashObject) {
if (!hashObject || !hashObject.mainview) {
return 0;
}
return this.mainTabs.indexOf(hashObject.mainview);
}
/**
* @param {Number} tabIndex
* @returns {Neo.component.Base}
*/
getView(tabIndex) {
return this.getReference(this.mainTabs[tabIndex]);
}
/**
*
*/
loadData() {
let me = this;
fetch(me.apiUrl)
.then(response => response.json())
.catch(err => console.log('Can’t access ' + me.apiUrl, err))
.then(data => me.addStoreItems(data));
}
/**
*
*/
loadSummaryData() {
let me = this;
fetch(me.apiSummaryUrl)
.then(response => response.json())
.catch(err => console.log('Can’t access ' + me.apiSummaryUrl, err))
.then(data => me.applySummaryData(data));
setTimeout(() => {
if (!me.summaryData) {
me.onLoadSummaryDataFail();
}
}, 2000);
}
/**
*
*/
onComponentConstructed() {
super.onComponentConstructed();
if (!Neo.config.hash) {
this.onHashChange({
country : 'all',
hash : {mainview: 'table'},
hashString: 'mainview=table'
}, null);
}
}
/**
*
*/
onConstructed() {
super.onConstructed();
let me = this;
me.loadData();
me.loadSummaryData();
me.component.on('mounted', me.onMainViewMounted, me);
}
/**
* @param {Object} data
*/
onCountryFieldChange(data) {
let component = data.component,
store = component.store,
value = data.value,
record;
if (store.getCount() > 0) {
if (Neo.isObject(value)) {
record = value;
value = value[component.displayField];
} else {
record = value && store.find('country', value)?.[0];
}
this.getModel().setData({
country : value,
countryRecord: record || null
});
}
}
/**
* @param {Object} value
* @param {Object} oldValue
*/
onHashChange(value, oldValue) {
let me = this,
activeIndex = me.getTabIndex(value.hash),
activeView = me.getView(activeIndex),
country = value.hash?.country,
countryRecord, ntype;
me.getReference('tab-container').activeIndex = activeIndex;
me.activeMainTabIndex = activeIndex;
if (!activeView) {
setTimeout(() => {
me.onHashChange(value, oldValue);
}, 10);
return;
}
me.getModel().setData({
country: country || null
});
ntype = activeView.ntype;
// todo: this will only load each store once. adjust the logic in case we want to support reloading the API
if (me.data && activeView.store?.getCount() < 1) {
activeView.store.data = me.data;
}
if (ntype === 'mapboxgl' && me.data) {
if (me.mapboxStyle) {
activeView.mapboxStyle = activeView[me.mapboxStyle];
delete me.mapboxStyle;
}
if (!me.mapboxglMapHasData) {
activeView.chartData = me.data;
me.mapboxglMapHasData = true;
}
countryRecord = me.getModel().data.countryRecord;
countryRecord && MainContainerController.selectMapboxGlCountry(activeView, countryRecord);
activeView.autoResize();
} else if (ntype === 'covid-world-map' && me.data) {
if (!me.worldMapHasData) {
activeView.loadData(me.data);
me.worldMapHasData = true;
}
}
}
/**
*
*/
onLoadSummaryDataFail() {
let table = this.getReference('table'),
vdom = table.vdom;
vdom.cn[0].cn[1].cn.push({
tag : 'div',
cls : ['neo-box-label', 'neo-label'],
html: [
'Summary data did not arrive after 2s.</br>',
'Please double-check if the API is offline:</br></br>',
'<a target="_blank" href="https://disease.sh/all">NovelCOVID/API all endpoint</a></br></br>',
'and if so please try again later.'
].join(''),
style: {
margin: '20px'
}
});
table.vdom = vdom;
}
/**
*
*/
onMainViewMounted() {
let me = this;
Neo.main.DomAccess.addScript({
async: true,
defer: true,
src : 'https://buttons.github.io/buttons.js'
});
me.getReference('tab-container').on('moveTo', me.onTabMove, me);
}
/**
* @param {Object} data
*/
onReloadDataButtonClick(data) {
this.loadData();
this.loadSummaryData();
}
/**
* @param {Object} data
*/
onRemoveFooterButtonClick(data) {
let me = this,
activeTab = me.getReference('tab-container').getActiveCard();
me.component.remove(me.getReference('footer'), true);
if (activeTab.ntype === 'covid-mapboxgl-container') {
me.getReference('mapboxglmap').autoResize();
}
}
/**
* @param {Object} data
*/
onSwitchThemeButtonClick(data) {
let me = this,
button = data.component,
component = me.component,
logo = me.getReference('logo'),
logoPath = 'https://raw.githubusercontent.com/neomjs/pages/master/resources/images/apps/covid/',
mapView = me.getReference('mapboxglmap'),
themeLight = button.text === 'Theme Light',
vdom = logo.vdom,
buttonText, cls, href, iconCls, mapViewStyle, theme;
if (themeLight) {
buttonText = 'Theme Dark';
href = '../dist/development/neo-theme-light-no-css-vars.css';
iconCls = 'fa fa-moon';
mapViewStyle = mapView?.mapboxStyleLight;
theme = 'neo-theme-light';
} else {
buttonText = 'Theme Light';
href = '../dist/development/neo-theme-dark-no-css-vars.css';
iconCls = 'fa fa-sun';
mapViewStyle = mapView?.mapboxStyleDark;
theme = 'neo-theme-dark';
}
vdom.src = logoPath + (theme === 'neo-theme-dark' ? 'covid_logo_dark.jpg' : 'covid_logo_light.jpg');
logo.vdom = vdom;
if (Neo.config.useCssVars) {
cls = [...component.cls];
component.cls.forEach(item => {
if (item.includes('neo-theme')) {
NeoArray.remove(cls, item);
}
});
NeoArray.add(cls, theme);
component.cls = cls;
button.set({
iconCls: iconCls,
text : buttonText
});
} else {
Neo.main.addon.Stylesheet.swapStyleSheet({
href: href,
id : 'neo-theme'
}).then(data => {
button.text = buttonText;
});
}
if (mapView) {
mapView.mapboxStyle = mapViewStyle;
} else {
me.mapboxStyle = themeLight ? 'mapboxStyleLight' : 'mapboxStyleDark';
}
}
/**
* @param {Object} data
*/
onTabMove(data) {
NeoArray.move(this.mainTabs, data.fromIndex, data.toIndex);
}
/**
* @param view
* @param record
*/
static selectMapboxGlCountry(view, record) {console.log(record.countryInfo.iso2);
// https://github.com/neomjs/neo/issues/490
// there are missing iso2&3 values on natural earth vector
const map = {
FRA : ['match', ['get', 'NAME'], ['France'], true, false],
NOR : ['match', ['get', 'NAME'], ['Norway'], true, false],
default: ['match', ['get', 'ISO_A3'], [record.countryInfo.iso3], true, false]
};
view.setFilter({
layerId: 'ne-10m-admin-0-countries-4s7rvf',
value : map[record.countryInfo.iso3] || map['default']
});
view.flyTo({
lat: record.countryInfo.lat,
lng: record.countryInfo.long
});
view.zoom = 5; // todo: we could use a different value for big countries (Russia, USA,...)
}
}
Neo.applyClassConfig(MainContainerController);
export {MainContainerController as default};