@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,235 @@
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import {Y_AXIS_WIDTH} from '../helpers/axis_sizes';
|
4
|
+
import StateController from '../state/state_controller';
|
5
|
+
import {
|
6
|
+
useAlwaysTooltipped,
|
7
|
+
useAxes,
|
8
|
+
useHighlightedSeries,
|
9
|
+
useLeftAxisCount,
|
10
|
+
usePrimarySize,
|
11
|
+
useRightAxisCount,
|
12
|
+
useSeries, useShowingOptions
|
13
|
+
} from '../state/hooks';
|
14
|
+
import SeriesKeyItem from './series_key_item';
|
15
|
+
import SeriesKeyAxisContainer from './series_key_axis_container';
|
16
|
+
|
17
|
+
function calculateStyles({stateController, keyWidth}) {
|
18
|
+
const rightAxisCount = useRightAxisCount(stateController);
|
19
|
+
const leftAxisCount = useLeftAxisCount(stateController);
|
20
|
+
const showingOptions = useShowingOptions(stateController);
|
21
|
+
|
22
|
+
let marginRight = Y_AXIS_WIDTH*rightAxisCount;
|
23
|
+
if (rightAxisCount > 0) {
|
24
|
+
marginRight += 5;
|
25
|
+
}
|
26
|
+
|
27
|
+
marginRight += 250;
|
28
|
+
|
29
|
+
if (showingOptions) {
|
30
|
+
marginRight += 70;
|
31
|
+
}
|
32
|
+
|
33
|
+
let marginLeft = Y_AXIS_WIDTH*leftAxisCount;
|
34
|
+
|
35
|
+
if (leftAxisCount > 0) {
|
36
|
+
marginLeft += 5;
|
37
|
+
}
|
38
|
+
|
39
|
+
marginLeft = Math.max(marginRight, marginLeft);
|
40
|
+
marginRight = Math.max(marginRight, marginLeft);
|
41
|
+
|
42
|
+
let marginBottom;
|
43
|
+
|
44
|
+
const { elementWidth } = usePrimarySize(stateController);
|
45
|
+
|
46
|
+
const shouldBreak = elementWidth - marginLeft - marginRight < keyWidth;
|
47
|
+
|
48
|
+
if (shouldBreak) {
|
49
|
+
marginLeft = 0;
|
50
|
+
marginRight = 0;
|
51
|
+
marginBottom = 10;
|
52
|
+
}
|
53
|
+
|
54
|
+
return { marginRight, marginLeft, marginBottom };
|
55
|
+
}
|
56
|
+
|
57
|
+
export default React.memo(SeriesKey);
|
58
|
+
|
59
|
+
function SeriesKey({ stateController, theme, draggingY, grapherID, dragPositionYOffset=0 }) {
|
60
|
+
const series = useSeries(stateController);
|
61
|
+
const [draggedSeries, setDraggedSeries] = useState(null);
|
62
|
+
const [dragPosition, setDragPosition] = useState({
|
63
|
+
x: 0,
|
64
|
+
y: 0
|
65
|
+
});
|
66
|
+
|
67
|
+
const highlightedSeries = useHighlightedSeries(stateController);
|
68
|
+
|
69
|
+
const keyRef = useRef(null);
|
70
|
+
const [keyWidth, setKeyWidth] = useState(series.map(({ name, yKey }, i) => (name || yKey || i).toString().length).reduce((a, b) => a + b, 0) * 5);
|
71
|
+
|
72
|
+
let sizeCalculationHandle;
|
73
|
+
useEffect(() => {
|
74
|
+
if (keyRef.current) {
|
75
|
+
cancelAnimationFrame(sizeCalculationHandle);
|
76
|
+
|
77
|
+
sizeCalculationHandle = requestAnimationFrame(() => {
|
78
|
+
if (!keyRef.current) { // has become invalid in the last frame
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
|
82
|
+
const width = [...keyRef.current.querySelectorAll('.series-key-axis-container')]
|
83
|
+
.map((el) => el.clientWidth)
|
84
|
+
.reduce((a, b) => a + b, 0);
|
85
|
+
setKeyWidth(width);
|
86
|
+
});
|
87
|
+
}
|
88
|
+
}, [series, keyRef.current]);
|
89
|
+
|
90
|
+
const style = calculateStyles({ stateController, keyWidth });
|
91
|
+
|
92
|
+
const axes = useAxes(stateController);
|
93
|
+
const alwaysTooltipped = useAlwaysTooltipped(stateController);
|
94
|
+
|
95
|
+
const startDragging = (event, singleSeries) => {
|
96
|
+
let seriesKeyEl = event.target;
|
97
|
+
while (seriesKeyEl && seriesKeyEl.className !== 'series-key') {
|
98
|
+
seriesKeyEl = seriesKeyEl.parentNode;
|
99
|
+
}
|
100
|
+
const targetLeft = event.target.getBoundingClientRect().left;
|
101
|
+
let seriesKeyLeft = seriesKeyEl.getBoundingClientRect().left;
|
102
|
+
let seriesKeyMarginLeft = seriesKeyEl.style.marginLeft;
|
103
|
+
|
104
|
+
const startX = event.clientX;
|
105
|
+
const startY = event.clientY;
|
106
|
+
const clientStartX = event.clientX;
|
107
|
+
const clientStartY = event.clientY;
|
108
|
+
|
109
|
+
setDraggedSeries(singleSeries);
|
110
|
+
setDragPosition({
|
111
|
+
x: event.clientX - startX + (targetLeft - seriesKeyLeft) - 2,
|
112
|
+
y: event.clientY - startY + 1
|
113
|
+
});
|
114
|
+
|
115
|
+
const onMouseMove = (moveEvent) => {
|
116
|
+
if (seriesKeyEl.style.marginLeft !== seriesKeyMarginLeft) {
|
117
|
+
seriesKeyLeft = seriesKeyEl.getBoundingClientRect().left;
|
118
|
+
seriesKeyMarginLeft = seriesKeyEl.style.marginLeft;
|
119
|
+
}
|
120
|
+
|
121
|
+
setDragPosition({
|
122
|
+
x: moveEvent.clientX - startX + (targetLeft - seriesKeyLeft) - 2,
|
123
|
+
y: moveEvent.clientY - startY + 1
|
124
|
+
});
|
125
|
+
};
|
126
|
+
|
127
|
+
const onMouseUp = (mouseUpEvent) => {
|
128
|
+
window.removeEventListener('mousemove', onMouseMove);
|
129
|
+
window.removeEventListener('mouseup', onMouseUp);
|
130
|
+
|
131
|
+
let target = mouseUpEvent.target;
|
132
|
+
while (target && !(target.dataset || {}).axisIndex && !(target.dataset || {}).grapherId) {
|
133
|
+
target = target.parentNode;
|
134
|
+
}
|
135
|
+
|
136
|
+
setDraggedSeries(null);
|
137
|
+
stateController.finalizeDrag(singleSeries, target && (target.dataset || {}).axisIndex, target && (target.dataset || {}).grapherId);
|
138
|
+
|
139
|
+
if (mouseUpEvent.clientX === clientStartX && mouseUpEvent.clientY === clientStartY) {
|
140
|
+
stateController.toggleAlwaysTooltipped(singleSeries, mouseUpEvent.shiftKey);
|
141
|
+
}
|
142
|
+
};
|
143
|
+
|
144
|
+
window.addEventListener('mousemove', onMouseMove);
|
145
|
+
window.addEventListener('mouseup', onMouseUp);
|
146
|
+
|
147
|
+
stateController.markDragStart();
|
148
|
+
};
|
149
|
+
|
150
|
+
return (
|
151
|
+
<div className="series-key" style={style} ref={keyRef}>
|
152
|
+
{
|
153
|
+
draggingY &&
|
154
|
+
<div
|
155
|
+
className="series-key-axis-container"
|
156
|
+
data-axis-index="new-left"
|
157
|
+
data-grapher-id={grapherID}
|
158
|
+
/>
|
159
|
+
}
|
160
|
+
|
161
|
+
{
|
162
|
+
axes.map((axis, i) => {
|
163
|
+
if (!axis.series.length && axes.length > 1) {
|
164
|
+
return null;
|
165
|
+
}
|
166
|
+
|
167
|
+
return (
|
168
|
+
<SeriesKeyAxisContainer
|
169
|
+
key={i}
|
170
|
+
label={axis.label}
|
171
|
+
axisIndex={axis.axisIndex}
|
172
|
+
scale={axis.scale}
|
173
|
+
stateController={stateController}
|
174
|
+
grapherID={grapherID}
|
175
|
+
>
|
176
|
+
{
|
177
|
+
axis.series.map((singleSeries) => {
|
178
|
+
if (singleSeries.hideFromKey) {
|
179
|
+
return null;
|
180
|
+
}
|
181
|
+
|
182
|
+
return (
|
183
|
+
<SeriesKeyItem
|
184
|
+
key={singleSeries.index}
|
185
|
+
series={singleSeries}
|
186
|
+
i={singleSeries.index}
|
187
|
+
onMouseDown={(event, toggleTooltipped) => startDragging(event, singleSeries, toggleTooltipped)}
|
188
|
+
theme={theme}
|
189
|
+
stateController={stateController}
|
190
|
+
highlighted={highlightedSeries === singleSeries.index || alwaysTooltipped.has(singleSeries)}
|
191
|
+
/>
|
192
|
+
);
|
193
|
+
})
|
194
|
+
}
|
195
|
+
</SeriesKeyAxisContainer>
|
196
|
+
);
|
197
|
+
})
|
198
|
+
}
|
199
|
+
|
200
|
+
{
|
201
|
+
draggingY &&
|
202
|
+
<div
|
203
|
+
className="series-key-axis-container"
|
204
|
+
data-axis-index="new-right"
|
205
|
+
data-grapher-id={grapherID}
|
206
|
+
/>
|
207
|
+
}
|
208
|
+
|
209
|
+
{
|
210
|
+
draggedSeries &&
|
211
|
+
<SeriesKeyItem
|
212
|
+
style={{
|
213
|
+
left: dragPosition.x,
|
214
|
+
top: dragPosition.y - dragPositionYOffset,
|
215
|
+
position: 'absolute',
|
216
|
+
pointerEvents: 'none',
|
217
|
+
zIndex: 1
|
218
|
+
}}
|
219
|
+
series={draggedSeries}
|
220
|
+
i={draggedSeries.index}
|
221
|
+
theme={theme}
|
222
|
+
stateController={stateController}
|
223
|
+
/>
|
224
|
+
}
|
225
|
+
</div>
|
226
|
+
);
|
227
|
+
}
|
228
|
+
|
229
|
+
SeriesKey.propTypes = {
|
230
|
+
stateController: PropTypes.instanceOf(StateController).isRequired,
|
231
|
+
theme: PropTypes.string.isRequired,
|
232
|
+
draggingY: PropTypes.bool.isRequired,
|
233
|
+
grapherID: PropTypes.string,
|
234
|
+
dragPositionYOffset: PropTypes.number
|
235
|
+
};
|
@@ -0,0 +1,70 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import StateController from '../state/state_controller';
|
4
|
+
|
5
|
+
export default class SeriesKeyAxisContainer extends React.PureComponent {
|
6
|
+
|
7
|
+
constructor(props) {
|
8
|
+
super(props);
|
9
|
+
|
10
|
+
this.state = {
|
11
|
+
showingLabelInput: false
|
12
|
+
};
|
13
|
+
|
14
|
+
this.toggleLabelInputShowing = this.toggleLabelInputShowing.bind(this);
|
15
|
+
}
|
16
|
+
|
17
|
+
toggleLabelInputShowing() {
|
18
|
+
this.setState(({ showingLabelInput }) => {
|
19
|
+
return {
|
20
|
+
showingLabelInput: !showingLabelInput
|
21
|
+
};
|
22
|
+
});
|
23
|
+
}
|
24
|
+
|
25
|
+
render() {
|
26
|
+
const {children, stateController, axisIndex, scale, label, grapherID} = this.props;
|
27
|
+
|
28
|
+
const { showingLabelInput } = this.state;
|
29
|
+
|
30
|
+
return (
|
31
|
+
<div
|
32
|
+
className={`series-key-axis-container${showingLabelInput ? ' series-key-axis-container-showing-label' : ''}`}
|
33
|
+
data-axis-index={axisIndex}
|
34
|
+
data-grapher-id={grapherID}
|
35
|
+
>
|
36
|
+
<div className="scale-label" onClick={() => stateController.toggleScale({axisIndex})}>
|
37
|
+
{scale.slice(0, showingLabelInput ? 6 : 3)}
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<div className="series-key-axis-container-body">
|
41
|
+
<div>
|
42
|
+
{children}
|
43
|
+
|
44
|
+
<svg className="label-input-toggler" viewBox="0 0 512 512" onClick={this.toggleLabelInputShowing}>
|
45
|
+
<path
|
46
|
+
d="M0 252.118V48C0 21.49 21.49 0 48 0h204.118a48 48 0 0 1 33.941 14.059l211.882 211.882c18.745 18.745 18.745 49.137 0 67.882L293.823 497.941c-18.745 18.745-49.137 18.745-67.882 0L14.059 286.059A48 48 0 0 1 0 252.118zM112 64c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48z"/>
|
47
|
+
</svg>
|
48
|
+
</div>
|
49
|
+
|
50
|
+
<div className="series-key-axis-label-container">
|
51
|
+
<input
|
52
|
+
onChange={(event) => stateController.setLabel({axisIndex, label: event.target.value})}
|
53
|
+
placeholder="Enter label"
|
54
|
+
value={label || ''}
|
55
|
+
/>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
);
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
SeriesKeyAxisContainer.propTypes = {
|
64
|
+
stateController: PropTypes.instanceOf(StateController).isRequired,
|
65
|
+
children: PropTypes.node.isRequired,
|
66
|
+
axisIndex: PropTypes.number.isRequired,
|
67
|
+
scale: PropTypes.string.isRequired,
|
68
|
+
label: PropTypes.string,
|
69
|
+
grapherID: PropTypes.string
|
70
|
+
};
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import getColor from '../helpers/colors';
|
3
|
+
import CustomPropTypes from '../helpers/custom_prop_types';
|
4
|
+
import PropTypes from 'prop-types';
|
5
|
+
import StateController from '../state/state_controller';
|
6
|
+
import nameForSeries from '../helpers/name_for_series.js';
|
7
|
+
|
8
|
+
export default React.memo(SeriesKeyItem);
|
9
|
+
|
10
|
+
function SeriesKeyItem({ series, i, style, onMouseDown, theme, stateController, highlighted }) {
|
11
|
+
const color = getColor(series.color, i, series.multigrapherSeriesIndex);
|
12
|
+
|
13
|
+
if (theme === 'day') {
|
14
|
+
style = Object.assign({}, style, {
|
15
|
+
backgroundColor: color
|
16
|
+
});
|
17
|
+
} else {
|
18
|
+
style = Object.assign({}, style, {
|
19
|
+
color
|
20
|
+
});
|
21
|
+
}
|
22
|
+
|
23
|
+
const classes = ['series-key-item'];
|
24
|
+
if (highlighted) {
|
25
|
+
classes.push('series-key-item-highlighted');
|
26
|
+
}
|
27
|
+
|
28
|
+
const name = nameForSeries(series, i);
|
29
|
+
|
30
|
+
return (
|
31
|
+
<div
|
32
|
+
className={classes.join(' ')}
|
33
|
+
style={style}
|
34
|
+
onMouseDown={onMouseDown}
|
35
|
+
onMouseOver={() => stateController.setHighlightedSeries(i)}
|
36
|
+
onMouseOut={() => stateController.setHighlightedSeries(null)}
|
37
|
+
onClick={() => stateController.registerSeriesClick(i)}
|
38
|
+
>
|
39
|
+
{name}
|
40
|
+
</div>
|
41
|
+
);
|
42
|
+
}
|
43
|
+
|
44
|
+
SeriesKeyItem.propTypes = {
|
45
|
+
series: CustomPropTypes.SingleSeries.isRequired,
|
46
|
+
stateController: PropTypes.instanceOf(StateController),
|
47
|
+
i: PropTypes.number.isRequired,
|
48
|
+
style: PropTypes.object,
|
49
|
+
onMouseDown: PropTypes.func,
|
50
|
+
theme: PropTypes.string.isRequired,
|
51
|
+
highlighted: PropTypes.bool
|
52
|
+
};
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import StateController from '../state/state_controller';
|
4
|
+
import {useSeries} from '../state/hooks';
|
5
|
+
import CustomPropTypes from '../helpers/custom_prop_types';
|
6
|
+
import getColor from '../helpers/colors';
|
7
|
+
|
8
|
+
class SeriesToggle extends React.PureComponent {
|
9
|
+
|
10
|
+
constructor(props) {
|
11
|
+
super(props);
|
12
|
+
|
13
|
+
this.onChange = this.onChange.bind(this);
|
14
|
+
}
|
15
|
+
|
16
|
+
// NOTE: event listeners here do not follow React best practices, because when handled through React
|
17
|
+
// there were ~500ms of latency when toggling the checkbox. It's unclear why
|
18
|
+
onChange(event) {
|
19
|
+
setTimeout(() => {
|
20
|
+
this.props.stateController.setShowing(this.props.singleSeries, event.target.checked);
|
21
|
+
});
|
22
|
+
}
|
23
|
+
|
24
|
+
render() {
|
25
|
+
const { singleSeries } = this.props;
|
26
|
+
const color = getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex);
|
27
|
+
|
28
|
+
let name = singleSeries.name || singleSeries.yKey;
|
29
|
+
|
30
|
+
if (!name) {
|
31
|
+
name = singleSeries.index;
|
32
|
+
}
|
33
|
+
|
34
|
+
return (
|
35
|
+
<div className="series-toggle">
|
36
|
+
<label>
|
37
|
+
<input
|
38
|
+
type="checkbox"
|
39
|
+
defaultChecked={!singleSeries.hidden}
|
40
|
+
ref={(el) => el && el.addEventListener('change', this.onChange)}
|
41
|
+
/>
|
42
|
+
<span className="checkmark" style={{ background: color, borderColor: color }} />
|
43
|
+
|
44
|
+
{name}
|
45
|
+
</label>
|
46
|
+
</div>
|
47
|
+
);
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
SeriesToggle.propTypes = {
|
52
|
+
singleSeries: CustomPropTypes.SingleSeries.isRequired,
|
53
|
+
stateController: PropTypes.instanceOf(StateController).isRequired
|
54
|
+
};
|
55
|
+
|
56
|
+
export default function Sidebar({ stateController }) {
|
57
|
+
const series = useSeries(stateController);
|
58
|
+
|
59
|
+
return (
|
60
|
+
<div className="grapher-sidebar">
|
61
|
+
{
|
62
|
+
series.map((singleSeries, i) =>
|
63
|
+
<SeriesToggle
|
64
|
+
key={i}
|
65
|
+
singleSeries={singleSeries}
|
66
|
+
stateController={stateController}
|
67
|
+
/>
|
68
|
+
)
|
69
|
+
}
|
70
|
+
</div>
|
71
|
+
);
|
72
|
+
}
|
73
|
+
|
74
|
+
Sidebar.propTypes = {
|
75
|
+
stateController: PropTypes.instanceOf(StateController).isRequired
|
76
|
+
};
|
@@ -0,0 +1,244 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import {formatX, formatY} from '../helpers/format';
|
4
|
+
import {Y_AXIS_WIDTH} from '../helpers/axis_sizes';
|
5
|
+
import CustomPropTypes from '../helpers/custom_prop_types';
|
6
|
+
|
7
|
+
function getYLabelContent({ yLabel, y, fullYPrecision}) {
|
8
|
+
if (fullYPrecision && !yLabel) {
|
9
|
+
if (y === undefined) {
|
10
|
+
return 'undefined';
|
11
|
+
}
|
12
|
+
|
13
|
+
if (y === null) {
|
14
|
+
return 'null';
|
15
|
+
}
|
16
|
+
|
17
|
+
return y.toString();
|
18
|
+
}
|
19
|
+
|
20
|
+
if (typeof yLabel === 'number') {
|
21
|
+
if (fullYPrecision) {
|
22
|
+
return yLabel.toString();
|
23
|
+
} else {
|
24
|
+
return formatY(yLabel);
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
return yLabel || formatY(y);
|
29
|
+
}
|
30
|
+
|
31
|
+
function TooltipLabel({ axisLabel, x, y, xLabel, yLabel, textLeft, textTop, includeSeriesLabel, includeXValue, includeYValue, includeXLabel, includeYLabel, fullYPrecision, formatXOptions }) {
|
32
|
+
let i = 0;
|
33
|
+
|
34
|
+
return (
|
35
|
+
<g>
|
36
|
+
{
|
37
|
+
includeSeriesLabel &&
|
38
|
+
<text x={textLeft} y={textTop + (i++)*12}>
|
39
|
+
{axisLabel}
|
40
|
+
</text>
|
41
|
+
}
|
42
|
+
|
43
|
+
{
|
44
|
+
includeXValue &&
|
45
|
+
<text x={textLeft} y={textTop + (i++) * 12}>
|
46
|
+
{includeXLabel && 'x: '}{xLabel || formatX(x, formatXOptions)}
|
47
|
+
</text>
|
48
|
+
}
|
49
|
+
|
50
|
+
{
|
51
|
+
includeYValue &&
|
52
|
+
<text x={textLeft} y={textTop + (i++) * 12}>
|
53
|
+
{includeYLabel && 'y: '}{getYLabelContent({ yLabel, y, fullYPrecision})}
|
54
|
+
</text>
|
55
|
+
}
|
56
|
+
</g>
|
57
|
+
);
|
58
|
+
}
|
59
|
+
|
60
|
+
TooltipLabel.propTypes = {
|
61
|
+
axisLabel: PropTypes.string,
|
62
|
+
x: PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(Date)]),
|
63
|
+
y: PropTypes.number,
|
64
|
+
xLabel: PropTypes.string,
|
65
|
+
yLabel: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
66
|
+
textLeft: PropTypes.number.isRequired,
|
67
|
+
textTop: PropTypes.number.isRequired,
|
68
|
+
fullYPrecision: PropTypes.bool,
|
69
|
+
formatXOptions: PropTypes.object,
|
70
|
+
...CustomPropTypes.TooltipOptionsRaw
|
71
|
+
};
|
72
|
+
|
73
|
+
export default class Tooltip extends React.PureComponent {
|
74
|
+
|
75
|
+
render() {
|
76
|
+
let height = 12*3 + 6;
|
77
|
+
|
78
|
+
if (!this.props.includeSeriesLabel) {
|
79
|
+
height -= 12;
|
80
|
+
}
|
81
|
+
|
82
|
+
if (!this.props.includeXValue) {
|
83
|
+
height -= 12;
|
84
|
+
}
|
85
|
+
|
86
|
+
if (!this.props.includeYValue) {
|
87
|
+
height -= 12;
|
88
|
+
}
|
89
|
+
|
90
|
+
const caretSize = 7;
|
91
|
+
const halfHeight = height/2;
|
92
|
+
const caretPadding = 4;
|
93
|
+
|
94
|
+
const textTop = -halfHeight + 3;
|
95
|
+
|
96
|
+
const formatXOptions = {
|
97
|
+
clockStyle: this.props.clockStyle,
|
98
|
+
timeZone: this.props.timeZone
|
99
|
+
};
|
100
|
+
|
101
|
+
const passThroughProps = {
|
102
|
+
includeSeriesLabel: this.props.includeSeriesLabel,
|
103
|
+
includeXLabel: this.props.includeXLabel,
|
104
|
+
includeYLabel: this.props.includeYLabel,
|
105
|
+
includeXValue: this.props.includeXValue,
|
106
|
+
includeYValue: this.props.includeYValue,
|
107
|
+
formatXOptions
|
108
|
+
};
|
109
|
+
|
110
|
+
return (
|
111
|
+
<div className="grapher-tooltip">
|
112
|
+
<svg>
|
113
|
+
{
|
114
|
+
this.props.tooltips.map(({ x, y, color, pixelY, pixelX, series, index, xLabel, yLabel, fullYPrecision }, i) => {
|
115
|
+
const axisLabel = (series.name || series.yKey || index).toString();
|
116
|
+
const width = Math.max(axisLabel.length, (xLabel || formatX(x, formatXOptions)).length + 4, getYLabelContent({ yLabel, y, fullYPrecision}).length + 4) * 7.5;
|
117
|
+
|
118
|
+
let fixedPosition = this.props.elementWidth < (width + 2*caretSize + 2*caretPadding);
|
119
|
+
|
120
|
+
let multiplier = 1;
|
121
|
+
if (pixelX >= this.props.elementWidth - (width + 2*caretSize + caretPadding)) {
|
122
|
+
multiplier = -1;
|
123
|
+
}
|
124
|
+
|
125
|
+
if (pixelX < width + 2*caretSize + caretPadding && multiplier === -1) {
|
126
|
+
fixedPosition = true;
|
127
|
+
}
|
128
|
+
|
129
|
+
if (y === null) {
|
130
|
+
fixedPosition = true;
|
131
|
+
}
|
132
|
+
|
133
|
+
let textLeft = caretSize + caretPadding;
|
134
|
+
if (multiplier < 0) {
|
135
|
+
textLeft = -width - textLeft;
|
136
|
+
} else {
|
137
|
+
textLeft += 6;
|
138
|
+
}
|
139
|
+
|
140
|
+
if (!isFinite(pixelX)) {
|
141
|
+
return null;
|
142
|
+
}
|
143
|
+
|
144
|
+
const transform = `translate(${pixelX},${pixelY})`;
|
145
|
+
|
146
|
+
const commonLabelProps = {
|
147
|
+
fullYPrecision: fullYPrecision || this.props.maxPrecision,
|
148
|
+
x,
|
149
|
+
y,
|
150
|
+
axisLabel,
|
151
|
+
xLabel,
|
152
|
+
yLabel,
|
153
|
+
...passThroughProps
|
154
|
+
};
|
155
|
+
|
156
|
+
// display in a fixed position if not wide enough
|
157
|
+
if (fixedPosition || this.props.alwaysFixedPosition) {
|
158
|
+
textLeft = 6;
|
159
|
+
|
160
|
+
let baseLeft = this.props.elementWidth/2 - width/2;
|
161
|
+
|
162
|
+
if (width > this.props.elementWidth && !this.props.floating) {
|
163
|
+
baseLeft -= Y_AXIS_WIDTH*this.props.axisCount/2;
|
164
|
+
}
|
165
|
+
|
166
|
+
let yTranslation = 18;
|
167
|
+
|
168
|
+
if (this.props.floating) {
|
169
|
+
if (this.props.floatPosition === 'bottom') {
|
170
|
+
yTranslation = this.props.elementHeight + halfHeight + 4;
|
171
|
+
} else {
|
172
|
+
yTranslation = -height;
|
173
|
+
}
|
174
|
+
|
175
|
+
if (this.props.floatDelta) {
|
176
|
+
yTranslation += this.props.floatDelta;
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
return (
|
181
|
+
<g key={i} className="tooltip-item tooltip-item-fixed">
|
182
|
+
<circle r={4} fill={color} transform={transform} />
|
183
|
+
|
184
|
+
<g transform={`translate(${baseLeft}, ${yTranslation})`}>
|
185
|
+
<path stroke={color} d={`M0,0 V-${halfHeight} h${width} V${halfHeight} h${-width} V0`} />
|
186
|
+
|
187
|
+
<TooltipLabel
|
188
|
+
textLeft={textLeft} textTop={textTop}
|
189
|
+
{...commonLabelProps}
|
190
|
+
/>
|
191
|
+
</g>
|
192
|
+
</g>
|
193
|
+
);
|
194
|
+
}
|
195
|
+
|
196
|
+
return (
|
197
|
+
<g key={i} transform={transform} className="tooltip-item">
|
198
|
+
<circle r={4} fill={color} />
|
199
|
+
|
200
|
+
<path stroke={color} d={`M${multiplier*caretPadding},0 L${multiplier*caretSize*2},-${caretSize} V-${halfHeight} h${multiplier*width} V${halfHeight} h${multiplier*-width} V${caretSize} L${multiplier*caretPadding},0`} />
|
201
|
+
|
202
|
+
<TooltipLabel
|
203
|
+
textLeft={textLeft} textTop={textTop}
|
204
|
+
{...commonLabelProps}
|
205
|
+
/>
|
206
|
+
</g>
|
207
|
+
);
|
208
|
+
})
|
209
|
+
}
|
210
|
+
</svg>
|
211
|
+
</div>
|
212
|
+
);
|
213
|
+
}
|
214
|
+
|
215
|
+
}
|
216
|
+
|
217
|
+
Tooltip.defaultProps = {
|
218
|
+
includeSeriesLabel: true,
|
219
|
+
includeXLabel: true,
|
220
|
+
includeYLabel: true,
|
221
|
+
includeXValue: true,
|
222
|
+
includeYValue: true
|
223
|
+
};
|
224
|
+
|
225
|
+
Tooltip.propTypes = {
|
226
|
+
mouseX: PropTypes.number.isRequired,
|
227
|
+
mouseY: PropTypes.number.isRequired,
|
228
|
+
elementHeight: PropTypes.number.isRequired,
|
229
|
+
elementWidth: PropTypes.number.isRequired,
|
230
|
+
tooltips: PropTypes.arrayOf(PropTypes.shape({
|
231
|
+
x: PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(Date)]),
|
232
|
+
y: PropTypes.number,
|
233
|
+
pixelY: PropTypes.number,
|
234
|
+
color: PropTypes.string,
|
235
|
+
xLabel: PropTypes.string,
|
236
|
+
yLabel: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
237
|
+
fullYPrecision: PropTypes.bool
|
238
|
+
})),
|
239
|
+
axisCount: PropTypes.number.isRequired,
|
240
|
+
maxPrecision: PropTypes.bool.isRequired,
|
241
|
+
clockStyle: PropTypes.oneOf(['12h', '24h']),
|
242
|
+
timeZone: PropTypes.string,
|
243
|
+
...CustomPropTypes.TooltipOptionsRaw
|
244
|
+
};
|