@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,70 @@
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 {useAxisBounds, useSizing} from '../state/hooks.js';
6
+
7
+ export default React.memo(VerticalLines);
8
+
9
+ function VerticalLines({ stateController, verticalLines }) {
10
+ const bounds = useAxisBounds(stateController)[0];
11
+ const sizing = useSizing(stateController);
12
+
13
+ if (!sizing) {
14
+ return null;
15
+ }
16
+
17
+ return (
18
+ <div className="grapher-vertical-lines">
19
+ <svg width={sizing.elementWidth} height={sizing.elementHeight}>
20
+ {
21
+ verticalLines.map((line, index) => {
22
+ const xT = (line.x - bounds.minX)/(bounds.maxX - bounds.minX);
23
+
24
+ if (xT < 0 || xT > 1) {
25
+ return null;
26
+ }
27
+
28
+ const pixelX = xT * sizing.elementWidth;
29
+
30
+ const lineStyle = {
31
+ stroke: line.color,
32
+ strokeWidth: line.width,
33
+ ...(line.style || {})
34
+ };
35
+
36
+ const markerStyle = {
37
+ fill: line.color,
38
+ ...(line.markerStyle || {})
39
+ };
40
+
41
+ return (
42
+ <React.Fragment key={index}>
43
+ <line
44
+ x1={pixelX}
45
+ y1={0}
46
+ x2={pixelX}
47
+ y2={sizing.elementHeight}
48
+ style={lineStyle}
49
+ />
50
+
51
+ {
52
+ line.markTop &&
53
+ <polygon
54
+ points={`${pixelX - 3},0 ${pixelX + 3},0 ${pixelX},3`}
55
+ style={markerStyle}
56
+ />
57
+ }
58
+ </React.Fragment>
59
+ );
60
+ })
61
+ }
62
+ </svg>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ VerticalLines.propTypes = {
68
+ stateController: PropTypes.instanceOf(StateController).isRequired,
69
+ verticalLines: CustomPropTypes.VerticalLines.isRequired
70
+ };
@@ -0,0 +1,124 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {calculatePrecision, calculateTimePrecision, formatX} from '../helpers/format';
4
+ import placeGrid from '../helpers/place_grid';
5
+ import {useEnumMap, useHasXEnum, usePrimarySize, useSelection} from '../state/hooks';
6
+ import StateController from '../state/state_controller';
7
+
8
+ export default React.memo(XAxis);
9
+
10
+ function XAxis({ showAxes, showGrid, stateController, bigLabels, xTickUnit, clockStyle, timeZone, integersOnly }) {
11
+ if (!showAxes && !showGrid) {
12
+ return null;
13
+ }
14
+
15
+ const { elementWidth, elementHeight } = usePrimarySize(stateController);
16
+ const {minX, maxX, dates} = useSelection(stateController);
17
+ const hasXEnum = useHasXEnum(stateController);
18
+ const enumMap = useEnumMap(stateController);
19
+
20
+ let precision;
21
+ if (dates) {
22
+ precision = calculateTimePrecision(minX, maxX);
23
+ } else {
24
+ precision = calculatePrecision(maxX - minX);
25
+ }
26
+
27
+ let inverseEnumMap = null;
28
+ if (hasXEnum) {
29
+ inverseEnumMap = {};
30
+ for (const [key, value] of Object.entries(enumMap)) {
31
+ inverseEnumMap[value] = key;
32
+ }
33
+ }
34
+
35
+ const formatOptions = {
36
+ unitOverride: xTickUnit,
37
+ clockStyle,
38
+ timeZone,
39
+ integersOnly,
40
+ inverseEnumMap
41
+ };
42
+
43
+ const minLabel = formatX(minX, {...formatOptions, dates, precision }).toString();
44
+ const maxLabel = formatX(maxX, {...formatOptions, dates, precision }).toString();
45
+
46
+ let expectedLabelWidth = Math.max(minLabel.length, maxLabel.length) * 4;
47
+ if (bigLabels) {
48
+ expectedLabelWidth *= 2;
49
+ }
50
+
51
+ const labelPadding = 30; // space in between labels in the expected case
52
+
53
+ const ticks = placeGrid({
54
+ min: minX,
55
+ max: maxX,
56
+ totalSize: elementWidth,
57
+ precision,
58
+ dates,
59
+ formatter: formatX,
60
+ expectedLabelSize: expectedLabelWidth,
61
+ labelPadding,
62
+ formatOptions
63
+ });
64
+
65
+ const xAxisHeight = 20;
66
+
67
+ return (
68
+ <svg className="axis x-axis" style={showAxes ? undefined : {marginBottom: -20}}>
69
+ {
70
+ showAxes &&
71
+ <path d={`M-1,0 H${elementWidth}`} className="axis-line" />
72
+ }
73
+ {
74
+ showAxes &&
75
+ <path d={`M-2,1 H${elementWidth + 1}`} className="axis-line-shadow" />
76
+ }
77
+
78
+ {
79
+ ticks.map(({ pixelValue, label, size, position, skipGrid }, i) => {
80
+ if (isNaN(pixelValue)) {
81
+ return null;
82
+ }
83
+
84
+ const classes = ['axis-item', `axis-item-${size}`, `axis-item-${position}`];
85
+ if (bigLabels) {
86
+ classes.push('axis-item-big-labels');
87
+ }
88
+
89
+ return (
90
+ <g key={i} className={classes.join(' ')}>
91
+ {
92
+ showAxes &&
93
+ <path d={`M${pixelValue},1 v6`} className="axis-tick" />
94
+ }
95
+
96
+ {
97
+ showGrid && !skipGrid &&
98
+ <path d={`M${pixelValue},0 v-${elementHeight}`} />
99
+ }
100
+
101
+ {
102
+ showAxes &&
103
+ <text x={pixelValue} y={xAxisHeight - 5}>
104
+ {label}
105
+ </text>
106
+ }
107
+ </g>
108
+ );
109
+ })
110
+ }
111
+ </svg>
112
+ );
113
+ }
114
+
115
+ XAxis.propTypes = {
116
+ stateController: PropTypes.instanceOf(StateController).isRequired,
117
+ showAxes: PropTypes.bool.isRequired,
118
+ showGrid: PropTypes.bool.isRequired,
119
+ bigLabels: PropTypes.bool,
120
+ xTickUnit: PropTypes.oneOf(['year']),
121
+ clockStyle: PropTypes.oneOf(['12h', '24h']),
122
+ timeZone: PropTypes.string,
123
+ integersOnly: PropTypes.bool
124
+ };
@@ -0,0 +1,239 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {calculatePrecision, formatY} from '../helpers/format';
4
+ import {Y_AXIS_WIDTH, BIG_Y_AXIS_LABEL_OFFSET} from '../helpers/axis_sizes';
5
+ import scaleBounds from '../renderer/scale_bounds';
6
+ import getColor from '../helpers/colors';
7
+ import placeGrid from '../helpers/place_grid';
8
+ import {useAxes, useAxisBounds, useHighlightedSeries, usePrimarySize} from '../state/hooks';
9
+ import StateController from '../state/state_controller';
10
+ import CustomPropTypes from '../helpers/custom_prop_types';
11
+
12
+ export default React.memo(YAxis);
13
+
14
+ function YAxis({ stateController, showAxes, showGrid, showSeriesKey, axis, sideIndex, bodyHeight, theme, grapherID, dragPositionYOffset=0, bigLabels, showAxisColors }) {
15
+ if (!showAxes && !showGrid) {
16
+ return null;
17
+ }
18
+
19
+ const { side, scale, axisIndex, label } = useAxes(stateController)[axis.axisIndex];
20
+
21
+ const { elementWidth, elementHeight } = usePrimarySize(stateController);
22
+ let { minY, maxY } = useAxisBounds(stateController)[axisIndex];
23
+
24
+ const scaledBounds = scaleBounds({ minY, maxY, scale});
25
+ minY = scaledBounds.minY;
26
+ maxY = scaledBounds.maxY;
27
+
28
+ const ticks = placeGrid({
29
+ min: minY,
30
+ max: maxY,
31
+ totalSize: elementHeight,
32
+ scale,
33
+ precision: calculatePrecision(maxY - minY),
34
+ formatter: formatY,
35
+ inverted: true,
36
+ expectedLabelSize: bigLabels ? 20 : 10,
37
+ labelPadding: 30
38
+ });
39
+
40
+ const colorBoxSize = 10;
41
+ const colorBoxPadding = 4;
42
+
43
+ let sidePadding = 5;
44
+
45
+ if (scale === 'log') {
46
+ sidePadding = 2;
47
+ }
48
+
49
+ const highlightedSeries = useHighlightedSeries(stateController);
50
+
51
+ const [draggedSeries, setDraggedSeries] = useState(null);
52
+ const [dragDelta, setDragDelta] = useState({ dx: 0, dy: 0 });
53
+
54
+ const startDrag = (event, singleSeries) => {
55
+ let startX = event.clientX;
56
+ let startY = event.clientY;
57
+
58
+ if (side === 'left') {
59
+ startX += Y_AXIS_WIDTH;
60
+ }
61
+
62
+ if (side === 'right') {
63
+ startX -= Y_AXIS_WIDTH;
64
+ }
65
+
66
+ setDragDelta({
67
+ dx: 0,
68
+ dy: 0
69
+ });
70
+
71
+ const onMouseMove = (moveEvent) => {
72
+ setDragDelta({
73
+ dx: moveEvent.clientX - startX,
74
+ dy: moveEvent.clientY - startY
75
+ });
76
+ };
77
+
78
+ const onMouseUp = (mouseUpEvent) => {
79
+ window.removeEventListener('mousemove', onMouseMove);
80
+ window.removeEventListener('mouseup', onMouseUp);
81
+
82
+ let target = mouseUpEvent.target;
83
+ while (target && !(target.dataset || {}).axisIndex) {
84
+ target = target.parentNode;
85
+ }
86
+
87
+ setDraggedSeries(null);
88
+ stateController.finalizeDrag(singleSeries, target && (target.dataset || {}).axisIndex, target && (target.dataset || {}).grapherId);
89
+ };
90
+
91
+ window.addEventListener('mousemove', onMouseMove);
92
+ window.addEventListener('mouseup', onMouseUp);
93
+
94
+ setDraggedSeries(singleSeries);
95
+ stateController.markDragStart();
96
+ };
97
+
98
+ const highlightedOpacity = theme === 'day' ? 1.0 : 0.5;
99
+ const unhighlightedOpacity = theme === 'day' ? 0.8 : 0.3;
100
+
101
+ return (
102
+ <svg className={`axis y-axis y-axis-${side}`}
103
+ data-axis-index={axisIndex}
104
+ data-grapher-id={grapherID}
105
+ style={{
106
+ width: Y_AXIS_WIDTH,
107
+ marginLeft: showAxes ? undefined : -Y_AXIS_WIDTH,
108
+ height: typeof bodyHeight === 'number' ? bodyHeight : undefined
109
+ }}
110
+ >
111
+ {
112
+ showAxes &&
113
+ showAxisColors &&
114
+ axis.series.map((singleSeries, i) => {
115
+ const height = elementHeight/axis.series.length;
116
+
117
+ return (
118
+ <rect
119
+ x={side === 'left' ? 0 : 2}
120
+ y={i*height}
121
+ width={Y_AXIS_WIDTH - 2}
122
+ height={height}
123
+ key={singleSeries.index}
124
+ fill={getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex)}
125
+ opacity={singleSeries.index === highlightedSeries ? highlightedOpacity : unhighlightedOpacity}
126
+ data-element-height={elementHeight}
127
+ data-series-length={axis.series.length}
128
+ />
129
+ );
130
+ })
131
+ }
132
+
133
+ {
134
+ showAxes &&
135
+ <path d={`M${side === 'left' ? Y_AXIS_WIDTH-1 : 1},3 V${elementHeight}`} className="axis-line" />
136
+ }
137
+
138
+ {
139
+ showAxes &&
140
+ <path d={`M${side === 'left' ? Y_AXIS_WIDTH-2 : 0},3 V${elementHeight + 1}`} className="axis-line-shadow" />
141
+ }
142
+
143
+ {
144
+ ticks.map(({ pixelValue, label, size, skipGrid }, i) => {
145
+ const edge = side === 'left' ? (sideIndex + 1) * Y_AXIS_WIDTH : -sideIndex*Y_AXIS_WIDTH;
146
+ const length = (side === 'left' ? 1 : - 1) * (elementWidth+1);
147
+
148
+ const classes = ['axis-item', `axis-item-${size}`];
149
+ if (bigLabels) {
150
+ classes.push('axis-item-big-labels');
151
+ }
152
+
153
+ return (
154
+ <g key={i} className={classes.join(' ')}>
155
+ {
156
+ showGrid &&
157
+ !skipGrid &&
158
+ <path d={`M${edge},${pixelValue} h${length}`} />
159
+ }
160
+
161
+ {
162
+ showGrid &&
163
+ !skipGrid &&
164
+ sideIndex > 0 &&
165
+ <path
166
+ d={`M${side === 'left' ? Y_AXIS_WIDTH : 0},${pixelValue} h${(side === 'left' ? 1 : -1) * sideIndex * Y_AXIS_WIDTH}`}
167
+ strokeDasharray={'2,2'}
168
+ />
169
+ }
170
+
171
+ {
172
+ showAxes &&
173
+ <text x={side === 'left' ? Y_AXIS_WIDTH-sidePadding : sidePadding} y={pixelValue}>
174
+ {label}
175
+ </text>
176
+ }
177
+ </g>
178
+ );
179
+ })
180
+ }
181
+
182
+ {
183
+ showSeriesKey && showAxes &&
184
+ axis.series.map((singleSeries, i) => {
185
+
186
+ let x = (Y_AXIS_WIDTH - colorBoxSize - colorBoxPadding) + (i % 2 - 1)*(colorBoxSize + colorBoxPadding);
187
+ let y = -(colorBoxPadding + colorBoxSize) * Math.ceil(axis.series.length / 2) + (colorBoxSize + colorBoxPadding) * Math.floor(i / 2);
188
+
189
+ if (singleSeries === draggedSeries) {
190
+ x += dragDelta.dx;
191
+ y += dragDelta.dy - dragPositionYOffset;
192
+ }
193
+
194
+ return (
195
+ <rect
196
+ className="series-color-box"
197
+ onMouseDown={(event) => startDrag(event, singleSeries)}
198
+ x={x}
199
+ y={y}
200
+ width={colorBoxSize}
201
+ height={colorBoxSize}
202
+ key={singleSeries.index}
203
+ fill={getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex)}
204
+ onMouseOver={() => stateController.setHighlightedSeries(singleSeries.index)}
205
+ onMouseOut={() => stateController.setHighlightedSeries(null)}
206
+ />
207
+ );
208
+ })
209
+ }
210
+
211
+ {
212
+ showAxes && !!label &&
213
+ <text
214
+ className={`y-axis-label${bigLabels ? ' y-axis-big-label' : ''}`}
215
+ x={side === 'left' ? 10 : Y_AXIS_WIDTH - 10}
216
+ y={elementHeight/2 + (side === 'left' ? -1 : 1)*(axisIndex + 1)*(bigLabels ? BIG_Y_AXIS_LABEL_OFFSET : 0)}
217
+ transform={`rotate(${side === 'left' ? -90 : 90}, ${side === 'left' ? 10 : Y_AXIS_WIDTH - 10}, ${elementHeight/2})`}
218
+ >
219
+ {label}
220
+ </text>
221
+ }
222
+ </svg>
223
+ );
224
+ }
225
+
226
+ YAxis.propTypes = {
227
+ stateController: PropTypes.instanceOf(StateController),
228
+ showAxes: PropTypes.bool.isRequired,
229
+ showGrid: PropTypes.bool.isRequired,
230
+ showAxisColors: PropTypes.bool.isRequired,
231
+ showSeriesKey: PropTypes.bool.isRequired,
232
+ axis: CustomPropTypes.Axis.isRequired,
233
+ sideIndex: PropTypes.number.isRequired,
234
+ bodyHeight: PropTypes.number,
235
+ theme: PropTypes.string,
236
+ grapherID: PropTypes.string,
237
+ dragPositionYOffset: PropTypes.number,
238
+ bigLabels: PropTypes.bool
239
+ };
@@ -0,0 +1,65 @@
1
+ export default class Eventable {
2
+
3
+ constructor() {
4
+ this._listeners = {};
5
+ }
6
+
7
+ /**
8
+ * Clears all listeners
9
+ * @protected
10
+ */
11
+ clearListeners() {
12
+ this._listeners = {};
13
+ }
14
+
15
+ /**
16
+ * Create a new listener for the event
17
+ * When the event is emitted, it will call the provided function
18
+ *
19
+ * @param {String|symbol} eventName - the name of the event to start listening to
20
+ * @param {Function} fn - the listener to add
21
+ */
22
+ on(eventName, fn) {
23
+ this._listeners[eventName] = this._listeners[eventName] || new Set();
24
+ this._listeners[eventName].add(fn);
25
+ }
26
+
27
+ /**
28
+ * Removes an existing listener for the event
29
+ *
30
+ * @param {String|symbol} eventName - the name of the event to stop listening to
31
+ * @param {Function} fn - the listener to remove
32
+ */
33
+ off(eventName, fn) {
34
+ if (!this._listeners[eventName]) {
35
+ return;
36
+ }
37
+
38
+ this._listeners[eventName].delete(fn);
39
+
40
+ if (this._listeners[eventName].size === 0) {
41
+ delete this._listeners[eventName];
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Emits an event that will go out to all _listeners on eventName
47
+ *
48
+ * @param {String|symbol} eventName - the name of the event to emit
49
+ */
50
+ emit(eventName) {
51
+ if (!this._listeners[eventName]) {
52
+ return;
53
+ }
54
+
55
+ const args = [];
56
+ for (let i = 1; i < arguments.length; i++) {
57
+ args.push(arguments[i]);
58
+ }
59
+
60
+ this._listeners[eventName].forEach((fn) => {
61
+ fn.apply(this, args);
62
+ });
63
+ }
64
+
65
+ }