| Index: charted/lib/charts/behaviors/hovercard.dart
|
| diff --git a/charted/lib/charts/behaviors/hovercard.dart b/charted/lib/charts/behaviors/hovercard.dart
|
| deleted file mode 100644
|
| index f01d2ac7292919990e427d4b0d3397b961ca9e3a..0000000000000000000000000000000000000000
|
| --- a/charted/lib/charts/behaviors/hovercard.dart
|
| +++ /dev/null
|
| @@ -1,404 +0,0 @@
|
| -//
|
| -// Copyright 2014 Google Inc. All rights reserved.
|
| -//
|
| -// Use of this source code is governed by a BSD-style
|
| -// license that can be found in the LICENSE file or at
|
| -// https://developers.google.com/open-source/licenses/bsd
|
| -//
|
| -
|
| -part of charted.charts;
|
| -
|
| -typedef Element HovercardBuilder(int column, int row);
|
| -
|
| -///
|
| -/// Subscribe to events on the chart and display more information about the
|
| -/// visualization that is hovered or highlighted.
|
| -///
|
| -/// This behavior supports two event modes:
|
| -/// (1) State change: Subscribes to changes on ChartState
|
| -/// (2) Mouse tracking: Subscribe to onValueMouseOver and onValueMouseOut
|
| -///
|
| -/// Supports two placement modes for mouse-tracking:
|
| -/// (1) Place relative to mouse position
|
| -/// (2) Place relative to the visualized measure value.
|
| -///
|
| -/// Supports two modes for displayed content:
|
| -/// (1) Show all measure values at the current dimension value.
|
| -/// (2) Show the hovered value only
|
| -///
|
| -/// Optionally, takes a builder that is passed row, column values that
|
| -/// can be used to build custom tooltip
|
| -///
|
| -/// What makes the positioning logic complex?
|
| -/// (1) Is this a CartesianArea?
|
| -/// (2) Does the CartesianArea use two dimensions or just one?
|
| -/// (3) Does the CartesianArea use "bands" along the axes?
|
| -/// (4) How does measure correspond to positioning? Are the bars stacked?
|
| -///
|
| -/// So, how is the position computed?
|
| -/// (1) Uses ChartConfig to figure out which renderers are being used, asks
|
| -/// for extent of the row to roughly get the position along measure axis.
|
| -/// (2) Position along dimension axes is computed separately based on how
|
| -/// many dimensions are being used and if any of them use bands.
|
| -///
|
| -/// Constraints and known issues:
|
| -/// (0) The implementation isn't complete yet! Specifically for CartesianArea
|
| -/// that uses two axes.
|
| -/// (1) Even with all the logic, single value mode does not work well
|
| -/// with StackedBarChartRenderer
|
| -/// (2) Only mouse relative positioning is supported on LayoutArea
|
| -/// (3) Positioning only works for renderers that determine extent given a
|
| -/// single row. Eg: Would not work with a water-fall chart.
|
| -///
|
| -class Hovercard implements ChartBehavior {
|
| - final HovercardBuilder builder;
|
| -
|
| - bool _isMouseTracking;
|
| - bool _isMultiValue;
|
| - bool _showDimensionTitle;
|
| - Iterable<int> _columnsToShow;
|
| -
|
| - Iterable placementOrder =
|
| - const['orientation', 'top', 'right', 'bottom', 'left', 'orientation'];
|
| - int offset = 20;
|
| -
|
| - ChartArea _area;
|
| - ChartState _state;
|
| - SubscriptionsDisposer _disposer = new SubscriptionsDisposer();
|
| -
|
| - Element _hovercardRoot;
|
| -
|
| - Hovercard({
|
| - bool isMouseTracking,
|
| - bool isMultiValue: false,
|
| - bool showDimensionTitle: false,
|
| - List columnsToShow: const [],
|
| - this.builder}) {
|
| - _isMouseTracking = isMouseTracking;
|
| - _isMultiValue = isMultiValue;
|
| - _showDimensionTitle = showDimensionTitle;
|
| - _columnsToShow = columnsToShow;
|
| - }
|
| -
|
| - void init(ChartArea area, Selection _, Selection __) {
|
| - _area = area;
|
| - _state = area.state;
|
| -
|
| - // If we don't have state, fall back to mouse events.
|
| - _isMouseTracking =
|
| - _isMouseTracking == true || _state == null || _area is LayoutArea;
|
| -
|
| - // Subscribe to events.
|
| - if (_isMouseTracking) {
|
| - _disposer.addAll([
|
| - _area.onValueMouseOver.listen(_handleMouseOver),
|
| - _area.onValueMouseOut.listen(_handleMouseOut)
|
| - ]);
|
| - } else {
|
| - _disposer.add(_state.changes.listen(_handleStateChange));
|
| - }
|
| - }
|
| -
|
| - void dispose() {
|
| - _disposer.dispose();
|
| - if (_hovercardRoot != null) _hovercardRoot.remove();
|
| - }
|
| -
|
| - void _handleMouseOver(ChartEvent e) {
|
| - _ensureHovercard();
|
| - _hovercardRoot.children.clear();
|
| - _hovercardRoot.append(builder != null
|
| - ? builder(e.column, e.row)
|
| - : _createTooltip(e.column, e.row));
|
| - _hovercardRoot.style
|
| - ..visibility = 'visible'
|
| - ..opacity = '1.0';
|
| - _updateTooltipPosition(evt: e);
|
| - }
|
| -
|
| - void _handleMouseOut(ChartEvent e) {
|
| - _ensureHovercard();
|
| - _hovercardRoot.style
|
| - ..visibility = 'hidden'
|
| - ..opacity = '$EPSILON';
|
| - }
|
| -
|
| - void _handleStateChange(Iterable<ChangeRecord> changes) {
|
| - _ensureHovercard();
|
| -
|
| - var value = _state.hovered;
|
| - if (_state.highlights.length == 1) {
|
| - value = _state.highlights.first;
|
| - }
|
| -
|
| - if (value == null) {
|
| - _hovercardRoot.style
|
| - ..visibility = 'hidden'
|
| - ..opacity = '$EPSILON';
|
| - } else {
|
| - _hovercardRoot.children.clear();
|
| - _hovercardRoot.append(builder != null
|
| - ? builder(value.first, value.last)
|
| - : _createTooltip(value.first, value.last));
|
| - _hovercardRoot.style
|
| - ..visibility = 'visible'
|
| - ..opacity = '1.0';
|
| - _updateTooltipPosition(column: value.first, row: value.last);
|
| - }
|
| - }
|
| -
|
| - void _ensureHovercard() {
|
| - if (_hovercardRoot != null) return;
|
| - _hovercardRoot = new Element.div();
|
| - _hovercardRoot.classes.add('hovercard');
|
| - if (_area.config.isRTL) {
|
| - _hovercardRoot.attributes['dir'] = 'rtl';
|
| - _hovercardRoot.classes.add('rtl');
|
| - }
|
| - _area.host.style.position = 'relative';
|
| - _area.host.append(_hovercardRoot);
|
| - }
|
| -
|
| - void _updateTooltipPosition({ChartEvent evt, int column, int row}) {
|
| - assert(evt != null || column != null && row != null);
|
| - if (_isMouseTracking && evt != null) {
|
| - _positionAtMousePointer(evt);
|
| - } else if (_area is CartesianArea) {
|
| - if ((_area as CartesianArea).useTwoDimensionAxes) {
|
| - _positionOnTwoDimensionCartesian(column, row);
|
| - } else {
|
| - _positionOnSingleDimensionCartesian(column, row);
|
| - }
|
| - } else {
|
| - _positionOnLayout(column, row);
|
| - }
|
| - }
|
| -
|
| - void _positionAtMousePointer(ChartEvent e) =>
|
| - _positionAtPoint(e.chartX, e.chartY, offset, offset, false, false);
|
| -
|
| - void _positionOnLayout(column, row) {
|
| - // Currently for layouts, when hovercard is triggered due to change
|
| - // in ChartState, we render hovercard in the middle of layout.
|
| - // TODO: Get bounding rect from LayoutRenderer and position relative to it.
|
| - }
|
| -
|
| - void _positionOnTwoDimensionCartesian(int column, int row) {
|
| - // TODO: Implement multi dimension positioning.
|
| - }
|
| -
|
| - void _positionOnSingleDimensionCartesian(int column, int row) {
|
| - CartesianArea area = _area;
|
| - var dimensionCol = area.config.dimensions.first,
|
| - dimensionScale = area.dimensionScales.first,
|
| - measureScale = _getScaleForColumn(column),
|
| - dimensionOffset = this.offset,
|
| - dimensionCenterOffset = 0;
|
| -
|
| - // If we are using bands on the one axis that is shown
|
| - // update position and offset accordingly.
|
| - if (area.dimensionsUsingBands.contains(dimensionCol)) {
|
| - assert(dimensionScale is OrdinalScale);
|
| - dimensionOffset = (dimensionScale as OrdinalScale).rangeBand / 2;
|
| - dimensionCenterOffset = dimensionOffset;
|
| - }
|
| -
|
| - var rowData = area.data.rows.elementAt(row),
|
| - measurePosition = 0,
|
| - isNegative = false,
|
| - dimensionPosition = dimensionScale.scale(
|
| - rowData.elementAt(dimensionCol)) + dimensionCenterOffset;
|
| -
|
| - if (_isMultiValue) {
|
| - var max = SMALL_INT_MIN,
|
| - min = SMALL_INT_MAX;
|
| - area.config.series.forEach((ChartSeries series) {
|
| - CartesianRenderer renderer = series.renderer;
|
| - Extent extent = renderer.extentForRow(rowData);
|
| - if (extent.min < min) min = extent.min;
|
| - if (extent.max > max) max = extent.max;
|
| - measurePosition = measureScale.scale(max);
|
| - isNegative = max < 0;
|
| - });
|
| - } else {
|
| - var value = rowData.elementAt(column);
|
| - isNegative = value < 0;
|
| - measurePosition = measureScale.scale(rowData.elementAt(column));
|
| - }
|
| -
|
| - if (area.config.isLeftAxisPrimary) {
|
| - _positionAtPoint(measurePosition, dimensionPosition,
|
| - 0, dimensionOffset, isNegative, true);
|
| - } else {
|
| - _positionAtPoint(dimensionPosition, measurePosition,
|
| - dimensionOffset, 0, isNegative, false);
|
| - }
|
| - }
|
| -
|
| - void _positionAtPoint(num x, num y,
|
| - num xBand, num yBand, bool negative, [bool isLeftPrimary = false]) {
|
| - var rect = _hovercardRoot.getBoundingClientRect(),
|
| - width = rect.width,
|
| - height = rect.height,
|
| - scaleToHostY =
|
| - (_area.theme.padding != null ? _area.theme.padding.top : 0) +
|
| - (_area.layout.renderArea.y),
|
| - scaleToHostX =
|
| - (_area.theme.padding != null ? _area.theme.padding.start: 0) +
|
| - (_area.layout.renderArea.x),
|
| - renderAreaHeight = _area.layout.renderArea.height,
|
| - renderAreaWidth = _area.layout.renderArea.width;
|
| -
|
| - if (scaleToHostY < 0) scaleToHostY = 0;
|
| - if (scaleToHostX < 0) scaleToHostX = 0;
|
| -
|
| - num top = 0, left = 0;
|
| - for (int i = 0, len = placementOrder.length; i < len; ++i) {
|
| - String placement = placementOrder.elementAt(i);
|
| -
|
| - // Place the popup based on the orientation.
|
| - if (placement == 'orientation') {
|
| - placement = isLeftPrimary ? 'right' : 'top';
|
| - }
|
| -
|
| - if (placement == 'top') {
|
| - top = negative ? y + yBand : y - (height + yBand);
|
| - left = isLeftPrimary ? x - width : x - width / 2;
|
| - }
|
| - if (placement == 'right') {
|
| - top = isLeftPrimary ? y - height / 2 : y;
|
| - left = negative ? x - (width + xBand) : x + xBand;
|
| - }
|
| - if (placement == 'left') {
|
| - top = isLeftPrimary ? y - height / 2 : y;
|
| - left = negative ? x + xBand : x - (width + xBand);
|
| - }
|
| - if (placement == 'bottom') {
|
| - top = negative ? y - (height + yBand) : y + yBand;
|
| - left = isLeftPrimary ? x - width : x - width / 2;
|
| - }
|
| -
|
| - // Check if the popup is contained in the RenderArea.
|
| - // If not, try other placements.
|
| - if (top > 0 && left > 0 &&
|
| - top + height < renderAreaHeight && left + width < renderAreaWidth) {
|
| - break;
|
| - }
|
| - }
|
| -
|
| - _hovercardRoot.style
|
| - ..top = '${top + scaleToHostY}px'
|
| - ..left = '${left + scaleToHostX}px';
|
| - }
|
| -
|
| - Element _createTooltip(int column, int row) {
|
| - var element = new Element.div();
|
| - if (_showDimensionTitle) {
|
| - var titleElement = new Element.div()
|
| - ..className = 'hovercard-title'
|
| - ..text = _getDimensionTitle(column, row);
|
| - element.append(titleElement);
|
| - }
|
| -
|
| - var measureVals = _getMeasuresData(column, row);
|
| - measureVals.forEach((ChartLegendItem item) {
|
| - var labelElement = new Element.div()
|
| - ..className = 'hovercard-measure-label'
|
| - ..text = item.label,
|
| - valueElement = new Element.div()
|
| - ..style.color = item.color
|
| - ..className = 'hovercard-measure-value'
|
| - ..text = item.value,
|
| - measureElement = new Element.div()
|
| - ..append(labelElement)
|
| - ..append(valueElement);
|
| -
|
| - measureElement.className = _columnsToShow.length > 1 || _isMultiValue
|
| - ? 'hovercard-measure hovercard-multi'
|
| - : 'hovercard-measure hovercard-single';
|
| - element.append(measureElement);
|
| - });
|
| -
|
| - return element;
|
| - }
|
| -
|
| - Iterable<ChartLegendItem> _getMeasuresData(int column, int row) {
|
| - var measureVals = <ChartLegendItem>[];
|
| -
|
| - if (_columnsToShow.isNotEmpty) {
|
| - _columnsToShow.forEach((int column) {
|
| - measureVals.add(_createHovercardItem(column, row));
|
| - });
|
| - } else if (_columnsToShow.length > 1 || _isMultiValue) {
|
| - var displayedCols = [];
|
| - _area.config.series.forEach((ChartSeries series) {
|
| - series.measures.forEach((int column) {
|
| - if (!displayedCols.contains(column)) displayedCols.add(column);
|
| - });
|
| - });
|
| - displayedCols.sort();
|
| - displayedCols.forEach((int column) {
|
| - measureVals.add(_createHovercardItem(column, row));
|
| - });
|
| - } else {
|
| - measureVals.add(_createHovercardItem(column, row));
|
| - }
|
| -
|
| - return measureVals;
|
| - }
|
| -
|
| - ChartLegendItem _createHovercardItem(int column, int row) {
|
| - var rowData = _area.data.rows.elementAt(row),
|
| - columns = _area.data.columns,
|
| - spec = columns.elementAt(column),
|
| - colorKey = _area.useRowColoring ? row : column,
|
| - formatter = _getFormatterForColumn(column),
|
| - label = _area.useRowColoring
|
| - ? rowData.elementAt(_area.config.dimensions.first)
|
| - : spec.label;
|
| - return new ChartLegendItem(
|
| - label: label,
|
| - value: formatter(rowData.elementAt(column)),
|
| - color: _area.theme.getColorForKey(colorKey));
|
| - }
|
| -
|
| - String _getDimensionTitle(int column, int row) {
|
| - var rowData = _area.data.rows.elementAt(row),
|
| - colSpec = _area.data.columns.elementAt(column);
|
| - if (_area.useRowColoring) {
|
| - return colSpec.label;
|
| - } else {
|
| - var count = (_area as CartesianArea).useTwoDimensionAxes ? 2 : 1,
|
| - dimensions = _area.config.dimensions.take(count);
|
| - return dimensions.map(
|
| - (int c) =>
|
| - _getFormatterForColumn(c)(rowData.elementAt(c))).join(', ');
|
| - }
|
| - }
|
| -
|
| - // TODO: Move this to a common place?
|
| - Scale _getScaleForColumn(int column) {
|
| - var series = _area.config.series.firstWhere(
|
| - (ChartSeries x) => x.measures.contains(column), orElse: () => null);
|
| - return series != null
|
| - ? (_area as CartesianArea).measureScales(series).first
|
| - : null;
|
| - }
|
| -
|
| - // TODO: Move this to a common place?
|
| - FormatFunction _getFormatterForColumn(int column) {
|
| - var formatter = _area.data.columns.elementAt(column).formatter;
|
| - if (formatter == null && _area is CartesianArea) {
|
| - var scale = _getScaleForColumn(column);
|
| - if (scale != null) {
|
| - formatter = scale.createTickFormatter();
|
| - }
|
| - }
|
| - if (formatter == null) {
|
| - // Formatter function must return String. Default to identity function
|
| - // but return the toString() instead.
|
| - formatter = (x) => x.toString();
|
| - }
|
| - return formatter;
|
| - }
|
| -}
|
|
|