@windborne/grapher 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +85 -0
- package/.idea/codeStyles/Project.xml +19 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/grapher.iml +12 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/0.bundle.js +2 -0
- package/0.bundle.js.map +1 -0
- package/1767282193a714f63082.module.wasm +0 -0
- package/537.bundle.js +2 -0
- package/537.bundle.js.map +1 -0
- package/831.bundle.js +2 -0
- package/831.bundle.js.map +1 -0
- package/bundle.js +2 -0
- package/bundle.js.map +1 -0
- package/package.json +75 -0
- package/readme.md +129 -0
- package/src/components/annotations.js +62 -0
- package/src/components/context_menu.js +73 -0
- package/src/components/draggable_points.js +114 -0
- package/src/components/graph_body.js +292 -0
- package/src/components/graph_title.js +16 -0
- package/src/components/options.js +111 -0
- package/src/components/percentile_button.js +72 -0
- package/src/components/range_graph.js +352 -0
- package/src/components/range_selection.js +175 -0
- package/src/components/range_selection_button.js +26 -0
- package/src/components/range_selection_button_base.js +51 -0
- package/src/components/series_key.js +235 -0
- package/src/components/series_key_axis_container.js +70 -0
- package/src/components/series_key_item.js +52 -0
- package/src/components/sidebar.js +76 -0
- package/src/components/tooltip.js +244 -0
- package/src/components/vertical_lines.js +70 -0
- package/src/components/x_axis.js +124 -0
- package/src/components/y_axis.js +239 -0
- package/src/eventable.js +65 -0
- package/src/grapher.js +367 -0
- package/src/grapher.scss +914 -0
- package/src/helpers/axis_sizes.js +2 -0
- package/src/helpers/binary_search.js +67 -0
- package/src/helpers/color_to_vector.js +35 -0
- package/src/helpers/colors.js +27 -0
- package/src/helpers/custom_prop_types.js +159 -0
- package/src/helpers/flatten_simple_data.js +81 -0
- package/src/helpers/format.js +233 -0
- package/src/helpers/generator_params_equal.js +10 -0
- package/src/helpers/name_for_series.js +16 -0
- package/src/helpers/place_grid.js +257 -0
- package/src/helpers/pyodide_ready.js +13 -0
- package/src/multigrapher.js +105 -0
- package/src/renderer/background.frag +7 -0
- package/src/renderer/background.vert +7 -0
- package/src/renderer/background_program.js +48 -0
- package/src/renderer/circle.frag +26 -0
- package/src/renderer/circle.vert +12 -0
- package/src/renderer/create_gl_program.js +36 -0
- package/src/renderer/draw_area.js +159 -0
- package/src/renderer/draw_background.js +15 -0
- package/src/renderer/draw_bars.js +80 -0
- package/src/renderer/draw_line.js +69 -0
- package/src/renderer/draw_zero_line.js +24 -0
- package/src/renderer/extract_vertices.js +137 -0
- package/src/renderer/graph_body_renderer.js +293 -0
- package/src/renderer/line.frag +51 -0
- package/src/renderer/line.vert +32 -0
- package/src/renderer/line_program.js +125 -0
- package/src/renderer/paths_from.js +72 -0
- package/src/renderer/scale_bounds.js +28 -0
- package/src/renderer/size_canvas.js +59 -0
- package/src/rust/Cargo.lock +233 -0
- package/src/rust/Cargo.toml +35 -0
- package/src/rust/pkg/grapher_rs.d.ts +42 -0
- package/src/rust/pkg/grapher_rs.js +351 -0
- package/src/rust/pkg/grapher_rs_bg.d.ts +11 -0
- package/src/rust/pkg/grapher_rs_bg.wasm +0 -0
- package/src/rust/pkg/index.js +342 -0
- package/src/rust/pkg/index_bg.wasm +0 -0
- package/src/rust/pkg/package.json +14 -0
- package/src/rust/src/extract_vertices.rs +83 -0
- package/src/rust/src/get_point_number.rs +50 -0
- package/src/rust/src/lib.rs +15 -0
- package/src/rust/src/selected_space_to_render_space.rs +131 -0
- package/src/state/average_loop_times.js +15 -0
- package/src/state/bound_calculator_from_selection.js +36 -0
- package/src/state/bound_calculators.js +41 -0
- package/src/state/calculate_annotations_state.js +59 -0
- package/src/state/calculate_data_bounds.js +104 -0
- package/src/state/calculate_tooltip_state.js +241 -0
- package/src/state/data_types.js +13 -0
- package/src/state/expand_bounds.js +58 -0
- package/src/state/find_matching_axis.js +31 -0
- package/src/state/get_default_bounds_calculator.js +15 -0
- package/src/state/hooks.js +164 -0
- package/src/state/infer_type.js +74 -0
- package/src/state/merge_bounds.js +64 -0
- package/src/state/multigraph_state_controller.js +334 -0
- package/src/state/selection_from_global_bounds.js +25 -0
- package/src/state/space_conversions/condense_data_space.js +115 -0
- package/src/state/space_conversions/data_space_to_selected_space.js +328 -0
- package/src/state/space_conversions/selected_space_to_background_space.js +144 -0
- package/src/state/space_conversions/selected_space_to_render_space.js +161 -0
- package/src/state/space_conversions/simple_series_to_data_space.js +229 -0
- package/src/state/state_controller.js +1770 -0
- package/src/state/sync_pool.js +101 -0
- package/test/setup.js +15 -0
- package/test/space_conversions/data_space_to_selected_space.test.js +434 -0
- package/webpack.dev.config.js +109 -0
- package/webpack.prod.config.js +60 -0
- package/webpack.test.config.js +59 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
/**
|
2
|
+
* Finds the closest point to the target
|
3
|
+
*
|
4
|
+
* @param {Array} data - the data, in data space
|
5
|
+
* @param {Number} targetX - the x coordinate to get closest to
|
6
|
+
* @param {Object} [searchParams]
|
7
|
+
* @param {String} [searchParams.searchType] - whether to search for the closest, one before, or one after
|
8
|
+
* @param {Boolean} [searchParams.returnIndex] - whether to return the index or the object itself
|
9
|
+
* @param {Number} [startIndex] - where to start the search from
|
10
|
+
* @param {Number} [endIndex] - where to end the search
|
11
|
+
* @return {Array|Number}
|
12
|
+
*/
|
13
|
+
export default function binarySearch(data, targetX, searchParams={}, startIndex=0, endIndex=undefined) {
|
14
|
+
if (endIndex === undefined) {
|
15
|
+
endIndex = data.length - 1;
|
16
|
+
}
|
17
|
+
|
18
|
+
if (data.length === 0) {
|
19
|
+
return searchParams.returnIndex ? -1 : [null, null];
|
20
|
+
}
|
21
|
+
|
22
|
+
const middleIndex = Math.floor((startIndex + endIndex)/2);
|
23
|
+
|
24
|
+
if (targetX === data[middleIndex][0] || (data[middleIndex][0] instanceof Date && data[middleIndex][0].valueOf() === targetX)) {
|
25
|
+
if (searchParams.returnIndex) {
|
26
|
+
return middleIndex;
|
27
|
+
} else {
|
28
|
+
return data[middleIndex];
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
if (startIndex === endIndex) {
|
33
|
+
if (data[startIndex][0] < targetX && searchParams.searchType === 'before') {
|
34
|
+
return searchParams.returnIndex ? startIndex : data[startIndex];
|
35
|
+
} else if (data[startIndex][0] > targetX && searchParams.searchType === 'after') {
|
36
|
+
return searchParams.returnIndex ? startIndex : data[startIndex];
|
37
|
+
} else {
|
38
|
+
return searchParams.returnIndex ? -1 : [null, null];
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
if (endIndex - 1 === startIndex) {
|
43
|
+
let index;
|
44
|
+
|
45
|
+
if (searchParams.searchType === 'before') {
|
46
|
+
index = startIndex;
|
47
|
+
// index = (targetX <= data[startIndex][0]) ? startIndex : endIndex;
|
48
|
+
} else if (searchParams.searchType === 'after') {
|
49
|
+
index = endIndex;
|
50
|
+
// index = (targetX >= data[endIndex][0]) ? endIndex : startIndex;
|
51
|
+
} else {
|
52
|
+
index = Math.abs(data[startIndex][0] - targetX) > Math.abs(data[endIndex][0] - targetX) ?
|
53
|
+
endIndex :
|
54
|
+
startIndex;
|
55
|
+
}
|
56
|
+
|
57
|
+
return searchParams.returnIndex ? index : data[index];
|
58
|
+
}
|
59
|
+
|
60
|
+
if (targetX > data[middleIndex][0]) {
|
61
|
+
return binarySearch(data, targetX, searchParams, middleIndex, endIndex);
|
62
|
+
}
|
63
|
+
|
64
|
+
if (targetX < data[middleIndex][0]) {
|
65
|
+
return binarySearch(data, targetX, searchParams, startIndex, middleIndex);
|
66
|
+
}
|
67
|
+
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
/**
|
2
|
+
* Returns the color as a [r, g, b, a] array with domain 0 to 1 for use in webgl
|
3
|
+
*
|
4
|
+
* @param color
|
5
|
+
* @return {[number, number, number, number]|[number, number, number, number]}
|
6
|
+
*/
|
7
|
+
export default function colorToVector(color) {
|
8
|
+
if (color === 'black') {
|
9
|
+
color = '#000000';
|
10
|
+
} else if (color === 'white') {
|
11
|
+
color = '#FFFFFF';
|
12
|
+
} else if (color === 'transparent') {
|
13
|
+
return [0, 0, 0, 0];
|
14
|
+
}
|
15
|
+
|
16
|
+
if (color.startsWith('rgb')) {
|
17
|
+
const parts = color.split(',').map((part) => parseFloat(part.match(/\d+(\.\d+)?/)[0]));
|
18
|
+
return [
|
19
|
+
parts[0]/255,
|
20
|
+
parts[1]/255,
|
21
|
+
parts[2]/255,
|
22
|
+
parts.length >= 4 ? parts[3] : 1
|
23
|
+
];
|
24
|
+
}
|
25
|
+
|
26
|
+
if (typeof color !== 'string' || !/^#[\dA-F]{6}$/i.test(color)) {
|
27
|
+
throw new Error(`Color must be a hex string: ${color}`);
|
28
|
+
}
|
29
|
+
|
30
|
+
const r = parseInt(color.substr(1, 2), 16)/255;
|
31
|
+
const g = parseInt(color.substr(3, 2), 16)/255;
|
32
|
+
const b = parseInt(color.substr(5, 2), 16)/255;
|
33
|
+
const a = 1.0;
|
34
|
+
return [r, g, b, a];
|
35
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
export const LINE_COLORS = [
|
2
|
+
'#F1C232',
|
3
|
+
'#1259f8',
|
4
|
+
'#cb4b4b',
|
5
|
+
'#4da74d',
|
6
|
+
'#9440ed',
|
7
|
+
'#61e0ed',
|
8
|
+
'#ed6d2c',
|
9
|
+
'#ed13c6',
|
10
|
+
'#bbed59'
|
11
|
+
];
|
12
|
+
|
13
|
+
export default function getColor(seriesColor, i, multigrapherSeriesIndex) {
|
14
|
+
if (typeof seriesColor === 'string') {
|
15
|
+
return seriesColor;
|
16
|
+
}
|
17
|
+
|
18
|
+
if (typeof seriesColor === 'number') {
|
19
|
+
return LINE_COLORS[seriesColor % LINE_COLORS.length];
|
20
|
+
}
|
21
|
+
|
22
|
+
if (multigrapherSeriesIndex !== undefined) {
|
23
|
+
return LINE_COLORS[multigrapherSeriesIndex % LINE_COLORS.length];
|
24
|
+
}
|
25
|
+
|
26
|
+
return LINE_COLORS[i % LINE_COLORS.length];
|
27
|
+
}
|
@@ -0,0 +1,159 @@
|
|
1
|
+
import PropTypes from 'prop-types';
|
2
|
+
import {DATA_TYPES} from '../state/data_types';
|
3
|
+
|
4
|
+
// checking the shape of data is slow if there's a lot of it
|
5
|
+
// it can be convenient in development, but should not be done when testing high cardinality data
|
6
|
+
const ENABLE_DATA_CHECK = false;
|
7
|
+
|
8
|
+
const PointY = function (props, propName) {
|
9
|
+
if (typeof props[propName] !== 'number' && props[propName] !== null) {
|
10
|
+
return new Error(`${propName} needs to be a number or null`);
|
11
|
+
}
|
12
|
+
|
13
|
+
return null;
|
14
|
+
};
|
15
|
+
|
16
|
+
const PointTuple = function (props, propName) {
|
17
|
+
if (!Array.isArray(props) || props.length !== 2) { // eslint-disable-line react/prop-types
|
18
|
+
return new Error(`${propName} needs to be an array of length two`);
|
19
|
+
}
|
20
|
+
|
21
|
+
const [x, y] = props;
|
22
|
+
if (typeof x !== 'number' && !(x instanceof Date)) {
|
23
|
+
return new Error(`${propName}.x needs to be a number or date`);
|
24
|
+
}
|
25
|
+
|
26
|
+
if (typeof y !== 'number' && y !== null) {
|
27
|
+
return new Error(`${propName}.y needs to be a number or null`);
|
28
|
+
}
|
29
|
+
|
30
|
+
return null;
|
31
|
+
};
|
32
|
+
|
33
|
+
const Data = ENABLE_DATA_CHECK ? PropTypes.oneOfType([
|
34
|
+
PropTypes.arrayOf(PointY), // values
|
35
|
+
PropTypes.arrayOf(PropTypes.arrayOf(PointTuple)), // tuples
|
36
|
+
PropTypes.arrayOf(PropTypes.object), // objects
|
37
|
+
PropTypes.shape({ observe: PropTypes.func.isRequired }), // observable
|
38
|
+
PropTypes.func // generator function
|
39
|
+
]) : PropTypes.any;
|
40
|
+
|
41
|
+
const SingleSeries = PropTypes.shape({
|
42
|
+
data: Data.isRequired,
|
43
|
+
type: PropTypes.oneOf([
|
44
|
+
...DATA_TYPES,
|
45
|
+
'infer'
|
46
|
+
]),
|
47
|
+
xKey: PropTypes.string,
|
48
|
+
yKey: PropTypes.string,
|
49
|
+
xUnixDates: PropTypes.bool,
|
50
|
+
color: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
51
|
+
name: PropTypes.string,
|
52
|
+
xLabel: PropTypes.string,
|
53
|
+
yLabel: PropTypes.string,
|
54
|
+
ignoreDiscontinuities: PropTypes.bool,
|
55
|
+
dashed: PropTypes.bool,
|
56
|
+
dashPattern: PropTypes.arrayOf(PropTypes.number),
|
57
|
+
width: PropTypes.number,
|
58
|
+
rangeSelectorWidth: PropTypes.number,
|
59
|
+
axis: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
60
|
+
expandYWith: PropTypes.arrayOf(PropTypes.number),
|
61
|
+
defaultAlwaysTooltipped: PropTypes.bool,
|
62
|
+
square: PropTypes.bool,
|
63
|
+
shiftXBy: PropTypes.number,
|
64
|
+
graph: PropTypes.number, // affects multigrapher only
|
65
|
+
background: PropTypes.object,
|
66
|
+
hideFromKey: PropTypes.bool,
|
67
|
+
showIndividualPoints: PropTypes.bool,
|
68
|
+
rendering: PropTypes.oneOf(['line', 'bar', 'area']), // defaults to line
|
69
|
+
negativeColor: PropTypes.string, // only applies to bar
|
70
|
+
gradient: PropTypes.array, // only applies to area
|
71
|
+
zeroLineWidth: PropTypes.number, // only applies to bar and area
|
72
|
+
zeroLineColor: PropTypes.string, // only applies to bar and area
|
73
|
+
pointRadius: PropTypes.number // only applies to area
|
74
|
+
});
|
75
|
+
|
76
|
+
const Series = PropTypes.arrayOf(SingleSeries);
|
77
|
+
|
78
|
+
const Axis = PropTypes.shape({
|
79
|
+
axisIndex: PropTypes.number.isRequired,
|
80
|
+
series: PropTypes.array.isRequired,
|
81
|
+
side: PropTypes.oneOf(['left', 'right']).isRequired,
|
82
|
+
scale: PropTypes.oneOf(['linear', 'log']).isRequired,
|
83
|
+
label: PropTypes.string
|
84
|
+
});
|
85
|
+
|
86
|
+
const Axes = PropTypes.arrayOf(Axis);
|
87
|
+
|
88
|
+
const CustomBoundsSelector = PropTypes.shape({
|
89
|
+
label: PropTypes.string.isRequired,
|
90
|
+
calculator: PropTypes.func.isRequired,
|
91
|
+
datesOnly: PropTypes.bool
|
92
|
+
});
|
93
|
+
|
94
|
+
const CustomBoundsSelectors = PropTypes.arrayOf(CustomBoundsSelector);
|
95
|
+
|
96
|
+
const TooltipOptionsRaw = {
|
97
|
+
includeSeriesLabel: PropTypes.bool,
|
98
|
+
includeXLabel: PropTypes.bool,
|
99
|
+
includeYLabel: PropTypes.bool,
|
100
|
+
includeXValue: PropTypes.bool,
|
101
|
+
includeYValue: PropTypes.bool,
|
102
|
+
floating: PropTypes.bool,
|
103
|
+
alwaysFixedPosition: PropTypes.bool,
|
104
|
+
floatPosition: PropTypes.oneOf(['top', 'bottom']),
|
105
|
+
floatDelta: PropTypes.number,
|
106
|
+
savingDisabled: PropTypes.bool
|
107
|
+
};
|
108
|
+
|
109
|
+
const TooltipOptions = PropTypes.shape(TooltipOptionsRaw);
|
110
|
+
|
111
|
+
const Annotation = PropTypes.shape({
|
112
|
+
x: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date)]).isRequired,
|
113
|
+
xEnd: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date)]),
|
114
|
+
series: PropTypes.arrayOf(PropTypes.string),
|
115
|
+
content: PropTypes.string.isRequired
|
116
|
+
});
|
117
|
+
const Annotations = PropTypes.arrayOf(Annotation);
|
118
|
+
|
119
|
+
const DraggablePoint = PropTypes.shape({
|
120
|
+
x: PropTypes.number.isRequired,
|
121
|
+
y: PropTypes.number.isRequired,
|
122
|
+
radius: PropTypes.number,
|
123
|
+
fillColor: PropTypes.string,
|
124
|
+
strokeColor: PropTypes.string,
|
125
|
+
strokeWidth: PropTypes.number,
|
126
|
+
onClick: PropTypes.func,
|
127
|
+
onDoubleClick: PropTypes.func
|
128
|
+
});
|
129
|
+
const DraggablePoints = PropTypes.arrayOf(DraggablePoint);
|
130
|
+
|
131
|
+
const VerticalLine = PropTypes.shape({
|
132
|
+
x: PropTypes.number.isRequired,
|
133
|
+
color: PropTypes.string,
|
134
|
+
width: PropTypes.number,
|
135
|
+
markTop: PropTypes.bool,
|
136
|
+
style: PropTypes.object,
|
137
|
+
markerStyle: PropTypes.object
|
138
|
+
});
|
139
|
+
const VerticalLines = PropTypes.arrayOf(VerticalLine);
|
140
|
+
|
141
|
+
const CustomPropTypes = {
|
142
|
+
Data,
|
143
|
+
SingleSeries,
|
144
|
+
Series,
|
145
|
+
Axis,
|
146
|
+
Axes,
|
147
|
+
CustomBoundsSelector,
|
148
|
+
CustomBoundsSelectors,
|
149
|
+
TooltipOptions,
|
150
|
+
TooltipOptionsRaw,
|
151
|
+
Annotations,
|
152
|
+
DraggablePoint,
|
153
|
+
DraggablePoints,
|
154
|
+
VerticalLine,
|
155
|
+
VerticalLines
|
156
|
+
};
|
157
|
+
|
158
|
+
|
159
|
+
export default CustomPropTypes;
|
@@ -0,0 +1,81 @@
|
|
1
|
+
/**
|
2
|
+
* Flattens simple data
|
3
|
+
* Returns an array of tuples (x value, simple point)
|
4
|
+
*
|
5
|
+
* @param simpleData
|
6
|
+
* @param series
|
7
|
+
* @param inDataSpace
|
8
|
+
* @return {[]}
|
9
|
+
*/
|
10
|
+
export default function flattenSimpleData(simpleData, {series, inDataSpace }) {
|
11
|
+
const flattened = [];
|
12
|
+
let fI = 0;
|
13
|
+
|
14
|
+
for (let point of simpleData) {
|
15
|
+
const yValue = extractYValue(point, series);
|
16
|
+
|
17
|
+
if (series.yKey && Array.isArray(yValue)) {
|
18
|
+
for (let subpoint of point[series.yKey]) {
|
19
|
+
if (series.ignoreDiscontinuities && typeof subpoint[series.yKey] !== 'number' && !Array.isArray(subpoint)) {
|
20
|
+
continue;
|
21
|
+
}
|
22
|
+
|
23
|
+
const x = inDataSpace[fI++][0];
|
24
|
+
|
25
|
+
if (series.square && flattened.length > 0) {
|
26
|
+
flattened.push([x, flattened[flattened.length - 1][1]]);
|
27
|
+
}
|
28
|
+
|
29
|
+
flattened.push([x, subpoint]);
|
30
|
+
}
|
31
|
+
|
32
|
+
continue;
|
33
|
+
}
|
34
|
+
|
35
|
+
if (series.ignoreDiscontinuities && (yValue === undefined || yValue === null)) {
|
36
|
+
continue;
|
37
|
+
}
|
38
|
+
|
39
|
+
const x = inDataSpace[fI++][0];
|
40
|
+
|
41
|
+
if (series.square && flattened.length > 0) {
|
42
|
+
flattened.push([x, flattened[flattened.length - 1][1]]);
|
43
|
+
}
|
44
|
+
|
45
|
+
flattened.push([x, point]);
|
46
|
+
}
|
47
|
+
|
48
|
+
if (flattened.length !== inDataSpace.length) {
|
49
|
+
console.warn('Flattening didn\'t give the same length as it has in data space'); // eslint-disable-line no-console
|
50
|
+
}
|
51
|
+
|
52
|
+
return flattened;
|
53
|
+
}
|
54
|
+
|
55
|
+
export function extractYValue(point, series) {
|
56
|
+
if (series.yKey) {
|
57
|
+
return point[series.yKey];
|
58
|
+
}
|
59
|
+
|
60
|
+
if (Array.isArray(point)) {
|
61
|
+
if (point.length === 1) {
|
62
|
+
return point[0];
|
63
|
+
} else {
|
64
|
+
return point[1];
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
return point;
|
69
|
+
}
|
70
|
+
|
71
|
+
export function extractXValue(point, series) {
|
72
|
+
if (series.xKey) {
|
73
|
+
return point[series.xKey];
|
74
|
+
}
|
75
|
+
|
76
|
+
if (Array.isArray(point)) {
|
77
|
+
return point[0];
|
78
|
+
}
|
79
|
+
|
80
|
+
return point;
|
81
|
+
}
|
@@ -0,0 +1,233 @@
|
|
1
|
+
export function calculatePrecision(value) {
|
2
|
+
return Math.max(-Math.log10(Math.abs(value)) + 2, 0);
|
3
|
+
}
|
4
|
+
|
5
|
+
export function calculateTimePrecision(minDate, maxDate) {
|
6
|
+
minDate = new Date(minDate);
|
7
|
+
maxDate = new Date(maxDate);
|
8
|
+
|
9
|
+
const range = maxDate.valueOf() - minDate.valueOf();
|
10
|
+
if (range < 3*1000) {
|
11
|
+
return 'ms';
|
12
|
+
} else if (range < 10*60*1000) {
|
13
|
+
return 's';
|
14
|
+
} else {
|
15
|
+
return 'm';
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
export function roundToPrecision(value, precision=null) {
|
20
|
+
if (precision === null) {
|
21
|
+
precision = calculatePrecision(value);
|
22
|
+
}
|
23
|
+
|
24
|
+
if (isNaN(precision) || precision > 100) {
|
25
|
+
return value.toString();
|
26
|
+
}
|
27
|
+
|
28
|
+
const rounded = value.toFixed(Math.ceil(precision));
|
29
|
+
let stripped = rounded;
|
30
|
+
if (rounded.includes('.')) {
|
31
|
+
stripped = stripped.replace(/\.?0+$/g, '');
|
32
|
+
}
|
33
|
+
|
34
|
+
if (stripped === '') {
|
35
|
+
return '0';
|
36
|
+
}
|
37
|
+
|
38
|
+
return stripped;
|
39
|
+
}
|
40
|
+
|
41
|
+
const DATE_TIME_FORMATTERS = {};
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Given a timezone string, gets the offset relative to utc in milliseconds
|
45
|
+
* For example, America/New_York in winter is GMT-05:00, so it evaluates to -5*60*60*1000
|
46
|
+
*
|
47
|
+
* @param {String} timeZone - the time zone string
|
48
|
+
* @param {Date} sampleDate - a date to use in the conversions, since it can be time-of-year dependent with Daylight Savings Time
|
49
|
+
* @return {number|null}
|
50
|
+
*/
|
51
|
+
function timezoneToOffsetMS(timeZone, sampleDate) {
|
52
|
+
try { // formats are finicky, so give up rather than abort rendering
|
53
|
+
let datetimeFormatter = DATE_TIME_FORMATTERS[timeZone];
|
54
|
+
if (!datetimeFormatter) {
|
55
|
+
let properTimeZone = timeZone;
|
56
|
+
if (!timeZone || timeZone === 'local') {
|
57
|
+
properTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
58
|
+
}
|
59
|
+
|
60
|
+
datetimeFormatter = new Intl.DateTimeFormat('en-US', {
|
61
|
+
timeZone: properTimeZone,
|
62
|
+
timeZoneName: 'longOffset'
|
63
|
+
});
|
64
|
+
DATE_TIME_FORMATTERS[timeZone] = datetimeFormatter;
|
65
|
+
}
|
66
|
+
|
67
|
+
const parts = datetimeFormatter.formatToParts(sampleDate);
|
68
|
+
const offsetPart = parts.find(part => part.type === 'timeZoneName');
|
69
|
+
|
70
|
+
if (!offsetPart) {
|
71
|
+
return null;
|
72
|
+
}
|
73
|
+
|
74
|
+
if (offsetPart.value === 'GMT') {
|
75
|
+
return 0;
|
76
|
+
}
|
77
|
+
|
78
|
+
if (!/^GMT[+-]\d{2}:\d{2}$/.test(offsetPart.value)) {
|
79
|
+
return null;
|
80
|
+
}
|
81
|
+
|
82
|
+
const [hours, minutes] = offsetPart.value.slice(3).split(':');
|
83
|
+
|
84
|
+
return parseInt(hours)*60*60*1000 + parseInt(minutes)*60*1000;
|
85
|
+
} catch (e) {
|
86
|
+
console.error(new Error(`Could not parse timezone offset for ${sampleDate} in ${timeZone}`)); // eslint-disable-line no-console
|
87
|
+
console.error(e); // eslint-disable-line no-console
|
88
|
+
return null;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
/**
|
93
|
+
* Gets the Date object that represents the start of day in a given timezone
|
94
|
+
* Note that this is still a native date object, so it will be in the local timezone
|
95
|
+
* Its timestamp, however, will correspond to the start of the day in the given timezone
|
96
|
+
*
|
97
|
+
* @param date
|
98
|
+
* @param timezone
|
99
|
+
* @return {Date}
|
100
|
+
*/
|
101
|
+
export function startOfDayInTimezone(date, timezone) {
|
102
|
+
if (!timezone) {
|
103
|
+
const startOfDay = new Date(date);
|
104
|
+
startOfDay.setHours(0, 0, 0, 0);
|
105
|
+
return startOfDay;
|
106
|
+
}
|
107
|
+
|
108
|
+
const offset = timezoneToOffsetMS(timezone, date); // ms between timezone and utc
|
109
|
+
let startOfDay = new Date(date);
|
110
|
+
startOfDay.setUTCHours(0, 0, 0, 0);
|
111
|
+
|
112
|
+
const difference = startOfDay.valueOf() - date.valueOf();
|
113
|
+
|
114
|
+
// if we would have gone forward a day when offset is taken into account, we need to go back a day again
|
115
|
+
if (difference > offset) {
|
116
|
+
startOfDay = new Date(startOfDay.valueOf() - 24*60*60*1000);
|
117
|
+
}
|
118
|
+
|
119
|
+
return new Date(startOfDay.valueOf() - offset);
|
120
|
+
}
|
121
|
+
|
122
|
+
function formatTime(time, {precision, justTime, justDate, justMonthAndDay, unitOverride, clockStyle='24h', timeZone}) {
|
123
|
+
const utc = timeZone && timeZone.toLowerCase() === 'utc';
|
124
|
+
if (timeZone && !utc && timeZone !== 'local' && window.Intl && window.Intl.DateTimeFormat) {
|
125
|
+
const offset = timezoneToOffsetMS(timeZone, time);
|
126
|
+
const localOffset = timezoneToOffsetMS('local', time);
|
127
|
+
|
128
|
+
if (typeof offset === 'number' && typeof localOffset === 'number') {
|
129
|
+
time = new Date(time.valueOf() + offset - localOffset);
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
const year = utc ? time.getUTCFullYear() : time.getFullYear();
|
134
|
+
const month = (utc ? time.getUTCMonth() : time.getMonth()) + 1;
|
135
|
+
const day = utc ? time.getUTCDate() : time.getDate();
|
136
|
+
|
137
|
+
if (unitOverride === 'year') {
|
138
|
+
return year.toString();
|
139
|
+
}
|
140
|
+
|
141
|
+
if (justDate) {
|
142
|
+
return utc ? `${month}/${day}/${year}` : time.toLocaleDateString();
|
143
|
+
}
|
144
|
+
|
145
|
+
if (justMonthAndDay) {
|
146
|
+
// eg Jan 19
|
147
|
+
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
148
|
+
return `${monthNames[month - 1]} ${day}`;
|
149
|
+
}
|
150
|
+
|
151
|
+
const hours = utc ? time.getUTCHours() : time.getHours();
|
152
|
+
const minutes = utc ? time.getUTCMinutes() : time.getMinutes();
|
153
|
+
const seconds = utc ? time.getUTCSeconds() : time.getSeconds();
|
154
|
+
const milliseconds = utc ? time.getUTCMilliseconds() : time.getMilliseconds();
|
155
|
+
|
156
|
+
let timeString;
|
157
|
+
|
158
|
+
if (clockStyle === '12h') {
|
159
|
+
timeString = `${((hours + 11) % 12 + 1).toString()}:${minutes.toString().padStart(2, '0')}`;
|
160
|
+
} else {
|
161
|
+
timeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
162
|
+
}
|
163
|
+
|
164
|
+
if (precision === 's' || precision === 'ms') {
|
165
|
+
timeString += `:${seconds.toString().padStart(2, '0')}`;
|
166
|
+
}
|
167
|
+
|
168
|
+
if (precision === 'ms') {
|
169
|
+
timeString += `.${milliseconds.toString().padStart(3, '0')}`;
|
170
|
+
}
|
171
|
+
|
172
|
+
if (clockStyle === '12h') {
|
173
|
+
timeString += hours >= 12 ? 'pm' : 'am';
|
174
|
+
}
|
175
|
+
|
176
|
+
if (justTime) {
|
177
|
+
return timeString;
|
178
|
+
}
|
179
|
+
|
180
|
+
if (utc) {
|
181
|
+
timeString += ' UTC';
|
182
|
+
}
|
183
|
+
|
184
|
+
return (utc ? `${month}/${day}/${year}` : time.toLocaleDateString()) + ' ' + timeString;
|
185
|
+
}
|
186
|
+
|
187
|
+
export function formatX(x, {dates=false, precision=null, justTime=false, justDate=false, justMonthAndDay=false, clockStyle='24h', unitOverride, timeZone, integersOnly=false, inverseEnumMap}={}) {
|
188
|
+
if (dates && !(x instanceof Date)) {
|
189
|
+
x = new Date(x);
|
190
|
+
|
191
|
+
if (isNaN(x)) {
|
192
|
+
return 'Invalid Date';
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
if (x instanceof Date) {
|
197
|
+
return formatTime(x, {precision, justTime, justDate, justMonthAndDay, unitOverride, clockStyle, timeZone});
|
198
|
+
}
|
199
|
+
|
200
|
+
if (isNaN(x)) {
|
201
|
+
return 'NaN';
|
202
|
+
}
|
203
|
+
|
204
|
+
if (inverseEnumMap) {
|
205
|
+
if (Math.abs(x - Math.round(x)) > 1e-10) {
|
206
|
+
return '';
|
207
|
+
}
|
208
|
+
|
209
|
+
return inverseEnumMap[Math.round(x)];
|
210
|
+
}
|
211
|
+
|
212
|
+
if (integersOnly && Math.abs(x - Math.round(x)) > 1e-10) {
|
213
|
+
return '';
|
214
|
+
}
|
215
|
+
|
216
|
+
return roundToPrecision(x, precision);
|
217
|
+
}
|
218
|
+
|
219
|
+
export function formatY(y, {precision=null, log=false}={}) {
|
220
|
+
if (y === null) {
|
221
|
+
return 'null';
|
222
|
+
}
|
223
|
+
|
224
|
+
if (isNaN(y)) {
|
225
|
+
return 'NaN';
|
226
|
+
}
|
227
|
+
|
228
|
+
if (log) {
|
229
|
+
return `10^${roundToPrecision(Math.log10(y), precision)}`;
|
230
|
+
}
|
231
|
+
|
232
|
+
return roundToPrecision(y, precision);
|
233
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
export default function generatorParamsEqual(a, b) {
|
2
|
+
if (a === undefined || b === undefined) {
|
3
|
+
return a === b;
|
4
|
+
}
|
5
|
+
|
6
|
+
return (a.minX === b.minX) &&
|
7
|
+
(a.maxX === b.maxX) &&
|
8
|
+
(a.sizing.elementWidth === b.sizing.elementWidth) &&
|
9
|
+
(a.sizing.renderWidth === b.sizing.renderWidth);
|
10
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
/**
|
2
|
+
* Gets the proper name for a series
|
3
|
+
*
|
4
|
+
* @param {Object} singleSeries
|
5
|
+
* @param {Number} index
|
6
|
+
* @return {string}
|
7
|
+
*/
|
8
|
+
export default function nameForSeries(singleSeries, index) {
|
9
|
+
let name = singleSeries.name || singleSeries.yKey;
|
10
|
+
|
11
|
+
if (!name) {
|
12
|
+
name = index.toString();
|
13
|
+
}
|
14
|
+
|
15
|
+
return name;
|
16
|
+
}
|