| 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 |