@@ -82,6 +82,8 @@ function fire(target, type, properties) { | |||
return target.dispatchEvent(evt); | |||
} | |||
// https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/ | |||
class SvgTip { | |||
constructor({ | |||
parent = null, | |||
@@ -467,7 +469,7 @@ function makeGradient(svgDefElem, color, lighter = false) { | |||
return gradientId; | |||
} | |||
function makeHeatSquare(className, x, y, size, fill='none', data={}) { | |||
function heatSquare(className, x, y, size, fill='none', data={}) { | |||
let args = { | |||
className: className, | |||
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: { | |||
layerClass: 'y axis', | |||
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: { | |||
layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; }, | |||
makeElements(data) { | |||
@@ -2158,13 +2209,13 @@ function treatAsUtc(date) { | |||
return result; | |||
} | |||
function getDdMmYyyy(date) { | |||
function getYyyyMmDd(date) { | |||
let dd = date.getDate(); | |||
let mm = date.getMonth() + 1; // getMonth() is zero-based | |||
return [ | |||
(dd>9 ? '' : '0') + dd, | |||
date.getFullYear(), | |||
(mm>9 ? '' : '0') + mm, | |||
date.getFullYear() | |||
(dd>9 ? '' : '0') + dd | |||
].join('-'); | |||
} | |||
@@ -2176,8 +2227,11 @@ function clone(date) { | |||
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) { | |||
@@ -2185,18 +2239,28 @@ function getDaysBetween(startDate, endDate) { | |||
return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay; | |||
} | |||
function areInSameMonth(startDate, endDate) { | |||
return startDate.getMonth() === endDate.getMonth() | |||
&& startDate.getFullYear() === endDate.getFullYear(); | |||
} | |||
function getMonthName(i, short=false) { | |||
let monthName = MONTH_NAMES[i]; | |||
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 | |||
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 | |||
@@ -2417,8 +2481,6 @@ function getMaxCheckpoint(value, distribution) { | |||
const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE; | |||
const ROW_HEIGHT = COL_WIDTH; | |||
const DAY_INCR = 1; | |||
class Heatmap extends BaseChart { | |||
constructor(parent, options) { | |||
super(parent, options); | |||
@@ -2427,6 +2489,11 @@ class Heatmap extends BaseChart { | |||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | |||
this.countLabel = options.countLabel || ''; | |||
let validStarts = ['Sunday', 'Monday']; | |||
let startSubDomain = validStarts.includes(options.startSubDomain) | |||
? options.startSubDomain : 'Sunday'; | |||
this.startSubDomainIndex = validStarts.indexOf(startSubDomain); | |||
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) { | |||
if(data.start && data.end && data.start > data.end) { | |||
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.setFullYear( data.start.getFullYear() - 1 ); | |||
} | |||
console.log(data.start); | |||
if(!data.end) { data.end = new Date(); } | |||
data.dataPoints = data.dataPoints || {}; | |||
@@ -2465,7 +2522,7 @@ class Heatmap extends BaseChart { | |||
let points = {}; | |||
Object.keys(data.dataPoints).forEach(timestampSec$$1 => { | |||
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; | |||
} | |||
@@ -2479,10 +2536,42 @@ class Heatmap extends BaseChart { | |||
s.start = this.data.start; | |||
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( | |||
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) { | |||
@@ -2494,105 +2583,6 @@ class Heatmap extends BaseChart { | |||
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() { | |||
Array.prototype.slice.call( | |||
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) { | |||
@@ -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 | |||
// ================================================================================ | |||
@@ -176,6 +176,7 @@ export const moonData = { | |||
let today = new Date(); | |||
let start = clone(today); | |||
addDays(start, 1); | |||
let end = clone(today); | |||
start.setFullYear( start.getFullYear() - 2 ); | |||
end.setFullYear( end.getFullYear() - 1 ); | |||
@@ -189,7 +190,7 @@ startTs = timestampToMidnight(startTs); | |||
endTs = timestampToMidnight(endTs, true); | |||
while (startTs < endTs) { | |||
dataPoints[parseInt(startTs)] = Math.floor(Math.random() * 5); | |||
dataPoints[parseInt(startTs)] = Math.floor(Math.random() * 17); | |||
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"]; | |||
// https://stackoverflow.com/a/11252167/6495043 | |||
function clone(date) { | |||
@@ -115,10 +114,19 @@ function timestampToMidnight(timestamp) { | |||
// mutates | |||
// mutates | |||
function addDays(date, numberOfDays) { | |||
date.setDate(date.getDate() + numberOfDays); | |||
} | |||
var reportCountList = [152, 222, 199, 287, 534, 709, 1179, 1256, 1632, 1856, 1850]; | |||
@@ -231,6 +239,7 @@ var moonData = { | |||
var today = new Date(); | |||
var start = clone(today); | |||
addDays(start, 1); | |||
var end = clone(today); | |||
start.setFullYear(start.getFullYear() - 2); | |||
end.setFullYear(end.getFullYear() - 1); | |||
@@ -244,7 +253,7 @@ startTs = timestampToMidnight(startTs); | |||
endTs = timestampToMidnight(endTs, true); | |||
while (startTs < endTs) { | |||
dataPoints[parseInt(startTs)] = Math.floor(Math.random() * 5); | |||
dataPoints[parseInt(startTs)] = Math.floor(Math.random() * 17); | |||
startTs += SEC_IN_DAY; | |||
} | |||
@@ -254,8 +263,6 @@ var heatmapData = { | |||
end: end | |||
}; | |||
// ================================================================================ | |||
var c1 = document.querySelector("#chart-composite-1"); | |||
var c2 = document.querySelector("#chart-composite-2"); | |||
@@ -1,6 +1,7 @@ | |||
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'; | |||
import { calcDistribution, getMaxCheckpoint } from '../utils/intervals'; | |||
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.countLabel = options.countLabel || ''; | |||
let validStarts = ['Sunday', 'Monday']; | |||
let startSubDomain = validStarts.includes(options.startSubDomain) | |||
? options.startSubDomain : 'Sunday'; | |||
this.startSubDomainIndex = validStarts.indexOf(startSubDomain); | |||
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) { | |||
if(data.start && data.end && data.start > data.end) { | |||
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.setFullYear( data.start.getFullYear() - 1 ); | |||
} | |||
console.log(data.start); | |||
if(!data.end) { data.end = new Date(); } | |||
data.dataPoints = data.dataPoints || {}; | |||
@@ -56,7 +52,7 @@ export default class Heatmap extends BaseChart { | |||
let points = {}; | |||
Object.keys(data.dataPoints).forEach(timestampSec => { | |||
let date = new Date(timestampSec * NO_OF_MILLIS); | |||
points[getDdMmYyyy(date)] = data.dataPoints[timestampSec]; | |||
points[getYyyyMmDd(date)] = data.dataPoints[timestampSec]; | |||
}); | |||
data.dataPoints = points; | |||
} | |||
@@ -70,10 +66,42 @@ export default class Heatmap extends BaseChart { | |||
s.start = this.data.start; | |||
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( | |||
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) { | |||
@@ -85,106 +113,6 @@ export default class Heatmap extends BaseChart { | |||
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() { | |||
Array.prototype.slice.call( | |||
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 { 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 { translateHoriLine, translateVertLine, animateRegion, animateBar, | |||
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: { | |||
layerClass: 'y axis', | |||
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: { | |||
layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; }, | |||
makeElements(data) { | |||
@@ -18,13 +18,13 @@ function treatAsUtc(date) { | |||
return result; | |||
} | |||
export function getDdMmYyyy(date) { | |||
export function getYyyyMmDd(date) { | |||
let dd = date.getDate(); | |||
let mm = date.getMonth() + 1; // getMonth() is zero-based | |||
return [ | |||
(dd>9 ? '' : '0') + dd, | |||
date.getFullYear(), | |||
(mm>9 ? '' : '0') + mm, | |||
date.getFullYear() | |||
(dd>9 ? '' : '0') + dd | |||
].join('-'); | |||
} | |||
@@ -44,8 +44,11 @@ export function timestampToMidnight(timestamp, roundAhead = false) { | |||
return midnightTs; | |||
} | |||
export function getMonthsBetween(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) { | |||
@@ -53,18 +56,28 @@ export function getDaysBetween(startDate, endDate) { | |||
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) { | |||
let monthName = MONTH_NAMES[i]; | |||
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 | |||
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 | |||
@@ -109,3 +109,22 @@ export function fire(target, type, properties) { | |||
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; | |||
} | |||
export function makeHeatSquare(className, x, y, size, fill='none', data={}) { | |||
export function heatSquare(className, x, y, size, fill='none', data={}) { | |||
let args = { | |||
className: className, | |||
x: x, | |||