Ver código fonte

fix: ignore local timezones when creating a heatmap (#377)

* [Heatmap] Adjusting grid calculation for consistent grid layout

Date math is hard!

This addresses a problem in how the heatmap lays calculates days to layout the grid. The calculations were being thrown off in some scenarios by a combination of factors including timezone offsets, time of day, and daylight savings time.

This patch should correct all this by forcing each day to be midnight UTC when laying out the grid.

Co-authored-by: Michael Bester <michael@kimili.com>
Co-authored-by: t47io <t47@alumni.stanford.edu>
Co-authored-by: Saqib Ansari <nextchamp.saqib@gmail.com>
tags/v1.6.3
Rohan 3 anos atrás
committed by GitHub
pai
commit
d19f03ac99
Nenhuma chave conhecida encontrada para esta assinatura no banco de dados ID da chave GPG: 4AEE18F83AFDEB23
14 arquivos alterados com 21976 adições e 12462 exclusões
  1. +1
    -1
      docs/assets/js/data.js
  2. +1
    -1
      docs/assets/js/frappe-charts.min.js
  3. +1
    -1
      docs/assets/js/frappe-charts.min.js.map
  4. +1
    -205
      docs/assets/js/index.min.js
  5. +1
    -1
      docs/assets/js/index.min.js.map
  6. +21912
    -5686
      package-lock.json
  7. +12
    -13
      package.json
  8. +9
    -9
      rollup.config.js
  9. +10
    -4
      src/js/charts/Heatmap.js
  10. +1
    -1
      src/js/utils/axis-chart-utils.js
  11. +6
    -0
      src/js/utils/date-utils.js
  12. +20
    -20
      src/js/utils/helpers.js
  13. +1
    -1
      src/js/utils/intervals.js
  14. +0
    -6519
      yarn.lock

+ 1
- 1
docs/assets/js/data.js Ver arquivo

@@ -275,4 +275,4 @@ export const moonData = {
masses: [14819000, 10759000, 8931900, 4800000],
distances: [1070.412, 1882.709, 421.7, 671.034],
diameters: [5262.4, 4820.6, 3637.4, 3121.6],
};
};

+ 1
- 1
docs/assets/js/frappe-charts.min.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 1
- 1
docs/assets/js/frappe-charts.min.js.map
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 1
- 205
docs/assets/js/index.min.js Ver arquivo

@@ -1,149 +1,11 @@
(function () {
'use strict';

// Fixed 5-color theme,
// More colors are difficult to parse visually









var HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'];
var HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'];



// Universal constants

var asyncGenerator = function () {
function AwaitValue(value) {
this.value = value;
}

function AsyncGenerator(gen) {
var front, back;

function send(key, arg) {
return new Promise(function (resolve, reject) {
var request = {
key: key,
arg: arg,
resolve: resolve,
reject: reject,
next: null
};

if (back) {
back = back.next = request;
} else {
front = back = request;
resume(key, arg);
}
});
}

function resume(key, arg) {
try {
var result = gen[key](arg);
var value = result.value;

if (value instanceof AwaitValue) {
Promise.resolve(value.value).then(function (arg) {
resume("next", arg);
}, function (arg) {
resume("throw", arg);
});
} else {
settle(result.done ? "return" : "normal", result.value);
}
} catch (err) {
settle("throw", err);
}
}

function settle(type, value) {
switch (type) {
case "return":
front.resolve({
value: value,
done: true
});
break;

case "throw":
front.reject(value);
break;

default:
front.resolve({
value: value,
done: false
});
break;
}

front = front.next;

if (front) {
resume(front.key, front.arg);
} else {
back = null;
}
}

this._invoke = send;

if (typeof gen.return !== "function") {
this.return = undefined;
}
}

if (typeof Symbol === "function" && Symbol.asyncIterator) {
AsyncGenerator.prototype[Symbol.asyncIterator] = function () {
return this;
};
}

AsyncGenerator.prototype.next = function (arg) {
return this._invoke("next", arg);
};

AsyncGenerator.prototype.throw = function (arg) {
return this._invoke("throw", arg);
};

AsyncGenerator.prototype.return = function (arg) {
return this._invoke("return", arg);
};

return {
wrap: function (fn) {
return function () {
return new AsyncGenerator(fn.apply(this, arguments));
};
},
await: function (value) {
return new AwaitValue(value);
}
};
}();

/**
* Returns the value of a number upto 2 decimal places.
* @param {Number} d Any number
*/


/**
* Returns whether or not two given arrays are equal.
* @param {Array} arr1 First array
* @param {Array} arr2 Second array
*/

var ANGLE_RATIO = Math.PI / 180;

/**
* Shuffles array in place. ES6 version
@@ -164,24 +26,6 @@ function shuffle(array) {
return array;
}

/**
* Fill an array with extra points
* @param {Array} array Array
* @param {Number} count number of filler elements
* @param {Object} element element to fill with
* @param {Boolean} start fill at start?
*/


/**
* Returns pixel width of string.
* @param {String} string
* @param {Number} charWidth Width of single char in pixels
*/




// https://stackoverflow.com/a/29325222
function getRandomBias(min, max, bias, influence) {
var range = max - min;
@@ -192,42 +36,11 @@ function getRandomBias(min, max, bias, influence) {
return rnd * (1 - mix) + biasValue * mix; // mix full range and bias
}



/**
* Check if a number is valid for svg attributes
* @param {object} candidate Candidate to test
* @param {Boolean} nonNegative flag to treat negative number as invalid
*/


/**
* Round a number to the closes precision, max max precision 4
* @param {Number} d Any Number
*/


/**
* Creates a deep clone of an object
* @param {Object} candidate Any Object
*/

// Playing around with dates




var NO_OF_MILLIS = 1000;
var SEC_IN_DAY = 86400;


var MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];






function clone(date) {
return new Date(date.getTime());
}
@@ -246,21 +59,6 @@ function timestampToMidnight(timestamp) {
return midnightTs;
}

// export function getMonthsBetween(startDate, endDate) {}











// mutates


// mutates
function addDays(date, numberOfDays) {
date.setDate(date.getDate() + numberOfDays);
@@ -413,8 +211,6 @@ var demoConfig = {
}
};

/* eslint-disable no-unused-vars */
/* eslint-enable no-unused-vars */
// import { lineComposite, barComposite } from './demoConfig';
// ================================================================================



+ 1
- 1
docs/assets/js/index.min.js.map
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 21912
- 5686
package-lock.json
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 12
- 13
package.json Ver arquivo

@@ -34,7 +34,7 @@
},
"homepage": "https://github.com/frappe/charts#readme",
"devDependencies": {
"autoprefixer": "^8.2.0",
"autoprefixer": "^8.1.0",
"babel-core": "^6.26.3",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-istanbul": "^5.1.4",
@@ -45,24 +45,23 @@
"coveralls": "^3.0.0",
"cross-env": "^5.1.4",
"cssnano": "^4.1.10",
"eslint": "^4.18.2",
"mocha": "^5.0.5",
"node-sass": "^4.12.0",
"npm-run-all": "^4.1.1",
"nyc": "^14.1.1",
"eslint": "^8.9.0",
"mocha": "^9.2.0",
"node-sass": "^7.0.1",
"npm-run-all": "^4.1.2",
"nyc": "^15.1.0",
"postcss": "^6.0.21",
"postcss-cssnext": "^3.0.2",
"postcss-nested": "^2.1.2",
"postcss-cssnext": "^3.1.0",
"postcss-nested": "^3.0.0",
"precss": "^3.1.2",
"rollup": "^0.50.0",
"rollup-plugin-babel": "^3.0.2",
"rollup": "^0.56.5",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-eslint": "^6.0.0",
"rollup-plugin-node-resolve": "^3.0.0",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-postcss": "^2.0.3",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-uglify": "^2.0.1",
"rollup-plugin-uglify-es": "0.0.1",
"rollup-watch": "^4.3.1"
},
"dependencies": {}
}
}

+ 9
- 9
rollup.config.js Ver arquivo

@@ -20,9 +20,9 @@ import autoprefixer from 'autoprefixer';
import fs from 'fs';

fs.readFile('src/css/charts.scss', (err, css) => {
postcss([precss, autoprefixer])
.process(css, { from: 'src/css/charts.scss', to: 'src/css/charts.css' })
.then(result => {
postcss([precss, autoprefixer])
.process(css, { from: 'src/css/charts.scss', to: 'src/css/charts.css' })
.then(result => {
let options = {
level: {
1: {
@@ -33,8 +33,8 @@ fs.readFile('src/css/charts.scss', (err, css) => {
let output = new CleanCSS(options).minify(result.css);
let res = JSON.stringify(output.styles).replace(/"/g, "'");
let js = `export const CSSTEXT = "${res.slice(1, -1)}";`;
fs.writeFile('src/css/chartsCss.js', js);
});
fs.writeFile('src/css/chartsCss.js', js);
});
});

export default [
@@ -58,7 +58,7 @@ export default [
const result = sass.renderSync({ file: id })
resolve({ code: result.css.toString() })
}),
extensions: [ '.scss' ],
extensions: ['.scss'],
plugins: [
nested(),
cssnext({ warnForDuplicates: false }),
@@ -97,7 +97,7 @@ export default [
const result = sass.renderSync({ file: id })
resolve({ code: result.css.toString() })
}),
extensions: [ '.scss' ],
extensions: ['.scss'],
plugins: [
nested(),
cssnext({ warnForDuplicates: false }),
@@ -137,7 +137,7 @@ export default [
const result = sass.renderSync({ file: id })
resolve({ code: result.css.toString() })
}),
extensions: [ '.scss' ],
extensions: ['.scss'],
plugins: [
nested(),
cssnext({ warnForDuplicates: false }),
@@ -173,7 +173,7 @@ export default [
const result = sass.renderSync({ file: id })
resolve({ code: result.css.toString() })
}),
extensions: [ '.scss' ],
extensions: ['.scss'],
extract: 'dist/frappe-charts.min.css',
plugins: [
nested(),


+ 10
- 4
src/js/charts/Heatmap.js Ver arquivo

@@ -1,7 +1,7 @@
import BaseChart from './BaseChart';
import { getComponent } from '../objects/ChartComponents';
import { makeText, heatSquare } from '../utils/draw';
import { DAY_NAMES_SHORT, addDays, areInSameMonth, getLastDateInMonth, setDayToSunday, getYyyyMmDd, getWeeksBetween, getMonthName, clone,
import { DAY_NAMES_SHORT, toMidnightUTC, 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 { getExtraHeight, getExtraWidth, HEATMAP_DISTRIBUTION_SIZE, HEATMAP_SQUARE_SIZE,
@@ -58,7 +58,13 @@ export default class Heatmap extends BaseChart {
data.start = new Date();
data.start.setFullYear( data.start.getFullYear() - 1 );
}
if(!data.end) { data.end = new Date(); }
data.start = toMidnightUTC(data.start);

if(!data.end) {
data.end = new Date();
}
data.end = toMidnightUTC(data.end);

data.dataPoints = data.dataPoints || {};

if(parseInt(Object.keys(data.dataPoints)[0]) > 100000) {
@@ -230,7 +236,7 @@ export default class Heatmap extends BaseChart {
getDomainConfig(startDate, endDate='') {
let [month, year] = [startDate.getMonth(), startDate.getFullYear()];
let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well
endDate = clone(endDate) || getLastDateInMonth(month, year);
endDate = endDate ? clone(endDate) : toMidnightUTC(getLastDateInMonth(month, year));

let domainConfig = {
index: month,
@@ -245,7 +251,7 @@ export default class Heatmap extends BaseChart {
col = this.getCol(startOfWeek, month);
cols.push(col);

startOfWeek = new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd);
startOfWeek = toMidnightUTC(new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd));
addDays(startOfWeek, 1);
}



+ 1
- 1
src/js/utils/axis-chart-utils.js Ver arquivo

@@ -36,7 +36,7 @@ export function dataPrep(data, type) {

// Set type
if(!d.chartType ) {
if(!AXIS_DATASET_CHART_TYPES.includes(type)) type === DEFAULT_AXIS_CHART_TYPE;
if(!AXIS_DATASET_CHART_TYPES.includes(type)) type = DEFAULT_AXIS_CHART_TYPE;
d.chartType = type;
}



+ 6
- 0
src/js/utils/date-utils.js Ver arquivo

@@ -22,6 +22,12 @@ function treatAsUtc(date) {
return result;
}

export function toMidnightUTC(date) {
let result = new Date(date);
result.setUTCHours(0, result.getTimezoneOffset(), 0, 0);
return result;
}

export function getYyyyMmDd(date) {
let dd = date.getDate();
let mm = date.getMonth() + 1; // getMonth() is zero-based


+ 20
- 20
src/js/utils/helpers.js Ver arquivo

@@ -14,10 +14,10 @@ export function floatTwo(d) {
* @param {Array} arr2 Second array
*/
export function arraysEqual(arr1, arr2) {
if(arr1.length !== arr2.length) return false;
if (arr1.length !== arr2.length) return false;
let areEqual = true;
arr1.map((d, i) => {
if(arr2[i] !== d) areEqual = false;
if (arr2[i] !== d) areEqual = false;
});
return areEqual;
}
@@ -46,8 +46,8 @@ export function shuffle(array) {
* @param {Object} element element to fill with
* @param {Boolean} start fill at start?
*/
export function fillArray(array, count, element, start=false) {
if(!element) {
export function fillArray(array, count, element, start = false) {
if (!element) {
element = start ? array[0] : array[array.length - 1];
}
let fillerArray = new Array(Math.abs(count)).fill(element);
@@ -61,16 +61,16 @@ export function fillArray(array, count, element, start=false) {
* @param {Number} charWidth Width of single char in pixels
*/
export function getStringWidth(string, charWidth) {
return (string+"").length * charWidth;
return (string + "").length * charWidth;
}

export function bindChange(obj, getFn, setFn) {
return new Proxy(obj, {
set: function(target, prop, value) {
set: function (target, prop, value) {
setFn();
return Reflect.set(target, prop, value);
},
get: function(target, prop) {
get: function (target, prop) {
getFn();
return Reflect.get(target, prop);
}
@@ -98,7 +98,7 @@ export function getPositionByAngle(angle, radius) {
* @param {object} candidate Candidate to test
* @param {Boolean} nonNegative flag to treat negative number as invalid
*/
export function isValidNumber(candidate, nonNegative=false) {
export function isValidNumber(candidate, nonNegative = false) {
if (Number.isNaN(candidate)) return false;
else if (candidate === undefined) return false;
else if (!Number.isFinite(candidate)) return false;
@@ -120,24 +120,24 @@ export function round(d) {
* Creates a deep clone of an object
* @param {Object} candidate Any Object
*/
export function deepClone(candidate) {
export function deepClone(candidate) {
let cloned, value, key;
if (candidate instanceof Date) {
return new Date(candidate.getTime());
return new Date(candidate.getTime());
}
if (typeof candidate !== "object" || candidate === null) {
return candidate;
return candidate;
}
cloned = Array.isArray(candidate) ? [] : {};
for (key in candidate) {
value = candidate[key];
cloned[key] = deepClone(value);
value = candidate[key];
cloned[key] = deepClone(value);
}
return cloned;
}
}

+ 1
- 1
src/js/utils/intervals.js Ver arquivo

@@ -160,7 +160,7 @@ export function calcChartIntervals(values, withMinimum=false) {
intervals = intervals.reverse().map(d => d * (-1));
}

return intervals;
return intervals.sort((a, b) => (a - b));
}

export function getZeroIndex(yPts) {


+ 0
- 6519
yarn.lock
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


Carregando…
Cancelar
Salvar