@@ -82,6 +82,8 @@ function fire(target, type, properties) { | |||||
return target.dispatchEvent(evt); | return target.dispatchEvent(evt); | ||||
} | } | ||||
// https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/ | |||||
class SvgTip { | class SvgTip { | ||||
constructor({ | constructor({ | ||||
parent = null, | parent = null, | ||||
@@ -467,7 +469,7 @@ function makeGradient(svgDefElem, color, lighter = false) { | |||||
return gradientId; | return gradientId; | ||||
} | } | ||||
function makeHeatSquare(className, x, y, size, fill='none', data={}) { | |||||
function heatSquare(className, x, y, size, fill='none', data={}) { | |||||
let args = { | let args = { | ||||
className: className, | className: className, | ||||
x: x, | x: x, | ||||
@@ -1695,6 +1697,22 @@ let componentConfigs = { | |||||
); | ); | ||||
} | } | ||||
}, | }, | ||||
percentageBars: { | |||||
layerClass: 'percentage-bars', | |||||
makeElements(data) { | |||||
// return data.sliceStrings.map((s, i) =>{ | |||||
// let slice = makePath(s, 'pie-path', 'none', data.colors[i]); | |||||
// slice.style.transition = 'transform .3s;'; | |||||
// return slice; | |||||
// }); | |||||
}, | |||||
animateElements(newData) { | |||||
// return this.store.map((slice, i) => | |||||
// animatePathStr(slice, newData.sliceStrings[i]) | |||||
// ); | |||||
} | |||||
}, | |||||
yAxis: { | yAxis: { | ||||
layerClass: 'y axis', | layerClass: 'y axis', | ||||
makeElements(data) { | makeElements(data) { | ||||
@@ -1826,6 +1844,39 @@ let componentConfigs = { | |||||
} | } | ||||
}, | }, | ||||
heatDomain: { | |||||
layerClass: function() { return 'heat-domain domain-' + this.constants.index; }, | |||||
makeElements(data) { | |||||
let {colWidth, rowHeight, squareSize, xTranslate} = this.constants; | |||||
let x = xTranslate, y = 0; | |||||
this.serializedSubDomains = []; | |||||
data.cols.map(week => { | |||||
week.map((day, i) => { | |||||
let data = { | |||||
'data-date': day.YyyyMmDd, | |||||
'data-value': day.dataValue, | |||||
'data-day': i | |||||
}; | |||||
let square = heatSquare('day', x, y, squareSize, day.fill, data); | |||||
this.serializedSubDomains.push(square); | |||||
y += rowHeight; | |||||
}); | |||||
y = 0; | |||||
x += colWidth; | |||||
}); | |||||
return this.serializedSubDomains; | |||||
}, | |||||
animateElements(newData) { | |||||
// return this.store.map((slice, i) => | |||||
// animatePathStr(slice, newData.sliceStrings[i]) | |||||
// ); | |||||
} | |||||
}, | |||||
barGraph: { | barGraph: { | ||||
layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; }, | layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; }, | ||||
makeElements(data) { | makeElements(data) { | ||||
@@ -2158,13 +2209,13 @@ function treatAsUtc(date) { | |||||
return result; | return result; | ||||
} | } | ||||
function getDdMmYyyy(date) { | |||||
function getYyyyMmDd(date) { | |||||
let dd = date.getDate(); | let dd = date.getDate(); | ||||
let mm = date.getMonth() + 1; // getMonth() is zero-based | let mm = date.getMonth() + 1; // getMonth() is zero-based | ||||
return [ | return [ | ||||
(dd>9 ? '' : '0') + dd, | |||||
date.getFullYear(), | |||||
(mm>9 ? '' : '0') + mm, | (mm>9 ? '' : '0') + mm, | ||||
date.getFullYear() | |||||
(dd>9 ? '' : '0') + dd | |||||
].join('-'); | ].join('-'); | ||||
} | } | ||||
@@ -2176,8 +2227,11 @@ function clone(date) { | |||||
function getWeeksBetween(startDate, endDate) { | function getWeeksBetween(startDate, endDate) { | ||||
return Math.ceil(getDaysBetween(startDate, endDate) / NO_OF_DAYS_IN_WEEK); | |||||
let weekStartDate = setDayToSunday(startDate); | |||||
return Math.ceil(getDaysBetween(weekStartDate, endDate) / NO_OF_DAYS_IN_WEEK); | |||||
} | } | ||||
function getDaysBetween(startDate, endDate) { | function getDaysBetween(startDate, endDate) { | ||||
@@ -2185,18 +2239,28 @@ function getDaysBetween(startDate, endDate) { | |||||
return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay; | return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay; | ||||
} | } | ||||
function areInSameMonth(startDate, endDate) { | |||||
return startDate.getMonth() === endDate.getMonth() | |||||
&& startDate.getFullYear() === endDate.getFullYear(); | |||||
} | |||||
function getMonthName(i, short=false) { | function getMonthName(i, short=false) { | ||||
let monthName = MONTH_NAMES[i]; | let monthName = MONTH_NAMES[i]; | ||||
return short ? monthName.slice(0, 3) : monthName; | return short ? monthName.slice(0, 3) : monthName; | ||||
} | } | ||||
function getLastDateInMonth (month, year) { | |||||
return new Date(year, month + 1, 0); // 0: last day in previous month | |||||
} | |||||
// mutates | // mutates | ||||
function setDayToSunday(date) { | function setDayToSunday(date) { | ||||
const day = date.getDay(); | |||||
if(day !== NO_OF_DAYS_IN_WEEK) { | |||||
addDays(date, (-1) * day); | |||||
let newDate = clone(date); | |||||
const day = newDate.getDay(); | |||||
if(day !== 0) { | |||||
addDays(newDate, (-1) * day); | |||||
} | } | ||||
return date; | |||||
return newDate; | |||||
} | } | ||||
// mutates | // mutates | ||||
@@ -2417,8 +2481,6 @@ function getMaxCheckpoint(value, distribution) { | |||||
const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE; | const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE; | ||||
const ROW_HEIGHT = COL_WIDTH; | const ROW_HEIGHT = COL_WIDTH; | ||||
const DAY_INCR = 1; | |||||
class Heatmap extends BaseChart { | class Heatmap extends BaseChart { | ||||
constructor(parent, options) { | constructor(parent, options) { | ||||
super(parent, options); | super(parent, options); | ||||
@@ -2427,6 +2489,11 @@ class Heatmap extends BaseChart { | |||||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | ||||
this.countLabel = options.countLabel || ''; | this.countLabel = options.countLabel || ''; | ||||
let validStarts = ['Sunday', 'Monday']; | |||||
let startSubDomain = validStarts.includes(options.startSubDomain) | |||||
? options.startSubDomain : 'Sunday'; | |||||
this.startSubDomainIndex = validStarts.indexOf(startSubDomain); | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
@@ -2438,17 +2505,6 @@ class Heatmap extends BaseChart { | |||||
} | } | ||||
} | } | ||||
makeChartArea() { | |||||
super.makeChartArea(); | |||||
this.domainLabelGroup = makeSVGGroup(this.drawArea, | |||||
'domain-label-group chart-label'); | |||||
this.colGroups = makeSVGGroup(this.drawArea, | |||||
'data-groups', | |||||
`translate(0, 20)` | |||||
); | |||||
} | |||||
prepareData(data=this.data) { | prepareData(data=this.data) { | ||||
if(data.start && data.end && data.start > data.end) { | if(data.start && data.end && data.start > data.end) { | ||||
throw new Error('Start date cannot be greater than end date.'); | throw new Error('Start date cannot be greater than end date.'); | ||||
@@ -2458,6 +2514,7 @@ class Heatmap extends BaseChart { | |||||
data.start = new Date(); | data.start = new Date(); | ||||
data.start.setFullYear( data.start.getFullYear() - 1 ); | data.start.setFullYear( data.start.getFullYear() - 1 ); | ||||
} | } | ||||
console.log(data.start); | |||||
if(!data.end) { data.end = new Date(); } | if(!data.end) { data.end = new Date(); } | ||||
data.dataPoints = data.dataPoints || {}; | data.dataPoints = data.dataPoints || {}; | ||||
@@ -2465,7 +2522,7 @@ class Heatmap extends BaseChart { | |||||
let points = {}; | let points = {}; | ||||
Object.keys(data.dataPoints).forEach(timestampSec$$1 => { | Object.keys(data.dataPoints).forEach(timestampSec$$1 => { | ||||
let date = new Date(timestampSec$$1 * NO_OF_MILLIS); | let date = new Date(timestampSec$$1 * NO_OF_MILLIS); | ||||
points[getDdMmYyyy(date)] = data.dataPoints[timestampSec$$1]; | |||||
points[getYyyyMmDd(date)] = data.dataPoints[timestampSec$$1]; | |||||
}); | }); | ||||
data.dataPoints = points; | data.dataPoints = points; | ||||
} | } | ||||
@@ -2479,10 +2536,42 @@ class Heatmap extends BaseChart { | |||||
s.start = this.data.start; | s.start = this.data.start; | ||||
s.end = this.data.end; | s.end = this.data.end; | ||||
s.firstWeekStart = setDayToSunday(clone(s.start)); | |||||
s.noOfWeeks = getWeeksBetween(s.firstWeekStart, s.end); | |||||
s.firstWeekStart = setDayToSunday(s.start); | |||||
s.noOfWeeks = getWeeksBetween(s.start, s.end); | |||||
s.distribution = calcDistribution( | s.distribution = calcDistribution( | ||||
Object.values(this.data.dataPoints), HEATMAP_DISTRIBUTION_SIZE); | Object.values(this.data.dataPoints), HEATMAP_DISTRIBUTION_SIZE); | ||||
s.domainConfigs = this.getDomains(); | |||||
} | |||||
setupComponents() { | |||||
let s = this.state; | |||||
console.log(s.domainConfigs); | |||||
let componentConfigs = s.domainConfigs.map((config, i) => [ | |||||
'heatDomain', | |||||
{ | |||||
index: i, | |||||
colWidth: COL_WIDTH, | |||||
rowHeight: ROW_HEIGHT, | |||||
squareSize: HEATMAP_SQUARE_SIZE, | |||||
xTranslate: s.domainConfigs | |||||
.filter((config, j) => j < i) | |||||
.map(config => config.cols.length) | |||||
.reduce((a, b) => a + b, 0) | |||||
* COL_WIDTH | |||||
}, | |||||
function() { | |||||
return s.domainConfigs[i]; | |||||
}.bind(this) | |||||
]); | |||||
this.components = new Map(componentConfigs | |||||
.map(args => { | |||||
let component = getComponent(...args); | |||||
return [args[0], component]; | |||||
})); | |||||
} | } | ||||
update(data) { | update(data) { | ||||
@@ -2494,105 +2583,6 @@ class Heatmap extends BaseChart { | |||||
this.bindTooltip(); | this.bindTooltip(); | ||||
} | } | ||||
render() { | |||||
this.domainLabelGroup.textContent = ''; | |||||
this.colGroups.textContent = ''; | |||||
let currentWeekSunday = new Date(this.state.firstWeekStart); | |||||
this.currentWeekCol = 0; | |||||
this.currentMonth = currentWeekSunday.getMonth(); | |||||
this.months = [this.currentMonth + '']; | |||||
this.monthWeeks = {}, this.monthStartPoints = []; | |||||
this.monthWeeks[this.currentMonth] = 0; | |||||
for(var i = 0; i < this.state.noOfWeeks; i++) { | |||||
let colGroup, monthChange = 0; | |||||
let day = new Date(currentWeekSunday); | |||||
[colGroup, monthChange] = this.getWeekSquaresGroup(day, this.currentWeekCol); | |||||
this.colGroups.appendChild(colGroup); | |||||
this.currentWeekCol += 1 + parseInt(this.discreteDomains && monthChange); | |||||
this.monthWeeks[this.currentMonth]++; | |||||
if(monthChange) { | |||||
this.currentMonth = (this.currentMonth + 1) % NO_OF_YEAR_MONTHS; | |||||
this.months.push(this.currentMonth + ''); | |||||
this.monthWeeks[this.currentMonth] = 1; | |||||
} | |||||
addDays(currentWeekSunday, NO_OF_DAYS_IN_WEEK); | |||||
} | |||||
this.renderMonthLabels(); | |||||
} | |||||
getWeekSquaresGroup(currentDate, currentWeekCol) { | |||||
let monthChange = 0; | |||||
let weekColChange = 0; | |||||
let colGroup = makeSVGGroup(this.colGroups, 'data-group'); | |||||
for(var y = 0, i = 0; i < NO_OF_DAYS_IN_WEEK; i += DAY_INCR, y += ROW_HEIGHT) { | |||||
let ddmmyyyy = getDdMmYyyy(currentDate); | |||||
let dataValue = this.data.dataPoints[ddmmyyyy] || 0; | |||||
let colorIndex = getMaxCheckpoint(dataValue, this.state.distribution); | |||||
let x = (currentWeekCol + weekColChange) * COL_WIDTH; | |||||
let dataAttr = { | |||||
'data-date': ddmmyyyy, | |||||
'data-value': dataValue, | |||||
'data-day': currentDate.getDay() | |||||
}; | |||||
let heatSquare = makeHeatSquare('day', x, y, HEATMAP_SQUARE_SIZE, | |||||
this.colors[colorIndex], dataAttr); | |||||
colGroup.appendChild(heatSquare); | |||||
let nextDate = new Date(currentDate); | |||||
addDays(nextDate, 1); | |||||
if(nextDate > this.state.end) break; | |||||
if(nextDate.getMonth() - currentDate.getMonth()) { | |||||
monthChange = 1; | |||||
if(this.discreteDomains) { | |||||
weekColChange = 1; | |||||
} | |||||
this.monthStartPoints.push((currentWeekCol + weekColChange) * COL_WIDTH); | |||||
} | |||||
currentDate = nextDate; | |||||
} | |||||
return [colGroup, monthChange]; | |||||
} | |||||
renderMonthLabels() { | |||||
// this.first_month_label = 1; | |||||
// if (this.state.firstWeekStart.getDate() > 8) { | |||||
// this.first_month_label = 0; | |||||
// } | |||||
// this.last_month_label = 1; | |||||
// let first_month = this.months.shift(); | |||||
// let first_month_start = this.monthStartPoints.shift(); | |||||
// render first month if | |||||
// let last_month = this.months.pop(); | |||||
// let last_month_start = this.monthStartPoints.pop(); | |||||
// render last month if | |||||
this.months.shift(); | |||||
this.monthStartPoints.shift(); | |||||
this.months.pop(); | |||||
this.monthStartPoints.pop(); | |||||
this.monthStartPoints.map((start, i) => { | |||||
let month_name = getMonthName(this.months[i], true); | |||||
let text = makeText('y-value-text', start + COL_WIDTH, HEATMAP_SQUARE_SIZE, month_name); | |||||
this.domainLabelGroup.appendChild(text); | |||||
}); | |||||
} | |||||
bindTooltip() { | bindTooltip() { | ||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
document.querySelectorAll(".data-group .day") | document.querySelectorAll(".data-group .day") | ||||
@@ -2616,6 +2606,88 @@ class Heatmap extends BaseChart { | |||||
}); | }); | ||||
}); | }); | ||||
} | } | ||||
getDomains() { | |||||
let s = this.state; | |||||
const [startMonth, startYear] = [s.start.getMonth(), s.start.getFullYear()]; | |||||
const [endMonth, endYear] = [s.end.getMonth(), s.end.getFullYear()]; | |||||
const noOfMonths = (endMonth - startMonth + 1) + (endYear - startYear) * 12; | |||||
let domainConfigs = []; | |||||
let startOfMonth = clone(s.start); | |||||
for(var i = 0; i < noOfMonths; i++) { | |||||
let endDate = s.end; | |||||
if(!areInSameMonth(startOfMonth, s.end)) { | |||||
let [month, year] = [startOfMonth.getMonth(), startOfMonth.getFullYear()]; | |||||
endDate = getLastDateInMonth(month, year); | |||||
} | |||||
domainConfigs.push(this.getDomainConfig(startOfMonth, endDate)); | |||||
addDays(endDate, 1); | |||||
startOfMonth = endDate; | |||||
} | |||||
return domainConfigs; | |||||
} | |||||
getDomainConfig(startDate, endDate='') { | |||||
let [month, year] = [startDate.getMonth(), startDate.getFullYear()]; | |||||
let startOfWeek = setDayToSunday(startDate); | |||||
endDate = clone(endDate) || getLastDateInMonth(month, year); | |||||
let domainConfig = { | |||||
index: month, | |||||
cols: [] | |||||
}; | |||||
let noOfMonthWeeks = getWeeksBetween(startOfWeek, endDate); | |||||
let cols = []; | |||||
for(var i = 0; i < noOfMonthWeeks; i++) { | |||||
const col = this.getCol(startOfWeek, month); | |||||
cols.push(col); | |||||
startOfWeek = new Date(col[NO_OF_DAYS_IN_WEEK - 1].dateStr); | |||||
addDays(startOfWeek, 1); | |||||
} | |||||
if(startOfWeek.getDay() === this.startSubDomainIndex) { | |||||
cols.push(new Array(NO_OF_DAYS_IN_WEEK).fill(0)); | |||||
} | |||||
domainConfig.cols = cols; | |||||
return domainConfig; | |||||
} | |||||
getCol(startDate, month) { | |||||
// startDate is the start of week | |||||
let currentDate = clone(startDate); | |||||
let col = []; | |||||
for(var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) { | |||||
let config = 0; | |||||
if(currentDate.getMonth() === month) { | |||||
config = this.getSubDomainConfig(currentDate); | |||||
} | |||||
col.push(config); | |||||
} | |||||
return col; | |||||
} | |||||
getSubDomainConfig(date) { | |||||
let YyyyMmDd = getYyyyMmDd(date); | |||||
let dataValue = this.data.dataPoints[YyyyMmDd]; | |||||
let config = { | |||||
YyyyMmDd: YyyyMmDd, | |||||
dataValue: dataValue || 0, | |||||
fill: this.colors[getMaxCheckpoint(dataValue, this.state.distribution)] | |||||
}; | |||||
return config; | |||||
} | |||||
} | } | ||||
function dataPrep(data, type) { | function dataPrep(data, type) { | ||||
@@ -1,4 +1,4 @@ | |||||
import { DAYS_IN_YEAR, SEC_IN_DAY, MONTH_NAMES_SHORT, clone, timestampToMidnight, timestampSec } from '../../../src/js/utils/date-utils'; | |||||
import { DAYS_IN_YEAR, SEC_IN_DAY, MONTH_NAMES_SHORT, clone, timestampToMidnight, timestampSec, addDays } from '../../../src/js/utils/date-utils'; | |||||
// Composite Chart | // Composite Chart | ||||
// ================================================================================ | // ================================================================================ | ||||
@@ -176,6 +176,7 @@ export const moonData = { | |||||
let today = new Date(); | let today = new Date(); | ||||
let start = clone(today); | let start = clone(today); | ||||
addDays(start, 1); | |||||
let end = clone(today); | let end = clone(today); | ||||
start.setFullYear( start.getFullYear() - 2 ); | start.setFullYear( start.getFullYear() - 2 ); | ||||
end.setFullYear( end.getFullYear() - 1 ); | end.setFullYear( end.getFullYear() - 1 ); | ||||
@@ -189,7 +190,7 @@ startTs = timestampToMidnight(startTs); | |||||
endTs = timestampToMidnight(endTs, true); | endTs = timestampToMidnight(endTs, true); | ||||
while (startTs < endTs) { | while (startTs < endTs) { | ||||
dataPoints[parseInt(startTs)] = Math.floor(Math.random() * 5); | |||||
dataPoints[parseInt(startTs)] = Math.floor(Math.random() * 17); | |||||
startTs += SEC_IN_DAY; | startTs += SEC_IN_DAY; | ||||
} | } | ||||
@@ -88,7 +88,6 @@ var SEC_IN_DAY = 86400; | |||||
var MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | var MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | ||||
// https://stackoverflow.com/a/11252167/6495043 | |||||
function clone(date) { | function clone(date) { | ||||
@@ -115,10 +114,19 @@ function timestampToMidnight(timestamp) { | |||||
// mutates | // mutates | ||||
// mutates | // mutates | ||||
function addDays(date, numberOfDays) { | |||||
date.setDate(date.getDate() + numberOfDays); | |||||
} | |||||
var reportCountList = [152, 222, 199, 287, 534, 709, 1179, 1256, 1632, 1856, 1850]; | var reportCountList = [152, 222, 199, 287, 534, 709, 1179, 1256, 1632, 1856, 1850]; | ||||
@@ -231,6 +239,7 @@ var moonData = { | |||||
var today = new Date(); | var today = new Date(); | ||||
var start = clone(today); | var start = clone(today); | ||||
addDays(start, 1); | |||||
var end = clone(today); | var end = clone(today); | ||||
start.setFullYear(start.getFullYear() - 2); | start.setFullYear(start.getFullYear() - 2); | ||||
end.setFullYear(end.getFullYear() - 1); | end.setFullYear(end.getFullYear() - 1); | ||||
@@ -244,7 +253,7 @@ startTs = timestampToMidnight(startTs); | |||||
endTs = timestampToMidnight(endTs, true); | endTs = timestampToMidnight(endTs, true); | ||||
while (startTs < endTs) { | while (startTs < endTs) { | ||||
dataPoints[parseInt(startTs)] = Math.floor(Math.random() * 5); | |||||
dataPoints[parseInt(startTs)] = Math.floor(Math.random() * 17); | |||||
startTs += SEC_IN_DAY; | startTs += SEC_IN_DAY; | ||||
} | } | ||||
@@ -254,8 +263,6 @@ var heatmapData = { | |||||
end: end | end: end | ||||
}; | }; | ||||
// ================================================================================ | |||||
var c1 = document.querySelector("#chart-composite-1"); | var c1 = document.querySelector("#chart-composite-1"); | ||||
var c2 = document.querySelector("#chart-composite-2"); | var c2 = document.querySelector("#chart-composite-2"); | ||||
@@ -1,6 +1,7 @@ | |||||
import BaseChart from './BaseChart'; | import BaseChart from './BaseChart'; | ||||
import { makeSVGGroup, makeHeatSquare, makeText } from '../utils/draw'; | |||||
import { addDays, setDayToSunday, getDdMmYyyy, getWeeksBetween, getMonthName, clone, | |||||
import { makeSVGGroup, makeText } from '../utils/draw'; | |||||
import { getComponent } from '../objects/ChartComponents'; | |||||
import { addDays, areInSameMonth, getLastDateInMonth, setDayToSunday, getYyyyMmDd, getWeeksBetween, getMonthName, clone, | |||||
NO_OF_MILLIS, NO_OF_YEAR_MONTHS, NO_OF_DAYS_IN_WEEK } from '../utils/date-utils'; | NO_OF_MILLIS, NO_OF_YEAR_MONTHS, NO_OF_DAYS_IN_WEEK } from '../utils/date-utils'; | ||||
import { calcDistribution, getMaxCheckpoint } from '../utils/intervals'; | import { calcDistribution, getMaxCheckpoint } from '../utils/intervals'; | ||||
import { HEATMAP_DISTRIBUTION_SIZE, HEATMAP_SQUARE_SIZE, | import { HEATMAP_DISTRIBUTION_SIZE, HEATMAP_SQUARE_SIZE, | ||||
@@ -18,6 +19,11 @@ export default class Heatmap extends BaseChart { | |||||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | ||||
this.countLabel = options.countLabel || ''; | this.countLabel = options.countLabel || ''; | ||||
let validStarts = ['Sunday', 'Monday']; | |||||
let startSubDomain = validStarts.includes(options.startSubDomain) | |||||
? options.startSubDomain : 'Sunday'; | |||||
this.startSubDomainIndex = validStarts.indexOf(startSubDomain); | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
@@ -29,17 +35,6 @@ export default class Heatmap extends BaseChart { | |||||
} | } | ||||
} | } | ||||
makeChartArea() { | |||||
super.makeChartArea(); | |||||
this.domainLabelGroup = makeSVGGroup(this.drawArea, | |||||
'domain-label-group chart-label'); | |||||
this.colGroups = makeSVGGroup(this.drawArea, | |||||
'data-groups', | |||||
`translate(0, 20)` | |||||
); | |||||
} | |||||
prepareData(data=this.data) { | prepareData(data=this.data) { | ||||
if(data.start && data.end && data.start > data.end) { | if(data.start && data.end && data.start > data.end) { | ||||
throw new Error('Start date cannot be greater than end date.'); | throw new Error('Start date cannot be greater than end date.'); | ||||
@@ -49,6 +44,7 @@ export default class Heatmap extends BaseChart { | |||||
data.start = new Date(); | data.start = new Date(); | ||||
data.start.setFullYear( data.start.getFullYear() - 1 ); | data.start.setFullYear( data.start.getFullYear() - 1 ); | ||||
} | } | ||||
console.log(data.start); | |||||
if(!data.end) { data.end = new Date(); } | if(!data.end) { data.end = new Date(); } | ||||
data.dataPoints = data.dataPoints || {}; | data.dataPoints = data.dataPoints || {}; | ||||
@@ -56,7 +52,7 @@ export default class Heatmap extends BaseChart { | |||||
let points = {}; | let points = {}; | ||||
Object.keys(data.dataPoints).forEach(timestampSec => { | Object.keys(data.dataPoints).forEach(timestampSec => { | ||||
let date = new Date(timestampSec * NO_OF_MILLIS); | let date = new Date(timestampSec * NO_OF_MILLIS); | ||||
points[getDdMmYyyy(date)] = data.dataPoints[timestampSec]; | |||||
points[getYyyyMmDd(date)] = data.dataPoints[timestampSec]; | |||||
}); | }); | ||||
data.dataPoints = points; | data.dataPoints = points; | ||||
} | } | ||||
@@ -70,10 +66,42 @@ export default class Heatmap extends BaseChart { | |||||
s.start = this.data.start; | s.start = this.data.start; | ||||
s.end = this.data.end; | s.end = this.data.end; | ||||
s.firstWeekStart = setDayToSunday(clone(s.start)); | |||||
s.noOfWeeks = getWeeksBetween(s.firstWeekStart, s.end); | |||||
s.firstWeekStart = setDayToSunday(s.start); | |||||
s.noOfWeeks = getWeeksBetween(s.start, s.end); | |||||
s.distribution = calcDistribution( | s.distribution = calcDistribution( | ||||
Object.values(this.data.dataPoints), HEATMAP_DISTRIBUTION_SIZE); | Object.values(this.data.dataPoints), HEATMAP_DISTRIBUTION_SIZE); | ||||
s.domainConfigs = this.getDomains(); | |||||
} | |||||
setupComponents() { | |||||
let s = this.state; | |||||
console.log(s.domainConfigs); | |||||
let componentConfigs = s.domainConfigs.map((config, i) => [ | |||||
'heatDomain', | |||||
{ | |||||
index: i, | |||||
colWidth: COL_WIDTH, | |||||
rowHeight: ROW_HEIGHT, | |||||
squareSize: HEATMAP_SQUARE_SIZE, | |||||
xTranslate: s.domainConfigs | |||||
.filter((config, j) => j < i) | |||||
.map(config => config.cols.length) | |||||
.reduce((a, b) => a + b, 0) | |||||
* COL_WIDTH | |||||
}, | |||||
function() { | |||||
return s.domainConfigs[i]; | |||||
}.bind(this) | |||||
]) | |||||
this.components = new Map(componentConfigs | |||||
.map(args => { | |||||
let component = getComponent(...args); | |||||
return [args[0], component]; | |||||
})); | |||||
} | } | ||||
update(data) { | update(data) { | ||||
@@ -85,106 +113,6 @@ export default class Heatmap extends BaseChart { | |||||
this.bindTooltip(); | this.bindTooltip(); | ||||
} | } | ||||
render() { | |||||
this.domainLabelGroup.textContent = ''; | |||||
this.colGroups.textContent = ''; | |||||
let currentWeekSunday = new Date(this.state.firstWeekStart); | |||||
this.currentWeekCol = 0; | |||||
this.currentMonth = currentWeekSunday.getMonth(); | |||||
this.months = [this.currentMonth + '']; | |||||
this.monthWeeks = {}, | |||||
this.monthStartPoints = []; | |||||
this.monthWeeks[this.currentMonth] = 0; | |||||
for(var i = 0; i < this.state.noOfWeeks; i++) { | |||||
let colGroup, monthChange = 0; | |||||
let day = new Date(currentWeekSunday); | |||||
[colGroup, monthChange] = this.getWeekSquaresGroup(day, this.currentWeekCol); | |||||
this.colGroups.appendChild(colGroup); | |||||
this.currentWeekCol += 1 + parseInt(this.discreteDomains && monthChange); | |||||
this.monthWeeks[this.currentMonth]++; | |||||
if(monthChange) { | |||||
this.currentMonth = (this.currentMonth + 1) % NO_OF_YEAR_MONTHS; | |||||
this.months.push(this.currentMonth + ''); | |||||
this.monthWeeks[this.currentMonth] = 1; | |||||
} | |||||
addDays(currentWeekSunday, NO_OF_DAYS_IN_WEEK); | |||||
} | |||||
this.renderMonthLabels(); | |||||
} | |||||
getWeekSquaresGroup(currentDate, currentWeekCol) { | |||||
let monthChange = 0; | |||||
let weekColChange = 0; | |||||
let colGroup = makeSVGGroup(this.colGroups, 'data-group'); | |||||
for(var y = 0, i = 0; i < NO_OF_DAYS_IN_WEEK; i += DAY_INCR, y += ROW_HEIGHT) { | |||||
let ddmmyyyy = getDdMmYyyy(currentDate); | |||||
let dataValue = this.data.dataPoints[ddmmyyyy] || 0; | |||||
let colorIndex = getMaxCheckpoint(dataValue, this.state.distribution); | |||||
let x = (currentWeekCol + weekColChange) * COL_WIDTH; | |||||
let dataAttr = { | |||||
'data-date': ddmmyyyy, | |||||
'data-value': dataValue, | |||||
'data-day': currentDate.getDay() | |||||
}; | |||||
let heatSquare = makeHeatSquare('day', x, y, HEATMAP_SQUARE_SIZE, | |||||
this.colors[colorIndex], dataAttr); | |||||
colGroup.appendChild(heatSquare); | |||||
let nextDate = new Date(currentDate); | |||||
addDays(nextDate, 1); | |||||
if(nextDate > this.state.end) break; | |||||
if(nextDate.getMonth() - currentDate.getMonth()) { | |||||
monthChange = 1; | |||||
if(this.discreteDomains) { | |||||
weekColChange = 1; | |||||
} | |||||
this.monthStartPoints.push((currentWeekCol + weekColChange) * COL_WIDTH); | |||||
} | |||||
currentDate = nextDate; | |||||
} | |||||
return [colGroup, monthChange]; | |||||
} | |||||
renderMonthLabels() { | |||||
// this.first_month_label = 1; | |||||
// if (this.state.firstWeekStart.getDate() > 8) { | |||||
// this.first_month_label = 0; | |||||
// } | |||||
// this.last_month_label = 1; | |||||
// let first_month = this.months.shift(); | |||||
// let first_month_start = this.monthStartPoints.shift(); | |||||
// render first month if | |||||
// let last_month = this.months.pop(); | |||||
// let last_month_start = this.monthStartPoints.pop(); | |||||
// render last month if | |||||
this.months.shift(); | |||||
this.monthStartPoints.shift(); | |||||
this.months.pop(); | |||||
this.monthStartPoints.pop(); | |||||
this.monthStartPoints.map((start, i) => { | |||||
let month_name = getMonthName(this.months[i], true); | |||||
let text = makeText('y-value-text', start + COL_WIDTH, HEATMAP_SQUARE_SIZE, month_name); | |||||
this.domainLabelGroup.appendChild(text); | |||||
}); | |||||
} | |||||
bindTooltip() { | bindTooltip() { | ||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
document.querySelectorAll(".data-group .day") | document.querySelectorAll(".data-group .day") | ||||
@@ -208,4 +136,87 @@ export default class Heatmap extends BaseChart { | |||||
}); | }); | ||||
}); | }); | ||||
} | } | ||||
getDomains() { | |||||
let s = this.state; | |||||
const [startMonth, startYear] = [s.start.getMonth(), s.start.getFullYear()]; | |||||
const [endMonth, endYear] = [s.end.getMonth(), s.end.getFullYear()]; | |||||
const noOfMonths = (endMonth - startMonth + 1) + (endYear - startYear) * 12; | |||||
let domainConfigs = []; | |||||
let startOfMonth = clone(s.start); | |||||
for(var i = 0; i < noOfMonths; i++) { | |||||
let endDate = s.end; | |||||
if(!areInSameMonth(startOfMonth, s.end)) { | |||||
let [month, year] = [startOfMonth.getMonth(), startOfMonth.getFullYear()]; | |||||
endDate = getLastDateInMonth(month, year); | |||||
} | |||||
domainConfigs.push(this.getDomainConfig(startOfMonth, endDate)); | |||||
addDays(endDate, 1); | |||||
startOfMonth = endDate; | |||||
} | |||||
return domainConfigs; | |||||
} | |||||
getDomainConfig(startDate, endDate='') { | |||||
let [month, year] = [startDate.getMonth(), startDate.getFullYear()]; | |||||
let startOfWeek = setDayToSunday(startDate); | |||||
endDate = clone(endDate) || getLastDateInMonth(month, year); | |||||
let s = this.state; | |||||
let domainConfig = { | |||||
index: month, | |||||
cols: [] | |||||
}; | |||||
let noOfMonthWeeks = getWeeksBetween(startOfWeek, endDate); | |||||
let cols = []; | |||||
for(var i = 0; i < noOfMonthWeeks; i++) { | |||||
const col = this.getCol(startOfWeek, month); | |||||
cols.push(col); | |||||
startOfWeek = new Date(col[NO_OF_DAYS_IN_WEEK - 1].dateStr); | |||||
addDays(startOfWeek, 1); | |||||
} | |||||
if(startOfWeek.getDay() === this.startSubDomainIndex) { | |||||
cols.push(new Array(NO_OF_DAYS_IN_WEEK).fill(0)); | |||||
} | |||||
domainConfig.cols = cols; | |||||
return domainConfig; | |||||
} | |||||
getCol(startDate, month) { | |||||
// startDate is the start of week | |||||
let currentDate = clone(startDate); | |||||
let col = []; | |||||
for(var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) { | |||||
let config = 0; | |||||
if(currentDate.getMonth() === month) { | |||||
config = this.getSubDomainConfig(currentDate); | |||||
} | |||||
col.push(config); | |||||
} | |||||
return col; | |||||
} | |||||
getSubDomainConfig(date) { | |||||
let YyyyMmDd = getYyyyMmDd(date); | |||||
let dataValue = this.data.dataPoints[YyyyMmDd]; | |||||
let config = { | |||||
YyyyMmDd: YyyyMmDd, | |||||
dataValue: dataValue || 0, | |||||
fill: this.colors[getMaxCheckpoint(dataValue, this.state.distribution)] | |||||
} | |||||
return config; | |||||
} | |||||
} | } |
@@ -1,5 +1,5 @@ | |||||
import { makeSVGGroup } from '../utils/draw'; | import { makeSVGGroup } from '../utils/draw'; | ||||
import { makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, getPaths } from '../utils/draw'; | |||||
import { makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, getPaths, heatSquare } from '../utils/draw'; | |||||
import { equilizeNoOfElements } from '../utils/draw-utils'; | import { equilizeNoOfElements } from '../utils/draw-utils'; | ||||
import { translateHoriLine, translateVertLine, animateRegion, animateBar, | import { translateHoriLine, translateVertLine, animateRegion, animateBar, | ||||
animateDot, animatePath, animatePathStr } from '../utils/animate'; | animateDot, animatePath, animatePathStr } from '../utils/animate'; | ||||
@@ -80,6 +80,22 @@ let componentConfigs = { | |||||
); | ); | ||||
} | } | ||||
}, | }, | ||||
percentageBars: { | |||||
layerClass: 'percentage-bars', | |||||
makeElements(data) { | |||||
// return data.sliceStrings.map((s, i) =>{ | |||||
// let slice = makePath(s, 'pie-path', 'none', data.colors[i]); | |||||
// slice.style.transition = 'transform .3s;'; | |||||
// return slice; | |||||
// }); | |||||
}, | |||||
animateElements(newData) { | |||||
// return this.store.map((slice, i) => | |||||
// animatePathStr(slice, newData.sliceStrings[i]) | |||||
// ); | |||||
} | |||||
}, | |||||
yAxis: { | yAxis: { | ||||
layerClass: 'y axis', | layerClass: 'y axis', | ||||
makeElements(data) { | makeElements(data) { | ||||
@@ -211,6 +227,39 @@ let componentConfigs = { | |||||
} | } | ||||
}, | }, | ||||
heatDomain: { | |||||
layerClass: function() { return 'heat-domain domain-' + this.constants.index; }, | |||||
makeElements(data) { | |||||
let {colWidth, rowHeight, squareSize, xTranslate} = this.constants; | |||||
let x = xTranslate, y = 0; | |||||
this.serializedSubDomains = []; | |||||
data.cols.map(week => { | |||||
week.map((day, i) => { | |||||
let data = { | |||||
'data-date': day.YyyyMmDd, | |||||
'data-value': day.dataValue, | |||||
'data-day': i | |||||
}; | |||||
let square = heatSquare('day', x, y, squareSize, day.fill, data); | |||||
this.serializedSubDomains.push(square); | |||||
y += rowHeight; | |||||
}) | |||||
y = 0; | |||||
x += colWidth; | |||||
}) | |||||
return this.serializedSubDomains; | |||||
}, | |||||
animateElements(newData) { | |||||
// return this.store.map((slice, i) => | |||||
// animatePathStr(slice, newData.sliceStrings[i]) | |||||
// ); | |||||
} | |||||
}, | |||||
barGraph: { | barGraph: { | ||||
layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; }, | layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; }, | ||||
makeElements(data) { | makeElements(data) { | ||||
@@ -18,13 +18,13 @@ function treatAsUtc(date) { | |||||
return result; | return result; | ||||
} | } | ||||
export function getDdMmYyyy(date) { | |||||
export function getYyyyMmDd(date) { | |||||
let dd = date.getDate(); | let dd = date.getDate(); | ||||
let mm = date.getMonth() + 1; // getMonth() is zero-based | let mm = date.getMonth() + 1; // getMonth() is zero-based | ||||
return [ | return [ | ||||
(dd>9 ? '' : '0') + dd, | |||||
date.getFullYear(), | |||||
(mm>9 ? '' : '0') + mm, | (mm>9 ? '' : '0') + mm, | ||||
date.getFullYear() | |||||
(dd>9 ? '' : '0') + dd | |||||
].join('-'); | ].join('-'); | ||||
} | } | ||||
@@ -44,8 +44,11 @@ export function timestampToMidnight(timestamp, roundAhead = false) { | |||||
return midnightTs; | return midnightTs; | ||||
} | } | ||||
export function getMonthsBetween(startDate, endDate) {} | |||||
export function getWeeksBetween(startDate, endDate) { | export function getWeeksBetween(startDate, endDate) { | ||||
return Math.ceil(getDaysBetween(startDate, endDate) / NO_OF_DAYS_IN_WEEK); | |||||
let weekStartDate = setDayToSunday(startDate); | |||||
return Math.ceil(getDaysBetween(weekStartDate, endDate) / NO_OF_DAYS_IN_WEEK); | |||||
} | } | ||||
export function getDaysBetween(startDate, endDate) { | export function getDaysBetween(startDate, endDate) { | ||||
@@ -53,18 +56,28 @@ export function getDaysBetween(startDate, endDate) { | |||||
return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay; | return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay; | ||||
} | } | ||||
export function areInSameMonth(startDate, endDate) { | |||||
return startDate.getMonth() === endDate.getMonth() | |||||
&& startDate.getFullYear() === endDate.getFullYear(); | |||||
} | |||||
export function getMonthName(i, short=false) { | export function getMonthName(i, short=false) { | ||||
let monthName = MONTH_NAMES[i]; | let monthName = MONTH_NAMES[i]; | ||||
return short ? monthName.slice(0, 3) : monthName; | return short ? monthName.slice(0, 3) : monthName; | ||||
} | } | ||||
export function getLastDateInMonth (month, year) { | |||||
return new Date(year, month + 1, 0); // 0: last day in previous month | |||||
} | |||||
// mutates | // mutates | ||||
export function setDayToSunday(date) { | export function setDayToSunday(date) { | ||||
const day = date.getDay(); | |||||
if(day !== NO_OF_DAYS_IN_WEEK) { | |||||
addDays(date, (-1) * day); | |||||
let newDate = clone(date); | |||||
const day = newDate.getDay(); | |||||
if(day !== 0) { | |||||
addDays(newDate, (-1) * day); | |||||
} | } | ||||
return date; | |||||
return newDate; | |||||
} | } | ||||
// mutates | // mutates | ||||
@@ -109,3 +109,22 @@ export function fire(target, type, properties) { | |||||
return target.dispatchEvent(evt); | return target.dispatchEvent(evt); | ||||
} | } | ||||
// https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/ | |||||
export function forEachNode(nodeList, callback, scope) { | |||||
if(!nodeList) return; | |||||
for (var i = 0; i < nodeList.length; i++) { | |||||
callback.call(scope, nodeList[i], i); | |||||
} | |||||
} | |||||
export function activate($parent, $child, commonClass, activeClass='active', index = -1) { | |||||
let $children = $parent.querySelectorAll(`.${commonClass}.${activeClass}`); | |||||
forEachNode($children, (node, i) => { | |||||
if(index >= 0 && i <= index) return; | |||||
node.classList.remove(activeClass); | |||||
}) | |||||
$child.classList.add(activeClass); | |||||
} |
@@ -131,7 +131,7 @@ export function makeGradient(svgDefElem, color, lighter = false) { | |||||
return gradientId; | return gradientId; | ||||
} | } | ||||
export function makeHeatSquare(className, x, y, size, fill='none', data={}) { | |||||
export function heatSquare(className, x, y, size, fill='none', data={}) { | |||||
let args = { | let args = { | ||||
className: className, | className: className, | ||||
x: x, | x: x, | ||||