@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,292 @@
1
+ import React, { useCallback, useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import CustomPropTypes from '../helpers/custom_prop_types';
4
+ import GraphBodyRenderer from '../renderer/graph_body_renderer';
5
+ import Tooltip from './tooltip';
6
+ import ContextMenu from './context_menu';
7
+ import StateController from '../state/state_controller';
8
+ import {
9
+ useAnnotationState,
10
+ useAutoscaleY,
11
+ useAxes,
12
+ useMaxPrecision,
13
+ useShowingAnnotations,
14
+ useTooltipState,
15
+ useContextMenu
16
+ } from '../state/hooks';
17
+ import Annotations from './annotations.js';
18
+ import DraggablePoints from './draggable_points.js';
19
+ import VerticalLines from './vertical_lines.js';
20
+
21
+ export default React.memo(GraphBody);
22
+
23
+ function GraphBody({ stateController, webgl, bodyHeight, boundsSelectionEnabled, showTooltips, tooltipOptions, checkIntersection, draggablePoints, onPointDrag, onDraggablePointsDoubleClick, verticalLines, clockStyle, timeZone }) {
24
+ const canvasEl = useCallback((el) => {
25
+ if (stateController.primaryRenderer) {
26
+ stateController.primaryRenderer.dispose();
27
+ }
28
+
29
+ if (!el) {
30
+ return;
31
+ }
32
+
33
+ const renderer = new GraphBodyRenderer({
34
+ stateController: stateController,
35
+ canvasElement: el,
36
+ webgl,
37
+ checkIntersection
38
+ });
39
+
40
+ stateController.primaryRenderer = renderer;
41
+ renderer.resize();
42
+ }, [webgl, stateController]);
43
+
44
+
45
+ const [boundsSelection, setBoundsSelection] = useState({
46
+ showing: false,
47
+ start: {},
48
+ style: {}
49
+ });
50
+
51
+ const autoscaleY = useAutoscaleY(stateController);
52
+
53
+ const axisCount = useAxes(stateController).length;
54
+
55
+ const tooltip = useTooltipState(stateController);
56
+ const maxPrecision = useMaxPrecision(stateController);
57
+ const showAnnotations = useShowingAnnotations(stateController);
58
+ const annotationState = useAnnotationState(stateController);
59
+ const contextMenu = useContextMenu(stateController);
60
+
61
+ const onMouseDown = (event) => {
62
+ if (!boundsSelectionEnabled) {
63
+ return;
64
+ }
65
+
66
+ let currentNode = event.target;
67
+ for (let i = 0; i < 10 && currentNode; i++) {
68
+ if (currentNode.classList.contains('grapher-tooltip')) {
69
+ return;
70
+ }
71
+ currentNode = currentNode.parentNode;
72
+ }
73
+
74
+ const boundingRect = stateController.primaryRenderer.boundingRect;
75
+
76
+ const start = {
77
+ x: event.clientX - boundingRect.left,
78
+ y: event.clientY - boundingRect.top
79
+ };
80
+
81
+
82
+ setBoundsSelection({
83
+ showing: true,
84
+ start,
85
+ style: {
86
+ left: start.x,
87
+ top: start.y,
88
+ width: 0,
89
+ height: 0
90
+ }
91
+ });
92
+
93
+ const onMouseUp = (event) => {
94
+ window.removeEventListener('mouseup', onMouseUp);
95
+
96
+ if (!boundsSelectionEnabled) {
97
+ return;
98
+ }
99
+
100
+ const x = event.clientX - boundingRect.left;
101
+ const y = event.clientY - boundingRect.top;
102
+
103
+ stateController.setBoundsFromSelection({
104
+ minPixelX: Math.min(x, start.x),
105
+ maxPixelX: Math.max(x, start.x),
106
+ minPixelY: Math.min(y, start.y),
107
+ maxPixelY: Math.max(y, start.y)
108
+ });
109
+
110
+ setBoundsSelection({
111
+ showing: false,
112
+ style: {}
113
+ });
114
+ };
115
+
116
+ window.addEventListener('mouseup', onMouseUp);
117
+ };
118
+
119
+ const onMouseMove = (event) => {
120
+ if (boundsSelection.showing) {
121
+ const boundingRect = stateController.primaryRenderer.boundingRect;
122
+ const x = event.clientX - boundingRect.left;
123
+ const y = event.clientY - boundingRect.top;
124
+
125
+ setBoundsSelection(({ start }) => {
126
+ return {
127
+ showing: true,
128
+ start,
129
+ style: {
130
+ left: Math.min(x, start.x),
131
+ top: autoscaleY ? 0 : Math.min(y, start.y),
132
+ width: Math.abs(x - start.x),
133
+ height: autoscaleY ? boundingRect.height : Math.abs(y - start.y)
134
+ }
135
+ };
136
+ });
137
+ }
138
+ };
139
+
140
+ useEffect(() => {
141
+ const onScroll = () => {
142
+ if (!showTooltips) {
143
+ return;
144
+ }
145
+
146
+ stateController.recalculateTooltips();
147
+ };
148
+
149
+ const onGlobalMouseMove = (event) => {
150
+ if (!showTooltips) {
151
+ return;
152
+ }
153
+
154
+ stateController.setTooltipMousePosition({
155
+ clientX: event.clientX,
156
+ clientY: event.clientY,
157
+ shiftKey: event.shiftKey
158
+ });
159
+ };
160
+
161
+ window.addEventListener('scroll', onScroll, { passive: true });
162
+ window.addEventListener('mousemove', onGlobalMouseMove, { passive: true });
163
+
164
+ return () => {
165
+ window.removeEventListener('scroll', onScroll);
166
+ window.removeEventListener('mousemove', onGlobalMouseMove);
167
+ };
168
+ }, []);
169
+
170
+ const onMouseLeave = () => {
171
+ stateController.setContextMenuMousePosition({
172
+ showing: false
173
+ });
174
+ };
175
+
176
+ const onClick = (event) => {
177
+ stateController.registerClick({
178
+ clientX: event.clientX,
179
+ clientY: event.clientY
180
+ });
181
+
182
+ if (tooltipOptions && tooltipOptions.savingDisabled) {
183
+ stateController.clearSavedTooltips();
184
+ return;
185
+ }
186
+
187
+ // if it's NOT a child of 'grapher-context-menu', close the context menu
188
+ if (!event.target.closest('.grapher-context-menu')) {
189
+ stateController.setContextMenuMousePosition({
190
+ showing: false
191
+ });
192
+ }
193
+
194
+ if (!window.getSelection || window.getSelection().type !== 'Range') {
195
+ stateController.toggleTooltipSaved();
196
+ }
197
+ };
198
+ const onDoubleClick = () => {
199
+ stateController.clearSavedTooltips();
200
+ };
201
+
202
+ const onContextMenu = (event) => {
203
+ event.preventDefault();
204
+ stateController.setContextMenuMousePosition({
205
+ clientX: event.clientX,
206
+ clientY: event.clientY,
207
+ showing: true
208
+ });
209
+ };
210
+
211
+ return (
212
+ <div className="graph-body graph-body-primary"
213
+ onMouseMove={onMouseMove}
214
+ onMouseLeave={onMouseLeave}
215
+ onMouseDown={onMouseDown}
216
+ onClick={onClick}
217
+ onDoubleClick={onDoubleClick}
218
+ onContextMenu={onContextMenu}
219
+ style={typeof bodyHeight === 'number' ? { height: bodyHeight } : undefined}
220
+ >
221
+ <canvas ref={canvasEl} />
222
+
223
+ {
224
+ showTooltips &&
225
+ <Tooltip
226
+ axisCount={axisCount}
227
+ maxPrecision={maxPrecision}
228
+ clockStyle={clockStyle}
229
+ timeZone={timeZone}
230
+ {...(tooltipOptions || {})}
231
+ {...tooltip}
232
+ />
233
+ }
234
+
235
+ {
236
+ contextMenu.showing &&
237
+ <ContextMenu
238
+ contextMenu={contextMenu}
239
+ />
240
+ }
241
+
242
+ {
243
+ showAnnotations &&
244
+ <Annotations
245
+ bodyHeight={bodyHeight}
246
+ annotationState={annotationState}
247
+ />
248
+ }
249
+
250
+ {
251
+ verticalLines &&
252
+ <VerticalLines
253
+ stateController={stateController}
254
+ verticalLines={verticalLines}
255
+ />
256
+ }
257
+
258
+ {
259
+ draggablePoints &&
260
+ <DraggablePoints
261
+ stateController={stateController}
262
+ draggablePoints={draggablePoints}
263
+ onPointDrag={onPointDrag}
264
+ onDraggablePointsDoubleClick={onDraggablePointsDoubleClick}
265
+ />
266
+ }
267
+
268
+ {
269
+ boundsSelectionEnabled && boundsSelection.showing &&
270
+ <div className="bounds-selection"
271
+ style={boundsSelection.style}
272
+ />
273
+ }
274
+ </div>
275
+ );
276
+ }
277
+
278
+ GraphBody.propTypes = {
279
+ stateController: PropTypes.instanceOf(StateController).isRequired,
280
+ webgl: PropTypes.bool,
281
+ checkIntersection: PropTypes.bool,
282
+ bodyHeight: PropTypes.number,
283
+ boundsSelectionEnabled: PropTypes.bool.isRequired,
284
+ showTooltips: PropTypes.bool.isRequired,
285
+ tooltipOptions: CustomPropTypes.TooltipOptions,
286
+ verticalLines: CustomPropTypes.VerticalLines,
287
+ draggablePoints: CustomPropTypes.DraggablePoints,
288
+ onPointDrag: PropTypes.func,
289
+ onDraggablePointsDoubleClick: PropTypes.func,
290
+ clockStyle: PropTypes.oneOf(['12h', '24h']),
291
+ timeZone: PropTypes.string
292
+ };
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ export default React.memo(GraphTitle);
5
+
6
+ function GraphTitle({ title }) {
7
+ return (
8
+ <div className="grapher-title">
9
+ {title}
10
+ </div>
11
+ );
12
+ }
13
+
14
+ GraphTitle.propTypes = {
15
+ title: PropTypes.string.isRequired
16
+ };
@@ -0,0 +1,111 @@
1
+ import React from 'react';
2
+ import PercentileButton from './percentile_button';
3
+ import RangeSelectionButtonBase from './range_selection_button_base';
4
+ import {
5
+ useAutoscaleY,
6
+ useMaxPrecision,
7
+ useShowIndividualPoints, useShowingAnnotations,
8
+ useShowingSidebar, useTheme
9
+ } from '../state/hooks';
10
+ import PropTypes from 'prop-types';
11
+ import StateController from '../state/state_controller';
12
+
13
+ export default React.memo(Options);
14
+
15
+ function Options({stateController, sidebarEnabled}) {
16
+
17
+ const showIndividualPoints = useShowIndividualPoints(stateController);
18
+ const autoscaleY = useAutoscaleY(stateController);
19
+ const maxPrecision = useMaxPrecision(stateController);
20
+ const showingSidebar = useShowingSidebar(stateController);
21
+ const showingAnnotations = useShowingAnnotations(stateController);
22
+ const theme = useTheme(stateController);
23
+
24
+ return (
25
+ <div className="options-bar">
26
+ {
27
+ sidebarEnabled &&
28
+ <RangeSelectionButtonBase
29
+ selected={showingSidebar}
30
+ onClick={() => stateController.toggleShowingSidebar()}
31
+ description="Show sidebar"
32
+ >
33
+ <div className="icon-container icon-container-square">
34
+ <svg focusable="false" viewBox="0 0 512 512">
35
+ <path fill="currentColor"
36
+ d="M464 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zM224 416H64V160h160v256zm224 0H288V160h160v256z"/>
37
+ </svg>
38
+ </div>
39
+ </RangeSelectionButtonBase>
40
+ }
41
+
42
+ <RangeSelectionButtonBase
43
+ selected={theme === 'export'}
44
+ onClick={() => stateController.toggleExportMode()}
45
+ description="Export mode"
46
+ >
47
+ <div className="icon-container icon-container-square">
48
+ <svg focusable="false" viewBox="0 0 512 512">
49
+ <path fill="currentColor"
50
+ d="M167.02 309.34c-40.12 2.58-76.53 17.86-97.19 72.3-2.35 6.21-8 9.98-14.59 9.98-11.11 0-45.46-27.67-55.25-34.35C0 439.62 37.93 512 128 512c75.86 0 128-43.77 128-120.19 0-3.11-.65-6.08-.97-9.13l-88.01-73.34zM457.89 0c-15.16 0-29.37 6.71-40.21 16.45C213.27 199.05 192 203.34 192 257.09c0 13.7 3.25 26.76 8.73 38.7l63.82 53.18c7.21 1.8 14.64 3.03 22.39 3.03 62.11 0 98.11-45.47 211.16-256.46 7.38-14.35 13.9-29.85 13.9-45.99C512 20.64 486 0 457.89 0z" />
51
+ </svg>
52
+ </div>
53
+ </RangeSelectionButtonBase>
54
+
55
+ <RangeSelectionButtonBase
56
+ selected={showingAnnotations}
57
+ onClick={() => stateController.toggleShowingAnnotations()}
58
+ description="Show annotations"
59
+ >
60
+ <div className="icon-container icon-container-448">
61
+ <svg focusable="false" viewBox="0 0 448 512">
62
+ <path fill="currentColor" d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z" />
63
+ </svg>
64
+ </div>
65
+ </RangeSelectionButtonBase>
66
+
67
+ <PercentileButton stateController={stateController} />
68
+
69
+ <RangeSelectionButtonBase
70
+ selected={maxPrecision}
71
+ onClick={() => stateController.toggleMaxPrecision()}
72
+ description="Max precision"
73
+ >
74
+ <div className="icon-container icon-container-square">
75
+ <svg focusable="false" viewBox="0 0 512 512">
76
+ <path fill="currentColor" d="M478.21 334.093L336 256l142.21-78.093c11.795-6.477 15.961-21.384 9.232-33.037l-19.48-33.741c-6.728-11.653-21.72-15.499-33.227-8.523L296 186.718l3.475-162.204C299.763 11.061 288.937 0 275.48 0h-38.96c-13.456 0-24.283 11.061-23.994 24.514L216 186.718 77.265 102.607c-11.506-6.976-26.499-3.13-33.227 8.523l-19.48 33.741c-6.728 11.653-2.562 26.56 9.233 33.037L176 256 33.79 334.093c-11.795 6.477-15.961 21.384-9.232 33.037l19.48 33.741c6.728 11.653 21.721 15.499 33.227 8.523L216 325.282l-3.475 162.204C212.237 500.939 223.064 512 236.52 512h38.961c13.456 0 24.283-11.061 23.995-24.514L296 325.282l138.735 84.111c11.506 6.976 26.499 3.13 33.227-8.523l19.48-33.741c6.728-11.653 2.563-26.559-9.232-33.036z" />
77
+ </svg>
78
+ </div>
79
+ </RangeSelectionButtonBase>
80
+
81
+ <RangeSelectionButtonBase
82
+ selected={autoscaleY}
83
+ onClick={() => stateController.toggleYAutoscaling()}
84
+ description="Autoscale y axis"
85
+ >
86
+ <div className="icon-container">
87
+ <svg focusable="false" viewBox="0 0 256 512">
88
+ <path fill="currentColor" d="M168 416c-4.42 0-8-3.58-8-8v-16c0-4.42 3.58-8 8-8h88v-64h-88c-4.42 0-8-3.58-8-8v-16c0-4.42 3.58-8 8-8h88v-64h-88c-4.42 0-8-3.58-8-8v-16c0-4.42 3.58-8 8-8h88v-64h-88c-4.42 0-8-3.58-8-8v-16c0-4.42 3.58-8 8-8h88V32c0-17.67-14.33-32-32-32H32C14.33 0 0 14.33 0 32v448c0 17.67 14.33 32 32 32h192c17.67 0 32-14.33 32-32v-64h-88z"/>
89
+ </svg>
90
+ </div>
91
+ </RangeSelectionButtonBase>
92
+
93
+ <RangeSelectionButtonBase
94
+ selected={showIndividualPoints}
95
+ onClick={() => stateController.toggleIndividualPoints()}
96
+ description="Show individual points"
97
+ >
98
+ <div className="icon-container icon-container-narrow">
99
+ <svg focusable="false" viewBox="0 0 192 512">
100
+ <path fill="currentColor" d="M96 184c39.8 0 72 32.2 72 72s-32.2 72-72 72-72-32.2-72-72 32.2-72 72-72zM24 80c0 39.8 32.2 72 72 72s72-32.2 72-72S135.8 8 96 8 24 40.2 24 80zm0 352c0 39.8 32.2 72 72 72s72-32.2 72-72-32.2-72-72-72-72 32.2-72 72z" />
101
+ </svg>
102
+ </div>
103
+ </RangeSelectionButtonBase>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ Options.propTypes = {
109
+ stateController: PropTypes.instanceOf(StateController).isRequired,
110
+ sidebarEnabled: PropTypes.bool
111
+ };
@@ -0,0 +1,72 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import RangeSelectionButtonBase from './range_selection_button_base';
4
+ import StateController from '../state/state_controller';
5
+ import {usePercentile, usePercentileAsymmetry} from '../state/hooks';
6
+
7
+ export default React.memo(PercentileButton);
8
+
9
+ function PercentileButton({ stateController }) {
10
+ const percentile = usePercentile(stateController);
11
+ const percentileAsymmetry = usePercentileAsymmetry(stateController);
12
+ const [showing, setShowing] = useState(false);
13
+
14
+ return ([
15
+ showing ? <RangeSelectionButtonBase
16
+ key="asymmetry button"
17
+ className="percentile-button"
18
+ selected={true}
19
+ description="Percentile asymmetry"
20
+ >
21
+ <input
22
+ onClick={(e) => e.stopPropagation()}
23
+ value={percentileAsymmetry}
24
+ onChange={(e) => stateController.percentileAsymmetry = e.target.value}
25
+ onKeyUp={(e) => e.keyCode === 13 && setShowing(false)}
26
+ type="number"
27
+ min={-50}
28
+ max={50}
29
+ />
30
+
31
+ <div className="icon-container icon-container-square">
32
+ <svg focusable="false" viewBox="0 0 512 512">
33
+ <path fill="currentColor" d="M240 96h64a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16h-64a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zm0 128h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16H240a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zm256 192H240a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h256a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm-256-64h192a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16H240a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zM16 160h48v304a16 16 0 0 0 16 16h32a16 16 0 0 0 16-16V160h48c14.21 0 21.39-17.24 11.31-27.31l-80-96a16 16 0 0 0-22.62 0l-80 96C-5.35 142.74 1.78 160 16 160z" />
34
+ </svg>
35
+ </div>
36
+ </RangeSelectionButtonBase>: null,
37
+
38
+ <RangeSelectionButtonBase
39
+ key="main-button"
40
+ className="percentile-button"
41
+ selected={showing || parseFloat(percentile) !== 100}
42
+ onClick={() => setShowing(!showing)}
43
+ description="Edit percentile"
44
+ >
45
+ {
46
+ showing &&
47
+ <div>
48
+ <input
49
+ onClick={(e) => e.stopPropagation()}
50
+ autoFocus={true}
51
+ value={percentile}
52
+ onChange={(e) => stateController.percentile = e.target.value}
53
+ onKeyUp={(e) => e.keyCode === 13 && setShowing(false)}
54
+ type="number"
55
+ min={0}
56
+ max={100}
57
+ />
58
+ </div>
59
+ }
60
+
61
+ <div className="icon-container icon-container-narrow">
62
+ <svg focusable="false" viewBox="0 0 448 512">
63
+ <path fill="currentColor" d="M112 224c61.9 0 112-50.1 112-112S173.9 0 112 0 0 50.1 0 112s50.1 112 112 112zm0-160c26.5 0 48 21.5 48 48s-21.5 48-48 48-48-21.5-48-48 21.5-48 48-48zm224 224c-61.9 0-112 50.1-112 112s50.1 112 112 112 112-50.1 112-112-50.1-112-112-112zm0 160c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48zM392.3.2l31.6-.1c19.4-.1 30.9 21.8 19.7 37.8L77.4 501.6a23.95 23.95 0 0 1-19.6 10.2l-33.4.1c-19.5 0-30.9-21.9-19.7-37.8l368-463.7C377.2 4 384.5.2 392.3.2z" />
64
+ </svg>
65
+ </div>
66
+ </RangeSelectionButtonBase>
67
+ ]);
68
+ }
69
+
70
+ PercentileButton.propTypes = {
71
+ stateController: PropTypes.instanceOf(StateController).isRequired
72
+ };