@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.
Files changed (112) hide show
  1. package/.eslintrc.js +85 -0
  2. package/.idea/codeStyles/Project.xml +19 -0
  3. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  4. package/.idea/grapher.iml +12 -0
  5. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  6. package/.idea/misc.xml +6 -0
  7. package/.idea/modules.xml +8 -0
  8. package/.idea/vcs.xml +6 -0
  9. package/0.bundle.js +2 -0
  10. package/0.bundle.js.map +1 -0
  11. package/1767282193a714f63082.module.wasm +0 -0
  12. package/537.bundle.js +2 -0
  13. package/537.bundle.js.map +1 -0
  14. package/831.bundle.js +2 -0
  15. package/831.bundle.js.map +1 -0
  16. package/bundle.js +2 -0
  17. package/bundle.js.map +1 -0
  18. package/package.json +75 -0
  19. package/readme.md +129 -0
  20. package/src/components/annotations.js +62 -0
  21. package/src/components/context_menu.js +73 -0
  22. package/src/components/draggable_points.js +114 -0
  23. package/src/components/graph_body.js +292 -0
  24. package/src/components/graph_title.js +16 -0
  25. package/src/components/options.js +111 -0
  26. package/src/components/percentile_button.js +72 -0
  27. package/src/components/range_graph.js +352 -0
  28. package/src/components/range_selection.js +175 -0
  29. package/src/components/range_selection_button.js +26 -0
  30. package/src/components/range_selection_button_base.js +51 -0
  31. package/src/components/series_key.js +235 -0
  32. package/src/components/series_key_axis_container.js +70 -0
  33. package/src/components/series_key_item.js +52 -0
  34. package/src/components/sidebar.js +76 -0
  35. package/src/components/tooltip.js +244 -0
  36. package/src/components/vertical_lines.js +70 -0
  37. package/src/components/x_axis.js +124 -0
  38. package/src/components/y_axis.js +239 -0
  39. package/src/eventable.js +65 -0
  40. package/src/grapher.js +367 -0
  41. package/src/grapher.scss +914 -0
  42. package/src/helpers/axis_sizes.js +2 -0
  43. package/src/helpers/binary_search.js +67 -0
  44. package/src/helpers/color_to_vector.js +35 -0
  45. package/src/helpers/colors.js +27 -0
  46. package/src/helpers/custom_prop_types.js +159 -0
  47. package/src/helpers/flatten_simple_data.js +81 -0
  48. package/src/helpers/format.js +233 -0
  49. package/src/helpers/generator_params_equal.js +10 -0
  50. package/src/helpers/name_for_series.js +16 -0
  51. package/src/helpers/place_grid.js +257 -0
  52. package/src/helpers/pyodide_ready.js +13 -0
  53. package/src/multigrapher.js +105 -0
  54. package/src/renderer/background.frag +7 -0
  55. package/src/renderer/background.vert +7 -0
  56. package/src/renderer/background_program.js +48 -0
  57. package/src/renderer/circle.frag +26 -0
  58. package/src/renderer/circle.vert +12 -0
  59. package/src/renderer/create_gl_program.js +36 -0
  60. package/src/renderer/draw_area.js +159 -0
  61. package/src/renderer/draw_background.js +15 -0
  62. package/src/renderer/draw_bars.js +80 -0
  63. package/src/renderer/draw_line.js +69 -0
  64. package/src/renderer/draw_zero_line.js +24 -0
  65. package/src/renderer/extract_vertices.js +137 -0
  66. package/src/renderer/graph_body_renderer.js +293 -0
  67. package/src/renderer/line.frag +51 -0
  68. package/src/renderer/line.vert +32 -0
  69. package/src/renderer/line_program.js +125 -0
  70. package/src/renderer/paths_from.js +72 -0
  71. package/src/renderer/scale_bounds.js +28 -0
  72. package/src/renderer/size_canvas.js +59 -0
  73. package/src/rust/Cargo.lock +233 -0
  74. package/src/rust/Cargo.toml +35 -0
  75. package/src/rust/pkg/grapher_rs.d.ts +42 -0
  76. package/src/rust/pkg/grapher_rs.js +351 -0
  77. package/src/rust/pkg/grapher_rs_bg.d.ts +11 -0
  78. package/src/rust/pkg/grapher_rs_bg.wasm +0 -0
  79. package/src/rust/pkg/index.js +342 -0
  80. package/src/rust/pkg/index_bg.wasm +0 -0
  81. package/src/rust/pkg/package.json +14 -0
  82. package/src/rust/src/extract_vertices.rs +83 -0
  83. package/src/rust/src/get_point_number.rs +50 -0
  84. package/src/rust/src/lib.rs +15 -0
  85. package/src/rust/src/selected_space_to_render_space.rs +131 -0
  86. package/src/state/average_loop_times.js +15 -0
  87. package/src/state/bound_calculator_from_selection.js +36 -0
  88. package/src/state/bound_calculators.js +41 -0
  89. package/src/state/calculate_annotations_state.js +59 -0
  90. package/src/state/calculate_data_bounds.js +104 -0
  91. package/src/state/calculate_tooltip_state.js +241 -0
  92. package/src/state/data_types.js +13 -0
  93. package/src/state/expand_bounds.js +58 -0
  94. package/src/state/find_matching_axis.js +31 -0
  95. package/src/state/get_default_bounds_calculator.js +15 -0
  96. package/src/state/hooks.js +164 -0
  97. package/src/state/infer_type.js +74 -0
  98. package/src/state/merge_bounds.js +64 -0
  99. package/src/state/multigraph_state_controller.js +334 -0
  100. package/src/state/selection_from_global_bounds.js +25 -0
  101. package/src/state/space_conversions/condense_data_space.js +115 -0
  102. package/src/state/space_conversions/data_space_to_selected_space.js +328 -0
  103. package/src/state/space_conversions/selected_space_to_background_space.js +144 -0
  104. package/src/state/space_conversions/selected_space_to_render_space.js +161 -0
  105. package/src/state/space_conversions/simple_series_to_data_space.js +229 -0
  106. package/src/state/state_controller.js +1770 -0
  107. package/src/state/sync_pool.js +101 -0
  108. package/test/setup.js +15 -0
  109. package/test/space_conversions/data_space_to_selected_space.test.js +434 -0
  110. package/webpack.dev.config.js +109 -0
  111. package/webpack.prod.config.js +60 -0
  112. 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
+ };