All files / util chart-helpers.js

95.65% Statements 44/46
48.28% Branches 14/29
94.74% Functions 18/19
95.56% Lines 43/45
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231                                4x   4x 15x 54x   54x 15x     39x   39x                               26x   26x 150x                             48x   48x 80x                             117x   117x 135x                             13x       13x 7x     6x   6x                             25x                         89x                         5x   5x 22x         22x     5x                         15x 11x     4x   4x 10x                                     217x     121x 121x 121x 121x 121x 121x 121x 121x                                          
import _ from 'lodash';
import * as d3TimeFormat from 'd3-time-format';
import * as d3Time from 'd3-time';
 
/**
 * stackByFields
 *
 * D3's `stack` groups each series' data together but we sometimes we want the
 * stacked groups to remain grouped as in the original normalized data. This
 * function helps achieve that.
 *
 * @param {object[]} collection - normalized data you want to operate on
 * @param {string[]} fields - fields to pluck off for the y data
 * @return {array[]} - array of arrays, one for row in the original `collection`
 */
export function stackByFields(collection, fields) {
	const fieldsArray = _.castArray(fields);
 
	return _.map(collection, (d) => {
		return _.reduce(fieldsArray, (acc, field) => {
			const dataPoint = _.get(d, field, 0);
 
			if (_.isEmpty(acc)) {
				return acc.concat([[0, dataPoint]])
			}
 
			const last = _.last(_.last(acc));
 
			return acc.concat([[last, last + dataPoint]]);
		}, []);
	});
}
 
/**
 * extractFields
 *
 * This will return the data in a similar format to stackByFields but without
 * the stacking.
 *
 * @param {object[]} collection - normalized data you want to operate on
 * @param {string[]} fields - fields to pluck off for the y data
 * @return {array[]} - array of arrays, one for each field
 */
export function extractFields(collection, fields) {
	const fieldsArray = _.castArray(fields);
 
	return _.map(collection, (d) => {
		return _.map(fieldsArray, (field) => [0, _.get(d, field, 0)]);
	});
}
 
/**
 * groupByFields
 *
 * This will return the data in a similar format to d3Shape.stack
 * but without the stacking of the data.
 *
 * @param {object[]} collection - normalized data you want to operate on
 * @param {string[]} fields - fields to pluck off for the y data
 * @return {array[]} - array of arrays, one for each field
 */
export function groupByFields(collection, fields) {
	const fieldsArray = _.castArray(fields);
 
	return _.map(fieldsArray, (field) => {
		return _.map(collection, field);
	});
}
 
/**
 * byFields
 *
 * Takes a collection of data and returns an array of all the fields off that
 * collection.
 *
 * @param {object[]} collection
 * @param {string[]} fields
 * @return {array}
 */
export function byFields(collection, fields) {
	const fieldArray = _.castArray(fields);
 
	return _.reduce(fieldArray, (acc, field) => {
		return acc.concat(_.map(collection, field));
	}, []);
}
 
/**
 * nearest
 *
 * Divide and conquer algorithm that helps find the nearest element to `value`
 * in `nums`
 *
 * @param {number[]} nums - sorted array of numbers to search through
 * @param {number} value - value you're trying to locate the nearest array element for
 * @return {number} - the nearest array element to the value
 */
export function nearest(nums, value) {
	Iif (nums.length < 2) {
		return _.first(nums);
	}
 
	if (nums.length === 2) {
		return value > ((nums[0] + nums[1]) / 2) ? nums[1] : nums[0];
	}
 
	const mid = nums.length >>> 1;
 
	return nums[mid] > value
		? nearest(nums.slice(0, mid + 1), value)
		: nearest(nums.slice(mid), value);
}
 
/**
 * minByFields
 *
 * Returns the minimum element from a collection by a set of fields.
 *
 * @param {object[]} collection
 * @param {string[]} fields
 * @return {any}
 */
export function minByFields(collection, fields) {
	return _.min(byFields(collection, fields));
}
 
/**
 * maxByFields
 *
 * Returns the maximum element from a collection by a set of fields.
 *
 * @param {object[]} collection
 * @param {string[]} fields
 * @return {any}
 */
export function maxByFields(collection, fields) {
	return _.max(byFields(collection, fields));
}
 
/**
 * maxByFieldsStacked
 *
 * Returns the max sum of a set of fields from a collection
 *
 * @param {object[]} collection
 * @param {string[]} fields
 * @return {any}
 */
export function maxByFieldsStacked(collection, fields) {
	const fieldArray = _.castArray(fields);
 
	const sums = _.reduce(collection, (acc, item) => {
		const sum = _.chain(item)
			.pick(fieldArray)
			.toArray()
			.sum()
			.value();
		return acc.concat(sum);
	}, []);
 
	return _.max(sums);
}
 
/**
 * discreteTicks
 *
 * Returns `count` evenly spaced, representative values from the `array`.
 *
 * @param {array} array
 * @param {number} size - should be greater than 1
 * @return {array}
 */
export function discreteTicks(array, count) {
	if (!array || _.isNil(count) || array.length <= count) {
		return array;
	}
 
	const step = (array.length - 1) / Math.max(1, count - 1);
 
	return _.reduce(_.times(count), (acc, n) => {
		return acc.concat(array[Math.round(n  * step)]);
	}, []);
}
 
/**
 * transformFromCenter
 *
 * Scaling paths from their center is tricky. This function
 * helps do that be generating a translate/scale transform
 * string with the correct numbers.
 *
 * @param {number} x - the x data point where you want the path to be centered at
 * @param {number} y - the y data point where you want the path to be centered at
 * @param {number} xCenter - the x coordinate of the center of the path you're trying to transform
 * @param {number} yCenter - the x coordinate of the center of the path you're trying to transform
 * @param {number} scale - number to scale to, 2 would be 2x bigger
 * @return {string} - transform string
 */
export function transformFromCenter(x, y, xCenter, yCenter, scale) {
	return `translate(${((1 - scale) * xCenter) + (x - xCenter)}, ${((1 - scale) * yCenter) + (y - yCenter)}) scale(${scale})`;
}
 
const FORMAT_MILLISECOND = d3TimeFormat.timeFormat('.%L');
const FORMAT_SECOND = d3TimeFormat.timeFormat(':%S');
const FORMAT_MINUTE = d3TimeFormat.timeFormat('%I:%M');
const FORMAT_HOUR = d3TimeFormat.timeFormat('%I %p');
const FORMAT_DAY = d3TimeFormat.timeFormat('%a %d');
const FORMAT_WEEK = d3TimeFormat.timeFormat('%b %d');
const FORMAT_MONTH = d3TimeFormat.timeFormat('%b');
const FORMAT_YEAR = d3TimeFormat.timeFormat('%Y');
 
/**
 * formatDate
 *
 * This function was written to be used for tick formatting with d3 time
 * scales.
 *
 * @param {date} date - input date
 * @return {string} - formatted date
 */
export function formatDate(date) {
	return (d3Time.timeSecond(date) < date ? FORMAT_MILLISECOND
		: d3Time.timeMinute(date) < date ? FORMAT_SECOND
		: d3Time.timeHour(date) < date ? FORMAT_MINUTE
		: d3Time.timeDay(date) < date ? FORMAT_HOUR
		: d3Time.timeMonth(date) < date ? (d3Time.timeWeek(date) < date ? FORMAT_DAY : FORMAT_WEEK)
		: d3Time.timeYear(date) < date ? FORMAT_MONTH
		: FORMAT_YEAR)(date);
}