OLD | NEW |
1 // | 1 // |
2 // Copyright 2014 Google Inc. All rights reserved. | 2 // Copyright 2014 Google Inc. All rights reserved. |
3 // | 3 // |
4 // Use of this source code is governed by a BSD-style | 4 // Use of this source code is governed by a BSD-style |
5 // license that can be found in the LICENSE file or at | 5 // license that can be found in the LICENSE file or at |
6 // https://developers.google.com/open-source/licenses/bsd | 6 // https://developers.google.com/open-source/licenses/bsd |
7 // | 7 // |
8 | 8 |
9 part of charted.charts; | 9 part of charted.charts; |
10 | 10 |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
51 /// single row. Eg: Would not work with a water-fall chart. | 51 /// single row. Eg: Would not work with a water-fall chart. |
52 /// | 52 /// |
53 class Hovercard implements ChartBehavior { | 53 class Hovercard implements ChartBehavior { |
54 final HovercardBuilder builder; | 54 final HovercardBuilder builder; |
55 | 55 |
56 bool _isMouseTracking; | 56 bool _isMouseTracking; |
57 bool _isMultiValue; | 57 bool _isMultiValue; |
58 bool _showDimensionTitle; | 58 bool _showDimensionTitle; |
59 Iterable<int> _columnsToShow; | 59 Iterable<int> _columnsToShow; |
60 | 60 |
61 Iterable placementOrder = | 61 Iterable placementOrder = const [ |
62 const['orientation', 'top', 'right', 'bottom', 'left', 'orientation']; | 62 'orientation', |
| 63 'top', |
| 64 'right', |
| 65 'bottom', |
| 66 'left', |
| 67 'orientation' |
| 68 ]; |
63 int offset = 20; | 69 int offset = 20; |
64 | 70 |
65 ChartArea _area; | 71 ChartArea _area; |
66 ChartState _state; | 72 ChartState _state; |
67 SubscriptionsDisposer _disposer = new SubscriptionsDisposer(); | 73 SubscriptionsDisposer _disposer = new SubscriptionsDisposer(); |
68 | 74 |
69 Element _hovercardRoot; | 75 Element _hovercardRoot; |
70 | 76 |
71 Hovercard({ | 77 Hovercard( |
72 bool isMouseTracking, | 78 {bool isMouseTracking, |
73 bool isMultiValue: false, | 79 bool isMultiValue: false, |
74 bool showDimensionTitle: false, | 80 bool showDimensionTitle: false, |
75 List columnsToShow: const [], | 81 List columnsToShow: const [], |
76 this.builder}) { | 82 this.builder}) { |
77 _isMouseTracking = isMouseTracking; | 83 _isMouseTracking = isMouseTracking; |
78 _isMultiValue = isMultiValue; | 84 _isMultiValue = isMultiValue; |
79 _showDimensionTitle = showDimensionTitle; | 85 _showDimensionTitle = showDimensionTitle; |
80 _columnsToShow = columnsToShow; | 86 _columnsToShow = columnsToShow; |
81 } | 87 } |
82 | 88 |
83 void init(ChartArea area, Selection _, Selection __) { | 89 void init(ChartArea area, Selection _, Selection __) { |
84 _area = area; | 90 _area = area; |
85 _state = area.state; | 91 _state = area.state; |
86 | 92 |
87 // If we don't have state, fall back to mouse events. | 93 // If we don't have state, fall back to mouse events. |
88 _isMouseTracking = | 94 _isMouseTracking = |
89 _isMouseTracking == true || _state == null || _area is LayoutArea; | 95 _isMouseTracking == true || _state == null || _area is LayoutArea; |
90 | 96 |
91 // Subscribe to events. | 97 // Subscribe to events. |
92 if (_isMouseTracking) { | 98 if (_isMouseTracking) { |
93 _disposer.addAll([ | 99 _disposer.addAll([ |
94 _area.onValueMouseOver.listen(_handleMouseOver), | 100 _area.onValueMouseOver.listen(_handleMouseOver), |
95 _area.onValueMouseOut.listen(_handleMouseOut) | 101 _area.onValueMouseOut.listen(_handleMouseOut) |
96 ]); | 102 ]); |
97 } else { | 103 } else { |
98 _disposer.add(_state.changes.listen(_handleStateChange)); | 104 _disposer.add(_state.changes.listen(_handleStateChange)); |
99 } | 105 } |
100 } | 106 } |
101 | 107 |
102 void dispose() { | 108 void dispose() { |
103 _disposer.dispose(); | 109 _disposer.dispose(); |
104 if (_hovercardRoot != null) _hovercardRoot.remove(); | 110 if (_hovercardRoot != null) _hovercardRoot.remove(); |
105 } | 111 } |
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
199 // update position and offset accordingly. | 205 // update position and offset accordingly. |
200 if (area.dimensionsUsingBands.contains(dimensionCol)) { | 206 if (area.dimensionsUsingBands.contains(dimensionCol)) { |
201 assert(dimensionScale is OrdinalScale); | 207 assert(dimensionScale is OrdinalScale); |
202 dimensionOffset = (dimensionScale as OrdinalScale).rangeBand / 2; | 208 dimensionOffset = (dimensionScale as OrdinalScale).rangeBand / 2; |
203 dimensionCenterOffset = dimensionOffset; | 209 dimensionCenterOffset = dimensionOffset; |
204 } | 210 } |
205 | 211 |
206 var rowData = area.data.rows.elementAt(row), | 212 var rowData = area.data.rows.elementAt(row), |
207 measurePosition = 0, | 213 measurePosition = 0, |
208 isNegative = false, | 214 isNegative = false, |
209 dimensionPosition = dimensionScale.scale( | 215 dimensionPosition = dimensionScale |
210 rowData.elementAt(dimensionCol)) + dimensionCenterOffset; | 216 .scale(rowData.elementAt(dimensionCol)) + |
| 217 dimensionCenterOffset; |
211 | 218 |
212 if (_isMultiValue) { | 219 if (_isMultiValue) { |
213 var max = SMALL_INT_MIN, | 220 var max = SMALL_INT_MIN, min = SMALL_INT_MAX; |
214 min = SMALL_INT_MAX; | |
215 area.config.series.forEach((ChartSeries series) { | 221 area.config.series.forEach((ChartSeries series) { |
216 CartesianRenderer renderer = series.renderer; | 222 CartesianRenderer renderer = series.renderer; |
217 Extent extent = renderer.extentForRow(rowData); | 223 Extent extent = renderer.extentForRow(rowData); |
218 if (extent.min < min) min = extent.min; | 224 if (extent.min < min) min = extent.min; |
219 if (extent.max > max) max = extent.max; | 225 if (extent.max > max) max = extent.max; |
220 measurePosition = measureScale.scale(max); | 226 measurePosition = measureScale.scale(max); |
221 isNegative = max < 0; | 227 isNegative = max < 0; |
222 }); | 228 }); |
223 } else { | 229 } else { |
224 var value = rowData.elementAt(column); | 230 var value = rowData.elementAt(column); |
225 isNegative = value < 0; | 231 isNegative = value < 0; |
226 measurePosition = measureScale.scale(rowData.elementAt(column)); | 232 measurePosition = measureScale.scale(rowData.elementAt(column)); |
227 } | 233 } |
228 | 234 |
229 if (area.config.isLeftAxisPrimary) { | 235 if (area.config.isLeftAxisPrimary) { |
230 _positionAtPoint(measurePosition, dimensionPosition, | 236 _positionAtPoint(measurePosition, dimensionPosition, 0, dimensionOffset, |
231 0, dimensionOffset, isNegative, true); | 237 isNegative, true); |
232 } else { | 238 } else { |
233 _positionAtPoint(dimensionPosition, measurePosition, | 239 _positionAtPoint(dimensionPosition, measurePosition, dimensionOffset, 0, |
234 dimensionOffset, 0, isNegative, false); | 240 isNegative, false); |
235 } | 241 } |
236 } | 242 } |
237 | 243 |
238 void _positionAtPoint(num x, num y, | 244 void _positionAtPoint(num x, num y, num xBand, num yBand, bool negative, |
239 num xBand, num yBand, bool negative, [bool isLeftPrimary = false]) { | 245 [bool isLeftPrimary = false]) { |
240 var rect = _hovercardRoot.getBoundingClientRect(), | 246 var rect = _hovercardRoot.getBoundingClientRect(), |
241 width = rect.width, | 247 width = rect.width, |
242 height = rect.height, | 248 height = rect.height, |
243 scaleToHostY = | 249 scaleToHostY = (_area.theme.padding != null |
244 (_area.theme.padding != null ? _area.theme.padding.top : 0) + | 250 ? _area.theme.padding.top |
| 251 : 0) + |
245 (_area.layout.renderArea.y), | 252 (_area.layout.renderArea.y), |
246 scaleToHostX = | 253 scaleToHostX = (_area.theme.padding != null |
247 (_area.theme.padding != null ? _area.theme.padding.start: 0) + | 254 ? _area.theme.padding.start |
| 255 : 0) + |
248 (_area.layout.renderArea.x), | 256 (_area.layout.renderArea.x), |
249 renderAreaHeight = _area.layout.renderArea.height, | 257 renderAreaHeight = _area.layout.renderArea.height, |
250 renderAreaWidth = _area.layout.renderArea.width; | 258 renderAreaWidth = _area.layout.renderArea.width; |
251 | 259 |
252 if (scaleToHostY < 0) scaleToHostY = 0; | 260 if (scaleToHostY < 0) scaleToHostY = 0; |
253 if (scaleToHostX < 0) scaleToHostX = 0; | 261 if (scaleToHostX < 0) scaleToHostX = 0; |
254 | 262 |
255 num top = 0, left = 0; | 263 num top = 0, left = 0; |
256 for (int i = 0, len = placementOrder.length; i < len; ++i) { | 264 for (int i = 0, len = placementOrder.length; i < len; ++i) { |
257 String placement = placementOrder.elementAt(i); | 265 String placement = placementOrder.elementAt(i); |
(...skipping 15 matching lines...) Expand all Loading... |
273 top = isLeftPrimary ? y - height / 2 : y; | 281 top = isLeftPrimary ? y - height / 2 : y; |
274 left = negative ? x + xBand : x - (width + xBand); | 282 left = negative ? x + xBand : x - (width + xBand); |
275 } | 283 } |
276 if (placement == 'bottom') { | 284 if (placement == 'bottom') { |
277 top = negative ? y - (height + yBand) : y + yBand; | 285 top = negative ? y - (height + yBand) : y + yBand; |
278 left = isLeftPrimary ? x - width : x - width / 2; | 286 left = isLeftPrimary ? x - width : x - width / 2; |
279 } | 287 } |
280 | 288 |
281 // Check if the popup is contained in the RenderArea. | 289 // Check if the popup is contained in the RenderArea. |
282 // If not, try other placements. | 290 // If not, try other placements. |
283 if (top > 0 && left > 0 && | 291 if (top > 0 && |
284 top + height < renderAreaHeight && left + width < renderAreaWidth) { | 292 left > 0 && |
| 293 top + height < renderAreaHeight && |
| 294 left + width < renderAreaWidth) { |
285 break; | 295 break; |
286 } | 296 } |
287 } | 297 } |
288 | 298 |
289 _hovercardRoot.style | 299 _hovercardRoot.style |
290 ..top = '${top + scaleToHostY}px' | 300 ..top = '${top + scaleToHostY}px' |
291 ..left = '${left + scaleToHostX}px'; | 301 ..left = '${left + scaleToHostX}px'; |
292 } | 302 } |
293 | 303 |
294 Element _createTooltip(int column, int row) { | 304 Element _createTooltip(int column, int row) { |
295 var element = new Element.div(); | 305 var element = new Element.div(); |
296 if (_showDimensionTitle) { | 306 if (_showDimensionTitle) { |
297 var titleElement = new Element.div() | 307 var titleElement = new Element.div() |
298 ..className = 'hovercard-title' | 308 ..className = 'hovercard-title' |
299 ..text = _getDimensionTitle(column, row); | 309 ..text = _getDimensionTitle(column, row); |
300 element.append(titleElement); | 310 element.append(titleElement); |
301 } | 311 } |
302 | 312 |
303 var measureVals = _getMeasuresData(column, row); | 313 var measureVals = _getMeasuresData(column, row); |
304 measureVals.forEach((ChartLegendItem item) { | 314 measureVals.forEach((ChartLegendItem item) { |
305 var labelElement = new Element.div() | 315 var labelElement = new Element.div() |
306 ..className = 'hovercard-measure-label' | 316 ..className = 'hovercard-measure-label' |
307 ..text = item.label, | 317 ..text = item.label, |
308 valueElement = new Element.div() | 318 valueElement = new Element.div() |
309 ..style.color = item.color | 319 ..style.color = item.color |
310 ..className = 'hovercard-measure-value' | 320 ..className = 'hovercard-measure-value' |
311 ..text = item.value, | 321 ..text = item.value, |
312 measureElement = new Element.div() | 322 measureElement = new Element.div() |
313 ..append(labelElement) | 323 ..append(labelElement) |
(...skipping 28 matching lines...) Expand all Loading... |
342 }); | 352 }); |
343 } else { | 353 } else { |
344 measureVals.add(_createHovercardItem(column, row)); | 354 measureVals.add(_createHovercardItem(column, row)); |
345 } | 355 } |
346 | 356 |
347 return measureVals; | 357 return measureVals; |
348 } | 358 } |
349 | 359 |
350 ChartLegendItem _createHovercardItem(int column, int row) { | 360 ChartLegendItem _createHovercardItem(int column, int row) { |
351 var rowData = _area.data.rows.elementAt(row), | 361 var rowData = _area.data.rows.elementAt(row), |
352 columns = _area.data.columns, | 362 columns = _area.data.columns, |
353 spec = columns.elementAt(column), | 363 spec = columns.elementAt(column), |
354 colorKey = _area.useRowColoring ? row : column, | 364 colorKey = _area.useRowColoring ? row : column, |
355 formatter = _getFormatterForColumn(column), | 365 formatter = _getFormatterForColumn(column), |
356 label = _area.useRowColoring | 366 label = _area.useRowColoring |
357 ? rowData.elementAt(_area.config.dimensions.first) | 367 ? rowData.elementAt(_area.config.dimensions.first) |
358 : spec.label; | 368 : spec.label; |
359 return new ChartLegendItem( | 369 return new ChartLegendItem( |
360 label: label, | 370 label: label, |
361 value: formatter(rowData.elementAt(column)), | 371 value: formatter(rowData.elementAt(column)), |
362 color: _area.theme.getColorForKey(colorKey)); | 372 color: _area.theme.getColorForKey(colorKey)); |
363 } | 373 } |
364 | 374 |
365 String _getDimensionTitle(int column, int row) { | 375 String _getDimensionTitle(int column, int row) { |
366 var rowData = _area.data.rows.elementAt(row), | 376 var rowData = _area.data.rows.elementAt(row), |
367 colSpec = _area.data.columns.elementAt(column); | 377 colSpec = _area.data.columns.elementAt(column); |
368 if (_area.useRowColoring) { | 378 if (_area.useRowColoring) { |
369 return colSpec.label; | 379 return colSpec.label; |
370 } else { | 380 } else { |
371 var count = (_area as CartesianArea).useTwoDimensionAxes ? 2 : 1, | 381 var count = (_area as CartesianArea).useTwoDimensionAxes ? 2 : 1, |
372 dimensions = _area.config.dimensions.take(count); | 382 dimensions = _area.config.dimensions.take(count); |
373 return dimensions.map( | 383 return dimensions |
374 (int c) => | 384 .map((int c) => _getFormatterForColumn(c)(rowData.elementAt(c))) |
375 _getFormatterForColumn(c)(rowData.elementAt(c))).join(', '); | 385 .join(', '); |
376 } | 386 } |
377 } | 387 } |
378 | 388 |
379 // TODO: Move this to a common place? | 389 // TODO: Move this to a common place? |
380 Scale _getScaleForColumn(int column) { | 390 Scale _getScaleForColumn(int column) { |
381 var series = _area.config.series.firstWhere( | 391 var series = _area.config.series.firstWhere( |
382 (ChartSeries x) => x.measures.contains(column), orElse: () => null); | 392 (ChartSeries x) => x.measures.contains(column), |
| 393 orElse: () => null); |
383 return series != null | 394 return series != null |
384 ? (_area as CartesianArea).measureScales(series).first | 395 ? (_area as CartesianArea).measureScales(series).first |
385 : null; | 396 : null; |
386 } | 397 } |
387 | 398 |
388 // TODO: Move this to a common place? | 399 // TODO: Move this to a common place? |
389 FormatFunction _getFormatterForColumn(int column) { | 400 FormatFunction _getFormatterForColumn(int column) { |
390 var formatter = _area.data.columns.elementAt(column).formatter; | 401 var formatter = _area.data.columns.elementAt(column).formatter; |
391 if (formatter == null && _area is CartesianArea) { | 402 if (formatter == null && _area is CartesianArea) { |
392 var scale = _getScaleForColumn(column); | 403 var scale = _getScaleForColumn(column); |
393 if (scale != null) { | 404 if (scale != null) { |
394 formatter = scale.createTickFormatter(); | 405 formatter = scale.createTickFormatter(); |
395 } | 406 } |
396 } | 407 } |
397 if (formatter == null) { | 408 if (formatter == null) { |
398 // Formatter function must return String. Default to identity function | 409 // Formatter function must return String. Default to identity function |
399 // but return the toString() instead. | 410 // but return the toString() instead. |
400 formatter = (x) => x.toString(); | 411 formatter = (x) => x.toString(); |
401 } | 412 } |
402 return formatter; | 413 return formatter; |
403 } | 414 } |
404 } | 415 } |
OLD | NEW |