@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
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@windborne/grapher",
3
+ "version": "1.0.0",
4
+ "description": "Graphing library",
5
+ "main": "bundle.js",
6
+ "publishConfig": {},
7
+ "scripts": {
8
+ "lint-base": "eslint ./src ./examples ./test",
9
+ "lint": "npm run lint-base -- --fix",
10
+ "dev": "webpack-dev-server --config webpack.dev.config.js",
11
+ "build": "rm -f *.wasm && webpack --config webpack.prod.config.js && touch src/rust/pkg/.npmignore",
12
+ "analyze-size": "webpack --config webpack.prod.config.js --json | webpack-bundle-size-analyzer",
13
+ "test": "mocha-webpack --webpack-config webpack.test.config.js --require test/setup.js --recursive --glob \"*.test.js\" test",
14
+ "coverage": "NODE_ENV=coverage nyc --reporter=lcov --reporter=text npm run test"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/windborne/grapher.git"
19
+ },
20
+ "author": "",
21
+ "license": "UNLICENSED",
22
+ "bugs": {
23
+ "url": "https://github.com/windborne/grapher/issues"
24
+ },
25
+ "nyc": {
26
+ "include": [
27
+ "src/**/*.js"
28
+ ],
29
+ "instrument": false,
30
+ "sourceMap": false
31
+ },
32
+ "browserslist": [
33
+ "last 2 Chrome versions"
34
+ ],
35
+ "homepage": "https://github.com/windborne/grapher/readme.md",
36
+ "devDependencies": {
37
+ "@babel/core": "^7.7.5",
38
+ "@babel/polyfill": "^7.7.0",
39
+ "@babel/preset-env": "^7.7.6",
40
+ "@babel/preset-react": "^7.7.4",
41
+ "@wasm-tool/wasm-pack-plugin": "^1.2.0",
42
+ "babel-eslint": "^10.1.0",
43
+ "babel-loader": "^8.0.6",
44
+ "chai": "^4.2.0",
45
+ "css-loader": "^6.8.1",
46
+ "eslint": "^6.7.2",
47
+ "eslint-plugin-react": "^7.17.0",
48
+ "file-loader": "^6.2.0",
49
+ "html-webpack-plugin": "^5.6.0",
50
+ "istanbul-instrumenter-loader": "^3.0.1",
51
+ "jsdom": "^16.3.0",
52
+ "jsdom-global": "^3.0.2",
53
+ "kefir": "^3.8.6",
54
+ "mocha": "^7.2.0",
55
+ "mocha-webpack": "^2.0.0-beta.0",
56
+ "nyc": "^15.1.0",
57
+ "prop-types": "^15.7.2",
58
+ "react": "^16.12.0",
59
+ "react-dom": "^16.12.0",
60
+ "sass": "^1.68.0",
61
+ "sass-loader": "^13.3.2",
62
+ "sinon": "^9.0.2",
63
+ "sinon-chai": "^3.5.0",
64
+ "style-loader": "^3.3.3",
65
+ "util": "^0.12.5",
66
+ "webpack": "^5.9.1",
67
+ "webpack-cli": "^4.10.0",
68
+ "webpack-dev-server": "^4.15.2",
69
+ "webpack-glsl-loader": "^1.0.1",
70
+ "webpack-node-externals": "^2.5.0"
71
+ },
72
+ "peerDependencies": {
73
+ "react": ">= 16.8"
74
+ }
75
+ }
package/readme.md ADDED
@@ -0,0 +1,129 @@
1
+
2
+ # Grapher
3
+ Powerful, performant graphing.
4
+
5
+ Grapher is designed primarily for internal infrastructure with lots of data and lots of axes.
6
+ Key features include:
7
+ - Performance. Grapher can render a million points in 100ms and support new data coming in at hundreds of points per second without degraded performance.
8
+ - Async data sources. Grapher supports observables directly as well as generator functions to generate data on the fly
9
+ - Axis management. Series can be moved between axes and new axes created with ease.
10
+ - Long data ranges.
11
+
12
+ You _shouldn't_ use this package if:
13
+ - You need to support older browsers. While it may work in older browsers, no guarantees are made.
14
+ New features will be used as they become available without regard for maintaining legacy support.
15
+ - You don't want to add a react dependency (though the rendering engine may be used without react).
16
+ - Asset size is a big concern. Despite not having dependencies, this is a relatively heavy engine compared to alternatives.
17
+
18
+ ## As a user
19
+
20
+ ### Range selection
21
+
22
+ ### Axes
23
+
24
+ ### Tooltips
25
+
26
+ ### Options
27
+ Percentile
28
+
29
+ Auto-scale Y
30
+
31
+ ## As a developer
32
+
33
+ ### Installing
34
+
35
+ The package is hosted on github, so things are marginally more tricky than usual.
36
+ Add an .npmrc with the following contents:
37
+ ```
38
+ @windborne:registry=https://npm.pkg.github.com
39
+ registry=https://registry.npmjs.org
40
+ ```
41
+
42
+ Then, `npm install @windborne/grapher`.
43
+
44
+ It also requires React version 16.8 or greater as a peer dependency
45
+
46
+ ### Importing
47
+ The simplest option is to import it from the root.
48
+ ```ecmascript 6
49
+ import Grapher from '@windborne/grapher';
50
+ ```
51
+
52
+ This uses the webpacked version and allows you to not worry about any other dependencies.
53
+ However, since relative imports won't work, neither will the optional WASM extensions.
54
+ For most applications, WASM extensions won't be necessary, but if there are strong performance
55
+ requirements, you should consider them.
56
+
57
+ #### Importing with WASM extensions
58
+ To use the Rust WASM extensions, you should import the component itself.
59
+ ```ecmascript 6
60
+ import Grapher from '@windborne/grapher/src/grapher';
61
+ ```
62
+
63
+ However, this will require that you have the dependencies needed to build the whole project.
64
+ You can find a full list in the package.json devDependencies, but they can be summarized as follows:
65
+ - React and babel (include @babel/polyfill, @babel/preset-react, etc). You likely already have this in your project
66
+ - Sass support (eg node-sass, sass-loader)
67
+ - Webgl loader (webpack-glsl-loader)
68
+
69
+ ### props
70
+ Like any react component, grapher is primarily configured via props.
71
+ Refer to the grapher proptypes for information
72
+
73
+ **series**. This sets the data for the graph and is the only property that is truly required.
74
+ See passing in data section for more details.
75
+
76
+ **webgl**. If true, will render with webgl rather than a 2d context.
77
+ This is more performant, but uses more resources.
78
+
79
+ **requireWASM**. If true, will wait until the WASM extensions are ready before it renders.
80
+ This can be useful when your app has expensive initialization.
81
+
82
+ **onAxisChange**. If passed in, this function will be called every time the axes change.
83
+ It will be called with an array of objects, where each object is a single series object with the `axis`
84
+ property set to {left, right}-{index}. This can be useful for saving state between reloads.
85
+
86
+ **onRenderTime**. If passed in, this function will be called every time the grapher renders.
87
+ It will be called with diagnostic information about how long rendering took.
88
+
89
+ **timingFrameCount**. This will set the number of frames for when the state controller's
90
+ `averageLoopTime` method is called.
91
+
92
+ **theme**. Sets the theme of grapher to either day or night.
93
+ You can also override any css property directly in a stylesheet.
94
+
95
+ **bodyHeight**. Sets the height of the graph body (ie excluding range graph, series controls, etc).
96
+
97
+ **height**. Sets the height of the entire graph.
98
+
99
+ **width**. Sets the width of the graph.
100
+
101
+ **showAxes**. Whether to show the axes on the graph.
102
+
103
+ **showRangeGraph**. Whether to show the smaller range graph below the main graph.
104
+
105
+ **showRangeSelectors**. Whether to show the top bar with range selection (eg last day button) and other options.
106
+
107
+ **showSeriesKey**. Whether to show the key of which series have which colors.
108
+
109
+ **showGrid**
110
+
111
+ **showTooltips**
112
+
113
+ **boundsSelectionEnabled**
114
+
115
+ **tooltipOptions**
116
+
117
+ **customBoundsSelectors**
118
+
119
+ ### Usage from ruby
120
+
121
+ ### Usage from python
122
+
123
+ ## Developing
124
+ Other than an `npm install`, you'll need to install rust and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/).
125
+
126
+ ## License
127
+ All rights reserved.
128
+
129
+ This library uses icons from FontAwesome, licensed via https://fontawesome.com/license. No changes were made, nor does Font Awesome endorse this use.
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ export default class Annotations extends React.PureComponent {
5
+
6
+ render() {
7
+ const { bodyHeight, annotationState } = this.props;
8
+ const { annotations, elementWidth } = annotationState;
9
+
10
+ return (
11
+ <div className="grapher-annotations">
12
+ {
13
+ annotations.map(({ pixelX, width, content, lineOnly }, i) => {
14
+ if (lineOnly) {
15
+ return (
16
+ <div key={i} className="grapher-annotation" style={{ left: pixelX }}>
17
+ <div
18
+ className="annotation-marker"
19
+ style={{ width: width, height: bodyHeight }}
20
+ />
21
+ </div>
22
+ );
23
+ }
24
+
25
+ const textStyle = {
26
+ top: 21*i
27
+ };
28
+
29
+ if (elementWidth - pixelX < content.length*5.5) {
30
+ textStyle.left = pixelX - elementWidth;
31
+ } else {
32
+ textStyle.left = 0;
33
+ }
34
+
35
+ return (
36
+ <div key={i} className="grapher-annotation" style={{ left: pixelX }}>
37
+ <div
38
+ className="annotation-marker"
39
+ style={{ width: width, height: bodyHeight }}
40
+ />
41
+
42
+ <div className="annotation-text" style={textStyle}>
43
+ {content}
44
+ </div>
45
+ </div>
46
+ );
47
+ })
48
+ }
49
+ </div>
50
+ );
51
+ }
52
+
53
+ }
54
+
55
+ Annotations.propTypes = {
56
+ annotationState: PropTypes.shape({
57
+ annotations: PropTypes.arrayOf(PropTypes.object).isRequired,
58
+ elementWidth: PropTypes.number.isRequired
59
+ }),
60
+ bodyHeight: PropTypes.number,
61
+ lineOnly: PropTypes.bool
62
+ };
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ export default class ContextMenu extends React.PureComponent {
5
+ constructor(props) {
6
+ super(props);
7
+ this.setTextRef = this.setTextRef.bind(this);
8
+ }
9
+
10
+ setTextRef(ref) {
11
+ this.textRef = ref;
12
+
13
+ if (this.props.contextMenu.showing && this.textRef) {
14
+ const range = document.createRange();
15
+ const selection = window.getSelection();
16
+
17
+ selection.removeAllRanges();
18
+
19
+ range.selectNodeContents(this.textRef);
20
+ selection.addRange(range);
21
+ this.textRef.focus();
22
+ }
23
+ }
24
+
25
+ formatDateTime(dateTimeStr) {
26
+ const [datePart, timePart] = dateTimeStr.split(', ');
27
+
28
+ const [month, day, year] = datePart.split('/');
29
+ const formattedDate = `${month.padStart(2, '0')}/${day.padStart(2, '0')}/${year}`;
30
+
31
+ const [time, period] = timePart.split(' ');
32
+ const [hours, minutes] = time.split(':');
33
+ const formattedHours = hours.padStart(2, '0');
34
+ const formattedTime = `${formattedHours}:${minutes} ${period}`;
35
+
36
+ return `${formattedDate} ${formattedTime}`;
37
+ }
38
+
39
+ render() {
40
+ const { x, y, showing, value } = this.props.contextMenu;
41
+
42
+ const style = { left: x, top: y, width: '150px'};
43
+
44
+ if (!showing || !value || value.toLocaleString() === 'Invalid Date') {
45
+ return null;
46
+ }
47
+
48
+ const displayValue = value instanceof Date ? this.formatDateTime(value.toLocaleString()) : value;
49
+ return (
50
+ <div className="grapher-context-menu" style={style}>
51
+ <div className="menu-item">
52
+ <div className="menu-text" autoFocus={true} ref={this.setTextRef}>
53
+ {displayValue}
54
+ </div>
55
+ </div>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ }
61
+
62
+ ContextMenu.propTypes = {
63
+ contextMenu: PropTypes.shape({
64
+ x: PropTypes.number.isRequired,
65
+ y: PropTypes.number.isRequired,
66
+ showing: PropTypes.bool.isRequired,
67
+ value: PropTypes.oneOfType([
68
+ PropTypes.instanceOf(Date),
69
+ PropTypes.number,
70
+ PropTypes.object
71
+ ])
72
+ }).isRequired
73
+ };
@@ -0,0 +1,114 @@
1
+ import React from 'react';
2
+ import CustomPropTypes from '../helpers/custom_prop_types.js';
3
+ import PropTypes from 'prop-types';
4
+ import StateController from '../state/state_controller.js';
5
+ import {LINE_COLORS} from '../helpers/colors.js';
6
+ import {useAxisBounds, useSizing} from '../state/hooks.js';
7
+
8
+ export default React.memo(DraggablePoints);
9
+
10
+ function DraggablePoints({ stateController, draggablePoints, onPointDrag, onDraggablePointsDoubleClick }) {
11
+ const bounds = useAxisBounds(stateController)[0];
12
+ const sizing = useSizing(stateController);
13
+
14
+ if (!sizing) {
15
+ return null;
16
+ }
17
+
18
+ const onPointlessDoubleClick = onDraggablePointsDoubleClick && ((event) => {
19
+ const newXT = (event.clientX - sizing.boundingRect.left)/sizing.elementWidth;
20
+ const newYT = 1 - (event.clientY - sizing.boundingRect.top)/sizing.elementHeight;
21
+
22
+ onDraggablePointsDoubleClick(event, {
23
+ x: newXT*(bounds.maxX - bounds.minX) + bounds.minX,
24
+ y: newYT*(bounds.maxY - bounds.minY) + bounds.minY
25
+ });
26
+ });
27
+
28
+ return (
29
+ <div className="grapher-draggable-points" onDoubleClick={onPointlessDoubleClick}>
30
+ <svg width={sizing.elementWidth} height={sizing.elementHeight}>
31
+ {
32
+ draggablePoints.map((point, index) => {
33
+ const xT = (point.x - bounds.minX)/(bounds.maxX - bounds.minX);
34
+ const yT = (1 - (point.y - bounds.minY)/(bounds.maxY - bounds.minY));
35
+
36
+ if (xT < 0 || xT > 1 || yT < 0 || yT > 1) {
37
+ return null;
38
+ }
39
+
40
+ const pixelX = xT * sizing.elementWidth;
41
+ const pixelY = yT * sizing.elementHeight;
42
+
43
+ const radius = point.radius || 5;
44
+ const fillColor = point.fillColor || LINE_COLORS[0];
45
+ const strokeColor = point.strokeColor || 'white';
46
+ const strokeWidth = point.strokeWidth || 1;
47
+
48
+ const onMouseDown = (event) => {
49
+ event.stopPropagation();
50
+
51
+ if (!onPointDrag) {
52
+ return;
53
+ }
54
+
55
+ const xOffset = pixelX - event.clientX;
56
+ const yOffset = pixelY - event.clientY;
57
+
58
+ const onMouseMove = (moveEvent) => {
59
+ const newXT = (moveEvent.clientX + xOffset)/sizing.elementWidth;
60
+ const newYT = 1 - (moveEvent.clientY + yOffset)/sizing.elementHeight;
61
+
62
+ onPointDrag({
63
+ index,
64
+ point,
65
+ x: newXT*(bounds.maxX - bounds.minX) + bounds.minX,
66
+ y: newYT*(bounds.maxY - bounds.minY) + bounds.minY
67
+ }, moveEvent);
68
+ };
69
+
70
+ const onMouseUp = () => {
71
+ window.removeEventListener('mousemove', onMouseMove);
72
+ window.removeEventListener('mouseup', onMouseUp);
73
+ };
74
+
75
+ window.addEventListener('mousemove', onMouseMove);
76
+ window.addEventListener('mouseup', onMouseUp);
77
+ };
78
+
79
+ const onClick = point.onClick && ((event) => {
80
+ point.onClick(event, point);
81
+ });
82
+
83
+ const onDoubleClick = point.onDoubleClick && ((event) => {
84
+ event.stopPropagation();
85
+ point.onDoubleClick(event, point);
86
+ });
87
+
88
+ return (
89
+ <circle
90
+ key={index}
91
+ cx={pixelX}
92
+ cy={pixelY}
93
+ r={radius}
94
+ fill={fillColor}
95
+ stroke={strokeColor}
96
+ strokeWidth={strokeWidth}
97
+ onMouseDown={onMouseDown}
98
+ onClick={onClick}
99
+ onDoubleClick={onDoubleClick}
100
+ />
101
+ );
102
+ })
103
+ }
104
+ </svg>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ DraggablePoints.propTypes = {
110
+ stateController: PropTypes.instanceOf(StateController).isRequired,
111
+ draggablePoints: CustomPropTypes.DraggablePoints.isRequired,
112
+ onPointDrag: PropTypes.func,
113
+ onDraggablePointsDoubleClick: PropTypes.func
114
+ };