Merge pull request #53 from IDDT/master

Add custom legend for Heatmap chart.
This commit is contained in:
Prateeksha Singh 2017-11-12 23:11:38 +05:30 committed by GitHub
commit ebfd6bd8dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 281 additions and 127 deletions

View File

@ -675,6 +675,29 @@ function runSVGAnimation(svg_container, elements) {
// //
// }
function calc_distribution(values, distribution_size) {
// Assume non-negative values,
// implying distribution minimum at zero
var data_max_value = Math.max.apply(Math, toConsumableArray(values));
var distribution_step = 1 / (distribution_size - 1);
var distribution = [];
for (var i = 0; i < distribution_size; i++) {
var checkpoint = data_max_value * (distribution_step * i);
distribution.push(checkpoint);
}
return distribution;
}
function get_max_checkpoint(value, distribution) {
return distribution.filter(function (d) {
return d < value;
}).length;
}
function calc_y_intervals(array) {
//*** Where the magic happens ***
@ -2641,6 +2664,12 @@ function lighten_darken_color(col, amt) {
return (usePound ? "#" : "") + (g | b << 8 | r << 16).toString(16);
}
function is_valid_color(string) {
// https://stackoverflow.com/a/8027444/6495043
return (/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(string)
);
}
var ANGLE_RATIO = Math.PI / 180;
var FULL_ANGLE = 360;
@ -2945,7 +2974,9 @@ var Heatmap = function (_BaseChart) {
_ref$discrete_domains = _ref.discrete_domains,
discrete_domains = _ref$discrete_domains === undefined ? 0 : _ref$discrete_domains,
_ref$count_label = _ref.count_label,
count_label = _ref$count_label === undefined ? '' : _ref$count_label;
count_label = _ref$count_label === undefined ? '' : _ref$count_label,
_ref$legend_colors = _ref.legend_colors,
legend_colors = _ref$legend_colors === undefined ? [] : _ref$legend_colors;
classCallCheck(this, Heatmap);
var _this = possibleConstructorReturn(this, (Heatmap.__proto__ || Object.getPrototypeOf(Heatmap)).call(this, arguments[0]));
@ -2961,7 +2992,12 @@ var Heatmap = function (_BaseChart) {
var today = new Date();
_this.start = start || add_days(today, 365);
_this.legend_colors = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
legend_colors = legend_colors.slice(0, 5);
_this.legend_colors = _this.validate_colors(legend_colors) ? legend_colors : ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
// Hardcoded for a fixed 5-color theme,
// More colors are difficult to parse visually
_this.distribution_size = 5;
_this.translate_x = 0;
_this.setup();
@ -2969,6 +3005,21 @@ var Heatmap = function (_BaseChart) {
}
createClass(Heatmap, [{
key: 'validate_colors',
value: function validate_colors(colors) {
if (colors.length < 5) return 0;
var valid = 1;
colors.forEach(function (string) {
if (!is_valid_color(string)) {
valid = 0;
console.warn('"' + string + '" is not a valid color.');
}
}, this);
return valid;
}
}, {
key: 'setup_base_values',
value: function setup_base_values() {
this.today = new Date();
@ -3012,9 +3063,16 @@ var Heatmap = function (_BaseChart) {
}, {
key: 'setup_values',
value: function setup_values() {
var _this2 = this;
this.domain_label_group.textContent = '';
this.data_groups.textContent = '';
this.distribution = this.get_distribution(this.data, this.legend_colors);
var data_values = Object.keys(this.data).map(function (key) {
return _this2.data[key];
});
this.distribution = calc_distribution(data_values, this.distribution_size);
this.month_names = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
this.render_all_weeks_and_store_x_values(this.no_of_cols);
@ -3081,12 +3139,14 @@ var Heatmap = function (_BaseChart) {
if (this.data[timestamp]) {
data_value = this.data[timestamp];
color_index = this.get_max_checkpoint(data_value, this.distribution);
}
if (this.data[Math.round(timestamp)]) {
data_value = this.data[Math.round(timestamp)];
color_index = this.get_max_checkpoint(data_value, this.distribution);
}
if (data_value) {
color_index = get_max_checkpoint(data_value, this.distribution);
}
var x = 13 + (index + week_col_change) * 12;
@ -3124,7 +3184,7 @@ var Heatmap = function (_BaseChart) {
}, {
key: 'render_month_labels',
value: function render_month_labels() {
var _this2 = this;
var _this3 = this;
// this.first_month_label = 1;
// if (this.first_week_start.getDate() > 8) {
@ -3146,11 +3206,11 @@ var Heatmap = function (_BaseChart) {
this.month_start_points.pop();
this.month_start_points.map(function (start, i) {
var month_name = _this2.month_names[_this2.months[i]].substring(0, 3);
var month_name = _this3.month_names[_this3.months[i]].substring(0, 3);
$.createSVG('text', {
className: 'y-value-text',
inside: _this2.domain_label_group,
inside: _this3.domain_label_group,
x: start + 12,
y: 10,
dy: '.32em',
@ -3170,26 +3230,26 @@ var Heatmap = function (_BaseChart) {
}, {
key: 'bind_tooltip',
value: function bind_tooltip() {
var _this3 = this;
var _this4 = this;
Array.prototype.slice.call(document.querySelectorAll(".data-group .day")).map(function (el) {
el.addEventListener('mouseenter', function (e) {
var count = e.target.getAttribute('data-value');
var date_parts = e.target.getAttribute('data-date').split('-');
var month = _this3.month_names[parseInt(date_parts[1]) - 1].substring(0, 3);
var month = _this4.month_names[parseInt(date_parts[1]) - 1].substring(0, 3);
var g_off = _this3.chart_wrapper.getBoundingClientRect(),
var g_off = _this4.chart_wrapper.getBoundingClientRect(),
p_off = e.target.getBoundingClientRect();
var width = parseInt(e.target.getAttribute('width'));
var x = p_off.left - g_off.left + (width + 2) / 2;
var y = p_off.top - g_off.top - (width + 2) / 2;
var value = count + ' ' + _this3.count_label;
var value = count + ' ' + _this4.count_label;
var name = ' on ' + month + ' ' + date_parts[0] + ', ' + date_parts[2];
_this3.tip.set_values(x, y, name, value, [], 1);
_this3.tip.show_tip();
_this4.tip.set_values(x, y, name, value, [], 1);
_this4.tip.show_tip();
});
});
}
@ -3200,37 +3260,6 @@ var Heatmap = function (_BaseChart) {
this.setup_values();
this.bind_tooltip();
}
}, {
key: 'get_distribution',
value: function get_distribution() {
var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var mapper_array = arguments[1];
var data_values = Object.keys(data).map(function (key) {
return data[key];
});
var data_max_value = Math.max.apply(Math, toConsumableArray(data_values));
var distribution_step = 1 / (mapper_array.length - 1);
var distribution = [];
mapper_array.map(function (color, i) {
var checkpoint = data_max_value * (distribution_step * i);
distribution.push(checkpoint);
});
return distribution;
}
}, {
key: 'get_max_checkpoint',
value: function get_max_checkpoint(value, distribution) {
return distribution.filter(function (d, i) {
if (i === 1) {
return distribution[0] < value;
}
return d <= value;
}).length - 1;
}
}]);
return Heatmap;
}(BaseChart);

View File

@ -673,6 +673,29 @@ function runSVGAnimation(svg_container, elements) {
// //
// }
function calc_distribution(values, distribution_size) {
// Assume non-negative values,
// implying distribution minimum at zero
var data_max_value = Math.max.apply(Math, toConsumableArray(values));
var distribution_step = 1 / (distribution_size - 1);
var distribution = [];
for (var i = 0; i < distribution_size; i++) {
var checkpoint = data_max_value * (distribution_step * i);
distribution.push(checkpoint);
}
return distribution;
}
function get_max_checkpoint(value, distribution) {
return distribution.filter(function (d) {
return d < value;
}).length;
}
function calc_y_intervals(array) {
//*** Where the magic happens ***
@ -2639,6 +2662,12 @@ function lighten_darken_color(col, amt) {
return (usePound ? "#" : "") + (g | b << 8 | r << 16).toString(16);
}
function is_valid_color(string) {
// https://stackoverflow.com/a/8027444/6495043
return (/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(string)
);
}
var ANGLE_RATIO = Math.PI / 180;
var FULL_ANGLE = 360;
@ -2943,7 +2972,9 @@ var Heatmap = function (_BaseChart) {
_ref$discrete_domains = _ref.discrete_domains,
discrete_domains = _ref$discrete_domains === undefined ? 0 : _ref$discrete_domains,
_ref$count_label = _ref.count_label,
count_label = _ref$count_label === undefined ? '' : _ref$count_label;
count_label = _ref$count_label === undefined ? '' : _ref$count_label,
_ref$legend_colors = _ref.legend_colors,
legend_colors = _ref$legend_colors === undefined ? [] : _ref$legend_colors;
classCallCheck(this, Heatmap);
var _this = possibleConstructorReturn(this, (Heatmap.__proto__ || Object.getPrototypeOf(Heatmap)).call(this, arguments[0]));
@ -2959,7 +2990,12 @@ var Heatmap = function (_BaseChart) {
var today = new Date();
_this.start = start || add_days(today, 365);
_this.legend_colors = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
legend_colors = legend_colors.slice(0, 5);
_this.legend_colors = _this.validate_colors(legend_colors) ? legend_colors : ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
// Hardcoded for a fixed 5-color theme,
// More colors are difficult to parse visually
_this.distribution_size = 5;
_this.translate_x = 0;
_this.setup();
@ -2967,6 +3003,21 @@ var Heatmap = function (_BaseChart) {
}
createClass(Heatmap, [{
key: 'validate_colors',
value: function validate_colors(colors) {
if (colors.length < 5) return 0;
var valid = 1;
colors.forEach(function (string) {
if (!is_valid_color(string)) {
valid = 0;
console.warn('"' + string + '" is not a valid color.');
}
}, this);
return valid;
}
}, {
key: 'setup_base_values',
value: function setup_base_values() {
this.today = new Date();
@ -3010,9 +3061,16 @@ var Heatmap = function (_BaseChart) {
}, {
key: 'setup_values',
value: function setup_values() {
var _this2 = this;
this.domain_label_group.textContent = '';
this.data_groups.textContent = '';
this.distribution = this.get_distribution(this.data, this.legend_colors);
var data_values = Object.keys(this.data).map(function (key) {
return _this2.data[key];
});
this.distribution = calc_distribution(data_values, this.distribution_size);
this.month_names = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
this.render_all_weeks_and_store_x_values(this.no_of_cols);
@ -3079,12 +3137,14 @@ var Heatmap = function (_BaseChart) {
if (this.data[timestamp]) {
data_value = this.data[timestamp];
color_index = this.get_max_checkpoint(data_value, this.distribution);
}
if (this.data[Math.round(timestamp)]) {
data_value = this.data[Math.round(timestamp)];
color_index = this.get_max_checkpoint(data_value, this.distribution);
}
if (data_value) {
color_index = get_max_checkpoint(data_value, this.distribution);
}
var x = 13 + (index + week_col_change) * 12;
@ -3122,7 +3182,7 @@ var Heatmap = function (_BaseChart) {
}, {
key: 'render_month_labels',
value: function render_month_labels() {
var _this2 = this;
var _this3 = this;
// this.first_month_label = 1;
// if (this.first_week_start.getDate() > 8) {
@ -3144,11 +3204,11 @@ var Heatmap = function (_BaseChart) {
this.month_start_points.pop();
this.month_start_points.map(function (start, i) {
var month_name = _this2.month_names[_this2.months[i]].substring(0, 3);
var month_name = _this3.month_names[_this3.months[i]].substring(0, 3);
$.createSVG('text', {
className: 'y-value-text',
inside: _this2.domain_label_group,
inside: _this3.domain_label_group,
x: start + 12,
y: 10,
dy: '.32em',
@ -3168,26 +3228,26 @@ var Heatmap = function (_BaseChart) {
}, {
key: 'bind_tooltip',
value: function bind_tooltip() {
var _this3 = this;
var _this4 = this;
Array.prototype.slice.call(document.querySelectorAll(".data-group .day")).map(function (el) {
el.addEventListener('mouseenter', function (e) {
var count = e.target.getAttribute('data-value');
var date_parts = e.target.getAttribute('data-date').split('-');
var month = _this3.month_names[parseInt(date_parts[1]) - 1].substring(0, 3);
var month = _this4.month_names[parseInt(date_parts[1]) - 1].substring(0, 3);
var g_off = _this3.chart_wrapper.getBoundingClientRect(),
var g_off = _this4.chart_wrapper.getBoundingClientRect(),
p_off = e.target.getBoundingClientRect();
var width = parseInt(e.target.getAttribute('width'));
var x = p_off.left - g_off.left + (width + 2) / 2;
var y = p_off.top - g_off.top - (width + 2) / 2;
var value = count + ' ' + _this3.count_label;
var value = count + ' ' + _this4.count_label;
var name = ' on ' + month + ' ' + date_parts[0] + ', ' + date_parts[2];
_this3.tip.set_values(x, y, name, value, [], 1);
_this3.tip.show_tip();
_this4.tip.set_values(x, y, name, value, [], 1);
_this4.tip.show_tip();
});
});
}
@ -3198,37 +3258,6 @@ var Heatmap = function (_BaseChart) {
this.setup_values();
this.bind_tooltip();
}
}, {
key: 'get_distribution',
value: function get_distribution() {
var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var mapper_array = arguments[1];
var data_values = Object.keys(data).map(function (key) {
return data[key];
});
var data_max_value = Math.max.apply(Math, toConsumableArray(data_values));
var distribution_step = 1 / (mapper_array.length - 1);
var distribution = [];
mapper_array.map(function (color, i) {
var checkpoint = data_max_value * (distribution_step * i);
distribution.push(checkpoint);
});
return distribution;
}
}, {
key: 'get_max_checkpoint',
value: function get_max_checkpoint(value, distribution) {
return distribution.filter(function (d, i) {
if (i === 1) {
return distribution[0] < value;
}
return d <= value;
}).length - 1;
}
}]);
return Heatmap;
}(BaseChart);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -360,7 +360,7 @@ let current_date = new Date();
let timestamp = current_date.getTime()/1000;
timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight
for (var i = 0; i< 375; i++) {
heatmap_data[parseInt(timestamp)] = Math.floor(Math.random() * 6);
heatmap_data[parseInt(timestamp)] = Math.floor(Math.random() * 5);
timestamp = Math.floor(timestamp - 86400).toFixed(1);
}
@ -368,6 +368,7 @@ new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
type: 'heatmap',
legend_scale: [0, 1, 2, 4, 5],
height: 115,
discrete_domains: 1 // default 0
});
@ -384,12 +385,61 @@ Array.prototype.slice.call(
discrete_domains = 1;
}
let colors = [];
let colors_mode = document
.querySelector('.heatmap-color-buttons .active')
.getAttribute('data-color');
if(colors_mode === 'halloween') {
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'];
}
new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
type: 'heatmap',
legend_scale: [0, 1, 2, 4, 5],
height: 115,
discrete_domains: discrete_domains
discrete_domains: discrete_domains,
legend_colors: colors
});
Array.prototype.slice.call(
btn.parentNode.querySelectorAll('button')).map(el => {
el.classList.remove('active');
});
btn.classList.add('active');
});
});
Array.prototype.slice.call(
document.querySelectorAll('.heatmap-color-buttons button')
).map(el => {
el.addEventListener('click', (e) => {
let btn = e.target;
let colors_mode = btn.getAttribute('data-color');
let colors = [];
if(colors_mode === 'halloween') {
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'];
}
let discrete_domains = 1;
let view_mode = document
.querySelector('.heatmap-mode-buttons .active')
.getAttribute('data-mode');
if(view_mode === 'continuous') {
discrete_domains = 0;
}
new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
type: 'heatmap',
legend_scale: [0, 1, 2, 4, 5],
height: 115,
discrete_domains: discrete_domains,
legend_colors: colors
});
Array.prototype.slice.call(

View File

@ -227,7 +227,7 @@
<div class="col-sm-10 push-sm-1">
<div class="dashboard-section">
<h6 class="margin-vertical-rem">
And an Annual Heatmap
And a Month-wise Heatmap
</h6>
<div id="chart-heatmap" class="border"
style="overflow: scroll; text-align: center; padding: 20px;"></div>
@ -235,12 +235,28 @@
<button type="button" class="btn btn-sm btn-secondary active" data-mode="discrete">Discrete</button>
<button type="button" class="btn btn-sm btn-secondary" data-mode="continuous">Continuous</button>
</div>
<div class="heatmap-color-buttons btn-group mt-1 mx-auto" role="group">
<button type="button" class="btn btn-sm btn-secondary active" data-color="default">Default green</button>
<button type="button" class="btn btn-sm btn-secondary" data-color="halloween">GitHub's Halloween</button>
</div>
<pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart({
parent: "#heatmap",
data: heatmap_data, // object with date/timestamp-value pairs
type: 'heatmap',
height: 115,
discrete_domains: 1 // default 0
data: heatmap_data, // object with date/timestamp-value pairs
discrete_domains: 1 // default: 0
start: start_date,
// A Date object;
// default: today's date in past year
// for an annual heatmap
legend_colors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'],
// Set of five incremental colors,
// beginning with a low-saturation color for zero data;
// default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']
});</code></pre>
</div>
</div>

View File

@ -1,6 +1,8 @@
import BaseChart from './BaseChart';
import $ from '../utils/dom';
import { add_days, get_dd_mm_yyyy, get_weeks_between } from '../utils/date-utils';
import { calc_distribution, get_max_checkpoint } from '../utils/intervals';
import { is_valid_color } from '../utils/colors';
export default class Heatmap extends BaseChart {
constructor({
@ -9,7 +11,8 @@ export default class Heatmap extends BaseChart {
subdomain = '',
data = {},
discrete_domains = 0,
count_label = ''
count_label = '',
legend_colors = []
}) {
super(arguments[0]);
@ -24,12 +27,33 @@ export default class Heatmap extends BaseChart {
let today = new Date();
this.start = start || add_days(today, 365);
this.legend_colors = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
legend_colors = legend_colors.slice(0, 5);
this.legend_colors = this.validate_colors(legend_colors)
? legend_colors
: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
// Hardcoded for a fixed 5-color theme,
// More colors are difficult to parse visually
this.distribution_size = 5;
this.translate_x = 0;
this.setup();
}
validate_colors(colors) {
if(colors.length < 5) return 0;
let valid = 1;
colors.forEach(function(string) {
if(!is_valid_color(string)) {
valid = 0;
console.warn('"' + string + '" is not a valid color.');
}
}, this);
return valid;
}
setup_base_values() {
this.today = new Date();
@ -71,7 +95,10 @@ export default class Heatmap extends BaseChart {
setup_values() {
this.domain_label_group.textContent = '';
this.data_groups.textContent = '';
this.distribution = this.get_distribution(this.data, this.legend_colors);
let data_values = Object.keys(this.data).map(key => this.data[key]);
this.distribution = calc_distribution(data_values, this.distribution_size);
this.month_names = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
@ -131,12 +158,14 @@ export default class Heatmap extends BaseChart {
if(this.data[timestamp]) {
data_value = this.data[timestamp];
color_index = this.get_max_checkpoint(data_value, this.distribution);
}
if(this.data[Math.round(timestamp)]) {
data_value = this.data[Math.round(timestamp)];
color_index = this.get_max_checkpoint(data_value, this.distribution);
}
if(data_value) {
color_index = get_max_checkpoint(data_value, this.distribution);
}
let x = 13 + (index + week_col_change) * 12;
@ -204,7 +233,6 @@ export default class Heatmap extends BaseChart {
dy: '.32em',
innerHTML: month_name
});
});
}
@ -247,28 +275,4 @@ export default class Heatmap extends BaseChart {
this.setup_values();
this.bind_tooltip();
}
get_distribution(data={}, mapper_array) {
let data_values = Object.keys(data).map(key => data[key]);
let data_max_value = Math.max(...data_values);
let distribution_step = 1 / (mapper_array.length - 1);
let distribution = [];
mapper_array.map((color, i) => {
let checkpoint = data_max_value * (distribution_step * i);
distribution.push(checkpoint);
});
return distribution;
}
get_max_checkpoint(value, distribution) {
return distribution.filter((d, i) => {
if(i === 1) {
return distribution[0] < value;
}
return d <= value;
}).length - 1;
}
}

View File

@ -15,4 +15,9 @@ export function lighten_darken_color(col, amt) {
let b = limit_color(((num >> 8) & 0x00FF) + amt);
let g = limit_color((num & 0x0000FF) + amt);
return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16);
}
export function is_valid_color(string) {
// https://stackoverflow.com/a/8027444/6495043
return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(string);
}

View File

@ -32,6 +32,27 @@ export function clump_intervals(start, interval_size, count) {
// //
// }
export function calc_distribution(values, distribution_size) {
// Assume non-negative values,
// implying distribution minimum at zero
let data_max_value = Math.max(...values);
let distribution_step = 1 / (distribution_size - 1);
let distribution = [];
for(var i = 0; i < distribution_size; i++) {
let checkpoint = data_max_value * (distribution_step * i);
distribution.push(checkpoint);
}
return distribution;
}
export function get_max_checkpoint(value, distribution) {
return distribution.filter(d => d < value).length;
}
export function calc_y_intervals(array) {
//*** Where the magic happens ***