| 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);
}
|