| OLD | NEW |
| (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 library charted.svg.axis; | |
| 10 | |
| 11 import 'dart:html' show Element; | |
| 12 import 'dart:math' as math; | |
| 13 | |
| 14 import 'package:charted/core/scales.dart'; | |
| 15 import 'package:charted/core/utils.dart'; | |
| 16 import 'package:charted/selection/selection.dart'; | |
| 17 | |
| 18 /// | |
| 19 /// [SvgAxis] helps draw chart axes based on a given scale. | |
| 20 /// | |
| 21 class SvgAxis { | |
| 22 /// Store of axis roots mapped to currently used scales | |
| 23 static final _scales = new Expando<Scale>(); | |
| 24 | |
| 25 /// Orientation of the axis. Defaults to [ORIENTATION_BOTTOM]. | |
| 26 final String orientation; | |
| 27 | |
| 28 /// Scale used on this axis | |
| 29 final Scale scale; | |
| 30 | |
| 31 /// Size of all inner ticks | |
| 32 final num innerTickSize; | |
| 33 | |
| 34 /// Size of the outer two ticks | |
| 35 final num outerTickSize; | |
| 36 | |
| 37 /// Padding on the ticks | |
| 38 final num tickPadding; | |
| 39 | |
| 40 /// List of values to be used on the ticks | |
| 41 List _tickValues; | |
| 42 | |
| 43 /// Formatter for the tick labels | |
| 44 FormatFunction _tickFormat; | |
| 45 | |
| 46 SvgAxis({ | |
| 47 this.orientation: ORIENTATION_BOTTOM, | |
| 48 this.innerTickSize: 6, | |
| 49 this.outerTickSize: 6, | |
| 50 this.tickPadding: 3, | |
| 51 Iterable tickValues, | |
| 52 FormatFunction tickFormat, | |
| 53 Scale scale }) : scale = scale == null ? new LinearScale() : scale { | |
| 54 _tickFormat = tickFormat == null | |
| 55 ? this.scale.createTickFormatter() | |
| 56 : tickFormat; | |
| 57 _tickValues = isNullOrEmpty(tickValues) ? this.scale.ticks : tickValues; | |
| 58 } | |
| 59 | |
| 60 Iterable get tickValues => _tickValues; | |
| 61 | |
| 62 FormatFunction get tickFormat => _tickFormat; | |
| 63 | |
| 64 /// Draw an axis on each non-null element in selection | |
| 65 draw(Selection g, {SvgAxisTicks axisTicksBuilder, bool isRTL: false}) => | |
| 66 g.each((d, i, e) => create( | |
| 67 e, g.scope, axisTicksBuilder: axisTicksBuilder, isRTL: isRTL)); | |
| 68 | |
| 69 /// Create an axis on [element]. | |
| 70 create(Element element, SelectionScope scope, { | |
| 71 SvgAxisTicks axisTicksBuilder, bool isRTL: false}) { | |
| 72 | |
| 73 var group = scope.selectElements([element]), | |
| 74 older = _scales[element], | |
| 75 current = _scales[element] = scale.clone(), | |
| 76 isInitialRender = older == null; | |
| 77 | |
| 78 var isLeft = orientation == ORIENTATION_LEFT, | |
| 79 isRight = !isLeft && orientation == ORIENTATION_RIGHT, | |
| 80 isVertical = isLeft || isRight, | |
| 81 isBottom = !isVertical && orientation == ORIENTATION_BOTTOM, | |
| 82 isTop = !(isVertical || isBottom) && orientation == ORIENTATION_TOP, | |
| 83 isHorizontal = !isVertical; | |
| 84 | |
| 85 if (older == null) older = current; | |
| 86 if (axisTicksBuilder == null) { | |
| 87 axisTicksBuilder = new SvgAxisTicks(); | |
| 88 } | |
| 89 axisTicksBuilder.init(this); | |
| 90 | |
| 91 var values = axisTicksBuilder.ticks, | |
| 92 formatted = axisTicksBuilder.formattedTicks, | |
| 93 ellipsized = axisTicksBuilder.shortenedTicks; | |
| 94 | |
| 95 var ticks = group.selectAll('.tick').data(values, current.scale), | |
| 96 exit = ticks.exit, | |
| 97 transform = isVertical ? _yAxisTransform : _xAxisTransform, | |
| 98 sign = isTop || isLeft ? -1 : 1, | |
| 99 isEllipsized = ellipsized != formatted; | |
| 100 | |
| 101 var enter = ticks.enter.appendWithCallback((d, i, e) { | |
| 102 var group = Namespace.createChildElement('g', e) | |
| 103 ..attributes['class'] = 'tick' | |
| 104 ..append(Namespace.createChildElement('line', e)) | |
| 105 ..append(Namespace.createChildElement('text', e) | |
| 106 ..attributes['dy'] = | |
| 107 isVertical ? '0.32em' : (isBottom ? '0.71em' : '0')); | |
| 108 if (!isInitialRender) { | |
| 109 group.style.setProperty('opacity', EPSILON.toString()); | |
| 110 } | |
| 111 return group; | |
| 112 }); | |
| 113 | |
| 114 // All attributes/styles/classes that may change due to theme and scale. | |
| 115 // TODO(prsd): Order elements before updating ticks. | |
| 116 ticks.each((d, i, e) { | |
| 117 Element line = e.firstChild; | |
| 118 Element text = e.lastChild; | |
| 119 bool isRTLText = false; // FIXME(prsd) | |
| 120 | |
| 121 if (isHorizontal) { | |
| 122 line.attributes['y2'] = '${sign * innerTickSize}'; | |
| 123 text.attributes['y'] = | |
| 124 '${sign * (math.max(innerTickSize, 0) + tickPadding)}'; | |
| 125 | |
| 126 if (axisTicksBuilder.rotation != 0) { | |
| 127 text.attributes | |
| 128 ..['transform'] = | |
| 129 'rotate(${(isRTL ? -1 : 1) * axisTicksBuilder.rotation})' | |
| 130 ..['text-anchor'] = isRTL ? 'end' : 'start'; | |
| 131 } else { | |
| 132 text.attributes | |
| 133 ..remove('transform') | |
| 134 ..['text-anchor'] = 'middle'; | |
| 135 } | |
| 136 } else { | |
| 137 line.attributes['x2'] = '${sign * innerTickSize}'; | |
| 138 text.attributes | |
| 139 ..['x'] = '${sign * (math.max(innerTickSize, 0) + tickPadding)}' | |
| 140 ..['text-anchor'] = isLeft | |
| 141 ? (isRTLText ? 'start' : 'end') | |
| 142 : (isRTLText ? 'end' : 'start'); | |
| 143 } | |
| 144 | |
| 145 text.text = fixSimpleTextDirection(ellipsized.elementAt(i)); | |
| 146 if (isEllipsized) { | |
| 147 text.attributes['data-detail'] = formatted.elementAt(i); | |
| 148 } else { | |
| 149 text.attributes.remove('data-detail'); | |
| 150 } | |
| 151 | |
| 152 if (isInitialRender) { | |
| 153 var dx = current is OrdinalScale ? current.rangeBand / 2 : 0; | |
| 154 e.attributes['transform'] = isHorizontal | |
| 155 ? 'translate(${current.scale(d) + dx},0)' | |
| 156 : 'translate(0,${current.scale(d) + dx})'; | |
| 157 } else { | |
| 158 e.style.setProperty('opacity', '1.0'); | |
| 159 } | |
| 160 }); | |
| 161 | |
| 162 // Transition existing ticks to right positions | |
| 163 if (!isInitialRender) { | |
| 164 var transformFn; | |
| 165 if (current is OrdinalScale && current.rangeBand != 0) { | |
| 166 var dx = current.rangeBand / 2; | |
| 167 transformFn = (d) => current.scale(d) + dx; | |
| 168 } else if (older is OrdinalScale && older.rangeBand != 0) { | |
| 169 older = current; | |
| 170 } else { | |
| 171 transform(ticks, current.scale); | |
| 172 } | |
| 173 | |
| 174 transform(enter, transformFn != null ? transformFn : older.scale); | |
| 175 transform(ticks, transformFn != null ? transformFn : current.scale); | |
| 176 } | |
| 177 | |
| 178 exit.remove(); | |
| 179 | |
| 180 // Append the outer domain. | |
| 181 var path = element.querySelector('.domain'), | |
| 182 tickSize = sign * outerTickSize, | |
| 183 range = current.rangeExtent; | |
| 184 if (path == null) { | |
| 185 path = Namespace.createChildElement('path', element) | |
| 186 ..setAttribute('class', 'domain'); | |
| 187 } | |
| 188 path.attributes['d'] = isLeft || isRight | |
| 189 ? 'M${tickSize},${range.min}H0V${range.max}H${tickSize}' | |
| 190 : 'M${range.min},${tickSize}V0H${range.max}V${tickSize}'; | |
| 191 element.append(path); | |
| 192 } | |
| 193 | |
| 194 _xAxisTransform(Selection selection, transformFn) { | |
| 195 selection.transition() | |
| 196 ..attrWithCallback( | |
| 197 'transform', (d, i, e) => 'translate(${transformFn(d)},0)'); | |
| 198 } | |
| 199 | |
| 200 _yAxisTransform(Selection selection, transformFn) { | |
| 201 selection.transition() | |
| 202 ..attrWithCallback( | |
| 203 'transform', (d, i, e) => 'translate(0,${transformFn(d)})'); | |
| 204 } | |
| 205 } | |
| 206 | |
| 207 /// Interface and the default implementation of [SvgAxisTicks]. | |
| 208 /// SvgAxisTicks provides strategy to handle overlapping ticks on an | |
| 209 /// axis. Default implementation assumes that the ticks don't overlap. | |
| 210 class SvgAxisTicks { | |
| 211 int _rotation = 0; | |
| 212 Iterable _ticks; | |
| 213 Iterable _formattedTicks; | |
| 214 | |
| 215 void init(SvgAxis axis) { | |
| 216 _ticks = axis.tickValues; | |
| 217 _formattedTicks = _ticks.map((x) => axis.tickFormat(x)); | |
| 218 } | |
| 219 | |
| 220 /// When non-zero, indicates the angle by which each tick value must be | |
| 221 /// rotated to avoid the overlap. | |
| 222 int get rotation => _rotation; | |
| 223 | |
| 224 /// List of ticks that will be displayed on the axis. | |
| 225 Iterable get ticks => _ticks; | |
| 226 | |
| 227 /// List of formatted ticks values. | |
| 228 Iterable get formattedTicks => _formattedTicks; | |
| 229 | |
| 230 /// List of clipped tick values, if they had to be clipped. Must be same | |
| 231 /// as the [formattedTicks] if none of the ticks were ellipsized. | |
| 232 Iterable get shortenedTicks => _formattedTicks; | |
| 233 } | |
| OLD | NEW |