Summary Table

Categories Total Count
PII 0
URL 0
DNS 0
EKL 0
IP 0
PORT 0
VsID 0
CF 0
AI 0
VPD 0
PL 0
Other 0

File Content

import { Component, OnInit, Input } from '@angular/core';
import { Chart } from 'chart.js';
import { Observation } from '../../models/patientModels';
import * as _ from 'lodash';
import 'chartjs-plugin-datalabels';
import regression from 'regression';

@Component({
selector: 'app-chart',
templateUrl: './chart.component.html',
styleUrls: ['./chart.component.css']
})
export class ChartComponent implements OnInit {

// store the raw data
rawData: Observation[];

// stores chart datasets
dataSets: any[];

// stores y-axis infromation about datasets
dataSetAxes: any[];

// chart object
chart: any;

// used to distinguish each obervation type on the chart
availableColors = [
'#330033', '#003333', '#000033', '#003300', '#330000',
'#660066', '#006666', '#000066', '#006600', '#660000',
'#CC00CC', '#00CCCC', '#0000CC', '#00CC00', '#CC0000'];

// as a new observation is added to the chart, last of available colors is popped off the
// array & and added to this observation/color hash
chartColors = {};

// holds first/last dates from x-axis. used for determining x-axis tick scaling
chartRange = {
min: new Date(),
max: new Date()
};

// holds x-axis tick scale factor (hour, day, month)
chartTimeUnit = 'day';

// whether to plot regressions for each dataset
displayRegression = false;

// whether to display labels for each datapoint
displayLabels = false;

// incoming data. fires prep & set functions.
@Input()
set data(incoming: Observation[]) {
this.rawData = incoming;
this.prepareData();
this.setData();
}

constructor() { }

ngOnInit() {
}

/**
* initialize the Chart w/default settings.
*/
initChart() {
this.chart = new Chart('canvas', {
type: 'line',
data: {
datasets: [...this.dataSets]
},
options: {
animation: {
duration: 0
},
elements: {
line: {
tension: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 10,
fontSize: 10,
filter: (x, y) => !x.text.match(/\(R\)/)
}
},
scales: {
xAxes: [{
type: 'time',
time: {
unit: this.chartTimeUnit,
displayFormats: {
hour: 'MM/DD HH:mm',
day: 'MM/DD/YY',
month: 'MMM YY'
}
}
}],
yAxes: [...this.dataSetAxes]
},
plugins: {
datalabels: {
display: this.displayLabels,
formatter: function (value, context) {
return value.y + context.dataset.displayUnit;
},
align: 'bottom',
clip: false,
color: '#ffffff',
backgroundColor: '#000000',
opacity: .75,
borderRadius: 3,
padding: 3,
font: {
size: '10',
weight: 'normal'
}
}
},
tooltips: {
callbacks: {
label: function (tooltipItem, data) {
let label = data.datasets[tooltipItem.datasetIndex].label || '';

if (label) {
label += ': ';
}
label += tooltipItem.yLabel + data.datasets[tooltipItem.datasetIndex].displayUnit;
return label;
}
}
}
}
});
}

/**
* Using data+config prepared from the incoming Observation array, add them to the
* chart and update
*/
setData() {

const days = (+this.chartRange.max - +this.chartRange.min) / 86400000;
if (days > 1460) {
this.chartTimeUnit = 'year';
} else if ( days > 60) {
this.chartTimeUnit = 'month';
} else if (days < 3) {
this.chartTimeUnit = 'hour';
} else {
this.chartTimeUnit = 'day';
}

if (!this.chart) {
this.initChart();
} else {
this.chart.data.datasets = [...this.dataSets];
this.chart.options.scales.yAxes = [...this.dataSetAxes];
this.chart.options.scales.xAxes[0].time.unit = this.chartTimeUnit;
this.chart.update();
}
}


/**
* create dataset for chart
* */
prepareData() {

// clear existing data
this.dataSets = [];
this.dataSetAxes = [];
this.chartRange.min = null;
this.chartRange.max = null;

// grab observation types from data & loop through them
const keys = _.uniq(this.rawData.map(itm => itm.name));

keys.forEach((key, idx) => {

// filter out everything but our key
const keyData = _.filter(this.rawData, ['name', key]);

// prefer imperial over metric (sigh)
const measureType = (keyData[0] && keyData[0].imperial && keyData[0].imperial.value) ? 'imperial' : 'metric';

// grab the units for this dataset.
const measureUnit = (keyData[0] && keyData[0][measureType] && keyData[0][measureType].unit ) ? keyData[0][measureType].unit : '';

// check if a color has been selected for this key, if not, set one
if (!this.chartColors[key]) {
if (this.availableColors.length > 0) {
this.chartColors[key] = this.availableColors.pop();
} else {
this.chartColors[key] = '#000000';
}
}

// set up our options for this dataset.
const dataset: any = {
data: [],
fill: false,
label: key,
borderColor: this.chartColors[key],
yAxisID: measureUnit,
displayUnit: measureUnit
};

// create dataset values from observation, mapping only data with values.
dataset.data = keyData.map(itm => {
if (itm[measureType] && itm[measureType].value) {
return { x: new Date(itm.timeTaken), y: itm[measureType].value };
}
});

// pull out the min & max x-axis values, used to decide how to label the axis when rendering (ticks per day, month)
const minDate: any = _.min(dataset.data.map(itm => itm.x));
const maxDate: any = _.max(dataset.data.map(itm => itm.x));
if (!this.chartRange.min || minDate < this.chartRange.min) { this.chartRange.min = minDate; }
if (!this.chartRange.max || maxDate > this.chartRange.max) { this.chartRange.max = maxDate; }

// if an axis for this unit hasn't been added, add it.
if (!this.dataSetAxes[measureUnit]) {
const axis = {
id: measureUnit,
type: 'linear',
scaleLabel: {
display: true,
labelString: measureUnit,
lineHeight: .9
}
};
this.dataSetAxes.push(axis);
}

// push the dataset onto the stack
this.dataSets.push({...dataset});

// if we have linear regression, push the regression onto the stack
if (this.displayRegression) {
dataset.data = this.runRegression(dataset.data);
dataset.pointRadius = 0;
dataset.borderWidth = 1;
dataset.borderDash = [5, 5];
dataset.label += '(R)';
dataset['datalabels'] = {display: false};
this.dataSets.push({...dataset});
}
});
}

/**
* use the regression npm package https://www.npmjs.com/package/regression
* to run a linear regression on a dataset
* @param data array of objects {x:Date, y:number}
* @returns array of objects {x:Date, y:number}
*/
runRegression(data: any): any {

try {
// regression library requires data in the form of an array of arrays [[x,y],[x,y]]
// since we have dates on the x-axis, convert them to milliseconds, which is evidently just
// too big for the library, so divide that by a billion
const complete = regression.linear(data.map((itm) => [+itm.x / 1000000000, +itm.y]));

// build the resultset, multiplying the x value by a billion to reverse above & then convert back to a date
const retVal = complete.points.map((itm) => {
return {x: new Date(itm[0] * 1000000000), y: +itm[1]};
});

return retVal;
} catch (e) {
return;
}
}

/**
* chart doesn't update automatically on a switch in option properties, so update it manually
* @param evt event that fired this function
*/
toggleLabels(evt) {
this.displayLabels = !this.displayLabels;

// looks like this property isn't set automatically by updating the component property
this.chart.options.plugins.datalabels.display = this.displayLabels;
this.chart.update();
}

/**
* toggle whether or not to plot regression lines & rebuild chart
* @param evt event that fired this function
*/
toggleRegression(evt) {
this.displayRegression = !this.displayRegression;
this.prepareData();
this.setData();
}
}