@windborne/grapher 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windborne/grapher",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Graphing library",
5
5
  "main": "bundle.js",
6
6
  "publishConfig": {},
package/readme.md CHANGED
@@ -15,31 +15,11 @@ You _shouldn't_ use this package if:
15
15
  - You don't want to add a react dependency (though the rendering engine may be used without react).
16
16
  - Asset size is a big concern. Despite not having dependencies, this is a relatively heavy engine compared to alternatives.
17
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
18
+ ## As a developer importing this package
32
19
 
33
20
  ### Installing
34
21
 
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`.
22
+ `npm install @windborne/grapher`.
43
23
 
44
24
  It also requires React version 16.8 or greater as a peer dependency
45
25
 
@@ -65,60 +45,257 @@ You can find a full list in the package.json devDependencies, but they can be su
65
45
  - React and babel (include @babel/polyfill, @babel/preset-react, etc). You likely already have this in your project
66
46
  - Sass support (eg node-sass, sass-loader)
67
47
  - 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
48
 
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.
49
+ # Grapher Component
50
+
51
+ ## Props
52
+ Like any React component, Grapher is primarily configured via props.
53
+ Refer to the Grapher PropTypes for complete type information.
54
+
55
+ ### Core Props
56
+
57
+ **series** (required)
58
+ This sets the data for the graph and is the only property that is truly required.
59
+ See "Series" section for more details.
75
60
 
76
- **webgl**. If true, will render with webgl rather than a 2d context.
61
+ **webgl**
62
+ If true, will render with WebGL rather than a 2D context.
77
63
  This is more performant, but uses more resources.
78
64
 
79
- **requireWASM**. If true, will wait until the WASM extensions are ready before it renders.
65
+ **requireWASM**
66
+ If true, will wait until the WASM extensions are ready before it renders.
80
67
  This can be useful when your app has expensive initialization.
81
68
 
82
- **onAxisChange**. If passed in, this function will be called every time the axes change.
69
+ ### Series Format
70
+ The `series` prop requires an array of objects, where each object represents a data series with the following properties:
71
+
72
+ - **data** (required): The actual data points (see Data Formats below)
73
+ - **type**: Data type or 'infer' to automatically detect
74
+ - **xKey**: Property name for x-values when using object data
75
+ - **yKey**: Property name for y-values when using object data
76
+ - **xUnixDates**: Whether x-values are Unix timestamps (boolean)
77
+ - **color**: Series color (string or number)
78
+ - **name**: Series name for display in legend
79
+ - **xLabel**: Label for x-axis
80
+ - **yLabel**: Label for y-axis
81
+ - **rendering**: Visual representation ('line', 'bar', or 'area', defaults to 'line')
82
+ - **ignoreDiscontinuities**: Whether to connect points across gaps (boolean)
83
+ - **dashed**: Whether to use dashed lines (boolean)
84
+ - **dashPattern**: Array defining dash pattern (array of numbers)
85
+ - **width**: Line width (number)
86
+ - **axis**: Axis specification for the series (string or object)
87
+ - **rangeSelectorWidth**: Width of the range selector for this series (number)
88
+ - **expandYWith**: Values to include when calculating y-axis range (array of numbers)
89
+ - **defaultAlwaysTooltipped**: Whether to always show tooltip for this series (boolean)
90
+ - **square**: Whether to render the series with square points (boolean)
91
+ - **shiftXBy**: Value to shift x-coordinates by (number)
92
+ - **graph**: Affects which graph this series belongs to in multigrapher (number)
93
+ - **background**: Background configuration (object)
94
+ - **hideFromKey**: Whether to hide this series from the legend (boolean)
95
+ - **showIndividualPoints**: Whether to show individual data points (boolean)
96
+ - **negativeColor**: Color for negative values
97
+ - **gradient**: Gradient configuration, only applies to area rendering (array)
98
+ - **zeroLineWidth**: Width of the zero line, only applies to bar and area rendering (number)
99
+ - **zeroLineColor**: Color of the zero line, only applies to bar and area rendering (string)
100
+ - **pointRadius**: Radius of points, only applies to area rendering (number)
101
+ - **tooltipWidth**: Expected width of the tooltip (number). Will make the tooltip switch sides when this width plus the tooltip left position is greater than the graph width.
102
+
103
+ #### Series Data Formats
104
+ Grapher supports multiple data formats within a series:
105
+
106
+ 1. **Array of y-values**: Simple array where index is used as x-value
107
+ 2. **Array of [x,y] tuples**: Each point defined as [x, y] pair
108
+ 3. **Array of objects**: Objects with properties for x and y values (as per the xKey and yKey properties)
109
+ 4. **Observable**: Object with an observe method, which may emit tuples or objects
110
+ 5. **Generator function**: Function that generates an array data points as a function of zoom
111
+
112
+ ### Event Handlers
113
+
114
+ **onAxisChange**
115
+ If passed in, this function will be called every time the axes change.
83
116
  It will be called with an array of objects, where each object is a single series object with the `axis`
84
117
  property set to {left, right}-{index}. This can be useful for saving state between reloads.
85
118
 
86
- **onRenderTime**. If passed in, this function will be called every time the grapher renders.
119
+ **onRenderTime**
120
+ If passed in, this function will be called every time the grapher renders.
87
121
  It will be called with diagnostic information about how long rendering took.
88
122
 
89
- **timingFrameCount**. This will set the number of frames for when the state controller's
90
- `averageLoopTime` method is called.
123
+ **onPointDrag**
124
+ Callback function that fires when draggable points are moved.
91
125
 
92
- **theme**. Sets the theme of grapher to either day or night.
93
- You can also override any css property directly in a stylesheet.
126
+ **onDraggablePointsDoubleClick**
127
+ Callback function that fires when draggable points are double-clicked.
94
128
 
95
- **bodyHeight**. Sets the height of the graph body (ie excluding range graph, series controls, etc).
129
+ **timingFrameCount**
130
+ Sets the number of frames for when the state controller's `averageLoopTime` method is called.
96
131
 
97
- **height**. Sets the height of the entire graph.
132
+ ### Appearance
98
133
 
99
- **width**. Sets the width of the graph.
134
+ **theme**
135
+ Sets the theme of grapher to either 'day', 'night', or 'export'.
136
+ You can also override any CSS property directly in a stylesheet.
100
137
 
101
- **showAxes**. Whether to show the axes on the graph.
138
+ **title**
139
+ Sets the title text for the graph.
102
140
 
103
- **showRangeGraph**. Whether to show the smaller range graph below the main graph.
141
+ **fullscreen**
142
+ If true, displays the graph in fullscreen mode.
104
143
 
105
- **showRangeSelectors**. Whether to show the top bar with range selection (eg last day button) and other options.
144
+ **bodyHeight**
145
+ Sets the height of the graph body (i.e., excluding range graph, series controls, etc.).
106
146
 
107
- **showSeriesKey**. Whether to show the key of which series have which colors.
147
+ **height**
148
+ Sets the height of the entire graph.
108
149
 
109
- **showGrid**
150
+ **width**
151
+ Sets the width of the graph.
152
+
153
+ ### Display Options
154
+
155
+ **showAxes**
156
+ Whether to show the axes on the graph.
157
+
158
+ **showRangeGraph**
159
+ Whether to show the smaller range graph below the main graph.
160
+
161
+ **showRangeSelectors**
162
+ Whether to show the top bar with range selection (e.g., "last day" button) and other options.
163
+
164
+ **showSeriesKey**
165
+ Whether to show the key of which series have which colors.
110
166
 
111
167
  **showTooltips**
168
+ Whether to display tooltips when hovering over data points.
112
169
 
113
- **boundsSelectionEnabled**
170
+ **showGrid**
171
+ Whether to show grid lines on the graph.
172
+
173
+ **showAxisColors**
174
+ Whether to color-code axes based on the series they represent.
175
+
176
+ **bigLabels**
177
+ If true, uses larger text for labels.
178
+
179
+ **xTickUnit**
180
+ Specifies the unit for x-axis ticks. Currently supports 'year'.
181
+
182
+ **xAxisIntegersOnly**
183
+ If true, only displays integer values on the x-axis.
184
+
185
+ **clockStyle**
186
+ Format for displaying time, either '12h' or '24h'.
187
+
188
+ **timeZone**
189
+ Time zone for date/time display. Can be 'local', 'utc', or a full timezone string.
190
+
191
+ **markRangeGraphDates**
192
+ Whether to mark significant dates on the range graph.
114
193
 
115
194
  **tooltipOptions**
195
+ Configures tooltip appearance and behavior with properties including:
196
+ - `includeSeriesLabel`: Whether to show series name in tooltip
197
+ - `includeXLabel`: Whether to show x-axis label in tooltip
198
+ - `includeYLabel`: Whether to show y-axis label in tooltip
199
+ - `includeXValue`: Whether to show x-axis value in tooltip
200
+ - `includeYValue`: Whether to show y-axis value in tooltip
201
+ - `floating`: Whether tooltip floats or is fixed position
202
+ - `alwaysFixedPosition`: Forces tooltip to always use fixed position
203
+ - `floatPosition`: Placement of floating tooltip ('top' or 'bottom')
204
+ - `floatDelta`: Pixel offset for floating tooltip positioning
205
+ - `savingDisabled`: Prevents tooltip settings from being saved
206
+ - `customTooltip`: A react component to use as a custom tooltip. See [examples/custom_tooltips_graph.js](examples/custom_tooltips_graph.js) for an example. If used in conjunction with `combineTooltips`, see [examples/combined_tooltips_graph.js](examples/combined_tooltips_graph.js)
207
+ - `combineTooltips`: If true, combines multiple tooltips into one when multiple series are shown. Can alternatively be set to a threshold in pixels for how close values need to be in order to be combined.
208
+
209
+ **customBoundsSelectors**
210
+ Array of custom range selector objects with properties:
211
+ - `label`: Display text for the selector
212
+ - `calculator`: Function that determines the bounds
213
+ - `datesOnly`: If true, only works with date values
214
+
215
+ **customBoundsSelectorsOnly**
216
+ If true, only displays custom bounds selectors.
217
+
218
+ **defaultBoundsCalculator**
219
+ String identifier for the default bounds calculator to use.
220
+
221
+ **defaultShowOptions**
222
+ Default visibility of the options panel.
223
+
224
+ **defaultShowIndividualPoints**
225
+ Default setting for showing individual data points.
226
+
227
+ **defaultShowSidebar**
228
+ Default visibility of the sidebar.
229
+
230
+ **defaultShowAnnotations**
231
+ Default visibility of annotations.
232
+
233
+ **defaultLineWidth**
234
+ Default width of the lines in the graph.
235
+
236
+ **boundsSelectionEnabled**
237
+ Whether to enable the bounds selection feature.
238
+
239
+ **sidebarEnabled**
240
+ Whether to enable the sidebar.
116
241
 
117
- **customBoundsSelectors**
242
+ **percentile**
243
+ Sets the percentile value for calculations.
118
244
 
119
- ### Usage from ruby
120
245
 
121
- ### Usage from python
246
+ ### Advanced Features
247
+
248
+ **tooltipOptions**
249
+ Configures tooltip appearance and behavior with properties including:
250
+ - `includeSeriesLabel`: Whether to show series name in tooltip
251
+ - `includeXLabel`: Whether to show x-axis label in tooltip
252
+ - `includeYLabel`: Whether to show y-axis label in tooltip
253
+ - `includeXValue`: Whether to show x-axis value in tooltip
254
+ - `includeYValue`: Whether to show y-axis value in tooltip
255
+ - `floating`: Whether tooltip floats or is fixed position
256
+ - `alwaysFixedPosition`: Forces tooltip to always use fixed position
257
+ - `floatPosition`: Placement of floating tooltip ('top' or 'bottom')
258
+ - `floatDelta`: Pixel offset for floating tooltip positioning
259
+ - `savingDisabled`: Prevents tooltip settings from being saved
260
+
261
+ **customBoundsSelectors**
262
+ Array of custom range selector objects with properties:
263
+ - `label`: Display text for the selector
264
+ - `calculator`: Function that determines the bounds
265
+ - `datesOnly`: If true, only works with date values
266
+
267
+ **customBoundsSelectorsOnly**
268
+ If true, only displays custom bounds selectors.
269
+
270
+ **defaultBoundsCalculator**
271
+ String identifier for the default bounds calculator to use.
272
+
273
+ **annotations**
274
+ Array of annotation objects to display on the graph with properties:
275
+ - `x`: Position on x-axis (string, number, or Date) where annotation should appear
276
+ - `xEnd`: Optional end position for range annotations
277
+ - `series`: Optional array of series names the annotation applies to
278
+ - `content`: Text content of the annotation
279
+
280
+ **draggablePoints**
281
+ Array of interactive point objects with properties:
282
+ - `x`: X-coordinate position
283
+ - `y`: Y-coordinate position
284
+ - `radius`: Optional size of the point
285
+ - `fillColor`: Optional interior color
286
+ - `strokeColor`: Optional outline color
287
+ - `strokeWidth`: Optional outline width
288
+ - `onClick`: Optional click handler function
289
+ - `onDoubleClick`: Optional double-click handler function
290
+
291
+ **verticalLines**
292
+ Array of vertical line objects to display on the graph with properties:
293
+ - `x`: X-coordinate position where the line should appear
294
+ - `color`: Optional line color
295
+ - `width`: Optional line width
296
+ - `markTop`: Whether to add a marker at the top of the line
297
+ - `style`: Optional styling object for the line
298
+ - `markerStyle`: Optional styling object for the marker
122
299
 
123
300
  ## Developing
124
301
  Other than an `npm install`, you'll need to install rust and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/).
@@ -41,7 +41,7 @@ export default class ContextMenu extends React.PureComponent {
41
41
 
42
42
  const style = { left: x, top: y, width: '150px'};
43
43
 
44
- if (!showing || !value || value.toLocaleString() === 'Invalid Date') {
44
+ if (!showing || !value || value.toLocaleString() === 'Invalid Date' || isNaN(x) || isNaN(y)) {
45
45
  return null;
46
46
  }
47
47
 
@@ -61,9 +61,9 @@ export default class ContextMenu extends React.PureComponent {
61
61
 
62
62
  ContextMenu.propTypes = {
63
63
  contextMenu: PropTypes.shape({
64
- x: PropTypes.number.isRequired,
65
- y: PropTypes.number.isRequired,
66
- showing: PropTypes.bool.isRequired,
64
+ x: PropTypes.number,
65
+ y: PropTypes.number,
66
+ showing: PropTypes.bool,
67
67
  value: PropTypes.oneOfType([
68
68
  PropTypes.instanceOf(Date),
69
69
  PropTypes.number,
@@ -25,6 +25,10 @@ function getYLabelContent({ yLabel, y, fullYPrecision}) {
25
25
  }
26
26
  }
27
27
 
28
+ if (typeof yLabel === 'object') {
29
+ return formatY(y);
30
+ }
31
+
28
32
  return yLabel || formatY(y);
29
33
  }
30
34
 
@@ -73,7 +77,8 @@ TooltipLabel.propTypes = {
73
77
  export default class Tooltip extends React.PureComponent {
74
78
 
75
79
  render() {
76
- let height = 12*3 + 6;
80
+ const textPadding = 3;
81
+ let height = 12*3 + 2*textPadding;
77
82
 
78
83
  if (!this.props.includeSeriesLabel) {
79
84
  height -= 12;
@@ -91,7 +96,7 @@ export default class Tooltip extends React.PureComponent {
91
96
  const halfHeight = height/2;
92
97
  const caretPadding = 4;
93
98
 
94
- const textTop = -halfHeight + 3;
99
+ const textTop = -halfHeight + textPadding;
95
100
 
96
101
  const formatXOptions = {
97
102
  clockStyle: this.props.clockStyle,
@@ -107,76 +112,187 @@ export default class Tooltip extends React.PureComponent {
107
112
  formatXOptions
108
113
  };
109
114
 
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;
115
+ const preparedTooltips = this.props.tooltips.map((tooltip) => {
116
+ const { x, y, pixelY, pixelX, series, index, xLabel, yLabel, fullYPrecision } = tooltip;
117
117
 
118
- let fixedPosition = this.props.elementWidth < (width + 2*caretSize + 2*caretPadding);
118
+ const axisLabel = (series.name || series.yKey || index).toString();
119
+ let width = Math.max(axisLabel.length, (xLabel || formatX(x, formatXOptions)).length + 4, getYLabelContent({ yLabel, y, fullYPrecision}).length + 4) * 7.5;
120
+ if (series.tooltipWidth) {
121
+ width = series.tooltipWidth;
122
+ }
119
123
 
120
- let multiplier = 1;
121
- if (pixelX >= this.props.elementWidth - (width + 2*caretSize + caretPadding)) {
122
- multiplier = -1;
123
- }
124
+ let fixedPosition = this.props.elementWidth < (width + 2*caretSize + 2*caretPadding);
124
125
 
125
- if (pixelX < width + 2*caretSize + caretPadding && multiplier === -1) {
126
- fixedPosition = true;
127
- }
126
+ let multiplier = 1;
127
+ if (pixelX >= this.props.elementWidth - (width + 2*caretSize + caretPadding)) {
128
+ multiplier = -1;
129
+ }
128
130
 
129
- if (y === null) {
130
- fixedPosition = true;
131
- }
131
+ if (pixelX < width + 2*caretSize + caretPadding && multiplier === -1) {
132
+ fixedPosition = true;
133
+ }
132
134
 
133
- let textLeft = caretSize + caretPadding;
134
- if (multiplier < 0) {
135
- textLeft = -width - textLeft;
136
- } else {
137
- textLeft += 6;
138
- }
135
+ if (y === null) {
136
+ fixedPosition = true;
137
+ }
139
138
 
140
- if (!isFinite(pixelX)) {
141
- return null;
142
- }
139
+ if (this.props.alwaysFixedPosition) {
140
+ fixedPosition = true;
141
+ }
143
142
 
144
- const transform = `translate(${pixelX},${pixelY})`;
143
+ let textLeft = caretSize + caretPadding;
144
+ if (multiplier < 0) {
145
+ textLeft = -width - textLeft;
146
+ } else {
147
+ textLeft += 6;
148
+ }
145
149
 
146
- const commonLabelProps = {
147
- fullYPrecision: fullYPrecision || this.props.maxPrecision,
148
- x,
149
- y,
150
- axisLabel,
151
- xLabel,
152
- yLabel,
153
- ...passThroughProps
154
- };
150
+ if (!isFinite(pixelX)) {
151
+ return null;
152
+ }
155
153
 
156
- // display in a fixed position if not wide enough
157
- if (fixedPosition || this.props.alwaysFixedPosition) {
158
- textLeft = 6;
154
+ const transform = `translate(${pixelX},${pixelY})`;
159
155
 
160
- let baseLeft = this.props.elementWidth/2 - width/2;
156
+ const commonLabelProps = {
157
+ fullYPrecision: fullYPrecision || this.props.maxPrecision,
158
+ x,
159
+ y,
160
+ axisLabel,
161
+ xLabel,
162
+ yLabel,
163
+ ...passThroughProps
164
+ };
161
165
 
162
- if (width > this.props.elementWidth && !this.props.floating) {
163
- baseLeft -= Y_AXIS_WIDTH*this.props.axisCount/2;
164
- }
166
+ let yTranslation = 0;
167
+ let baseLeft;
165
168
 
166
- let yTranslation = 18;
169
+ if (fixedPosition) {
170
+ textLeft = 6;
167
171
 
168
- if (this.props.floating) {
169
- if (this.props.floatPosition === 'bottom') {
170
- yTranslation = this.props.elementHeight + halfHeight + 4;
171
- } else {
172
- yTranslation = -height;
173
- }
172
+ baseLeft = this.props.elementWidth / 2 - width / 2;
174
173
 
175
- if (this.props.floatDelta) {
176
- yTranslation += this.props.floatDelta;
177
- }
178
- }
174
+ if (width > this.props.elementWidth && !this.props.floating) {
175
+ baseLeft -= Y_AXIS_WIDTH * this.props.axisCount / 2;
176
+ }
177
+
178
+ yTranslation = 18;
179
+
180
+ if (this.props.floating) {
181
+ if (this.props.floatPosition === 'bottom') {
182
+ yTranslation = this.props.elementHeight + halfHeight + 4;
183
+ } else {
184
+ yTranslation = -height;
185
+ }
186
+
187
+ if (this.props.floatDelta) {
188
+ yTranslation += this.props.floatDelta;
189
+ }
190
+ }
191
+ }
192
+
193
+ return {
194
+ ...tooltip,
195
+ label: axisLabel,
196
+ indexInAxis: series.axis.series.indexOf(series),
197
+ axisLabel,
198
+ width,
199
+ fixedPosition,
200
+ multiplier,
201
+ textLeft,
202
+ transform,
203
+ commonLabelProps,
204
+ textTop,
205
+ height,
206
+ caretSize,
207
+ halfHeight,
208
+ caretPadding,
209
+ yTranslation,
210
+ baseLeft
211
+ };
212
+ });
213
+
214
+ const CustomTooltipComponent = this.props.customTooltip;
215
+
216
+ let groupedTooltips;
217
+ if (this.props.combineTooltips) {
218
+ let combinationThreshold = 50; // in px how close tooltips should be to combine
219
+ if (typeof this.props.combineTooltips === 'number') {
220
+ combinationThreshold = this.props.combineTooltips;
221
+ }
179
222
 
223
+ groupedTooltips = [];
224
+
225
+ for (let tooltip of preparedTooltips) {
226
+ let added = false;
227
+ for (let group of groupedTooltips) {
228
+ if (Math.abs(group.pixelX - tooltip.pixelX) <= combinationThreshold) {
229
+ group.tooltips.push(tooltip);
230
+ if (tooltip.pixelX > group.pixelX) {
231
+ group.pixelX = tooltip.pixelX;
232
+ group.multiplier = tooltip.multiplier;
233
+ }
234
+
235
+ if (tooltip.pixelY < group.pixelY) {
236
+ group.pixelY = tooltip.pixelY;
237
+ }
238
+
239
+ added = true;
240
+ break;
241
+ }
242
+ }
243
+
244
+ if (!added) {
245
+ groupedTooltips.push({
246
+ pixelX: tooltip.pixelX,
247
+ pixelY: tooltip.pixelY,
248
+ multiplier: tooltip.multiplier,
249
+ tooltips: [tooltip]
250
+ });
251
+ }
252
+ }
253
+
254
+ for (let group of groupedTooltips) {
255
+ let totalHeight = 0;
256
+ let maxWidth = 0;
257
+
258
+ // sort by indexInAxis
259
+ group.tooltips.sort((a, b) => a.indexInAxis - b.indexInAxis);
260
+
261
+ for (let i = 0; i < group.tooltips.length; i++) {
262
+ group.tooltips[i].textTop = totalHeight;
263
+ totalHeight += group.tooltips[i].height;
264
+ maxWidth = Math.max(maxWidth, group.tooltips[i].width);
265
+ }
266
+
267
+ for (let i = 0; i < group.tooltips.length; i++) {
268
+ group.tooltips[i].textTop -= totalHeight/2;
269
+ group.tooltips[i].textTop += textPadding;
270
+ }
271
+
272
+ group.height = totalHeight;
273
+ group.halfHeight = totalHeight / 2;
274
+ group.caretSize = caretSize;
275
+ group.width = maxWidth;
276
+ }
277
+ }
278
+
279
+ return (
280
+ <div className="grapher-tooltip">
281
+ <svg>
282
+ {
283
+ preparedTooltips.map((tooltip, i) => {
284
+ const { color, fixedPosition, width, transform, baseLeft, commonLabelProps, yTranslation, multiplier, textLeft, textTop } = tooltip;
285
+
286
+ if (this.props.customTooltip || groupedTooltips) {
287
+ return (
288
+ <g key={i} transform={transform} className="tooltip-item">
289
+ <circle r={4} fill={color}/>
290
+ </g>
291
+ );
292
+ }
293
+
294
+ // display in a fixed position if not wide enough
295
+ if (fixedPosition) {
180
296
  return (
181
297
  <g key={i} className="tooltip-item tooltip-item-fixed">
182
298
  <circle r={4} fill={color} transform={transform} />
@@ -207,7 +323,40 @@ export default class Tooltip extends React.PureComponent {
207
323
  );
208
324
  })
209
325
  }
326
+
327
+ {
328
+ !this.props.customTooltip && groupedTooltips &&
329
+ groupedTooltips.map(({ tooltips, pixelX, pixelY, halfHeight, multiplier, color, width }, i) =>
330
+ <g key={i} transform={`translate(${pixelX},${pixelY})`} className="tooltip-item">
331
+ <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`} />
332
+
333
+ {
334
+ tooltips.map((tooltip, j) =>
335
+ <TooltipLabel
336
+ key={j}
337
+ textTop={tooltip.textTop}
338
+ textLeft={tooltip.textLeft}
339
+ {...tooltip.commonLabelProps}
340
+ />
341
+ )
342
+ }
343
+ </g>
344
+ )
345
+ }
210
346
  </svg>
347
+
348
+ {
349
+ this.props.customTooltip &&
350
+ (groupedTooltips || preparedTooltips).map((tooltip, i) =>
351
+ <div
352
+ key={i}
353
+ className="custom-tooltip-container"
354
+ style={{top: tooltip.pixelY, left: tooltip.pixelX}}
355
+ >
356
+ <CustomTooltipComponent {...tooltip} />
357
+ </div>
358
+ )
359
+ }
211
360
  </div>
212
361
  );
213
362
  }
@@ -233,7 +382,7 @@ Tooltip.propTypes = {
233
382
  pixelY: PropTypes.number,
234
383
  color: PropTypes.string,
235
384
  xLabel: PropTypes.string,
236
- yLabel: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
385
+ yLabel: PropTypes.any,
237
386
  fullYPrecision: PropTypes.bool
238
387
  })),
239
388
  axisCount: PropTypes.number.isRequired,