Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(121)

Side by Side Diff: charted/lib/charts/behaviors/hovercard.dart

Issue 1400473008: Roll Observatory packages and add a roll script (Closed) Base URL: git@github.com:dart-lang/observatory_pub_packages.git@master
Patch Set: Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 //
2 // Copyright 2014 Google Inc. All rights reserved.
3 //
4 // Use of this source code is governed by a BSD-style
5 // license that can be found in the LICENSE file or at
6 // https://developers.google.com/open-source/licenses/bsd
7 //
8
9 part of charted.charts;
10
11 typedef Element HovercardBuilder(int column, int row);
12
13 ///
14 /// Subscribe to events on the chart and display more information about the
15 /// visualization that is hovered or highlighted.
16 ///
17 /// This behavior supports two event modes:
18 /// (1) State change: Subscribes to changes on ChartState
19 /// (2) Mouse tracking: Subscribe to onValueMouseOver and onValueMouseOut
20 ///
21 /// Supports two placement modes for mouse-tracking:
22 /// (1) Place relative to mouse position
23 /// (2) Place relative to the visualized measure value.
24 ///
25 /// Supports two modes for displayed content:
26 /// (1) Show all measure values at the current dimension value.
27 /// (2) Show the hovered value only
28 ///
29 /// Optionally, takes a builder that is passed row, column values that
30 /// can be used to build custom tooltip
31 ///
32 /// What makes the positioning logic complex?
33 /// (1) Is this a CartesianArea?
34 /// (2) Does the CartesianArea use two dimensions or just one?
35 /// (3) Does the CartesianArea use "bands" along the axes?
36 /// (4) How does measure correspond to positioning? Are the bars stacked?
37 ///
38 /// So, how is the position computed?
39 /// (1) Uses ChartConfig to figure out which renderers are being used, asks
40 /// for extent of the row to roughly get the position along measure axis.
41 /// (2) Position along dimension axes is computed separately based on how
42 /// many dimensions are being used and if any of them use bands.
43 ///
44 /// Constraints and known issues:
45 /// (0) The implementation isn't complete yet! Specifically for CartesianArea
46 /// that uses two axes.
47 /// (1) Even with all the logic, single value mode does not work well
48 /// with StackedBarChartRenderer
49 /// (2) Only mouse relative positioning is supported on LayoutArea
50 /// (3) Positioning only works for renderers that determine extent given a
51 /// single row. Eg: Would not work with a water-fall chart.
52 ///
53 class Hovercard implements ChartBehavior {
54 final HovercardBuilder builder;
55
56 bool _isMouseTracking;
57 bool _isMultiValue;
58 bool _showDimensionTitle;
59 Iterable<int> _columnsToShow;
60
61 Iterable placementOrder =
62 const['orientation', 'top', 'right', 'bottom', 'left', 'orientation'];
63 int offset = 20;
64
65 ChartArea _area;
66 ChartState _state;
67 SubscriptionsDisposer _disposer = new SubscriptionsDisposer();
68
69 Element _hovercardRoot;
70
71 Hovercard({
72 bool isMouseTracking,
73 bool isMultiValue: false,
74 bool showDimensionTitle: false,
75 List columnsToShow: const [],
76 this.builder}) {
77 _isMouseTracking = isMouseTracking;
78 _isMultiValue = isMultiValue;
79 _showDimensionTitle = showDimensionTitle;
80 _columnsToShow = columnsToShow;
81 }
82
83 void init(ChartArea area, Selection _, Selection __) {
84 _area = area;
85 _state = area.state;
86
87 // If we don't have state, fall back to mouse events.
88 _isMouseTracking =
89 _isMouseTracking == true || _state == null || _area is LayoutArea;
90
91 // Subscribe to events.
92 if (_isMouseTracking) {
93 _disposer.addAll([
94 _area.onValueMouseOver.listen(_handleMouseOver),
95 _area.onValueMouseOut.listen(_handleMouseOut)
96 ]);
97 } else {
98 _disposer.add(_state.changes.listen(_handleStateChange));
99 }
100 }
101
102 void dispose() {
103 _disposer.dispose();
104 if (_hovercardRoot != null) _hovercardRoot.remove();
105 }
106
107 void _handleMouseOver(ChartEvent e) {
108 _ensureHovercard();
109 _hovercardRoot.children.clear();
110 _hovercardRoot.append(builder != null
111 ? builder(e.column, e.row)
112 : _createTooltip(e.column, e.row));
113 _hovercardRoot.style
114 ..visibility = 'visible'
115 ..opacity = '1.0';
116 _updateTooltipPosition(evt: e);
117 }
118
119 void _handleMouseOut(ChartEvent e) {
120 _ensureHovercard();
121 _hovercardRoot.style
122 ..visibility = 'hidden'
123 ..opacity = '$EPSILON';
124 }
125
126 void _handleStateChange(Iterable<ChangeRecord> changes) {
127 _ensureHovercard();
128
129 var value = _state.hovered;
130 if (_state.highlights.length == 1) {
131 value = _state.highlights.first;
132 }
133
134 if (value == null) {
135 _hovercardRoot.style
136 ..visibility = 'hidden'
137 ..opacity = '$EPSILON';
138 } else {
139 _hovercardRoot.children.clear();
140 _hovercardRoot.append(builder != null
141 ? builder(value.first, value.last)
142 : _createTooltip(value.first, value.last));
143 _hovercardRoot.style
144 ..visibility = 'visible'
145 ..opacity = '1.0';
146 _updateTooltipPosition(column: value.first, row: value.last);
147 }
148 }
149
150 void _ensureHovercard() {
151 if (_hovercardRoot != null) return;
152 _hovercardRoot = new Element.div();
153 _hovercardRoot.classes.add('hovercard');
154 if (_area.config.isRTL) {
155 _hovercardRoot.attributes['dir'] = 'rtl';
156 _hovercardRoot.classes.add('rtl');
157 }
158 _area.host.style.position = 'relative';
159 _area.host.append(_hovercardRoot);
160 }
161
162 void _updateTooltipPosition({ChartEvent evt, int column, int row}) {
163 assert(evt != null || column != null && row != null);
164 if (_isMouseTracking && evt != null) {
165 _positionAtMousePointer(evt);
166 } else if (_area is CartesianArea) {
167 if ((_area as CartesianArea).useTwoDimensionAxes) {
168 _positionOnTwoDimensionCartesian(column, row);
169 } else {
170 _positionOnSingleDimensionCartesian(column, row);
171 }
172 } else {
173 _positionOnLayout(column, row);
174 }
175 }
176
177 void _positionAtMousePointer(ChartEvent e) =>
178 _positionAtPoint(e.chartX, e.chartY, offset, offset, false, false);
179
180 void _positionOnLayout(column, row) {
181 // Currently for layouts, when hovercard is triggered due to change
182 // in ChartState, we render hovercard in the middle of layout.
183 // TODO: Get bounding rect from LayoutRenderer and position relative to it.
184 }
185
186 void _positionOnTwoDimensionCartesian(int column, int row) {
187 // TODO: Implement multi dimension positioning.
188 }
189
190 void _positionOnSingleDimensionCartesian(int column, int row) {
191 CartesianArea area = _area;
192 var dimensionCol = area.config.dimensions.first,
193 dimensionScale = area.dimensionScales.first,
194 measureScale = _getScaleForColumn(column),
195 dimensionOffset = this.offset,
196 dimensionCenterOffset = 0;
197
198 // If we are using bands on the one axis that is shown
199 // update position and offset accordingly.
200 if (area.dimensionsUsingBands.contains(dimensionCol)) {
201 assert(dimensionScale is OrdinalScale);
202 dimensionOffset = (dimensionScale as OrdinalScale).rangeBand / 2;
203 dimensionCenterOffset = dimensionOffset;
204 }
205
206 var rowData = area.data.rows.elementAt(row),
207 measurePosition = 0,
208 isNegative = false,
209 dimensionPosition = dimensionScale.scale(
210 rowData.elementAt(dimensionCol)) + dimensionCenterOffset;
211
212 if (_isMultiValue) {
213 var max = SMALL_INT_MIN,
214 min = SMALL_INT_MAX;
215 area.config.series.forEach((ChartSeries series) {
216 CartesianRenderer renderer = series.renderer;
217 Extent extent = renderer.extentForRow(rowData);
218 if (extent.min < min) min = extent.min;
219 if (extent.max > max) max = extent.max;
220 measurePosition = measureScale.scale(max);
221 isNegative = max < 0;
222 });
223 } else {
224 var value = rowData.elementAt(column);
225 isNegative = value < 0;
226 measurePosition = measureScale.scale(rowData.elementAt(column));
227 }
228
229 if (area.config.isLeftAxisPrimary) {
230 _positionAtPoint(measurePosition, dimensionPosition,
231 0, dimensionOffset, isNegative, true);
232 } else {
233 _positionAtPoint(dimensionPosition, measurePosition,
234 dimensionOffset, 0, isNegative, false);
235 }
236 }
237
238 void _positionAtPoint(num x, num y,
239 num xBand, num yBand, bool negative, [bool isLeftPrimary = false]) {
240 var rect = _hovercardRoot.getBoundingClientRect(),
241 width = rect.width,
242 height = rect.height,
243 scaleToHostY =
244 (_area.theme.padding != null ? _area.theme.padding.top : 0) +
245 (_area.layout.renderArea.y),
246 scaleToHostX =
247 (_area.theme.padding != null ? _area.theme.padding.start: 0) +
248 (_area.layout.renderArea.x),
249 renderAreaHeight = _area.layout.renderArea.height,
250 renderAreaWidth = _area.layout.renderArea.width;
251
252 if (scaleToHostY < 0) scaleToHostY = 0;
253 if (scaleToHostX < 0) scaleToHostX = 0;
254
255 num top = 0, left = 0;
256 for (int i = 0, len = placementOrder.length; i < len; ++i) {
257 String placement = placementOrder.elementAt(i);
258
259 // Place the popup based on the orientation.
260 if (placement == 'orientation') {
261 placement = isLeftPrimary ? 'right' : 'top';
262 }
263
264 if (placement == 'top') {
265 top = negative ? y + yBand : y - (height + yBand);
266 left = isLeftPrimary ? x - width : x - width / 2;
267 }
268 if (placement == 'right') {
269 top = isLeftPrimary ? y - height / 2 : y;
270 left = negative ? x - (width + xBand) : x + xBand;
271 }
272 if (placement == 'left') {
273 top = isLeftPrimary ? y - height / 2 : y;
274 left = negative ? x + xBand : x - (width + xBand);
275 }
276 if (placement == 'bottom') {
277 top = negative ? y - (height + yBand) : y + yBand;
278 left = isLeftPrimary ? x - width : x - width / 2;
279 }
280
281 // Check if the popup is contained in the RenderArea.
282 // If not, try other placements.
283 if (top > 0 && left > 0 &&
284 top + height < renderAreaHeight && left + width < renderAreaWidth) {
285 break;
286 }
287 }
288
289 _hovercardRoot.style
290 ..top = '${top + scaleToHostY}px'
291 ..left = '${left + scaleToHostX}px';
292 }
293
294 Element _createTooltip(int column, int row) {
295 var element = new Element.div();
296 if (_showDimensionTitle) {
297 var titleElement = new Element.div()
298 ..className = 'hovercard-title'
299 ..text = _getDimensionTitle(column, row);
300 element.append(titleElement);
301 }
302
303 var measureVals = _getMeasuresData(column, row);
304 measureVals.forEach((ChartLegendItem item) {
305 var labelElement = new Element.div()
306 ..className = 'hovercard-measure-label'
307 ..text = item.label,
308 valueElement = new Element.div()
309 ..style.color = item.color
310 ..className = 'hovercard-measure-value'
311 ..text = item.value,
312 measureElement = new Element.div()
313 ..append(labelElement)
314 ..append(valueElement);
315
316 measureElement.className = _columnsToShow.length > 1 || _isMultiValue
317 ? 'hovercard-measure hovercard-multi'
318 : 'hovercard-measure hovercard-single';
319 element.append(measureElement);
320 });
321
322 return element;
323 }
324
325 Iterable<ChartLegendItem> _getMeasuresData(int column, int row) {
326 var measureVals = <ChartLegendItem>[];
327
328 if (_columnsToShow.isNotEmpty) {
329 _columnsToShow.forEach((int column) {
330 measureVals.add(_createHovercardItem(column, row));
331 });
332 } else if (_columnsToShow.length > 1 || _isMultiValue) {
333 var displayedCols = [];
334 _area.config.series.forEach((ChartSeries series) {
335 series.measures.forEach((int column) {
336 if (!displayedCols.contains(column)) displayedCols.add(column);
337 });
338 });
339 displayedCols.sort();
340 displayedCols.forEach((int column) {
341 measureVals.add(_createHovercardItem(column, row));
342 });
343 } else {
344 measureVals.add(_createHovercardItem(column, row));
345 }
346
347 return measureVals;
348 }
349
350 ChartLegendItem _createHovercardItem(int column, int row) {
351 var rowData = _area.data.rows.elementAt(row),
352 columns = _area.data.columns,
353 spec = columns.elementAt(column),
354 colorKey = _area.useRowColoring ? row : column,
355 formatter = _getFormatterForColumn(column),
356 label = _area.useRowColoring
357 ? rowData.elementAt(_area.config.dimensions.first)
358 : spec.label;
359 return new ChartLegendItem(
360 label: label,
361 value: formatter(rowData.elementAt(column)),
362 color: _area.theme.getColorForKey(colorKey));
363 }
364
365 String _getDimensionTitle(int column, int row) {
366 var rowData = _area.data.rows.elementAt(row),
367 colSpec = _area.data.columns.elementAt(column);
368 if (_area.useRowColoring) {
369 return colSpec.label;
370 } else {
371 var count = (_area as CartesianArea).useTwoDimensionAxes ? 2 : 1,
372 dimensions = _area.config.dimensions.take(count);
373 return dimensions.map(
374 (int c) =>
375 _getFormatterForColumn(c)(rowData.elementAt(c))).join(', ');
376 }
377 }
378
379 // TODO: Move this to a common place?
380 Scale _getScaleForColumn(int column) {
381 var series = _area.config.series.firstWhere(
382 (ChartSeries x) => x.measures.contains(column), orElse: () => null);
383 return series != null
384 ? (_area as CartesianArea).measureScales(series).first
385 : null;
386 }
387
388 // TODO: Move this to a common place?
389 FormatFunction _getFormatterForColumn(int column) {
390 var formatter = _area.data.columns.elementAt(column).formatter;
391 if (formatter == null && _area is CartesianArea) {
392 var scale = _getScaleForColumn(column);
393 if (scale != null) {
394 formatter = scale.createTickFormatter();
395 }
396 }
397 if (formatter == null) {
398 // Formatter function must return String. Default to identity function
399 // but return the toString() instead.
400 formatter = (x) => x.toString();
401 }
402 return formatter;
403 }
404 }
OLDNEW
« no previous file with comments | « charted/lib/charts/behaviors/chart_tooltip.dart ('k') | charted/lib/charts/behaviors/line_marker.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698