mirror of
https://github.com/flutter/samples.git
synced 2026-03-30 16:23:23 +00:00
Add flutter_web samples (#75)
This commit is contained in:
committed by
Andrew Brogdon
parent
42f2dce01b
commit
3fe927cb29
594
web/charts/common/lib/src/chart/cartesian/axis/axis.dart
Normal file
594
web/charts/common/lib/src/chart/cartesian/axis/axis.dart
Normal file
@@ -0,0 +1,594 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math' show Rectangle, min, max;
|
||||
|
||||
import 'package:meta/meta.dart' show protected, visibleForTesting;
|
||||
|
||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../common/text_element.dart' show TextElement;
|
||||
import '../../../data/series.dart' show AttributeKey;
|
||||
import '../../common/chart_canvas.dart' show ChartCanvas;
|
||||
import '../../common/chart_context.dart' show ChartContext;
|
||||
import '../../layout/layout_view.dart'
|
||||
show
|
||||
LayoutPosition,
|
||||
LayoutView,
|
||||
LayoutViewConfig,
|
||||
LayoutViewPaintOrder,
|
||||
LayoutViewPositionOrder,
|
||||
ViewMeasuredSizes;
|
||||
import 'axis_tick.dart' show AxisTicks;
|
||||
import 'draw_strategy/small_tick_draw_strategy.dart' show SmallTickDrawStrategy;
|
||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import 'linear/linear_scale.dart' show LinearScale;
|
||||
import 'numeric_extents.dart' show NumericExtents;
|
||||
import 'numeric_scale.dart' show NumericScale;
|
||||
import 'numeric_tick_provider.dart' show NumericTickProvider;
|
||||
import 'ordinal_tick_provider.dart' show OrdinalTickProvider;
|
||||
import 'scale.dart'
|
||||
show MutableScale, RangeBandConfig, ScaleOutputExtent, Scale;
|
||||
import 'simple_ordinal_scale.dart' show SimpleOrdinalScale;
|
||||
import 'tick.dart' show Tick;
|
||||
import 'tick_formatter.dart'
|
||||
show TickFormatter, OrdinalTickFormatter, NumericTickFormatter;
|
||||
import 'tick_provider.dart' show TickProvider;
|
||||
|
||||
const measureAxisIdKey = const AttributeKey<String>('Axis.measureAxisId');
|
||||
const measureAxisKey = const AttributeKey<Axis>('Axis.measureAxis');
|
||||
const domainAxisKey = const AttributeKey<Axis>('Axis.domainAxis');
|
||||
|
||||
/// Orientation of an Axis.
|
||||
enum AxisOrientation { top, right, bottom, left }
|
||||
|
||||
abstract class ImmutableAxis<D> {
|
||||
/// Compare domain to the viewport.
|
||||
///
|
||||
/// 0 if the domain is in the viewport.
|
||||
/// 1 if the domain is to the right of the viewport.
|
||||
/// -1 if the domain is to the left of the viewport.
|
||||
int compareDomainValueToViewport(D domain);
|
||||
|
||||
/// Get location for the domain.
|
||||
double getLocation(D domain);
|
||||
|
||||
D getDomain(double location);
|
||||
|
||||
/// Rangeband for this axis.
|
||||
double get rangeBand;
|
||||
|
||||
/// Step size for this axis.
|
||||
double get stepSize;
|
||||
|
||||
/// Output range for this axis.
|
||||
ScaleOutputExtent get range;
|
||||
}
|
||||
|
||||
abstract class Axis<D> extends ImmutableAxis<D> implements LayoutView {
|
||||
static const primaryMeasureAxisId = 'primaryMeasureAxisId';
|
||||
static const secondaryMeasureAxisId = 'secondaryMeasureAxisId';
|
||||
|
||||
final MutableScale<D> _scale;
|
||||
|
||||
/// [Scale] of this axis.
|
||||
@protected
|
||||
MutableScale<D> get scale => _scale;
|
||||
|
||||
/// Previous [Scale] of this axis, used to calculate tick animation.
|
||||
MutableScale<D> _previousScale;
|
||||
|
||||
TickProvider<D> _tickProvider;
|
||||
|
||||
/// [TickProvider] for this axis.
|
||||
TickProvider<D> get tickProvider => _tickProvider;
|
||||
|
||||
set tickProvider(TickProvider<D> tickProvider) {
|
||||
_tickProvider = tickProvider;
|
||||
}
|
||||
|
||||
/// [TickFormatter] for this axis.
|
||||
TickFormatter<D> _tickFormatter;
|
||||
|
||||
set tickFormatter(TickFormatter<D> formatter) {
|
||||
if (_tickFormatter != formatter) {
|
||||
_tickFormatter = formatter;
|
||||
_formatterValueCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
TickFormatter<D> get tickFormatter => _tickFormatter;
|
||||
final _formatterValueCache = <D, String>{};
|
||||
|
||||
/// [TickDrawStrategy] for this axis.
|
||||
TickDrawStrategy<D> tickDrawStrategy;
|
||||
|
||||
/// [AxisOrientation] for this axis.
|
||||
AxisOrientation axisOrientation;
|
||||
|
||||
ChartContext context;
|
||||
|
||||
/// If the output range should be reversed.
|
||||
bool reverseOutputRange = false;
|
||||
|
||||
/// Whether or not the axis will configure the viewport to have "niced" ticks
|
||||
/// around the domain values.
|
||||
bool _autoViewport = true;
|
||||
|
||||
/// If the axis line should always be drawn.
|
||||
bool forceDrawAxisLine;
|
||||
|
||||
/// If true, do not allow axis to be modified.
|
||||
///
|
||||
/// Ticks (including their location) are not updated.
|
||||
/// Viewport changes not allowed.
|
||||
bool lockAxis = false;
|
||||
|
||||
/// Ticks provided by the tick provider.
|
||||
List<Tick> _providedTicks;
|
||||
|
||||
/// Ticks used by the axis for drawing.
|
||||
final _axisTicks = <AxisTicks<D>>[];
|
||||
|
||||
Rectangle<int> _componentBounds;
|
||||
Rectangle<int> _drawAreaBounds;
|
||||
GraphicsFactory _graphicsFactory;
|
||||
|
||||
/// Order for chart layout painting.
|
||||
///
|
||||
/// In general, domain axes should be drawn on top of measure axes to ensure
|
||||
/// that the domain axis line appears on top of any measure axis grid lines.
|
||||
int layoutPaintOrder = LayoutViewPaintOrder.measureAxis;
|
||||
|
||||
Axis(
|
||||
{TickProvider<D> tickProvider,
|
||||
TickFormatter<D> tickFormatter,
|
||||
MutableScale<D> scale})
|
||||
: this._scale = scale,
|
||||
this._tickProvider = tickProvider,
|
||||
this._tickFormatter = tickFormatter;
|
||||
|
||||
@protected
|
||||
MutableScale<D> get mutableScale => _scale;
|
||||
|
||||
/// Rangeband for this axis.
|
||||
@override
|
||||
double get rangeBand => _scale.rangeBand;
|
||||
|
||||
@override
|
||||
double get stepSize => _scale.stepSize;
|
||||
|
||||
@override
|
||||
ScaleOutputExtent get range => _scale.range;
|
||||
|
||||
/// Configures whether the viewport should be reset back to default values
|
||||
/// when the domain is reset.
|
||||
///
|
||||
/// This should generally be disabled when the viewport will be managed
|
||||
/// externally, e.g. from pan and zoom behaviors.
|
||||
set autoViewport(bool autoViewport) {
|
||||
_autoViewport = autoViewport;
|
||||
}
|
||||
|
||||
bool get autoViewport => _autoViewport;
|
||||
|
||||
void setRangeBandConfig(RangeBandConfig rangeBandConfig) {
|
||||
mutableScale.rangeBandConfig = rangeBandConfig;
|
||||
}
|
||||
|
||||
void addDomainValue(D domain) {
|
||||
if (lockAxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
_scale.addDomain(domain);
|
||||
}
|
||||
|
||||
void resetDomains() {
|
||||
if (lockAxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the series list changes, clear the cache.
|
||||
//
|
||||
// There are cases where tick formatter has not "changed", but if measure
|
||||
// formatter provided to the tick formatter uses a closure value, the
|
||||
// formatter cache needs to be cleared.
|
||||
//
|
||||
// This type of use case for the measure formatter surfaced where the series
|
||||
// list also changes. So this is a round about way to also clear the
|
||||
// tick formatter cache.
|
||||
//
|
||||
// TODO: Measure formatter should be changed from a typedef to
|
||||
// a concrete class to force users to create a new tick formatter when
|
||||
// formatting is different, so we can recognize when the tick formatter is
|
||||
// changed and then clear cache accordingly.
|
||||
//
|
||||
// Remove this when bug above is fixed, and verify it did not cause
|
||||
// regression for b/110371453.
|
||||
_formatterValueCache.clear();
|
||||
|
||||
_scale.resetDomain();
|
||||
reverseOutputRange = false;
|
||||
|
||||
if (_autoViewport) {
|
||||
_scale.resetViewportSettings();
|
||||
}
|
||||
|
||||
// TODO: Reset rangeband and step size when we port over config
|
||||
//scale.rangeBandConfig = get range band config
|
||||
//scale.stepSizeConfig = get step size config
|
||||
}
|
||||
|
||||
@override
|
||||
double getLocation(D domain) => domain != null ? _scale[domain] : null;
|
||||
|
||||
@override
|
||||
D getDomain(double location) => _scale.reverse(location);
|
||||
|
||||
@override
|
||||
int compareDomainValueToViewport(D domain) {
|
||||
return _scale.compareDomainValueToViewport(domain);
|
||||
}
|
||||
|
||||
void setOutputRange(int start, int end) {
|
||||
_scale.range = new ScaleOutputExtent(start, end);
|
||||
}
|
||||
|
||||
/// Request update ticks from tick provider and update the painted ticks.
|
||||
void updateTicks() {
|
||||
_updateProvidedTicks();
|
||||
_updateAxisTicks();
|
||||
}
|
||||
|
||||
/// Request ticks from tick provider.
|
||||
void _updateProvidedTicks() {
|
||||
if (lockAxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Ensure that tick providers take manually configured
|
||||
// viewport settings into account, so that we still get the right number.
|
||||
_providedTicks = tickProvider.getTicks(
|
||||
context: context,
|
||||
graphicsFactory: graphicsFactory,
|
||||
scale: _scale,
|
||||
formatter: tickFormatter,
|
||||
formatterValueCache: _formatterValueCache,
|
||||
tickDrawStrategy: tickDrawStrategy,
|
||||
orientation: axisOrientation,
|
||||
viewportExtensionEnabled: _autoViewport);
|
||||
}
|
||||
|
||||
/// Updates the ticks that are actually used for drawing.
|
||||
void _updateAxisTicks() {
|
||||
if (lockAxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
final providedTicks = new List.from(_providedTicks ?? []);
|
||||
|
||||
for (AxisTicks<D> animatedTick in _axisTicks) {
|
||||
final tick = providedTicks?.firstWhere(
|
||||
(t) => t.value == animatedTick.value,
|
||||
orElse: () => null);
|
||||
|
||||
if (tick != null) {
|
||||
// Swap out the text element only if the settings are different.
|
||||
// This prevents a costly new TextPainter in Flutter.
|
||||
if (!TextElement.elementSettingsSame(
|
||||
animatedTick.textElement, tick.textElement)) {
|
||||
animatedTick.textElement = tick.textElement;
|
||||
}
|
||||
// Update target for all existing ticks
|
||||
animatedTick.setNewTarget(_scale[tick.value]);
|
||||
providedTicks.remove(tick);
|
||||
} else {
|
||||
// Animate out ticks that do not exist any more.
|
||||
animatedTick.animateOut(_scale[animatedTick.value].toDouble());
|
||||
}
|
||||
}
|
||||
|
||||
// Add new ticks
|
||||
providedTicks?.forEach((tick) {
|
||||
final animatedTick = new AxisTicks<D>(tick);
|
||||
if (_previousScale != null) {
|
||||
animatedTick.animateInFrom(_previousScale[tick.value].toDouble());
|
||||
}
|
||||
_axisTicks.add(animatedTick);
|
||||
});
|
||||
|
||||
_axisTicks.sort();
|
||||
|
||||
// Save a copy of the current scale to be used as the previous scale when
|
||||
// ticks are updated.
|
||||
_previousScale = _scale.copy();
|
||||
}
|
||||
|
||||
/// Configures the zoom and translate.
|
||||
///
|
||||
/// [viewportScale] is the zoom factor to use, likely >= 1.0 where 1.0 maps
|
||||
/// the complete data extents to the output range, and 2.0 only maps half the
|
||||
/// data to the output range.
|
||||
///
|
||||
/// [viewportTranslatePx] is the translate/pan to use in pixel units,
|
||||
/// likely <= 0 which shifts the start of the data before the edge of the
|
||||
/// chart giving us a pan.
|
||||
///
|
||||
/// [drawAreaWidth] is the width of the draw area for the series data in pixel
|
||||
/// units, at minimum viewport scale level (1.0). When provided,
|
||||
/// [viewportTranslatePx] will be clamped such that the axis cannot be panned
|
||||
/// beyond the bounds of the data.
|
||||
void setViewportSettings(double viewportScale, double viewportTranslatePx,
|
||||
{int drawAreaWidth}) {
|
||||
// Don't let the viewport be panned beyond the bounds of the data.
|
||||
viewportTranslatePx = _clampTranslatePx(viewportScale, viewportTranslatePx,
|
||||
drawAreaWidth: drawAreaWidth);
|
||||
|
||||
_scale.setViewportSettings(viewportScale, viewportTranslatePx);
|
||||
}
|
||||
|
||||
/// Returns the current viewport scale.
|
||||
///
|
||||
/// A scale of 1.0 would map the data directly to the output range, while a
|
||||
/// value of 2.0 would map the data to an output of double the range so you
|
||||
/// only see half the data in the viewport. This is the equivalent to
|
||||
/// zooming. Its value is likely >= 1.0.
|
||||
double get viewportScalingFactor => _scale.viewportScalingFactor;
|
||||
|
||||
/// Returns the current pixel viewport offset
|
||||
///
|
||||
/// The translate is used by the scale function when it applies the scale.
|
||||
/// This is the equivalent to panning. Its value is likely <= 0 to pan the
|
||||
/// data to the left.
|
||||
double get viewportTranslatePx => _scale?.viewportTranslatePx;
|
||||
|
||||
/// Clamps a possible change in domain translation to fit within the range of
|
||||
/// the data.
|
||||
double _clampTranslatePx(
|
||||
double viewportScalingFactor, double viewportTranslatePx,
|
||||
{int drawAreaWidth}) {
|
||||
if (drawAreaWidth == null) {
|
||||
return viewportTranslatePx;
|
||||
}
|
||||
|
||||
// Bound the viewport translate to the range of the data.
|
||||
final maxNegativeTranslate =
|
||||
-1.0 * ((drawAreaWidth * viewportScalingFactor) - drawAreaWidth);
|
||||
|
||||
viewportTranslatePx =
|
||||
min(max(viewportTranslatePx, maxNegativeTranslate), 0.0);
|
||||
|
||||
return viewportTranslatePx;
|
||||
}
|
||||
|
||||
//
|
||||
// LayoutView methods.
|
||||
//
|
||||
|
||||
@override
|
||||
GraphicsFactory get graphicsFactory => _graphicsFactory;
|
||||
|
||||
@override
|
||||
set graphicsFactory(GraphicsFactory value) {
|
||||
_graphicsFactory = value;
|
||||
}
|
||||
|
||||
@override
|
||||
LayoutViewConfig get layoutConfig => new LayoutViewConfig(
|
||||
paintOrder: layoutPaintOrder,
|
||||
position: _layoutPosition,
|
||||
positionOrder: LayoutViewPositionOrder.axis);
|
||||
|
||||
/// Get layout position from axis orientation.
|
||||
LayoutPosition get _layoutPosition {
|
||||
LayoutPosition position;
|
||||
switch (axisOrientation) {
|
||||
case AxisOrientation.top:
|
||||
position = LayoutPosition.Top;
|
||||
break;
|
||||
case AxisOrientation.right:
|
||||
position = LayoutPosition.Right;
|
||||
break;
|
||||
case AxisOrientation.bottom:
|
||||
position = LayoutPosition.Bottom;
|
||||
break;
|
||||
case AxisOrientation.left:
|
||||
position = LayoutPosition.Left;
|
||||
break;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/// The axis is rendered vertically.
|
||||
bool get isVertical =>
|
||||
axisOrientation == AxisOrientation.left ||
|
||||
axisOrientation == AxisOrientation.right;
|
||||
|
||||
@override
|
||||
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
|
||||
return isVertical
|
||||
? _measureVerticalAxis(maxWidth, maxHeight)
|
||||
: _measureHorizontalAxis(maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
ViewMeasuredSizes _measureVerticalAxis(int maxWidth, int maxHeight) {
|
||||
setOutputRange(maxHeight, 0);
|
||||
_updateProvidedTicks();
|
||||
|
||||
return tickDrawStrategy.measureVerticallyDrawnTicks(
|
||||
_providedTicks, maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
ViewMeasuredSizes _measureHorizontalAxis(int maxWidth, int maxHeight) {
|
||||
setOutputRange(0, maxWidth);
|
||||
_updateProvidedTicks();
|
||||
|
||||
return tickDrawStrategy.measureHorizontallyDrawnTicks(
|
||||
_providedTicks, maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
/// Layout this component.
|
||||
@override
|
||||
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
|
||||
_componentBounds = componentBounds;
|
||||
_drawAreaBounds = drawAreaBounds;
|
||||
|
||||
// Update the output range if it is different than the current one.
|
||||
// This is necessary because during the measure cycle, the output range is
|
||||
// set between zero and the max range available. On layout, the output range
|
||||
// needs to be updated to account of the offset of the axis view.
|
||||
|
||||
final outputStart =
|
||||
isVertical ? _componentBounds.bottom : _componentBounds.left;
|
||||
final outputEnd =
|
||||
isVertical ? _componentBounds.top : _componentBounds.right;
|
||||
|
||||
final outputRange = reverseOutputRange
|
||||
? new ScaleOutputExtent(outputEnd, outputStart)
|
||||
: new ScaleOutputExtent(outputStart, outputEnd);
|
||||
|
||||
if (_scale.range != outputRange) {
|
||||
_scale.range = outputRange;
|
||||
}
|
||||
|
||||
_updateProvidedTicks();
|
||||
// Update animated ticks in layout, because updateTicks are called during
|
||||
// measure and we don't want to update the animation at that time.
|
||||
_updateAxisTicks();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isSeriesRenderer => false;
|
||||
|
||||
@override
|
||||
Rectangle<int> get componentBounds => this._componentBounds;
|
||||
|
||||
bool get drawAxisLine {
|
||||
if (forceDrawAxisLine != null) {
|
||||
return forceDrawAxisLine;
|
||||
}
|
||||
|
||||
return tickDrawStrategy is SmallTickDrawStrategy;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(ChartCanvas canvas, double animationPercent) {
|
||||
if (animationPercent == 1.0) {
|
||||
_axisTicks.removeWhere((t) => t.markedForRemoval);
|
||||
}
|
||||
|
||||
for (var i = 0; i < _axisTicks.length; i++) {
|
||||
final animatedTick = _axisTicks[i];
|
||||
tickDrawStrategy.draw(
|
||||
canvas, animatedTick..setCurrentTick(animationPercent),
|
||||
orientation: axisOrientation,
|
||||
axisBounds: _componentBounds,
|
||||
drawAreaBounds: _drawAreaBounds,
|
||||
isFirst: i == 0,
|
||||
isLast: i == _axisTicks.length - 1);
|
||||
}
|
||||
|
||||
if (drawAxisLine) {
|
||||
tickDrawStrategy.drawAxisLine(canvas, axisOrientation, _componentBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NumericAxis extends Axis<num> {
|
||||
NumericAxis({TickProvider<num> tickProvider})
|
||||
: super(
|
||||
tickProvider: tickProvider ?? new NumericTickProvider(),
|
||||
tickFormatter: new NumericTickFormatter(),
|
||||
scale: new LinearScale(),
|
||||
);
|
||||
|
||||
void setScaleViewport(NumericExtents viewport) {
|
||||
autoViewport = false;
|
||||
(_scale as NumericScale).viewportDomain = viewport;
|
||||
}
|
||||
}
|
||||
|
||||
class OrdinalAxis extends Axis<String> {
|
||||
OrdinalAxis({
|
||||
TickDrawStrategy tickDrawStrategy,
|
||||
TickProvider tickProvider,
|
||||
TickFormatter tickFormatter,
|
||||
}) : super(
|
||||
tickProvider: tickProvider ?? const OrdinalTickProvider(),
|
||||
tickFormatter: tickFormatter ?? const OrdinalTickFormatter(),
|
||||
scale: new SimpleOrdinalScale(),
|
||||
);
|
||||
|
||||
void setScaleViewport(OrdinalViewport viewport) {
|
||||
autoViewport = false;
|
||||
(_scale as SimpleOrdinalScale)
|
||||
.setViewport(viewport.dataSize, viewport.startingDomain);
|
||||
}
|
||||
|
||||
@override
|
||||
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
|
||||
super.layout(componentBounds, drawAreaBounds);
|
||||
|
||||
// We are purposely clearing the viewport starting domain and data size
|
||||
// post layout.
|
||||
//
|
||||
// Originally we set a flag in [setScaleViewport] to recalculate viewport
|
||||
// settings on next scale update and then reset the flag. This doesn't work
|
||||
// because chart's measure cycle provides different ranges to the scale,
|
||||
// causing the scale to update multiple times before it is finalized after
|
||||
// layout.
|
||||
//
|
||||
// By resetting the viewport after layout, we guarantee the correct range
|
||||
// was used to apply the viewport and behaviors that update the viewport
|
||||
// based on translate and scale changes will not be affected (pan/zoom).
|
||||
(_scale as SimpleOrdinalScale).setViewport(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Viewport to cover [dataSize] data points starting at [startingDomain] value.
|
||||
class OrdinalViewport {
|
||||
final String startingDomain;
|
||||
final int dataSize;
|
||||
|
||||
OrdinalViewport(this.startingDomain, this.dataSize);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is OrdinalViewport &&
|
||||
startingDomain == other.startingDomain &&
|
||||
dataSize == other.dataSize;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = startingDomain.hashCode;
|
||||
hashcode = (hashcode * 37) + dataSize;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class AxisTester<D> {
|
||||
final Axis<D> _axis;
|
||||
|
||||
AxisTester(this._axis);
|
||||
|
||||
List<AxisTicks<D>> get axisTicks => _axis._axisTicks;
|
||||
|
||||
MutableScale<D> get scale => _axis._scale;
|
||||
|
||||
List<D> get axisValues => axisTicks.map((t) => t.value).toList();
|
||||
}
|
||||
112
web/charts/common/lib/src/chart/cartesian/axis/axis_tick.dart
Normal file
112
web/charts/common/lib/src/chart/cartesian/axis/axis_tick.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'tick.dart' show Tick;
|
||||
|
||||
class AxisTicks<D> extends Tick<D> implements Comparable<AxisTicks<D>> {
|
||||
/// This tick is being animated out.
|
||||
bool _markedForRemoval;
|
||||
|
||||
/// This tick's current location.
|
||||
double _currentLocation;
|
||||
|
||||
/// This tick's previous target location.
|
||||
double _previousLocation;
|
||||
|
||||
/// This tick's current target location.
|
||||
double _targetLocation;
|
||||
|
||||
/// This tick's current opacity.
|
||||
double _currentOpacity;
|
||||
|
||||
/// This tick's previous opacity.
|
||||
double _previousOpacity;
|
||||
|
||||
/// This tick's target opacity.
|
||||
double _targetOpacity;
|
||||
|
||||
AxisTicks(Tick<D> tick)
|
||||
: super(
|
||||
value: tick.value,
|
||||
textElement: tick.textElement,
|
||||
locationPx: tick.locationPx,
|
||||
labelOffsetPx: tick.labelOffsetPx) {
|
||||
/// Set the initial target for a new animated tick.
|
||||
_markedForRemoval = false;
|
||||
_targetLocation = tick.locationPx;
|
||||
}
|
||||
|
||||
bool get markedForRemoval => _markedForRemoval;
|
||||
|
||||
/// Animate the tick in from [previousLocation].
|
||||
void animateInFrom(double previousLocation) {
|
||||
_markedForRemoval = false;
|
||||
_previousLocation = previousLocation;
|
||||
_previousOpacity = 0.0;
|
||||
_targetOpacity = 1.0;
|
||||
}
|
||||
|
||||
/// Animate out this tick to [newLocation].
|
||||
void animateOut(double newLocation) {
|
||||
_markedForRemoval = true;
|
||||
_previousLocation = _currentLocation;
|
||||
_targetLocation = newLocation;
|
||||
_previousOpacity = _currentOpacity;
|
||||
_targetOpacity = 0.0;
|
||||
}
|
||||
|
||||
/// Set new target for this tick to be [newLocation].
|
||||
void setNewTarget(double newLocation) {
|
||||
_markedForRemoval = false;
|
||||
_previousLocation = _currentLocation;
|
||||
_targetLocation = newLocation;
|
||||
_previousOpacity = _currentOpacity;
|
||||
_targetOpacity = 1.0;
|
||||
}
|
||||
|
||||
/// Update tick's location and opacity based on animation percent.
|
||||
void setCurrentTick(double animationPercent) {
|
||||
if (animationPercent == 1.0) {
|
||||
_currentLocation = _targetLocation;
|
||||
_previousLocation = _targetLocation;
|
||||
_currentOpacity = markedForRemoval ? 0.0 : 1.0;
|
||||
} else if (_previousLocation == null) {
|
||||
_currentLocation = _targetLocation;
|
||||
_currentOpacity = 1.0;
|
||||
} else {
|
||||
_currentLocation =
|
||||
_lerpDouble(_previousLocation, _targetLocation, animationPercent);
|
||||
_currentOpacity =
|
||||
_lerpDouble(_previousOpacity, _targetOpacity, animationPercent);
|
||||
}
|
||||
|
||||
locationPx = _currentLocation;
|
||||
textElement.opacity = _currentOpacity;
|
||||
}
|
||||
|
||||
/// Linearly interpolate between two numbers.
|
||||
///
|
||||
/// From lerpDouble in dart:ui which is Flutter only.
|
||||
double _lerpDouble(double a, double b, double t) {
|
||||
if (a == null && b == null) return null;
|
||||
a ??= 0.0;
|
||||
b ??= 0.0;
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
int compareTo(AxisTicks<D> other) {
|
||||
return _targetLocation.compareTo(other._targetLocation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
import 'tick.dart' show Tick;
|
||||
|
||||
/// A report that contains a list of ticks and if they collide.
|
||||
class CollisionReport {
|
||||
/// If [ticks] collide.
|
||||
final bool ticksCollide;
|
||||
|
||||
final List<Tick> ticks;
|
||||
|
||||
final bool alternateTicksUsed;
|
||||
|
||||
CollisionReport(
|
||||
{@required this.ticksCollide,
|
||||
@required this.ticks,
|
||||
bool alternateTicksUsed})
|
||||
: alternateTicksUsed = alternateTicksUsed ?? false;
|
||||
|
||||
CollisionReport.empty()
|
||||
: ticksCollide = false,
|
||||
ticks = [],
|
||||
alternateTicksUsed = false;
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:meta/meta.dart' show immutable, protected, required;
|
||||
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../../common/line_style.dart' show LineStyle;
|
||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
||||
import '../../../../common/text_element.dart' show TextDirection;
|
||||
import '../../../../common/text_style.dart' show TextStyle;
|
||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
|
||||
import '../axis.dart' show AxisOrientation;
|
||||
import '../collision_report.dart' show CollisionReport;
|
||||
import '../spec/axis_spec.dart'
|
||||
show
|
||||
TextStyleSpec,
|
||||
TickLabelAnchor,
|
||||
TickLabelJustification,
|
||||
LineStyleSpec,
|
||||
RenderSpec;
|
||||
import '../tick.dart' show Tick;
|
||||
import 'tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
|
||||
@immutable
|
||||
abstract class BaseRenderSpec<D> implements RenderSpec<D> {
|
||||
final TextStyleSpec labelStyle;
|
||||
final TickLabelAnchor labelAnchor;
|
||||
final TickLabelJustification labelJustification;
|
||||
|
||||
final int labelOffsetFromAxisPx;
|
||||
|
||||
/// Absolute distance from the tick to the text if using start/end
|
||||
final int labelOffsetFromTickPx;
|
||||
|
||||
final int minimumPaddingBetweenLabelsPx;
|
||||
|
||||
final LineStyleSpec axisLineStyle;
|
||||
|
||||
const BaseRenderSpec({
|
||||
this.labelStyle,
|
||||
this.labelAnchor,
|
||||
this.labelJustification,
|
||||
this.labelOffsetFromAxisPx,
|
||||
this.labelOffsetFromTickPx,
|
||||
this.minimumPaddingBetweenLabelsPx,
|
||||
this.axisLineStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is BaseRenderSpec &&
|
||||
labelStyle == other.labelStyle &&
|
||||
labelAnchor == other.labelAnchor &&
|
||||
labelJustification == other.labelJustification &&
|
||||
labelOffsetFromTickPx == other.labelOffsetFromTickPx &&
|
||||
labelOffsetFromAxisPx == other.labelOffsetFromAxisPx &&
|
||||
minimumPaddingBetweenLabelsPx ==
|
||||
other.minimumPaddingBetweenLabelsPx &&
|
||||
axisLineStyle == other.axisLineStyle);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = labelStyle?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + labelAnchor?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + labelJustification?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + labelOffsetFromTickPx?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + labelOffsetFromAxisPx?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + minimumPaddingBetweenLabelsPx?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + axisLineStyle?.hashCode ?? 0;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Base strategy that draws tick labels and checks for label collisions.
|
||||
abstract class BaseTickDrawStrategy<D> implements TickDrawStrategy<D> {
|
||||
final ChartContext chartContext;
|
||||
|
||||
LineStyle axisLineStyle;
|
||||
TextStyle labelStyle;
|
||||
TickLabelAnchor tickLabelAnchor;
|
||||
TickLabelJustification tickLabelJustification;
|
||||
int labelOffsetFromAxisPx;
|
||||
int labelOffsetFromTickPx;
|
||||
|
||||
int minimumPaddingBetweenLabelsPx;
|
||||
|
||||
BaseTickDrawStrategy(this.chartContext, GraphicsFactory graphicsFactory,
|
||||
{TextStyleSpec labelStyleSpec,
|
||||
LineStyleSpec axisLineStyleSpec,
|
||||
TickLabelAnchor labelAnchor,
|
||||
TickLabelJustification labelJustification,
|
||||
int labelOffsetFromAxisPx,
|
||||
int labelOffsetFromTickPx,
|
||||
int minimumPaddingBetweenLabelsPx}) {
|
||||
labelStyle = (graphicsFactory.createTextPaint()
|
||||
..color = labelStyleSpec?.color ?? StyleFactory.style.tickColor
|
||||
..fontFamily = labelStyleSpec?.fontFamily
|
||||
..fontSize = labelStyleSpec?.fontSize ?? 12);
|
||||
|
||||
axisLineStyle = graphicsFactory.createLinePaint()
|
||||
..color = axisLineStyleSpec?.color ?? labelStyle.color
|
||||
..dashPattern = axisLineStyleSpec?.dashPattern
|
||||
..strokeWidth = axisLineStyleSpec?.thickness ?? 1;
|
||||
|
||||
tickLabelAnchor = labelAnchor ?? TickLabelAnchor.centered;
|
||||
tickLabelJustification =
|
||||
labelJustification ?? TickLabelJustification.inside;
|
||||
this.labelOffsetFromAxisPx = labelOffsetFromAxisPx ?? 5;
|
||||
this.labelOffsetFromTickPx = labelOffsetFromTickPx ?? 5;
|
||||
this.minimumPaddingBetweenLabelsPx = minimumPaddingBetweenLabelsPx ?? 50;
|
||||
}
|
||||
|
||||
@override
|
||||
void decorateTicks(List<Tick<D>> ticks) {
|
||||
for (Tick<D> tick in ticks) {
|
||||
// If no style at all, set the default style.
|
||||
if (tick.textElement.textStyle == null) {
|
||||
tick.textElement.textStyle = labelStyle;
|
||||
} else {
|
||||
//Fill in whatever is missing
|
||||
tick.textElement.textStyle.color ??= labelStyle.color;
|
||||
tick.textElement.textStyle.fontFamily ??= labelStyle.fontFamily;
|
||||
tick.textElement.textStyle.fontSize ??= labelStyle.fontSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
CollisionReport collides(List<Tick<D>> ticks, AxisOrientation orientation) {
|
||||
// If there are no ticks, they do not collide.
|
||||
if (ticks == null) {
|
||||
return new CollisionReport(
|
||||
ticksCollide: false, ticks: ticks, alternateTicksUsed: false);
|
||||
}
|
||||
|
||||
final vertical = orientation == AxisOrientation.left ||
|
||||
orientation == AxisOrientation.right;
|
||||
|
||||
// First sort ticks by smallest locationPx first (NOT sorted by value).
|
||||
// This allows us to only check if a tick collides with the previous tick.
|
||||
ticks.sort((a, b) {
|
||||
if (a.locationPx < b.locationPx) {
|
||||
return -1;
|
||||
} else if (a.locationPx > b.locationPx) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
double previousEnd = double.negativeInfinity;
|
||||
bool collides = false;
|
||||
|
||||
for (final tick in ticks) {
|
||||
final tickSize = tick.textElement.measurement;
|
||||
|
||||
if (vertical) {
|
||||
final adjustedHeight =
|
||||
tickSize.verticalSliceWidth + minimumPaddingBetweenLabelsPx;
|
||||
|
||||
if (tickLabelAnchor == TickLabelAnchor.inside) {
|
||||
if (identical(tick, ticks.first)) {
|
||||
// Top most tick draws down from the location
|
||||
collides = false;
|
||||
previousEnd = tick.locationPx + adjustedHeight;
|
||||
} else if (identical(tick, ticks.last)) {
|
||||
// Bottom most tick draws up from the location
|
||||
collides = previousEnd > tick.locationPx - adjustedHeight;
|
||||
previousEnd = tick.locationPx;
|
||||
} else {
|
||||
// All other ticks is centered.
|
||||
final halfHeight = adjustedHeight / 2;
|
||||
collides = previousEnd > tick.locationPx - halfHeight;
|
||||
previousEnd = tick.locationPx + halfHeight;
|
||||
}
|
||||
} else {
|
||||
collides = previousEnd > tick.locationPx;
|
||||
previousEnd = tick.locationPx + adjustedHeight;
|
||||
}
|
||||
} else {
|
||||
// Use the text direction the ticks specified, unless the label anchor
|
||||
// is set to [TickLabelAnchor.inside]. When 'inside' is set, the text
|
||||
// direction is normalized such that the left most tick is drawn ltr,
|
||||
// the last tick is drawn rtl, and all other ticks are in the center.
|
||||
// This is not set until it is painted, so collision check needs to get
|
||||
// the value also.
|
||||
final textDirection = _normalizeHorizontalAnchor(
|
||||
tickLabelAnchor,
|
||||
chartContext.isRtl,
|
||||
identical(tick, ticks.first),
|
||||
identical(tick, ticks.last));
|
||||
final adjustedWidth =
|
||||
tickSize.horizontalSliceWidth + minimumPaddingBetweenLabelsPx;
|
||||
switch (textDirection) {
|
||||
case TextDirection.ltr:
|
||||
collides = previousEnd > tick.locationPx;
|
||||
previousEnd = tick.locationPx + adjustedWidth;
|
||||
break;
|
||||
case TextDirection.rtl:
|
||||
collides = previousEnd > (tick.locationPx - adjustedWidth);
|
||||
previousEnd = tick.locationPx;
|
||||
break;
|
||||
case TextDirection.center:
|
||||
final halfWidth = adjustedWidth / 2;
|
||||
collides = previousEnd > tick.locationPx - halfWidth;
|
||||
previousEnd = tick.locationPx + halfWidth;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (collides) {
|
||||
return new CollisionReport(
|
||||
ticksCollide: true, ticks: ticks, alternateTicksUsed: false);
|
||||
}
|
||||
}
|
||||
|
||||
return new CollisionReport(
|
||||
ticksCollide: false, ticks: ticks, alternateTicksUsed: false);
|
||||
}
|
||||
|
||||
@override
|
||||
ViewMeasuredSizes measureVerticallyDrawnTicks(
|
||||
List<Tick<D>> ticks, int maxWidth, int maxHeight) {
|
||||
// TODO: Add spacing to account for the distance between the
|
||||
// text and the axis baseline (even if it isn't drawn).
|
||||
final maxHorizontalSliceWidth = ticks
|
||||
.fold(
|
||||
0.0,
|
||||
(double prevMax, tick) => max(
|
||||
prevMax,
|
||||
tick.textElement.measurement.horizontalSliceWidth +
|
||||
labelOffsetFromAxisPx))
|
||||
.round();
|
||||
|
||||
return new ViewMeasuredSizes(
|
||||
preferredWidth: maxHorizontalSliceWidth, preferredHeight: maxHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
ViewMeasuredSizes measureHorizontallyDrawnTicks(
|
||||
List<Tick<D>> ticks, int maxWidth, int maxHeight) {
|
||||
final maxVerticalSliceWidth = ticks
|
||||
.fold(
|
||||
0.0,
|
||||
(double prevMax, tick) =>
|
||||
max(prevMax, tick.textElement.measurement.verticalSliceWidth))
|
||||
.round();
|
||||
|
||||
return new ViewMeasuredSizes(
|
||||
preferredWidth: maxWidth,
|
||||
preferredHeight: maxVerticalSliceWidth + labelOffsetFromAxisPx);
|
||||
}
|
||||
|
||||
@override
|
||||
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
|
||||
Rectangle<int> axisBounds) {
|
||||
Point<num> start;
|
||||
Point<num> end;
|
||||
|
||||
switch (orientation) {
|
||||
case AxisOrientation.top:
|
||||
start = axisBounds.bottomLeft;
|
||||
end = axisBounds.bottomRight;
|
||||
break;
|
||||
case AxisOrientation.bottom:
|
||||
start = axisBounds.topLeft;
|
||||
end = axisBounds.topRight;
|
||||
break;
|
||||
case AxisOrientation.right:
|
||||
start = axisBounds.topLeft;
|
||||
end = axisBounds.bottomLeft;
|
||||
break;
|
||||
case AxisOrientation.left:
|
||||
start = axisBounds.topRight;
|
||||
end = axisBounds.bottomRight;
|
||||
break;
|
||||
}
|
||||
|
||||
canvas.drawLine(
|
||||
points: [start, end],
|
||||
fill: axisLineStyle.color,
|
||||
stroke: axisLineStyle.color,
|
||||
strokeWidthPx: axisLineStyle.strokeWidth.toDouble(),
|
||||
dashPattern: axisLineStyle.dashPattern,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void drawLabel(ChartCanvas canvas, Tick<D> tick,
|
||||
{@required AxisOrientation orientation,
|
||||
@required Rectangle<int> axisBounds,
|
||||
@required Rectangle<int> drawAreaBounds,
|
||||
@required bool isFirst,
|
||||
@required bool isLast}) {
|
||||
final locationPx = tick.locationPx;
|
||||
final measurement = tick.textElement.measurement;
|
||||
final isRtl = chartContext.isRtl;
|
||||
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
|
||||
final labelOffsetPx = tick.labelOffsetPx ?? 0;
|
||||
|
||||
if (orientation == AxisOrientation.bottom ||
|
||||
orientation == AxisOrientation.top) {
|
||||
y = orientation == AxisOrientation.bottom
|
||||
? axisBounds.top + labelOffsetFromAxisPx
|
||||
: axisBounds.bottom -
|
||||
measurement.verticalSliceWidth.toInt() -
|
||||
labelOffsetFromAxisPx;
|
||||
|
||||
final direction =
|
||||
_normalizeHorizontalAnchor(tickLabelAnchor, isRtl, isFirst, isLast);
|
||||
tick.textElement.textDirection = direction;
|
||||
|
||||
switch (direction) {
|
||||
case TextDirection.rtl:
|
||||
x = (locationPx + labelOffsetFromTickPx + labelOffsetPx).toInt();
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
x = (locationPx - labelOffsetFromTickPx - labelOffsetPx).toInt();
|
||||
break;
|
||||
case TextDirection.center:
|
||||
default:
|
||||
x = (locationPx - labelOffsetPx).toInt();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (orientation == AxisOrientation.left) {
|
||||
if (tickLabelJustification == TickLabelJustification.inside) {
|
||||
x = axisBounds.right - labelOffsetFromAxisPx;
|
||||
tick.textElement.textDirection = TextDirection.rtl;
|
||||
} else {
|
||||
x = axisBounds.left + labelOffsetFromAxisPx;
|
||||
tick.textElement.textDirection = TextDirection.ltr;
|
||||
}
|
||||
} else {
|
||||
// orientation == right
|
||||
if (tickLabelJustification == TickLabelJustification.inside) {
|
||||
x = axisBounds.left + labelOffsetFromAxisPx;
|
||||
tick.textElement.textDirection = TextDirection.ltr;
|
||||
} else {
|
||||
x = axisBounds.right - labelOffsetFromAxisPx;
|
||||
tick.textElement.textDirection = TextDirection.rtl;
|
||||
}
|
||||
}
|
||||
|
||||
switch (_normalizeVerticalAnchor(tickLabelAnchor, isFirst, isLast)) {
|
||||
case _PixelVerticalDirection.over:
|
||||
y = (locationPx -
|
||||
measurement.verticalSliceWidth -
|
||||
labelOffsetFromTickPx -
|
||||
labelOffsetPx)
|
||||
.toInt();
|
||||
break;
|
||||
case _PixelVerticalDirection.under:
|
||||
y = (locationPx + labelOffsetFromTickPx + labelOffsetPx).toInt();
|
||||
break;
|
||||
case _PixelVerticalDirection.center:
|
||||
default:
|
||||
y = (locationPx - measurement.verticalSliceWidth / 2 + labelOffsetPx)
|
||||
.toInt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawText(tick.textElement, x, y);
|
||||
}
|
||||
|
||||
TextDirection _normalizeHorizontalAnchor(
|
||||
TickLabelAnchor anchor, bool isRtl, bool isFirst, bool isLast) {
|
||||
switch (anchor) {
|
||||
case TickLabelAnchor.before:
|
||||
return isRtl ? TextDirection.ltr : TextDirection.rtl;
|
||||
case TickLabelAnchor.after:
|
||||
return isRtl ? TextDirection.rtl : TextDirection.ltr;
|
||||
case TickLabelAnchor.inside:
|
||||
if (isFirst) {
|
||||
return TextDirection.ltr;
|
||||
}
|
||||
if (isLast) {
|
||||
return TextDirection.rtl;
|
||||
}
|
||||
return TextDirection.center;
|
||||
case TickLabelAnchor.centered:
|
||||
default:
|
||||
return TextDirection.center;
|
||||
}
|
||||
}
|
||||
|
||||
_PixelVerticalDirection _normalizeVerticalAnchor(
|
||||
TickLabelAnchor anchor, bool isFirst, bool isLast) {
|
||||
switch (anchor) {
|
||||
case TickLabelAnchor.before:
|
||||
return _PixelVerticalDirection.under;
|
||||
case TickLabelAnchor.after:
|
||||
return _PixelVerticalDirection.over;
|
||||
case TickLabelAnchor.inside:
|
||||
if (isFirst) {
|
||||
return _PixelVerticalDirection.over;
|
||||
}
|
||||
if (isLast) {
|
||||
return _PixelVerticalDirection.under;
|
||||
}
|
||||
return _PixelVerticalDirection.center;
|
||||
case TickLabelAnchor.centered:
|
||||
default:
|
||||
return _PixelVerticalDirection.center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _PixelVerticalDirection {
|
||||
over,
|
||||
center,
|
||||
under,
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:meta/meta.dart' show immutable, required;
|
||||
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../../common/line_style.dart' show LineStyle;
|
||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../axis.dart' show AxisOrientation;
|
||||
import '../spec/axis_spec.dart'
|
||||
show TextStyleSpec, LineStyleSpec, TickLabelAnchor, TickLabelJustification;
|
||||
import '../tick.dart' show Tick;
|
||||
import 'base_tick_draw_strategy.dart' show BaseTickDrawStrategy;
|
||||
import 'small_tick_draw_strategy.dart' show SmallTickRendererSpec;
|
||||
import 'tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
|
||||
@immutable
|
||||
class GridlineRendererSpec<D> extends SmallTickRendererSpec<D> {
|
||||
const GridlineRendererSpec({
|
||||
TextStyleSpec labelStyle,
|
||||
LineStyleSpec lineStyle,
|
||||
LineStyleSpec axisLineStyle,
|
||||
TickLabelAnchor labelAnchor,
|
||||
TickLabelJustification labelJustification,
|
||||
int tickLengthPx,
|
||||
int labelOffsetFromAxisPx,
|
||||
int labelOffsetFromTickPx,
|
||||
int minimumPaddingBetweenLabelsPx,
|
||||
}) : super(
|
||||
labelStyle: labelStyle,
|
||||
lineStyle: lineStyle,
|
||||
labelAnchor: labelAnchor,
|
||||
labelJustification: labelJustification,
|
||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx,
|
||||
tickLengthPx: tickLengthPx,
|
||||
axisLineStyle: axisLineStyle);
|
||||
|
||||
@override
|
||||
TickDrawStrategy<D> createDrawStrategy(
|
||||
ChartContext context, GraphicsFactory graphicsFactory) =>
|
||||
new GridlineTickDrawStrategy<D>(context, graphicsFactory,
|
||||
tickLengthPx: tickLengthPx,
|
||||
lineStyleSpec: lineStyle,
|
||||
labelStyleSpec: labelStyle,
|
||||
axisLineStyleSpec: axisLineStyle,
|
||||
labelAnchor: labelAnchor,
|
||||
labelJustification: labelJustification,
|
||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is GridlineRendererSpec && super == (other));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = super.hashCode;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws line across chart draw area for each tick.
|
||||
///
|
||||
/// Extends [BaseTickDrawStrategy].
|
||||
class GridlineTickDrawStrategy<D> extends BaseTickDrawStrategy<D> {
|
||||
int tickLength;
|
||||
LineStyle lineStyle;
|
||||
|
||||
GridlineTickDrawStrategy(
|
||||
ChartContext chartContext,
|
||||
GraphicsFactory graphicsFactory, {
|
||||
int tickLengthPx,
|
||||
LineStyleSpec lineStyleSpec,
|
||||
TextStyleSpec labelStyleSpec,
|
||||
LineStyleSpec axisLineStyleSpec,
|
||||
TickLabelAnchor labelAnchor,
|
||||
TickLabelJustification labelJustification,
|
||||
int labelOffsetFromAxisPx,
|
||||
int labelOffsetFromTickPx,
|
||||
int minimumPaddingBetweenLabelsPx,
|
||||
}) : super(chartContext, graphicsFactory,
|
||||
labelStyleSpec: labelStyleSpec,
|
||||
axisLineStyleSpec: axisLineStyleSpec ?? lineStyleSpec,
|
||||
labelAnchor: labelAnchor,
|
||||
labelJustification: labelJustification,
|
||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx) {
|
||||
lineStyle =
|
||||
StyleFactory.style.createGridlineStyle(graphicsFactory, lineStyleSpec);
|
||||
|
||||
this.tickLength = tickLengthPx ?? 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void draw(ChartCanvas canvas, Tick<D> tick,
|
||||
{@required AxisOrientation orientation,
|
||||
@required Rectangle<int> axisBounds,
|
||||
@required Rectangle<int> drawAreaBounds,
|
||||
@required bool isFirst,
|
||||
@required bool isLast}) {
|
||||
Point<num> lineStart;
|
||||
Point<num> lineEnd;
|
||||
switch (orientation) {
|
||||
case AxisOrientation.top:
|
||||
final x = tick.locationPx;
|
||||
lineStart = new Point(x, axisBounds.bottom - tickLength);
|
||||
lineEnd = new Point(x, drawAreaBounds.bottom);
|
||||
break;
|
||||
case AxisOrientation.bottom:
|
||||
final x = tick.locationPx;
|
||||
lineStart = new Point(x, drawAreaBounds.top + tickLength);
|
||||
lineEnd = new Point(x, axisBounds.top);
|
||||
break;
|
||||
case AxisOrientation.right:
|
||||
final y = tick.locationPx;
|
||||
if (tickLabelAnchor == TickLabelAnchor.after ||
|
||||
tickLabelAnchor == TickLabelAnchor.before) {
|
||||
lineStart = new Point(axisBounds.right, y);
|
||||
} else {
|
||||
lineStart = new Point(axisBounds.left + tickLength, y);
|
||||
}
|
||||
lineEnd = new Point(drawAreaBounds.left, y);
|
||||
break;
|
||||
case AxisOrientation.left:
|
||||
final y = tick.locationPx;
|
||||
|
||||
if (tickLabelAnchor == TickLabelAnchor.after ||
|
||||
tickLabelAnchor == TickLabelAnchor.before) {
|
||||
lineStart = new Point(axisBounds.left, y);
|
||||
} else {
|
||||
lineStart = new Point(axisBounds.right - tickLength, y);
|
||||
}
|
||||
lineEnd = new Point(drawAreaBounds.right, y);
|
||||
break;
|
||||
}
|
||||
|
||||
canvas.drawLine(
|
||||
points: [lineStart, lineEnd],
|
||||
dashPattern: lineStyle.dashPattern,
|
||||
fill: lineStyle.color,
|
||||
stroke: lineStyle.color,
|
||||
strokeWidthPx: lineStyle.strokeWidth.toDouble(),
|
||||
);
|
||||
|
||||
drawLabel(canvas, tick,
|
||||
orientation: orientation,
|
||||
axisBounds: axisBounds,
|
||||
drawAreaBounds: drawAreaBounds,
|
||||
isFirst: isFirst,
|
||||
isLast: isLast);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:meta/meta.dart' show immutable, required;
|
||||
|
||||
import '../../../../common/color.dart' show Color;
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../../common/line_style.dart' show LineStyle;
|
||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
||||
import '../../../../common/text_style.dart' show TextStyle;
|
||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
|
||||
import '../axis.dart' show AxisOrientation;
|
||||
import '../collision_report.dart' show CollisionReport;
|
||||
import '../spec/axis_spec.dart' show RenderSpec, LineStyleSpec;
|
||||
import '../tick.dart' show Tick;
|
||||
import 'tick_draw_strategy.dart';
|
||||
|
||||
/// Renders no ticks no labels, and claims no space in layout.
|
||||
/// However, it does render the axis line if asked to by the axis.
|
||||
@immutable
|
||||
class NoneRenderSpec<D> extends RenderSpec<D> {
|
||||
final LineStyleSpec axisLineStyle;
|
||||
|
||||
const NoneRenderSpec({this.axisLineStyle});
|
||||
|
||||
@override
|
||||
TickDrawStrategy<D> createDrawStrategy(
|
||||
ChartContext context, GraphicsFactory graphicFactory) =>
|
||||
new NoneDrawStrategy<D>(context, graphicFactory,
|
||||
axisLineStyleSpec: axisLineStyle);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || other is NoneRenderSpec;
|
||||
|
||||
@override
|
||||
int get hashCode => 0;
|
||||
}
|
||||
|
||||
class NoneDrawStrategy<D> implements TickDrawStrategy<D> {
|
||||
LineStyle axisLineStyle;
|
||||
TextStyle noneTextStyle;
|
||||
|
||||
NoneDrawStrategy(ChartContext chartContext, GraphicsFactory graphicsFactory,
|
||||
{LineStyleSpec axisLineStyleSpec}) {
|
||||
axisLineStyle = StyleFactory.style
|
||||
.createAxisLineStyle(graphicsFactory, axisLineStyleSpec);
|
||||
noneTextStyle = graphicsFactory.createTextPaint()
|
||||
..color = Color.transparent
|
||||
..fontSize = 0;
|
||||
}
|
||||
|
||||
@override
|
||||
CollisionReport collides(List<Tick> ticks, AxisOrientation orientation) =>
|
||||
new CollisionReport(ticksCollide: false, ticks: ticks);
|
||||
|
||||
@override
|
||||
void decorateTicks(List<Tick> ticks) {
|
||||
// Even though no text is rendered, the text style for each element should
|
||||
// still be set to handle the case of the draw strategy being switched to
|
||||
// a different draw strategy. The new draw strategy will try to animate
|
||||
// the old ticks out and the text style property is used.
|
||||
ticks.forEach((tick) => tick.textElement.textStyle = noneTextStyle);
|
||||
}
|
||||
|
||||
@override
|
||||
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
|
||||
Rectangle<int> axisBounds) {
|
||||
Point<num> start;
|
||||
Point<num> end;
|
||||
|
||||
switch (orientation) {
|
||||
case AxisOrientation.top:
|
||||
start = axisBounds.bottomLeft;
|
||||
end = axisBounds.bottomRight;
|
||||
|
||||
break;
|
||||
case AxisOrientation.bottom:
|
||||
start = axisBounds.topLeft;
|
||||
end = axisBounds.topRight;
|
||||
break;
|
||||
case AxisOrientation.right:
|
||||
start = axisBounds.topLeft;
|
||||
end = axisBounds.bottomLeft;
|
||||
break;
|
||||
case AxisOrientation.left:
|
||||
start = axisBounds.topRight;
|
||||
end = axisBounds.bottomRight;
|
||||
break;
|
||||
}
|
||||
|
||||
canvas.drawLine(
|
||||
points: [start, end],
|
||||
dashPattern: axisLineStyle.dashPattern,
|
||||
fill: axisLineStyle.color,
|
||||
stroke: axisLineStyle.color,
|
||||
strokeWidthPx: axisLineStyle.strokeWidth.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void draw(ChartCanvas canvas, Tick<D> tick,
|
||||
{@required AxisOrientation orientation,
|
||||
@required Rectangle<int> axisBounds,
|
||||
@required Rectangle<int> drawAreaBounds,
|
||||
@required bool isFirst,
|
||||
@required bool isLast}) {}
|
||||
|
||||
@override
|
||||
ViewMeasuredSizes measureHorizontallyDrawnTicks(
|
||||
List<Tick> ticks, int maxWidth, int maxHeight) {
|
||||
return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
ViewMeasuredSizes measureVerticallyDrawnTicks(
|
||||
List<Tick> ticks, int maxWidth, int maxHeight) {
|
||||
return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:meta/meta.dart' show immutable, required;
|
||||
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../../common/line_style.dart' show LineStyle;
|
||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../axis.dart' show AxisOrientation;
|
||||
import '../spec/axis_spec.dart'
|
||||
show TextStyleSpec, LineStyleSpec, TickLabelAnchor, TickLabelJustification;
|
||||
import '../tick.dart' show Tick;
|
||||
import 'base_tick_draw_strategy.dart' show BaseRenderSpec, BaseTickDrawStrategy;
|
||||
import 'tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
|
||||
///
|
||||
@immutable
|
||||
class SmallTickRendererSpec<D> extends BaseRenderSpec<D> {
|
||||
final LineStyleSpec lineStyle;
|
||||
final int tickLengthPx;
|
||||
|
||||
const SmallTickRendererSpec({
|
||||
TextStyleSpec labelStyle,
|
||||
this.lineStyle,
|
||||
LineStyleSpec axisLineStyle,
|
||||
TickLabelAnchor labelAnchor,
|
||||
TickLabelJustification labelJustification,
|
||||
int labelOffsetFromAxisPx,
|
||||
int labelOffsetFromTickPx,
|
||||
this.tickLengthPx,
|
||||
int minimumPaddingBetweenLabelsPx,
|
||||
}) : super(
|
||||
labelStyle: labelStyle,
|
||||
labelAnchor: labelAnchor,
|
||||
labelJustification: labelJustification,
|
||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx,
|
||||
axisLineStyle: axisLineStyle);
|
||||
|
||||
@override
|
||||
TickDrawStrategy<D> createDrawStrategy(
|
||||
ChartContext context, GraphicsFactory graphicsFactory) =>
|
||||
new SmallTickDrawStrategy<D>(context, graphicsFactory,
|
||||
tickLengthPx: tickLengthPx,
|
||||
lineStyleSpec: lineStyle,
|
||||
labelStyleSpec: labelStyle,
|
||||
axisLineStyleSpec: axisLineStyle,
|
||||
labelAnchor: labelAnchor,
|
||||
labelJustification: labelJustification,
|
||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is SmallTickRendererSpec &&
|
||||
lineStyle == other.lineStyle &&
|
||||
tickLengthPx == other.tickLengthPx &&
|
||||
super == (other));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = lineStyle?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + tickLengthPx?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + super.hashCode;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws small tick lines for each tick. Extends [BaseTickDrawStrategy].
|
||||
class SmallTickDrawStrategy<D> extends BaseTickDrawStrategy<D> {
|
||||
int tickLength;
|
||||
LineStyle lineStyle;
|
||||
|
||||
SmallTickDrawStrategy(
|
||||
ChartContext chartContext,
|
||||
GraphicsFactory graphicsFactory, {
|
||||
int tickLengthPx,
|
||||
LineStyleSpec lineStyleSpec,
|
||||
TextStyleSpec labelStyleSpec,
|
||||
LineStyleSpec axisLineStyleSpec,
|
||||
TickLabelAnchor labelAnchor,
|
||||
TickLabelJustification labelJustification,
|
||||
int labelOffsetFromAxisPx,
|
||||
int labelOffsetFromTickPx,
|
||||
int minimumPaddingBetweenLabelsPx,
|
||||
}) : super(chartContext, graphicsFactory,
|
||||
labelStyleSpec: labelStyleSpec,
|
||||
axisLineStyleSpec: axisLineStyleSpec ?? lineStyleSpec,
|
||||
labelAnchor: labelAnchor,
|
||||
labelJustification: labelJustification,
|
||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx) {
|
||||
this.tickLength = tickLengthPx ?? StyleFactory.style.tickLength;
|
||||
lineStyle =
|
||||
StyleFactory.style.createTickLineStyle(graphicsFactory, lineStyleSpec);
|
||||
}
|
||||
|
||||
@override
|
||||
void draw(ChartCanvas canvas, Tick<D> tick,
|
||||
{@required AxisOrientation orientation,
|
||||
@required Rectangle<int> axisBounds,
|
||||
@required Rectangle<int> drawAreaBounds,
|
||||
@required bool isFirst,
|
||||
@required bool isLast}) {
|
||||
Point<num> tickStart;
|
||||
Point<num> tickEnd;
|
||||
switch (orientation) {
|
||||
case AxisOrientation.top:
|
||||
double x = tick.locationPx;
|
||||
tickStart = new Point(x, axisBounds.bottom - tickLength);
|
||||
tickEnd = new Point(x, axisBounds.bottom);
|
||||
break;
|
||||
case AxisOrientation.bottom:
|
||||
double x = tick.locationPx;
|
||||
tickStart = new Point(x, axisBounds.top);
|
||||
tickEnd = new Point(x, axisBounds.top + tickLength);
|
||||
break;
|
||||
case AxisOrientation.right:
|
||||
double y = tick.locationPx;
|
||||
|
||||
tickStart = new Point(axisBounds.left, y);
|
||||
tickEnd = new Point(axisBounds.left + tickLength, y);
|
||||
break;
|
||||
case AxisOrientation.left:
|
||||
double y = tick.locationPx;
|
||||
|
||||
tickStart = new Point(axisBounds.right - tickLength, y);
|
||||
tickEnd = new Point(axisBounds.right, y);
|
||||
break;
|
||||
}
|
||||
|
||||
canvas.drawLine(
|
||||
points: [tickStart, tickEnd],
|
||||
dashPattern: lineStyle.dashPattern,
|
||||
fill: lineStyle.color,
|
||||
stroke: lineStyle.color,
|
||||
strokeWidthPx: lineStyle.strokeWidth.toDouble(),
|
||||
);
|
||||
|
||||
drawLabel(canvas, tick,
|
||||
orientation: orientation,
|
||||
axisBounds: axisBounds,
|
||||
drawAreaBounds: drawAreaBounds,
|
||||
isFirst: isFirst,
|
||||
isLast: isLast);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
||||
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
|
||||
import '../axis.dart' show AxisOrientation;
|
||||
import '../collision_report.dart' show CollisionReport;
|
||||
import '../tick.dart' show Tick;
|
||||
|
||||
/// Strategy for drawing ticks and checking for collisions.
|
||||
abstract class TickDrawStrategy<D> {
|
||||
/// Decorate the existing list of ticks.
|
||||
///
|
||||
/// This can be used to further modify ticks after they have been generated
|
||||
/// with location data and formatted labels.
|
||||
void decorateTicks(List<Tick<D>> ticks);
|
||||
|
||||
/// Returns a [CollisionReport] indicating if there are any collisions.
|
||||
CollisionReport collides(List<Tick<D>> ticks, AxisOrientation orientation);
|
||||
|
||||
/// Returns measurement of ticks drawn vertically.
|
||||
ViewMeasuredSizes measureVerticallyDrawnTicks(
|
||||
List<Tick<D>> ticks, int maxWidth, int maxHeight);
|
||||
|
||||
/// Returns measurement of ticks drawn horizontally.
|
||||
ViewMeasuredSizes measureHorizontallyDrawnTicks(
|
||||
List<Tick<D>> ticks, int maxWidth, int maxHeight);
|
||||
|
||||
/// Draws tick onto [ChartCanvas].
|
||||
///
|
||||
/// [orientation] the orientation of the axis that this [tick] belongs to.
|
||||
/// [axisBounds] the bounds of the axis.
|
||||
/// [drawAreaBounds] the bounds of the chart draw area adjacent to the axis.
|
||||
void draw(ChartCanvas canvas, Tick<D> tick,
|
||||
{@required AxisOrientation orientation,
|
||||
@required Rectangle<int> axisBounds,
|
||||
@required Rectangle<int> drawAreaBounds,
|
||||
@required bool isFirst,
|
||||
@required bool isLast});
|
||||
|
||||
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
|
||||
Rectangle<int> axisBounds);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../common/chart_context.dart' show ChartContext;
|
||||
import 'axis.dart' show AxisOrientation;
|
||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import 'numeric_scale.dart' show NumericScale;
|
||||
import 'ordinal_scale.dart' show OrdinalScale;
|
||||
import 'scale.dart' show MutableScale;
|
||||
import 'tick.dart' show Tick;
|
||||
import 'tick_formatter.dart' show TickFormatter;
|
||||
import 'tick_provider.dart' show BaseTickProvider, TickHint;
|
||||
import 'time/date_time_scale.dart' show DateTimeScale;
|
||||
|
||||
/// Tick provider that provides ticks at the two end points of the axis range.
|
||||
class EndPointsTickProvider<D> extends BaseTickProvider<D> {
|
||||
@override
|
||||
List<Tick<D>> getTicks({
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required MutableScale<D> scale,
|
||||
@required TickFormatter<D> formatter,
|
||||
@required Map<D, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
@required AxisOrientation orientation,
|
||||
bool viewportExtensionEnabled = false,
|
||||
TickHint<D> tickHint,
|
||||
}) {
|
||||
final ticks = <Tick<D>>[];
|
||||
|
||||
// Check to see if the axis has been configured with some domain values.
|
||||
//
|
||||
// An un-configured axis has no domain step size, and its scale defaults to
|
||||
// infinity.
|
||||
if (scale.domainStepSize.abs() != double.infinity) {
|
||||
final start = _getStartValue(tickHint, scale);
|
||||
final end = _getEndValue(tickHint, scale);
|
||||
|
||||
final labels = formatter.format([start, end], formatterValueCache,
|
||||
stepSize: scale.domainStepSize);
|
||||
|
||||
ticks.add(new Tick(
|
||||
value: start,
|
||||
textElement: graphicsFactory.createTextElement(labels[0]),
|
||||
locationPx: scale[start]));
|
||||
|
||||
ticks.add(new Tick(
|
||||
value: end,
|
||||
textElement: graphicsFactory.createTextElement(labels[1]),
|
||||
locationPx: scale[end]));
|
||||
|
||||
// Allow draw strategy to decorate the ticks.
|
||||
tickDrawStrategy.decorateTicks(ticks);
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
/// Get the start value from the scale.
|
||||
D _getStartValue(TickHint<D> tickHint, MutableScale<D> scale) {
|
||||
Object start;
|
||||
|
||||
if (tickHint != null) {
|
||||
start = tickHint.start;
|
||||
} else {
|
||||
if (scale is NumericScale) {
|
||||
start = (scale as NumericScale).viewportDomain.min;
|
||||
} else if (scale is DateTimeScale) {
|
||||
start = (scale as DateTimeScale).viewportDomain.start;
|
||||
} else if (scale is OrdinalScale) {
|
||||
start = (scale as OrdinalScale).domain.first;
|
||||
}
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
/// Get the end value from the scale.
|
||||
D _getEndValue(TickHint<D> tickHint, MutableScale<D> scale) {
|
||||
Object end;
|
||||
|
||||
if (tickHint != null) {
|
||||
end = tickHint.end;
|
||||
} else {
|
||||
if (scale is NumericScale) {
|
||||
end = (scale as NumericScale).viewportDomain.max;
|
||||
} else if (scale is DateTimeScale) {
|
||||
end = (scale as DateTimeScale).viewportDomain.end;
|
||||
} else if (scale is OrdinalScale) {
|
||||
end = (scale as OrdinalScale).domain.last;
|
||||
}
|
||||
}
|
||||
|
||||
return end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../axis.dart' show NumericAxis;
|
||||
import 'bucketing_numeric_tick_provider.dart' show BucketingNumericTickProvider;
|
||||
|
||||
/// A numeric [Axis] that positions all values beneath a certain [threshold]
|
||||
/// into a reserved space on the axis range. The label for the bucket line will
|
||||
/// be drawn in the middle of the bucket range, rather than aligned with the
|
||||
/// gridline for that value's position on the scale.
|
||||
///
|
||||
/// An example illustration of a bucketing measure axis on a point chart
|
||||
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
|
||||
/// of the axis, since they are less than the [threshold] value of 10%.
|
||||
///
|
||||
/// 100% ┠─────────────────────────
|
||||
/// ┃ *
|
||||
/// ┃ *
|
||||
/// 50% ┠──────*──────────────────
|
||||
/// ┃
|
||||
/// ┠─────────────────────────
|
||||
/// < 10% ┃ * *
|
||||
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
|
||||
/// 0 50 100
|
||||
///
|
||||
/// This axis will format numbers as percents by default.
|
||||
class BucketingNumericAxis extends NumericAxis {
|
||||
/// All values smaller than the threshold will be bucketed into the same
|
||||
/// position in the reserved space on the axis.
|
||||
num _threshold;
|
||||
|
||||
/// Whether or not measure values bucketed below the [threshold] should be
|
||||
/// visible on the chart, or collapsed.
|
||||
///
|
||||
/// If this is false, then any data with measure values smaller than
|
||||
/// [threshold] will be rendered at the baseline of the chart. The
|
||||
bool _showBucket;
|
||||
|
||||
BucketingNumericAxis()
|
||||
: super(tickProvider: new BucketingNumericTickProvider());
|
||||
|
||||
set threshold(num threshold) {
|
||||
_threshold = threshold;
|
||||
(tickProvider as BucketingNumericTickProvider).threshold = threshold;
|
||||
}
|
||||
|
||||
set showBucket(bool showBucket) {
|
||||
_showBucket = showBucket;
|
||||
(tickProvider as BucketingNumericTickProvider).showBucket = showBucket;
|
||||
}
|
||||
|
||||
/// Gets the location of [domain] on the axis, repositioning any value less
|
||||
/// than [threshold] to the middle of the reserved bucket.
|
||||
@override
|
||||
double getLocation(num domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
} else if (_threshold != null && domain < _threshold) {
|
||||
return _showBucket ? scale[_threshold / 2] : scale[0.0];
|
||||
} else {
|
||||
return scale[domain];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../axis.dart' show AxisOrientation;
|
||||
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import '../numeric_scale.dart' show NumericScale;
|
||||
import '../numeric_tick_provider.dart' show NumericTickProvider;
|
||||
import '../tick.dart' show Tick;
|
||||
import '../tick_formatter.dart' show SimpleTickFormatterBase, TickFormatter;
|
||||
import '../tick_provider.dart' show TickHint;
|
||||
|
||||
/// Tick provider that generates ticks for a [BucketingNumericAxis].
|
||||
///
|
||||
/// An example illustration of a bucketing measure axis on a point chart
|
||||
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
|
||||
/// of the axis, since they are less than the [threshold] value of 10%.
|
||||
///
|
||||
/// 100% ┠─────────────────────────
|
||||
/// ┃ *
|
||||
/// ┃ *
|
||||
/// 50% ┠──────*──────────────────
|
||||
/// ┃
|
||||
/// ┠─────────────────────────
|
||||
/// < 10% ┃ * *
|
||||
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
|
||||
/// 0 50 100
|
||||
///
|
||||
/// This tick provider will generate ticks using the same strategy as
|
||||
/// [NumericTickProvider], except that any ticks that are smaller than
|
||||
/// [threshold] will be hidden with an empty label. A special tick will be added
|
||||
/// at the [threshold] position, with a label offset that moves its label down
|
||||
/// to the middle of the bucket.
|
||||
class BucketingNumericTickProvider extends NumericTickProvider {
|
||||
/// All values smaller than the threshold will be bucketed into the same
|
||||
/// position in the reserved space on the axis.
|
||||
num _threshold;
|
||||
|
||||
set threshold(num threshold) {
|
||||
_threshold = threshold;
|
||||
}
|
||||
|
||||
/// Whether or not measure values bucketed below the [threshold] should be
|
||||
/// visible on the chart, or collapsed.
|
||||
bool _showBucket;
|
||||
|
||||
set showBucket(bool showBucket) {
|
||||
_showBucket = showBucket;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Tick<num>> getTicks({
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required NumericScale scale,
|
||||
@required TickFormatter<num> formatter,
|
||||
@required Map<num, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
@required AxisOrientation orientation,
|
||||
bool viewportExtensionEnabled = false,
|
||||
TickHint<num> tickHint,
|
||||
}) {
|
||||
if (_threshold == null) {
|
||||
throw ('Bucketing threshold must be set before getting ticks.');
|
||||
}
|
||||
|
||||
if (_showBucket == null) {
|
||||
throw ('The showBucket flag must be set before getting ticks.');
|
||||
}
|
||||
|
||||
final localFormatter = new _BucketingFormatter()
|
||||
..threshold = _threshold
|
||||
..originalFormatter = formatter;
|
||||
|
||||
final ticks = super.getTicks(
|
||||
context: context,
|
||||
graphicsFactory: graphicsFactory,
|
||||
scale: scale,
|
||||
formatter: localFormatter,
|
||||
formatterValueCache: formatterValueCache,
|
||||
tickDrawStrategy: tickDrawStrategy,
|
||||
orientation: orientation,
|
||||
viewportExtensionEnabled: viewportExtensionEnabled);
|
||||
|
||||
assert(scale != null);
|
||||
|
||||
// Create a tick for the threshold.
|
||||
final thresholdTick = new Tick<num>(
|
||||
value: _threshold,
|
||||
textElement: graphicsFactory
|
||||
.createTextElement(localFormatter.formatValue(_threshold)),
|
||||
locationPx: _showBucket ? scale[_threshold] : scale[0],
|
||||
labelOffsetPx:
|
||||
_showBucket ? -0.5 * (scale[_threshold] - scale[0]) : 0.0);
|
||||
tickDrawStrategy.decorateTicks(<Tick<num>>[thresholdTick]);
|
||||
|
||||
// Filter out ticks that sit below the threshold.
|
||||
ticks.removeWhere((Tick<num> tick) =>
|
||||
tick.value <= thresholdTick.value && tick.value != 0.0);
|
||||
|
||||
// Finally, add our threshold tick to the list.
|
||||
ticks.add(thresholdTick);
|
||||
|
||||
// Make sure they are sorted by increasing value.
|
||||
ticks.sort((a, b) {
|
||||
if (a.value < b.value) {
|
||||
return -1;
|
||||
} else if (a.value > b.value) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return ticks;
|
||||
}
|
||||
}
|
||||
|
||||
class _BucketingFormatter extends SimpleTickFormatterBase<num> {
|
||||
/// All values smaller than the threshold will be formatted into an empty
|
||||
/// string.
|
||||
num threshold;
|
||||
|
||||
SimpleTickFormatterBase<num> originalFormatter;
|
||||
|
||||
/// Formats a single tick value.
|
||||
String formatValue(num value) {
|
||||
if (value < threshold) {
|
||||
return '';
|
||||
} else if (value == threshold) {
|
||||
return '< ' + originalFormatter.formatValue(value);
|
||||
} else {
|
||||
return originalFormatter.formatValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../numeric_extents.dart' show NumericExtents;
|
||||
import '../numeric_scale.dart' show NumericScale;
|
||||
import '../scale.dart' show RangeBandConfig, ScaleOutputExtent, StepSizeConfig;
|
||||
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
|
||||
import 'linear_scale_function.dart' show LinearScaleFunction;
|
||||
import 'linear_scale_viewport.dart' show LinearScaleViewportSettings;
|
||||
|
||||
/// [NumericScale] that lays out the domain linearly across the range.
|
||||
///
|
||||
/// A [Scale] which converts numeric domain units to a given numeric range units
|
||||
/// linearly (as opposed to other methods like log scales). This is used to map
|
||||
/// the domain's values to the available pixel range of the chart using the
|
||||
/// apply method.
|
||||
///
|
||||
/// <p>The domain extent of the scale are determined by adding all domain
|
||||
/// values to the scale. It can, however, be overwritten by calling
|
||||
/// [domainOverride] to define the extent of the data.
|
||||
///
|
||||
/// <p>The scale can be zoomed & panned by calling either [setViewportSettings]
|
||||
/// with a zoom and translate, or by setting [viewportExtent] with the domain
|
||||
/// extent to show in the output range.
|
||||
///
|
||||
/// <p>[rangeBandConfig]: By default, this scale will map the domain extent
|
||||
/// exactly to the output range in a simple ratio mapping. If a
|
||||
/// [RangeBandConfig] other than NONE is used to define the width of bar groups,
|
||||
/// then the scale calculation may be altered to that there is a half a stepSize
|
||||
/// at the start and end of the range to ensure that a bar group can be shown
|
||||
/// and centered on the scale's result.
|
||||
///
|
||||
/// <p>[stepSizeConfig]: By default, this scale will calculate the stepSize as
|
||||
/// being auto detected using the minimal distance between two consecutive
|
||||
/// datum. If you don't assign a [RangeBandConfig], then changing the
|
||||
/// [stepSizeConfig] is a no-op.
|
||||
class LinearScale implements NumericScale {
|
||||
final LinearScaleDomainInfo _domainInfo;
|
||||
final LinearScaleViewportSettings _viewportSettings;
|
||||
final LinearScaleFunction _scaleFunction = new LinearScaleFunction();
|
||||
|
||||
RangeBandConfig rangeBandConfig = const RangeBandConfig.none();
|
||||
StepSizeConfig stepSizeConfig = const StepSizeConfig.auto();
|
||||
|
||||
bool _scaleReady = false;
|
||||
|
||||
LinearScale()
|
||||
: _domainInfo = new LinearScaleDomainInfo(),
|
||||
_viewportSettings = new LinearScaleViewportSettings();
|
||||
|
||||
LinearScale._copy(LinearScale other)
|
||||
: _domainInfo = new LinearScaleDomainInfo.copy(other._domainInfo),
|
||||
_viewportSettings =
|
||||
new LinearScaleViewportSettings.copy(other._viewportSettings),
|
||||
rangeBandConfig = other.rangeBandConfig,
|
||||
stepSizeConfig = other.stepSizeConfig;
|
||||
|
||||
@override
|
||||
LinearScale copy() => new LinearScale._copy(this);
|
||||
|
||||
//
|
||||
// Domain methods
|
||||
//
|
||||
|
||||
@override
|
||||
addDomain(num domainValue) {
|
||||
_domainInfo.addDomainValue(domainValue);
|
||||
}
|
||||
|
||||
@override
|
||||
resetDomain() {
|
||||
_scaleReady = false;
|
||||
_domainInfo.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
resetViewportSettings() {
|
||||
_viewportSettings.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
NumericExtents get dataExtent => new NumericExtents(
|
||||
_domainInfo.dataDomainStart, _domainInfo.dataDomainEnd);
|
||||
|
||||
@override
|
||||
num get minimumDomainStep => _domainInfo.minimumDetectedDomainStep;
|
||||
|
||||
@override
|
||||
bool canTranslate(_) => true;
|
||||
|
||||
@override
|
||||
set domainOverride(NumericExtents domainMaxExtent) {
|
||||
_domainInfo.domainOverride = domainMaxExtent;
|
||||
}
|
||||
|
||||
get domainOverride => _domainInfo.domainOverride;
|
||||
|
||||
@override
|
||||
int compareDomainValueToViewport(num domainValue) {
|
||||
NumericExtents dataExtent = _viewportSettings.domainExtent != null
|
||||
? _viewportSettings.domainExtent
|
||||
: _domainInfo.extent;
|
||||
return dataExtent.compareValue(domainValue);
|
||||
}
|
||||
|
||||
//
|
||||
// Viewport methods
|
||||
//
|
||||
|
||||
@override
|
||||
setViewportSettings(double viewportScale, double viewportTranslatePx) {
|
||||
_viewportSettings
|
||||
..scalingFactor = viewportScale
|
||||
..translatePx = viewportTranslatePx
|
||||
..domainExtent = null;
|
||||
_scaleReady = false;
|
||||
}
|
||||
|
||||
@override
|
||||
double get viewportScalingFactor => _viewportSettings.scalingFactor;
|
||||
|
||||
@override
|
||||
double get viewportTranslatePx => _viewportSettings.translatePx;
|
||||
|
||||
@override
|
||||
set viewportDomain(NumericExtents extent) {
|
||||
_scaleReady = false;
|
||||
_viewportSettings.domainExtent = extent;
|
||||
}
|
||||
|
||||
@override
|
||||
NumericExtents get viewportDomain {
|
||||
_configureScale();
|
||||
return _viewportSettings.domainExtent;
|
||||
}
|
||||
|
||||
@override
|
||||
set keepViewportWithinData(bool autoAdjustViewportToNiceValues) {
|
||||
_scaleReady = false;
|
||||
_viewportSettings.keepViewportWithinData = true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get keepViewportWithinData => _viewportSettings.keepViewportWithinData;
|
||||
|
||||
@override
|
||||
double computeViewportScaleFactor(double domainWindow) =>
|
||||
_domainInfo.domainDiff / domainWindow;
|
||||
|
||||
@override
|
||||
set range(ScaleOutputExtent extent) {
|
||||
_viewportSettings.range = extent;
|
||||
_scaleReady = false;
|
||||
}
|
||||
|
||||
@override
|
||||
ScaleOutputExtent get range => _viewportSettings.range;
|
||||
|
||||
//
|
||||
// Scale application methods
|
||||
//
|
||||
|
||||
@override
|
||||
num operator [](num domainValue) {
|
||||
_configureScale();
|
||||
return _scaleFunction[domainValue];
|
||||
}
|
||||
|
||||
@override
|
||||
num reverse(double viewPixels) {
|
||||
_configureScale();
|
||||
final num domain = _scaleFunction.reverse(viewPixels);
|
||||
return domain;
|
||||
}
|
||||
|
||||
@override
|
||||
double get rangeBand {
|
||||
_configureScale();
|
||||
return _scaleFunction.rangeBandPixels;
|
||||
}
|
||||
|
||||
@override
|
||||
double get stepSize {
|
||||
_configureScale();
|
||||
return _scaleFunction.stepSizePixels;
|
||||
}
|
||||
|
||||
@override
|
||||
double get domainStepSize => _domainInfo.minimumDetectedDomainStep.toDouble();
|
||||
|
||||
@override
|
||||
int get rangeWidth => (range.end - range.start).abs().toInt();
|
||||
|
||||
@override
|
||||
bool isRangeValueWithinViewport(double rangeValue) =>
|
||||
range.containsValue(rangeValue);
|
||||
|
||||
//
|
||||
// Private update
|
||||
//
|
||||
|
||||
_configureScale() {
|
||||
if (_scaleReady) return;
|
||||
|
||||
assert(_viewportSettings.range != null);
|
||||
|
||||
// If the viewport's domainExtent are set, then we can calculate the
|
||||
// viewport's scaleFactor now that the domainInfo has been loaded.
|
||||
// The viewport also has a chance to correct the scaleFactor.
|
||||
_viewportSettings.updateViewportScaleFactor(_domainInfo);
|
||||
// Now that the viewport's scalingFactor is setup, set it on the scale
|
||||
// function.
|
||||
_scaleFunction.updateScaleFactor(
|
||||
_viewportSettings, _domainInfo, rangeBandConfig, stepSizeConfig);
|
||||
|
||||
// If the viewport's domainExtent are set, then we can calculate the
|
||||
// viewport's translate now that the scaleFactor has been loaded.
|
||||
// The viewport also has a chance to correct the translate.
|
||||
_viewportSettings.updateViewportTranslatePx(
|
||||
_domainInfo, _scaleFunction.scalingFactor);
|
||||
// Now that the viewport has a chance to update the translate, set it on the
|
||||
// scale function.
|
||||
_scaleFunction.updateTranslateAndRangeBand(
|
||||
_viewportSettings, _domainInfo, rangeBandConfig);
|
||||
|
||||
// Now that the viewport's scaleFactor and translate have been updated
|
||||
// set the effective domainExtent of the viewport.
|
||||
_viewportSettings.updateViewportDomainExtent(
|
||||
_domainInfo, _scaleFunction.scalingFactor);
|
||||
|
||||
// Cached computed values are updated.
|
||||
_scaleReady = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../numeric_extents.dart' show NumericExtents;
|
||||
|
||||
/// Encapsulation of all the domain processing logic for the [LinearScale].
|
||||
class LinearScaleDomainInfo {
|
||||
/// User (or axis) overridden extent in domain units.
|
||||
NumericExtents domainOverride;
|
||||
|
||||
/// The minimum added domain value.
|
||||
num _dataDomainStart = double.infinity;
|
||||
num get dataDomainStart => _dataDomainStart;
|
||||
|
||||
/// The maximum added domain value.
|
||||
num _dataDomainEnd = double.negativeInfinity;
|
||||
num get dataDomainEnd => _dataDomainEnd;
|
||||
|
||||
/// Previous domain added so we can calculate minimumDetectedDomainStep.
|
||||
num _previouslyAddedDomain;
|
||||
|
||||
/// The step size between data points in domain units.
|
||||
///
|
||||
/// Measured as the minimum distance between consecutive added points.
|
||||
num _minimumDetectedDomainStep = double.infinity;
|
||||
num get minimumDetectedDomainStep => _minimumDetectedDomainStep;
|
||||
|
||||
///The diff of the nicedDomain extent.
|
||||
num get domainDiff => extent.width;
|
||||
|
||||
LinearScaleDomainInfo();
|
||||
|
||||
LinearScaleDomainInfo.copy(LinearScaleDomainInfo other) {
|
||||
if (other.domainOverride != null) {
|
||||
domainOverride = other.domainOverride;
|
||||
}
|
||||
_dataDomainStart = other._dataDomainStart;
|
||||
_dataDomainEnd = other._dataDomainEnd;
|
||||
_previouslyAddedDomain = other._previouslyAddedDomain;
|
||||
_minimumDetectedDomainStep = other._minimumDetectedDomainStep;
|
||||
}
|
||||
|
||||
/// Resets everything back to initial state.
|
||||
void reset() {
|
||||
_previouslyAddedDomain = null;
|
||||
_dataDomainStart = double.infinity;
|
||||
_dataDomainEnd = double.negativeInfinity;
|
||||
_minimumDetectedDomainStep = double.infinity;
|
||||
}
|
||||
|
||||
/// Updates the domain extent and detected step size given the [domainValue].
|
||||
void addDomainValue(num domainValue) {
|
||||
if (domainValue == null || !domainValue.isFinite) {
|
||||
return;
|
||||
}
|
||||
|
||||
extendDomain(domainValue);
|
||||
|
||||
if (_previouslyAddedDomain != null) {
|
||||
final domainStep = (domainValue - _previouslyAddedDomain).abs();
|
||||
if (domainStep != 0.0 && domainStep < minimumDetectedDomainStep) {
|
||||
_minimumDetectedDomainStep = domainStep;
|
||||
}
|
||||
}
|
||||
_previouslyAddedDomain = domainValue;
|
||||
}
|
||||
|
||||
/// Extends the data domain extent without modifying step size detection.
|
||||
///
|
||||
/// Returns whether the the domain interval was extended. If the domain value
|
||||
/// was already contained in the domain interval, the domain interval does not
|
||||
/// change.
|
||||
bool extendDomain(num domainValue) {
|
||||
if (domainValue == null || !domainValue.isFinite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool domainExtended = false;
|
||||
if (domainValue < _dataDomainStart) {
|
||||
_dataDomainStart = domainValue;
|
||||
domainExtended = true;
|
||||
}
|
||||
if (domainValue > _dataDomainEnd) {
|
||||
_dataDomainEnd = domainValue;
|
||||
domainExtended = true;
|
||||
}
|
||||
return domainExtended;
|
||||
}
|
||||
|
||||
/// Returns the extent based on the current domain range and overrides.
|
||||
NumericExtents get extent {
|
||||
num tmpDomainStart;
|
||||
num tmpDomainEnd;
|
||||
if (domainOverride != null) {
|
||||
// override was set.
|
||||
tmpDomainStart = domainOverride.min;
|
||||
tmpDomainEnd = domainOverride.max;
|
||||
} else {
|
||||
// domainEnd is less than domainStart if no domain values have been set.
|
||||
tmpDomainStart = _dataDomainStart.isFinite ? _dataDomainStart : 0.0;
|
||||
tmpDomainEnd = _dataDomainEnd.isFinite ? _dataDomainEnd : 1.0;
|
||||
}
|
||||
|
||||
return new NumericExtents(tmpDomainStart, tmpDomainEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../scale.dart'
|
||||
show RangeBandConfig, RangeBandType, StepSizeConfig, StepSizeType;
|
||||
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
|
||||
import 'linear_scale_viewport.dart' show LinearScaleViewportSettings;
|
||||
|
||||
/// Component of the LinearScale which actually handles the apply and reverse
|
||||
/// function of the scale.
|
||||
class LinearScaleFunction {
|
||||
/// Cached rangeBand width in pixels given the RangeBandConfig and the current
|
||||
/// domain & range.
|
||||
double rangeBandPixels = 0.0;
|
||||
|
||||
/// Cached amount in domain units to shift the input value as a part of
|
||||
/// translation.
|
||||
num domainTranslate = 0.0;
|
||||
|
||||
/// Cached translation ratio for scale translation.
|
||||
double scalingFactor = 1.0;
|
||||
|
||||
/// Cached amount in pixel units to shift the output value as a part of
|
||||
/// translation.
|
||||
double rangeTranslate = 0.0;
|
||||
|
||||
/// The calculated step size given the step size config.
|
||||
double stepSizePixels = 0.0;
|
||||
|
||||
/// Translates the given domainValue to the range output.
|
||||
double operator [](num domainValue) {
|
||||
return (((domainValue + domainTranslate) * scalingFactor) + rangeTranslate)
|
||||
.toDouble();
|
||||
}
|
||||
|
||||
/// Translates the given range output back to a domainValue.
|
||||
double reverse(double viewPixels) {
|
||||
return ((viewPixels - rangeTranslate) / scalingFactor) - domainTranslate;
|
||||
}
|
||||
|
||||
/// Update the scale function's scaleFactor given the current state of the
|
||||
/// viewport.
|
||||
void updateScaleFactor(
|
||||
LinearScaleViewportSettings viewportSettings,
|
||||
LinearScaleDomainInfo domainInfo,
|
||||
RangeBandConfig rangeBandConfig,
|
||||
StepSizeConfig stepSizeConfig) {
|
||||
double rangeDiff = viewportSettings.range.diff.toDouble();
|
||||
// Note: if you provided a nicing function that extends the domain, we won't
|
||||
// muck with the extended side.
|
||||
bool hasHalfStepAtStart =
|
||||
domainInfo.extent.min == domainInfo.dataDomainStart;
|
||||
bool hasHalfStepAtEnd = domainInfo.extent.max == domainInfo.dataDomainEnd;
|
||||
|
||||
// Determine the stepSize and reserved range values.
|
||||
// The percentage of the step reserved from the scale's range due to the
|
||||
// possible half step at the start and end.
|
||||
double reservedRangePercentOfStep =
|
||||
getStepReservationPercent(hasHalfStepAtStart, hasHalfStepAtEnd);
|
||||
_updateStepSizeAndScaleFactor(viewportSettings, domainInfo, rangeDiff,
|
||||
reservedRangePercentOfStep, rangeBandConfig, stepSizeConfig);
|
||||
}
|
||||
|
||||
/// Returns the percentage of the step reserved from the output range due to
|
||||
/// maybe having to hold half stepSizes on the start and end of the output.
|
||||
double getStepReservationPercent(
|
||||
bool hasHalfStepAtStart, bool hasHalfStepAtEnd) {
|
||||
if (!hasHalfStepAtStart && !hasHalfStepAtEnd) {
|
||||
return 0.0;
|
||||
}
|
||||
if (hasHalfStepAtStart && hasHalfStepAtEnd) {
|
||||
return 1.0;
|
||||
}
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
/// Updates the scale function's translate and rangeBand given the current
|
||||
/// state of the viewport.
|
||||
void updateTranslateAndRangeBand(LinearScaleViewportSettings viewportSettings,
|
||||
LinearScaleDomainInfo domainInfo, RangeBandConfig rangeBandConfig) {
|
||||
// Assign the rangeTranslate using the current viewportSettings.translatePx
|
||||
// and diffs.
|
||||
if (domainInfo.domainDiff == 0) {
|
||||
// Translate it to the center of the range.
|
||||
rangeTranslate =
|
||||
viewportSettings.range.start + (viewportSettings.range.diff / 2);
|
||||
} else {
|
||||
bool hasHalfStepAtStart =
|
||||
domainInfo.extent.min == domainInfo.dataDomainStart;
|
||||
// The pixel shift of the scale function due to the half a step at the
|
||||
// beginning.
|
||||
double reservedRangePixelShift =
|
||||
hasHalfStepAtStart ? (stepSizePixels / 2.0) : 0.0;
|
||||
|
||||
rangeTranslate = (viewportSettings.range.start +
|
||||
viewportSettings.translatePx +
|
||||
reservedRangePixelShift);
|
||||
}
|
||||
|
||||
// We need to subtract the start from any incoming domain to apply the
|
||||
// scale, so flip its sign.
|
||||
domainTranslate = -1 * domainInfo.extent.min;
|
||||
|
||||
// Update the rangeBand size.
|
||||
rangeBandPixels = _calculateRangeBandSize(rangeBandConfig);
|
||||
}
|
||||
|
||||
/// Calculates and stores the current rangeBand given the config and current
|
||||
/// step size.
|
||||
double _calculateRangeBandSize(RangeBandConfig rangeBandConfig) {
|
||||
switch (rangeBandConfig.type) {
|
||||
case RangeBandType.fixedDomain:
|
||||
return rangeBandConfig.size * scalingFactor;
|
||||
case RangeBandType.fixedPixel:
|
||||
return rangeBandConfig.size;
|
||||
case RangeBandType.fixedPixelSpaceFromStep:
|
||||
return stepSizePixels - rangeBandConfig.size;
|
||||
case RangeBandType.styleAssignedPercentOfStep:
|
||||
case RangeBandType.fixedPercentOfStep:
|
||||
return stepSizePixels * rangeBandConfig.size;
|
||||
case RangeBandType.none:
|
||||
return 0.0;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/// Calculates and Stores the current step size and scale factor together,
|
||||
/// given the viewport, domain, and config.
|
||||
///
|
||||
/// <p>Scale factor and step size are related closely and should be calculated
|
||||
/// together so that we do not lose accuracy due to double arithmetic.
|
||||
void _updateStepSizeAndScaleFactor(
|
||||
LinearScaleViewportSettings viewportSettings,
|
||||
LinearScaleDomainInfo domainInfo,
|
||||
double rangeDiff,
|
||||
double reservedRangePercentOfStep,
|
||||
RangeBandConfig rangeBandConfig,
|
||||
StepSizeConfig stepSizeConfig) {
|
||||
final domainDiff = domainInfo.domainDiff;
|
||||
|
||||
// If we are going to have any rangeBands, then ensure that we account for
|
||||
// needed space on the beginning and end of the range.
|
||||
if (rangeBandConfig.type != RangeBandType.none) {
|
||||
switch (stepSizeConfig.type) {
|
||||
case StepSizeType.autoDetect:
|
||||
double minimumDetectedDomainStep =
|
||||
domainInfo.minimumDetectedDomainStep.toDouble();
|
||||
if (minimumDetectedDomainStep != null &&
|
||||
minimumDetectedDomainStep.isFinite) {
|
||||
scalingFactor = viewportSettings.scalingFactor *
|
||||
(rangeDiff /
|
||||
(domainDiff +
|
||||
(minimumDetectedDomainStep *
|
||||
reservedRangePercentOfStep)));
|
||||
stepSizePixels = (minimumDetectedDomainStep * scalingFactor);
|
||||
} else {
|
||||
stepSizePixels = rangeDiff.abs();
|
||||
scalingFactor = 1.0;
|
||||
}
|
||||
return;
|
||||
case StepSizeType.fixedPixels:
|
||||
stepSizePixels = stepSizeConfig.size;
|
||||
double reservedRangeForStepPixels =
|
||||
stepSizePixels * reservedRangePercentOfStep;
|
||||
scalingFactor = domainDiff == 0
|
||||
? 1.0
|
||||
: viewportSettings.scalingFactor *
|
||||
(rangeDiff - reservedRangeForStepPixels) /
|
||||
domainDiff;
|
||||
return;
|
||||
case StepSizeType.fixedDomain:
|
||||
double domainStepWidth = stepSizeConfig.size;
|
||||
double totalDomainDiff =
|
||||
(domainDiff + (domainStepWidth * reservedRangePercentOfStep));
|
||||
scalingFactor = totalDomainDiff == 0
|
||||
? 1.0
|
||||
: viewportSettings.scalingFactor * (rangeDiff / totalDomainDiff);
|
||||
stepSizePixels = domainStepWidth * scalingFactor;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If no cases matched, use zero step size.
|
||||
stepSizePixels = 0.0;
|
||||
scalingFactor = domainDiff == 0
|
||||
? 1.0
|
||||
: viewportSettings.scalingFactor * rangeDiff / domainDiff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math' as math show max, min;
|
||||
|
||||
import '../numeric_extents.dart' show NumericExtents;
|
||||
import '../scale.dart' show ScaleOutputExtent;
|
||||
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
|
||||
|
||||
/// Component of the LinearScale responsible for the configuration and
|
||||
/// calculations of the viewport.
|
||||
class LinearScaleViewportSettings {
|
||||
/// Output extent for the scale, typically set by the axis as the pixel
|
||||
/// output.
|
||||
ScaleOutputExtent range;
|
||||
|
||||
/// Determines whether the scale should be extended to the nice values
|
||||
/// provided by the tick provider. If true, we wont touch the viewport config
|
||||
/// since the axis will configure it, if false, we will still ensure sane zoom
|
||||
/// and translates.
|
||||
bool keepViewportWithinData = true;
|
||||
|
||||
/// User configured viewport scale as a zoom multiplier where 1.0 is
|
||||
/// 100% (default) and 2.0 is 200% zooming in making the data take up twice
|
||||
/// the space (showing half as much data in the viewport).
|
||||
double scalingFactor = 1.0;
|
||||
|
||||
/// User configured viewport translate in pixel units.
|
||||
double translatePx = 0.0;
|
||||
|
||||
/// The current extent of the viewport in domain units.
|
||||
NumericExtents _domainExtent;
|
||||
set domainExtent(NumericExtents extent) {
|
||||
_domainExtent = extent;
|
||||
_manualDomainExtent = extent != null;
|
||||
}
|
||||
|
||||
NumericExtents get domainExtent => _domainExtent;
|
||||
|
||||
/// Indicates that the viewportExtends are to be read from to determine the
|
||||
/// internal scaleFactor and rangeTranslate.
|
||||
|
||||
bool _manualDomainExtent = false;
|
||||
|
||||
LinearScaleViewportSettings();
|
||||
|
||||
LinearScaleViewportSettings.copy(LinearScaleViewportSettings other) {
|
||||
range = other.range;
|
||||
keepViewportWithinData = other.keepViewportWithinData;
|
||||
scalingFactor = other.scalingFactor;
|
||||
translatePx = other.translatePx;
|
||||
_manualDomainExtent = other._manualDomainExtent;
|
||||
_domainExtent = other._domainExtent;
|
||||
}
|
||||
|
||||
/// Resets the viewport calculated fields back to their initial settings.
|
||||
void reset() {
|
||||
// Likely an auto assigned viewport (niced), so reset it between draws.
|
||||
scalingFactor = 1.0;
|
||||
translatePx = 0.0;
|
||||
domainExtent = null;
|
||||
}
|
||||
|
||||
int get rangeWidth => range.diff.abs().toInt();
|
||||
|
||||
bool isRangeValueWithinViewport(double rangeValue) =>
|
||||
range.containsValue(rangeValue);
|
||||
|
||||
/// Updates the viewport's internal scalingFactor given the current
|
||||
/// domainInfo.
|
||||
void updateViewportScaleFactor(LinearScaleDomainInfo domainInfo) {
|
||||
// If we are loading from the viewport, then update the scalingFactor given
|
||||
// the viewport size compared to the data size.
|
||||
if (_manualDomainExtent) {
|
||||
double viewportDomainDiff = _domainExtent?.width?.toDouble();
|
||||
if (domainInfo.domainDiff != 0.0) {
|
||||
scalingFactor = domainInfo.domainDiff / viewportDomainDiff;
|
||||
} else {
|
||||
scalingFactor = 1.0;
|
||||
// The domain claims to have no date, extend it to the viewport's
|
||||
domainInfo.extendDomain(_domainExtent?.min);
|
||||
domainInfo.extendDomain(_domainExtent?.max);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that the viewportSettings.scalingFactor is sane if desired.
|
||||
if (!keepViewportWithinData) {
|
||||
// Make sure we don't zoom out beyond the max domain extent.
|
||||
scalingFactor = math.max(1.0, scalingFactor);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the viewport's internal translate given the current domainInfo and
|
||||
/// main scalingFactor from LinearScaleFunction (not internal scalingFactor).
|
||||
void updateViewportTranslatePx(
|
||||
LinearScaleDomainInfo domainInfo, double scaleScalingFactor) {
|
||||
// If we are loading from the viewport, then update the translate now that
|
||||
// the scaleFactor has been setup.
|
||||
if (_manualDomainExtent) {
|
||||
translatePx = (-1.0 *
|
||||
scaleScalingFactor *
|
||||
(_domainExtent.min - domainInfo.extent.min));
|
||||
}
|
||||
|
||||
// Make sure that the viewportSettings.translatePx is sane if desired.
|
||||
if (!keepViewportWithinData) {
|
||||
int rangeDiff = range.diff.toInt();
|
||||
|
||||
// Make sure we don't translate beyond the max domain extent.
|
||||
translatePx = math.min(0.0, translatePx);
|
||||
translatePx = math.max(rangeDiff * (1.0 - scalingFactor), translatePx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates and stores the viewport's domainExtent if we did not load from
|
||||
/// them in the first place.
|
||||
void updateViewportDomainExtent(
|
||||
LinearScaleDomainInfo domainInfo, double scaleScalingFactor) {
|
||||
// If we didn't load from the viewport extent, then update them given the
|
||||
// current scale configuration.
|
||||
if (!_manualDomainExtent) {
|
||||
double viewportDomainDiff = domainInfo.domainDiff / scalingFactor;
|
||||
double viewportStart =
|
||||
(-1.0 * translatePx / scaleScalingFactor) + domainInfo.extent.min;
|
||||
_domainExtent =
|
||||
new NumericExtents(viewportStart, viewportStart + viewportDomainDiff);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'scale.dart' show Extents;
|
||||
|
||||
/// Represents the starting and ending extent of a dataset.
|
||||
class NumericExtents implements Extents<num> {
|
||||
final num min;
|
||||
final num max;
|
||||
|
||||
/// Precondition: [min] <= [max].
|
||||
// TODO: When initializer list asserts are supported everywhere,
|
||||
// add the precondition as an initializer list assert. This is supported in
|
||||
// Flutter only.
|
||||
const NumericExtents(this.min, this.max);
|
||||
|
||||
/// Returns [Extents] based on the min and max of the given values.
|
||||
/// Returns [NumericExtents.empty] if [values] are empty
|
||||
factory NumericExtents.fromValues(Iterable<num> values) {
|
||||
if (values.isEmpty) {
|
||||
return NumericExtents.empty;
|
||||
}
|
||||
var min = values.first;
|
||||
var max = values.first;
|
||||
for (final value in values) {
|
||||
if (value < min) {
|
||||
min = value;
|
||||
} else if (max < value) {
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
return new NumericExtents(min, max);
|
||||
}
|
||||
|
||||
/// Returns the union of this and other.
|
||||
NumericExtents plus(NumericExtents other) {
|
||||
if (min <= other.min) {
|
||||
if (max >= other.max) {
|
||||
return this;
|
||||
} else {
|
||||
return new NumericExtents(min, other.max);
|
||||
}
|
||||
} else {
|
||||
if (other.max >= max) {
|
||||
return other;
|
||||
} else {
|
||||
return new NumericExtents(other.min, max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares the given [value] against the extents.
|
||||
///
|
||||
/// Returns -1 if the value is less than the extents.
|
||||
/// Returns 0 if the value is within the extents inclusive.
|
||||
/// Returns 1 if the value is greater than the extents.
|
||||
int compareValue(num value) {
|
||||
if (value < min) {
|
||||
return -1;
|
||||
}
|
||||
if (value > max) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool _containsValue(double value) => compareValue(value) == 0;
|
||||
|
||||
// Returns true if these [NumericExtents] collides with [other].
|
||||
bool overlaps(NumericExtents other) {
|
||||
return _containsValue(other.min) ||
|
||||
_containsValue(other.max) ||
|
||||
other._containsValue(min) ||
|
||||
other._containsValue(max);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
return other is NumericExtents && min == other.min && max == other.max;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => (min.hashCode + (max.hashCode * 31));
|
||||
|
||||
num get width => max - min;
|
||||
|
||||
@override
|
||||
String toString() => 'Extent($min, $max)';
|
||||
|
||||
static const NumericExtents unbounded =
|
||||
const NumericExtents(double.negativeInfinity, double.infinity);
|
||||
static const NumericExtents empty = const NumericExtents(0.0, 0.0);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'numeric_extents.dart' show NumericExtents;
|
||||
import 'scale.dart' show MutableScale;
|
||||
|
||||
/// Scale used to convert numeric domain input units to output range units.
|
||||
///
|
||||
/// The input represents a continuous numeric domain which maps to a given range
|
||||
/// output. This is used to map the domain's values to the available pixel
|
||||
/// range of the chart.
|
||||
abstract class NumericScale extends MutableScale<num> {
|
||||
/// Keeps the scale and translate sane if true (default).
|
||||
///
|
||||
/// Setting this to false disables some pan/zoom protections that prevent you
|
||||
/// from going beyond the data extent.
|
||||
bool get keepViewportWithinData;
|
||||
set keepViewportWithinData(bool keep);
|
||||
|
||||
/// Returns the extent of the actual data (not the viewport max).
|
||||
NumericExtents get dataExtent;
|
||||
|
||||
/// Returns the minimum step size of the actual data.
|
||||
num get minimumDomainStep;
|
||||
|
||||
/// Overrides the domain extent if set, null otherwise.
|
||||
///
|
||||
/// Overrides the extent of the actual data to lie about the range of the
|
||||
/// data so that panning has a start and end point to go between beyond the
|
||||
/// received data. This allows lazy loading of data into the gaps in the
|
||||
/// expanded lied about areas.
|
||||
NumericExtents get domainOverride;
|
||||
set domainOverride(NumericExtents extent);
|
||||
|
||||
/// Returns the domain extent visible in the viewport of the drawArea.
|
||||
NumericExtents get viewportDomain;
|
||||
|
||||
/// Sets the domain extent visible in the viewport of the drawArea.
|
||||
///
|
||||
/// Invalidates the viewportScale & viewportTranslatePx.
|
||||
set viewportDomain(NumericExtents extent);
|
||||
|
||||
/// Returns the viewportScaleFactor needed to present the given domainWindow.
|
||||
double computeViewportScaleFactor(double domainWindow);
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math' show log, log10e, max, min, pow;
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../common/chart_context.dart' show ChartContext;
|
||||
import '../../common/unitconverter/identity_converter.dart'
|
||||
show IdentityConverter;
|
||||
import '../../common/unitconverter/unit_converter.dart' show UnitConverter;
|
||||
import 'axis.dart' show AxisOrientation;
|
||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import 'numeric_extents.dart' show NumericExtents;
|
||||
import 'numeric_scale.dart' show NumericScale;
|
||||
import 'tick.dart' show Tick;
|
||||
import 'tick_formatter.dart' show TickFormatter;
|
||||
import 'tick_provider.dart' show BaseTickProvider, TickHint;
|
||||
|
||||
/// Tick provider that allows you to specify how many ticks to present while
|
||||
/// also choosing tick values that appear "nice" or "rounded" to the user. By
|
||||
/// default it will try to guess an appropriate number of ticks given the size
|
||||
/// of the range available, but the min and max tick counts can be set by
|
||||
/// calling setTickCounts().
|
||||
///
|
||||
/// You can control whether the axis is bound to zero (default) or follows the
|
||||
/// data by calling setZeroBound().
|
||||
///
|
||||
/// This provider will choose "nice" ticks with the following priority order.
|
||||
/// * Ticks do not collide with each other.
|
||||
/// * Alternate rendering is not used to avoid collisions.
|
||||
/// * Provide the least amount of domain range covering all data points (while
|
||||
/// still selecting "nice" ticks values.
|
||||
class NumericTickProvider extends BaseTickProvider<num> {
|
||||
/// Used to determine the automatic tick count calculation.
|
||||
static const MIN_DIPS_BETWEEN_TICKS = 25;
|
||||
|
||||
/// Potential steps available to the baseTen value of the data.
|
||||
static const DEFAULT_STEPS = const [
|
||||
0.01,
|
||||
0.02,
|
||||
0.025,
|
||||
0.03,
|
||||
0.04,
|
||||
0.05,
|
||||
0.06,
|
||||
0.07,
|
||||
0.08,
|
||||
0.09,
|
||||
0.1,
|
||||
0.2,
|
||||
0.25,
|
||||
0.3,
|
||||
0.4,
|
||||
0.5,
|
||||
0.6,
|
||||
0.7,
|
||||
0.8,
|
||||
0.9,
|
||||
1.0,
|
||||
2.0,
|
||||
2.50,
|
||||
3.0,
|
||||
4.0,
|
||||
5.0,
|
||||
6.0,
|
||||
7.0,
|
||||
8.0,
|
||||
9.0
|
||||
];
|
||||
|
||||
// Settings
|
||||
|
||||
/// Sets whether the the tick provider should always include a zero tick.
|
||||
///
|
||||
/// If set the data range may be extended to include zero.
|
||||
///
|
||||
/// Note that the zero value in axis units is chosen, which may be different
|
||||
/// than zero value in data units if a data to axis unit converter is set.
|
||||
bool zeroBound = true;
|
||||
|
||||
/// If your data can only be in whole numbers, then set this to true.
|
||||
///
|
||||
/// It should prevent the scale from choosing fractional ticks. For example,
|
||||
/// if you had a office head count, don't generate a tick for 1.5, instead
|
||||
/// jump to 2.
|
||||
///
|
||||
/// Note that the provider will choose whole number ticks in the axis units,
|
||||
/// not data units if a data to axis unit converter is set.
|
||||
bool dataIsInWholeNumbers = true;
|
||||
|
||||
// Desired min and max tick counts are set by [setFixedTickCount] and
|
||||
// [setTickCount]. These are not guaranteed tick counts.
|
||||
int _desiredMaxTickCount;
|
||||
int _desiredMinTickCount;
|
||||
|
||||
/// Allowed steps the tick provider can choose from.
|
||||
var _allowedSteps = DEFAULT_STEPS;
|
||||
|
||||
/// Convert input data units to the desired units on the axis.
|
||||
/// If not set no conversion will take place.
|
||||
///
|
||||
/// Combining this with an appropriate [TickFormatter] would result in axis
|
||||
/// ticks that are in different unit than the actual data units.
|
||||
UnitConverter<num, num> dataToAxisUnitConverter =
|
||||
const IdentityConverter<num>();
|
||||
|
||||
// Tick calculation state
|
||||
num _low;
|
||||
num _high;
|
||||
int _rangeWidth;
|
||||
int _minTickCount;
|
||||
int _maxTickCount;
|
||||
|
||||
// The parameters used in previous tick calculation
|
||||
num _prevLow;
|
||||
num _prevHigh;
|
||||
int _prevRangeWidth;
|
||||
int _prevMinTickCount;
|
||||
int _prevMaxTickCount;
|
||||
bool _prevDataIsInWholeNumbers;
|
||||
|
||||
/// Sets the desired tick count.
|
||||
///
|
||||
/// While the provider will try to satisfy the requirement, it is not
|
||||
/// guaranteed, such as cases where ticks may overlap or are insufficient.
|
||||
///
|
||||
/// [tickCount] the fixed number of major (labeled) ticks to draw for the axis
|
||||
/// Passing null will result in falling back on the automatic tick count
|
||||
/// assignment.
|
||||
void setFixedTickCount(int tickCount) {
|
||||
// Don't allow a single tick, it doesn't make sense. so tickCount > 1
|
||||
_desiredMinTickCount =
|
||||
tickCount != null && tickCount > 1 ? tickCount : null;
|
||||
_desiredMaxTickCount = _desiredMinTickCount;
|
||||
}
|
||||
|
||||
/// Sets the desired min and max tick count when providing ticks.
|
||||
///
|
||||
/// The values are suggested requirements but are not guaranteed to be the
|
||||
/// actual tick count in cases where it is not possible.
|
||||
///
|
||||
/// [maxTickCount] The max tick count must be greater than 1.
|
||||
/// [minTickCount] The min tick count must be greater than 1.
|
||||
void setTickCount(int maxTickCount, int minTickCount) {
|
||||
// Don't allow a single tick, it doesn't make sense. so tickCount > 1
|
||||
if (maxTickCount != null && maxTickCount > 1) {
|
||||
_desiredMaxTickCount = maxTickCount;
|
||||
if (minTickCount != null &&
|
||||
minTickCount > 1 &&
|
||||
minTickCount <= _desiredMaxTickCount) {
|
||||
_desiredMinTickCount = minTickCount;
|
||||
} else {
|
||||
_desiredMinTickCount = 2;
|
||||
}
|
||||
} else {
|
||||
_desiredMaxTickCount = null;
|
||||
_desiredMinTickCount = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the allowed step sizes this tick provider can choose from.
|
||||
///
|
||||
/// All ticks will be a power of 10 multiple of the given step sizes.
|
||||
///
|
||||
/// Note that if only very few step sizes are allowed the tick range maybe
|
||||
/// much bigger than the data range.
|
||||
///
|
||||
/// The step sizes setup here apply in axis units, which is different than
|
||||
/// input units if a data to axis unit converter is set.
|
||||
///
|
||||
/// [steps] allowed step sizes in the [1, 10) range.
|
||||
set allowedSteps(List<double> steps) {
|
||||
assert(steps != null && steps.isNotEmpty);
|
||||
steps.sort();
|
||||
|
||||
final stepSet = new Set.from(steps);
|
||||
_allowedSteps = new List<double>(stepSet.length * 3);
|
||||
int stepIndex = 0;
|
||||
for (double step in stepSet) {
|
||||
assert(1.0 <= step && step < 10.0);
|
||||
_allowedSteps[stepIndex] = _removeRoundingErrors(step / 100);
|
||||
_allowedSteps[stepSet.length + stepIndex] =
|
||||
_removeRoundingErrors(step / 10).toDouble();
|
||||
_allowedSteps[2 * stepSet.length + stepIndex] =
|
||||
_removeRoundingErrors(step);
|
||||
stepIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
List<Tick<num>> _getTicksFromHint({
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required NumericScale scale,
|
||||
@required TickFormatter<num> formatter,
|
||||
@required Map<num, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
@required TickHint<num> tickHint,
|
||||
}) {
|
||||
final stepSize = (tickHint.end - tickHint.start) / (tickHint.tickCount - 1);
|
||||
// Find the first tick that is greater than or equal to the min
|
||||
// viewportDomain.
|
||||
final tickZeroShift = tickHint.start -
|
||||
(stepSize *
|
||||
(tickHint.start >= 0
|
||||
? (tickHint.start / stepSize).floor()
|
||||
: (tickHint.start / stepSize).ceil()));
|
||||
final tickStart =
|
||||
(scale.viewportDomain.min / stepSize).ceil() * stepSize + tickZeroShift;
|
||||
final stepInfo = new _TickStepInfo(stepSize.abs(), tickStart);
|
||||
final tickValues = _getTickValues(stepInfo, tickHint.tickCount);
|
||||
|
||||
// Create ticks from domain values.
|
||||
return createTicks(tickValues,
|
||||
context: context,
|
||||
graphicsFactory: graphicsFactory,
|
||||
scale: scale,
|
||||
formatter: formatter,
|
||||
formatterValueCache: formatterValueCache,
|
||||
tickDrawStrategy: tickDrawStrategy,
|
||||
stepSize: stepInfo.stepSize);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Tick<num>> getTicks({
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required NumericScale scale,
|
||||
@required TickFormatter<num> formatter,
|
||||
@required Map<num, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
@required AxisOrientation orientation,
|
||||
bool viewportExtensionEnabled = false,
|
||||
TickHint<num> tickHint,
|
||||
}) {
|
||||
List<Tick<num>> ticks;
|
||||
|
||||
_rangeWidth = scale.rangeWidth;
|
||||
_updateDomainExtents(scale.viewportDomain);
|
||||
|
||||
// Bypass searching for a tick range since we are getting ticks using
|
||||
// information in [tickHint].
|
||||
if (tickHint != null) {
|
||||
return _getTicksFromHint(
|
||||
context: context,
|
||||
graphicsFactory: graphicsFactory,
|
||||
scale: scale,
|
||||
formatter: formatter,
|
||||
formatterValueCache: formatterValueCache,
|
||||
tickDrawStrategy: tickDrawStrategy,
|
||||
tickHint: tickHint,
|
||||
);
|
||||
}
|
||||
|
||||
if (_hasTickParametersChanged() || ticks == null) {
|
||||
var selectedTicksRange = double.maxFinite;
|
||||
var foundPreferredTicks = false;
|
||||
var viewportDomain = scale.viewportDomain;
|
||||
final axisUnitsHigh = dataToAxisUnitConverter.convert(_high);
|
||||
final axisUnitsLow = dataToAxisUnitConverter.convert(_low);
|
||||
|
||||
_updateTickCounts(axisUnitsHigh, axisUnitsLow);
|
||||
|
||||
// Only create a copy of the scale if [viewportExtensionEnabled].
|
||||
NumericScale mutableScale =
|
||||
viewportExtensionEnabled ? scale.copy() : null;
|
||||
|
||||
// Walk to available tick count from max to min looking for the first one
|
||||
// that gives you the least amount of range used. If a non colliding tick
|
||||
// count is not found use the min tick count to generate ticks.
|
||||
for (int tickCount = _maxTickCount;
|
||||
tickCount >= _minTickCount;
|
||||
tickCount--) {
|
||||
final stepInfo =
|
||||
_getStepsForTickCount(tickCount, axisUnitsHigh, axisUnitsLow);
|
||||
if (stepInfo == null) {
|
||||
continue;
|
||||
}
|
||||
final firstTick = dataToAxisUnitConverter.invert(stepInfo.tickStart);
|
||||
final lastTick = dataToAxisUnitConverter
|
||||
.invert(stepInfo.tickStart + stepInfo.stepSize * (tickCount - 1));
|
||||
final range = lastTick - firstTick;
|
||||
// Calculate ticks if it is a better range or if preferred ticks have
|
||||
// not been found yet.
|
||||
if (range < selectedTicksRange || !foundPreferredTicks) {
|
||||
final tickValues = _getTickValues(stepInfo, tickCount);
|
||||
|
||||
if (viewportExtensionEnabled) {
|
||||
mutableScale.viewportDomain =
|
||||
new NumericExtents(firstTick, lastTick);
|
||||
}
|
||||
|
||||
// Create ticks from domain values.
|
||||
final preferredTicks = createTicks(tickValues,
|
||||
context: context,
|
||||
graphicsFactory: graphicsFactory,
|
||||
scale: viewportExtensionEnabled ? mutableScale : scale,
|
||||
formatter: formatter,
|
||||
formatterValueCache: formatterValueCache,
|
||||
tickDrawStrategy: tickDrawStrategy,
|
||||
stepSize: stepInfo.stepSize);
|
||||
|
||||
// Request collision check from draw strategy.
|
||||
final collisionReport =
|
||||
tickDrawStrategy.collides(preferredTicks, orientation);
|
||||
|
||||
// Don't choose colliding ticks unless it was our last resort
|
||||
if (collisionReport.ticksCollide && tickCount > _minTickCount) {
|
||||
continue;
|
||||
}
|
||||
// Only choose alternate ticks if preferred ticks is not found.
|
||||
if (foundPreferredTicks && collisionReport.alternateTicksUsed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ticks = collisionReport.alternateTicksUsed
|
||||
? collisionReport.ticks
|
||||
: preferredTicks;
|
||||
foundPreferredTicks = !collisionReport.alternateTicksUsed;
|
||||
selectedTicksRange = range;
|
||||
// If viewport extended, save the viewport used.
|
||||
viewportDomain = mutableScale?.viewportDomain ?? scale.viewportDomain;
|
||||
}
|
||||
}
|
||||
_setPreviousTickCalculationParameters();
|
||||
// If [viewportExtensionEnabled] and has changed, then set the scale's
|
||||
// viewport to what was used to generate ticks. By only setting viewport
|
||||
// when it has changed, we do not trigger the flag to recalculate scale.
|
||||
if (viewportExtensionEnabled && scale.viewportDomain != viewportDomain) {
|
||||
scale.viewportDomain = viewportDomain;
|
||||
}
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
/// Checks whether the parameters that are used in determining the right set
|
||||
/// of ticks changed from the last time we calculated ticks. If not we should
|
||||
/// be able to use the cached ticks.
|
||||
bool _hasTickParametersChanged() {
|
||||
return _low != _prevLow ||
|
||||
_high != _prevHigh ||
|
||||
_rangeWidth != _prevRangeWidth ||
|
||||
_minTickCount != _prevMinTickCount ||
|
||||
_maxTickCount != _prevMaxTickCount ||
|
||||
dataIsInWholeNumbers != _prevDataIsInWholeNumbers;
|
||||
}
|
||||
|
||||
/// Save the last set of parameters used while determining ticks.
|
||||
void _setPreviousTickCalculationParameters() {
|
||||
_prevLow = _low;
|
||||
_prevHigh = _high;
|
||||
_prevRangeWidth = _rangeWidth;
|
||||
_prevMinTickCount = _minTickCount;
|
||||
_prevMaxTickCount = _maxTickCount;
|
||||
_prevDataIsInWholeNumbers = dataIsInWholeNumbers;
|
||||
}
|
||||
|
||||
/// Calculates the domain extents that this provider will cover based on the
|
||||
/// axis extents passed in and the settings in the numeric tick provider.
|
||||
/// Stores the domain extents in [_low] and [_high].
|
||||
void _updateDomainExtents(NumericExtents axisExtents) {
|
||||
_low = axisExtents.min;
|
||||
_high = axisExtents.max;
|
||||
|
||||
// Correct the extents for zero bound
|
||||
if (zeroBound) {
|
||||
_low = _low > 0.0 ? 0.0 : _low;
|
||||
_high = _high < 0.0 ? 0.0 : _high;
|
||||
}
|
||||
|
||||
// Correct cases where high and low equal to give the tick provider an
|
||||
// actual range to go off of when picking ticks.
|
||||
if (_high == _low) {
|
||||
if (_high == 0.0) {
|
||||
// Corner case: the only values we've seen are zero, so lets just say
|
||||
// the high is 1 and leave the low at zero.
|
||||
_high = 1.0;
|
||||
} else {
|
||||
// The values are all the same, so assume a range of -5% to +5% from the
|
||||
// single value.
|
||||
if (_high > 0.0) {
|
||||
_high = _high * 1.05;
|
||||
_low = _low * 0.95;
|
||||
} else {
|
||||
// (high == low) < 0
|
||||
_high = _high * 0.95;
|
||||
_low = _low * 1.05;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given [tickCount] and the domain range, finds the smallest tick increment,
|
||||
/// chosen from power of 10 multiples of allowed steps, that covers the whole
|
||||
/// data range.
|
||||
_TickStepInfo _getStepsForTickCount(int tickCount, num high, num low) {
|
||||
// A region is the space between ticks.
|
||||
final regionCount = tickCount - 1;
|
||||
|
||||
// If the range contains zero, ensure that zero is a tick.
|
||||
if (high >= 0 && low <= 0) {
|
||||
// determine the ratio of regions that are above the zero axis.
|
||||
final posRegionRatio = (high > 0 ? min(1.0, high / (high - low)) : 0.0);
|
||||
var positiveRegionCount = (regionCount * posRegionRatio).ceil();
|
||||
var negativeRegionCount = regionCount - positiveRegionCount;
|
||||
// Ensure that negative regions are not excluded, unless there are no
|
||||
// regions to spare.
|
||||
if (negativeRegionCount == 0 && low < 0 && regionCount > 1) {
|
||||
positiveRegionCount--;
|
||||
negativeRegionCount++;
|
||||
}
|
||||
|
||||
// If we have positive and negative values, ensure that we have ticks in
|
||||
// both regions.
|
||||
//
|
||||
// This should not happen unless the axis is manually configured with a
|
||||
// tick count. [_updateTickCounts] should ensure that we have do not try
|
||||
// to generate fewer than three.
|
||||
assert(
|
||||
!(low < 0 &&
|
||||
high > 0 &&
|
||||
(negativeRegionCount == 0 || positiveRegionCount == 0)),
|
||||
'Numeric tick provider cannot generate ${tickCount} '
|
||||
'ticks when the axis range contains both positive and negative '
|
||||
'values. A minimum of three ticks are required to include zero.');
|
||||
|
||||
// Determine the "favored" axis direction (the one which will control the
|
||||
// ticks based on having a greater value / regions).
|
||||
//
|
||||
// Example: 13 / 3 (4.33 per tick) vs -5 / 1 (5 per tick)
|
||||
// making -5 the favored number. A step size that includes this number
|
||||
// ensures the other is also includes in the opposite direction.
|
||||
final favorPositive = (high > 0 ? high / positiveRegionCount : 0).abs() >
|
||||
(low < 0 ? low / negativeRegionCount : 0).abs();
|
||||
final favoredNum = (favorPositive ? high : low).abs();
|
||||
final favoredRegionCount =
|
||||
favorPositive ? positiveRegionCount : negativeRegionCount;
|
||||
final favoredTensBase = (_getEnclosingPowerOfTen(favoredNum)).abs();
|
||||
|
||||
// Check each step size and see if it would contain the "favored" value
|
||||
for (double step in _allowedSteps) {
|
||||
final tmpStepSize = _removeRoundingErrors(step * favoredTensBase);
|
||||
|
||||
// If prefer whole number, then don't allow a step that isn't one.
|
||||
if (dataIsInWholeNumbers && (tmpStepSize).round() != tmpStepSize) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Skip steps that format to the same string.
|
||||
// But wait until the last step to prevent the cost of the formatter.
|
||||
// Potentially store the formatted strings in TickStepInfo?
|
||||
if (tmpStepSize * favoredRegionCount >= favoredNum) {
|
||||
double stepStart = negativeRegionCount > 0
|
||||
? (-1 * tmpStepSize * negativeRegionCount)
|
||||
: 0.0;
|
||||
return new _TickStepInfo(tmpStepSize, stepStart);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Find the range base to calculate step sizes.
|
||||
final diffTensBase = _getEnclosingPowerOfTen(high - low);
|
||||
// Walk the step sizes calculating a starting point and seeing if the high
|
||||
// end is included in the range given that step size.
|
||||
for (double step in _allowedSteps) {
|
||||
final tmpStepSize = _removeRoundingErrors(step * diffTensBase);
|
||||
|
||||
// If prefer whole number, then don't allow a step that isn't one.
|
||||
if (dataIsInWholeNumbers && (tmpStepSize).round() != tmpStepSize) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Skip steps that format to the same string.
|
||||
// But wait until the last step to prevent the cost of the formatter.
|
||||
double tmpStepStart = _getStepLessThan(low, tmpStepSize);
|
||||
if (tmpStepStart + (tmpStepSize * regionCount) >= high) {
|
||||
return new _TickStepInfo(tmpStepSize, tmpStepStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new _TickStepInfo(1.0, low.floorToDouble());
|
||||
}
|
||||
|
||||
List<double> _getTickValues(_TickStepInfo steps, int tickCount) {
|
||||
final tickValues = new List<double>(tickCount);
|
||||
// We have our size and start, assign all the tick values to the given array.
|
||||
for (int i = 0; i < tickCount; i++) {
|
||||
tickValues[i] = dataToAxisUnitConverter.invert(
|
||||
_removeRoundingErrors(steps.tickStart + (i * steps.stepSize)));
|
||||
}
|
||||
return tickValues;
|
||||
}
|
||||
|
||||
/// Given the axisDimensions update the tick counts given they are not fixed.
|
||||
void _updateTickCounts(num high, num low) {
|
||||
int tmpMaxNumMajorTicks;
|
||||
int tmpMinNumMajorTicks;
|
||||
|
||||
// If the domain range contains both positive and negative values, then we
|
||||
// need a minimum of three ticks to include zero as a tick. Otherwise, we
|
||||
// only need an upper and lower tick.
|
||||
final absoluteMinTicks = (low < 0 && 0 < high) ? 3 : 2;
|
||||
|
||||
// If there is a desired tick range use it, if not calculate one.
|
||||
if (_desiredMaxTickCount != null) {
|
||||
tmpMinNumMajorTicks = max(_desiredMinTickCount, absoluteMinTicks);
|
||||
tmpMaxNumMajorTicks = max(_desiredMaxTickCount, tmpMinNumMajorTicks);
|
||||
} else {
|
||||
double minPixelsPerTick = MIN_DIPS_BETWEEN_TICKS.toDouble();
|
||||
tmpMinNumMajorTicks = absoluteMinTicks;
|
||||
tmpMaxNumMajorTicks =
|
||||
max(absoluteMinTicks, (_rangeWidth / minPixelsPerTick).floor());
|
||||
}
|
||||
|
||||
// Don't blow away the previous array if it hasn't changed.
|
||||
if (tmpMaxNumMajorTicks != _maxTickCount ||
|
||||
tmpMinNumMajorTicks != _minTickCount) {
|
||||
_maxTickCount = tmpMaxNumMajorTicks;
|
||||
_minTickCount = tmpMinNumMajorTicks;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the power of 10 which contains the [number].
|
||||
///
|
||||
/// If [number] is 0 returns 1.
|
||||
/// Examples:
|
||||
/// [number] of 63 returns 100
|
||||
/// [number] of -63 returns -100
|
||||
/// [number] of 0.63 returns 1
|
||||
static double _getEnclosingPowerOfTen(num number) {
|
||||
if (number == 0) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
return pow(10, (log10e * log(number.abs())).ceil()) *
|
||||
(number < 0.0 ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
/// Returns the step numerically less than the number by step increments.
|
||||
static double _getStepLessThan(double number, double stepSize) {
|
||||
if (number == 0.0 || stepSize == 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
return (stepSize > 0.0
|
||||
? (number / stepSize).floor()
|
||||
: (number / stepSize).ceil()) *
|
||||
stepSize;
|
||||
}
|
||||
|
||||
/// Attempts to slice off very small floating point rounding effects for the
|
||||
/// given number.
|
||||
///
|
||||
/// @param number the number to round.
|
||||
/// @return the rounded number.
|
||||
static double _removeRoundingErrors(double number) {
|
||||
// sufficiently large multiplier to handle generating ticks on the order
|
||||
// of 10^-9.
|
||||
const multiplier = 1.0e9;
|
||||
|
||||
return number > 100.0
|
||||
? number.roundToDouble()
|
||||
: (number * multiplier).roundToDouble() / multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
class _TickStepInfo {
|
||||
double stepSize;
|
||||
double tickStart;
|
||||
|
||||
_TickStepInfo(this.stepSize, this.tickStart);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:collection' show HashSet;
|
||||
import 'scale.dart' show Extents;
|
||||
|
||||
/// A range of ordinals.
|
||||
class OrdinalExtents extends Extents<String> {
|
||||
final List<String> _range;
|
||||
|
||||
/// The extents representing the ordinal values in [range].
|
||||
///
|
||||
/// The elements of [range] must all be unique.
|
||||
///
|
||||
/// [D] is the domain class type for the elements in the extents.
|
||||
OrdinalExtents(List<String> range) : _range = range {
|
||||
// This asserts that all elements in [range] are unique.
|
||||
final uniqueValueCount = new HashSet.from(_range).length;
|
||||
assert(uniqueValueCount == range.length);
|
||||
}
|
||||
|
||||
factory OrdinalExtents.all(List<String> range) => new OrdinalExtents(range);
|
||||
|
||||
bool get isEmpty => _range.isEmpty;
|
||||
|
||||
/// The number of values inside this extent.
|
||||
int get length => _range.length;
|
||||
|
||||
String operator [](int index) => _range[index];
|
||||
|
||||
int indexOf(String value) => _range.indexOf(value);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'ordinal_scale_domain_info.dart' show OrdinalScaleDomainInfo;
|
||||
import 'scale.dart' show MutableScale;
|
||||
|
||||
abstract class OrdinalScale extends MutableScale<String> {
|
||||
/// The current domain collection with all added unique values.
|
||||
OrdinalScaleDomainInfo get domain;
|
||||
|
||||
/// Sets the viewport of the scale based on the number of data points to show
|
||||
/// and the starting domain value.
|
||||
///
|
||||
/// [viewportDataSize] How many ordinal domain values to show in the viewport.
|
||||
/// [startingDomain] The starting domain value of the viewport. Note that if
|
||||
/// the starting domain is in terms of position less than [domainValuesToShow]
|
||||
/// from the last domain value the viewport will be fixed to the last value
|
||||
/// and not guaranteed that this domain value is the first in the viewport.
|
||||
void setViewport(int viewportDataSize, String startingDomain);
|
||||
|
||||
/// The number of full ordinal steps that fit in the viewport.
|
||||
int get viewportDataSize;
|
||||
|
||||
/// The first fully visible ordinal step within the viewport.
|
||||
///
|
||||
/// Null if no domains exist.
|
||||
String get viewportStartingDomain;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:collection' show HashMap;
|
||||
import 'ordinal_extents.dart' show OrdinalExtents;
|
||||
|
||||
/// A domain processor for [OrdinalScale].
|
||||
///
|
||||
/// [D] domain class type of the values being tracked.
|
||||
///
|
||||
/// Unique domain values are kept, so duplicates will not increase the extent.
|
||||
class OrdinalScaleDomainInfo {
|
||||
int _index = 0;
|
||||
|
||||
/// A map of domain value and the order it was added.
|
||||
final _domainsToOrder = new HashMap<String, int>();
|
||||
|
||||
/// A list of domain values kept to support [getDomainAtIndex].
|
||||
final _domainList = <String>[];
|
||||
|
||||
OrdinalScaleDomainInfo();
|
||||
|
||||
OrdinalScaleDomainInfo copy() {
|
||||
return new OrdinalScaleDomainInfo()
|
||||
.._domainsToOrder.addAll(_domainsToOrder)
|
||||
.._index = _index
|
||||
.._domainList.addAll(_domainList);
|
||||
}
|
||||
|
||||
void add(String domain) {
|
||||
if (!_domainsToOrder.containsKey(domain)) {
|
||||
_domainsToOrder[domain] = _index;
|
||||
_index += 1;
|
||||
_domainList.add(domain);
|
||||
}
|
||||
}
|
||||
|
||||
int indexOf(String domain) => _domainsToOrder[domain];
|
||||
|
||||
String getDomainAtIndex(int index) {
|
||||
assert(index >= 0);
|
||||
assert(index < _index);
|
||||
return _domainList[index];
|
||||
}
|
||||
|
||||
List<String> get domains => _domainList;
|
||||
|
||||
String get first => _domainList.isEmpty ? null : _domainList.first;
|
||||
|
||||
String get last => _domainList.isEmpty ? null : _domainList.last;
|
||||
|
||||
bool get isEmpty => (_index == 0);
|
||||
bool get isNotEmpty => !isEmpty;
|
||||
|
||||
OrdinalExtents get extent => new OrdinalExtents.all(_domainList);
|
||||
|
||||
int get size => _index;
|
||||
|
||||
/// Clears all domain values.
|
||||
void clear() {
|
||||
_domainsToOrder.clear();
|
||||
_domainList.clear();
|
||||
_index = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../common/chart_context.dart' show ChartContext;
|
||||
import 'axis.dart' show AxisOrientation;
|
||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import 'ordinal_scale.dart' show OrdinalScale;
|
||||
import 'tick.dart' show Tick;
|
||||
import 'tick_formatter.dart' show TickFormatter;
|
||||
import 'tick_provider.dart' show BaseTickProvider, TickHint;
|
||||
|
||||
/// A strategy for selecting ticks to draw given ordinal domain values.
|
||||
class OrdinalTickProvider extends BaseTickProvider<String> {
|
||||
const OrdinalTickProvider();
|
||||
|
||||
@override
|
||||
List<Tick<String>> getTicks({
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required List<String> domainValues,
|
||||
@required OrdinalScale scale,
|
||||
@required TickFormatter formatter,
|
||||
@required Map<String, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
@required AxisOrientation orientation,
|
||||
bool viewportExtensionEnabled = false,
|
||||
TickHint<String> tickHint,
|
||||
}) {
|
||||
return createTicks(scale.domain.domains,
|
||||
context: context,
|
||||
graphicsFactory: graphicsFactory,
|
||||
scale: scale,
|
||||
formatter: formatter,
|
||||
formatterValueCache: formatterValueCache,
|
||||
tickDrawStrategy: tickDrawStrategy);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) => other is OrdinalTickProvider;
|
||||
|
||||
@override
|
||||
int get hashCode => 31;
|
||||
}
|
||||
313
web/charts/common/lib/src/chart/cartesian/axis/scale.dart
Normal file
313
web/charts/common/lib/src/chart/cartesian/axis/scale.dart
Normal file
@@ -0,0 +1,313 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math' as math show max, min;
|
||||
|
||||
/// Scale used to convert data input domain units to output range units.
|
||||
///
|
||||
/// This is the immutable portion of the Scale definition. Used for converting
|
||||
/// data from the dataset in domain units to an output in range units (likely
|
||||
/// pixel range of the area to draw on).
|
||||
///
|
||||
/// <p>The Scale/MutableScale split is to show the intention of what you can or
|
||||
/// should be doing with the scale during different stages of chart draw
|
||||
/// process.
|
||||
///
|
||||
/// [D] is the domain class type for the values passed in.
|
||||
abstract class Scale<D> {
|
||||
/// Applies the scale function to the [domainValue].
|
||||
///
|
||||
/// Returns the pixel location for the given [domainValue] or null if the
|
||||
/// domainValue could not be found/translated by this scale.
|
||||
/// Non-numeric scales should be the only ones that can return null.
|
||||
num operator [](D domainValue);
|
||||
|
||||
/// Reverse application of the scale.
|
||||
D reverse(double pixelLocation);
|
||||
|
||||
/// Tests a [domainValue] to see if the scale can translate it.
|
||||
///
|
||||
/// Returns true if the scale can translate the given domainValue.
|
||||
/// (Ex: linear scales can translate any number, but ordinal scales can only
|
||||
/// translate values previously passed in.)
|
||||
bool canTranslate(D domainValue);
|
||||
|
||||
/// Returns the previously set output range for the scale function.
|
||||
ScaleOutputExtent get range;
|
||||
|
||||
/// Returns the absolute width between the max and min range values.
|
||||
int get rangeWidth;
|
||||
|
||||
/// Returns the configuration used to determine the rangeBand.
|
||||
///
|
||||
/// This is most often used to define the bar group width.
|
||||
RangeBandConfig get rangeBandConfig;
|
||||
|
||||
/// Returns the rangeBand width in pixels.
|
||||
///
|
||||
/// The rangeBand is determined using the RangeBandConfig potentially with the
|
||||
/// measured step size. This value is used as the bar group width. If
|
||||
/// StepSizeConfig is set to auto detect, then you must wait until after
|
||||
/// the chart's onPostLayout phase before you'll get a valid number.
|
||||
double get rangeBand;
|
||||
|
||||
/// Returns the stepSize width in pixels.
|
||||
///
|
||||
/// The step size is determined using the [StepSizeConfig].
|
||||
double get stepSize;
|
||||
|
||||
/// Returns the stepSize domain value.
|
||||
double get domainStepSize;
|
||||
|
||||
/// Tests whether the given [domainValue] is within the axis' range.
|
||||
///
|
||||
/// Returns < 0 if the [domainValue] would plot before the viewport, 0 if it
|
||||
/// would plot within the viewport and > 0 if it would plot beyond the
|
||||
/// viewport of the axis.
|
||||
int compareDomainValueToViewport(D domainValue);
|
||||
|
||||
/// Returns true if the given [rangeValue] point is within the output range.
|
||||
///
|
||||
/// Not to be confused with the start and end of the domain.
|
||||
bool isRangeValueWithinViewport(double rangeValue);
|
||||
|
||||
/// Returns the current viewport scale.
|
||||
///
|
||||
/// A scale of 1.0 would map the data directly to the output range, while a
|
||||
/// value of 2.0 would map the data to an output of double the range so you
|
||||
/// only see half the data in the viewport. This is the equivalent to
|
||||
/// zooming. Its value is likely >= 1.0.
|
||||
double get viewportScalingFactor;
|
||||
|
||||
/// Returns the current pixel viewport offset
|
||||
///
|
||||
/// The translate is used by the scale function when it applies the scale.
|
||||
/// This is the equivalent to panning. Its value is likely <= 0 to pan the
|
||||
/// data to the left.
|
||||
double get viewportTranslatePx;
|
||||
|
||||
/// Returns a mutable copy of the scale.
|
||||
///
|
||||
/// Mutating the returned scale will not effect the original one.
|
||||
MutableScale<D> copy();
|
||||
}
|
||||
|
||||
/// Mutable extension of the [Scale] definition.
|
||||
///
|
||||
/// Used for converting data from the dataset to some range (likely pixel range)
|
||||
/// of the area to draw on.
|
||||
///
|
||||
/// [D] the domain class type for the values passed in.
|
||||
abstract class MutableScale<D> extends Scale<D> {
|
||||
/// Reset the domain for this [Scale].
|
||||
void resetDomain();
|
||||
|
||||
/// Reset the viewport settings for this [Scale].
|
||||
void resetViewportSettings();
|
||||
|
||||
/// Add [domainValue] to this [Scale]'s domain.
|
||||
///
|
||||
/// Domains should be added in order to allow proper stepSize detection.
|
||||
/// [domainValue] is the data value to add to the scale used to update the
|
||||
/// domain extent.
|
||||
void addDomain(D domainValue);
|
||||
|
||||
/// Sets the output range to use for the scale's conversion.
|
||||
///
|
||||
/// The range start is mapped to the domain's min and the range end is
|
||||
/// mapped to the domain's max for the conversion using the domain nicing
|
||||
/// function.
|
||||
///
|
||||
/// [extent] is the extent of the range which will likely be the pixel
|
||||
/// range of the drawing area to convert to.
|
||||
set range(ScaleOutputExtent extent);
|
||||
|
||||
/// Configures the zoom and translate.
|
||||
///
|
||||
/// [viewportScale] is the zoom factor to use, likely >= 1.0 where 1.0 maps
|
||||
/// the complete data extents to the output range, and 2.0 only maps half the
|
||||
/// data to the output range.
|
||||
///
|
||||
/// [viewportTranslatePx] is the translate/pan to use in pixel units,
|
||||
/// likely <= 0 which shifts the start of the data before the edge of the
|
||||
/// chart giving us a pan.
|
||||
void setViewportSettings(double viewportScale, double viewportTranslatePx);
|
||||
|
||||
/// Sets the configuration used to determine the rangeBand (bar group width).
|
||||
set rangeBandConfig(RangeBandConfig barGroupWidthConfig);
|
||||
|
||||
/// Sets the method for determining the step size.
|
||||
///
|
||||
/// This is the domain space between data points.
|
||||
StepSizeConfig get stepSizeConfig;
|
||||
set stepSizeConfig(StepSizeConfig config);
|
||||
}
|
||||
|
||||
/// Tuple of the output for a scale in pixels from [start] to [end] inclusive.
|
||||
///
|
||||
/// It is different from [Extent] because it focuses on start and end and not
|
||||
/// min and max, meaning that start could be greater or less than end.
|
||||
class ScaleOutputExtent {
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
const ScaleOutputExtent(this.start, this.end);
|
||||
|
||||
int get min => math.min(start, end);
|
||||
int get max => math.max(start, end);
|
||||
|
||||
bool containsValue(double value) => value >= min && value <= max;
|
||||
|
||||
/// Returns the difference between the extents.
|
||||
///
|
||||
/// If the [end] is less than the [start] (think vertical measure axis), then
|
||||
/// this will correctly return a negative value.
|
||||
int get diff => end - start;
|
||||
|
||||
/// Returns the width of the extent.
|
||||
int get width => diff.abs();
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is ScaleOutputExtent && start == other.start && end == other.end;
|
||||
|
||||
@override
|
||||
int get hashCode => start.hashCode + (end.hashCode * 31);
|
||||
|
||||
@override
|
||||
String toString() => "ScaleOutputRange($start, $end)";
|
||||
}
|
||||
|
||||
/// Type of RangeBand used to determine the rangeBand size units.
|
||||
enum RangeBandType {
|
||||
/// No rangeBand (not suitable for bars or step line charts).
|
||||
none,
|
||||
|
||||
/// Size is specified in pixel units.
|
||||
fixedPixel,
|
||||
|
||||
/// Size is specified domain scale units.
|
||||
fixedDomain,
|
||||
|
||||
/// Size is a percentage of the minimum step size between points.
|
||||
fixedPercentOfStep,
|
||||
|
||||
/// Size is a style pack assigned percentage of the minimum step size between
|
||||
/// points.
|
||||
styleAssignedPercentOfStep,
|
||||
|
||||
/// Size is subtracted from the minimum step size between points in pixel
|
||||
/// units.
|
||||
fixedPixelSpaceFromStep,
|
||||
}
|
||||
|
||||
/// Defines the method for calculating the rangeBand of the Scale.
|
||||
///
|
||||
/// The rangeBand is used to determine the width of a group of bars. The term
|
||||
/// rangeBand comes from the d3 JavaScript library which the JS library uses
|
||||
/// internally.
|
||||
///
|
||||
/// <p>RangeBandConfig is immutable, See factory methods for creating one.
|
||||
class RangeBandConfig {
|
||||
final RangeBandType type;
|
||||
|
||||
/// The width of the band in units specified by the bandType.
|
||||
final double size;
|
||||
|
||||
/// Creates a rangeBand definition of zero, no rangeBand.
|
||||
const RangeBandConfig.none()
|
||||
: type = RangeBandType.none,
|
||||
size = 0.0;
|
||||
|
||||
/// Creates a fixed rangeBand definition in pixel width.
|
||||
///
|
||||
/// Used to determine a bar width or a step width in the line renderer.
|
||||
const RangeBandConfig.fixedPixel(double pixels)
|
||||
: type = RangeBandType.fixedPixel,
|
||||
size = pixels;
|
||||
|
||||
/// Creates a fixed rangeBand definition in domain unit width.
|
||||
///
|
||||
/// Used to determine a bar width or a step width in the line renderer.
|
||||
const RangeBandConfig.fixedDomain(double domainSize)
|
||||
: type = RangeBandType.fixedDomain,
|
||||
size = domainSize;
|
||||
|
||||
/// Creates a config that defines the rangeBand as equal to the stepSize.
|
||||
const RangeBandConfig.stepChartBand()
|
||||
: type = RangeBandType.fixedPercentOfStep,
|
||||
size = 1.0;
|
||||
|
||||
/// Creates a config that defines the rangeBand as percentage of the stepSize.
|
||||
///
|
||||
/// [percentOfStepWidth] is the percentage of the step from 0.0 - 1.0.
|
||||
RangeBandConfig.percentOfStep(double percentOfStepWidth)
|
||||
: type = RangeBandType.fixedPercentOfStep,
|
||||
size = percentOfStepWidth {
|
||||
assert(percentOfStepWidth >= 0 && percentOfStepWidth <= 1.0);
|
||||
}
|
||||
|
||||
/// Creates a config that assigns the rangeBand according to the stylepack.
|
||||
///
|
||||
/// <p>Note: renderers can detect this setting and update the percent based on
|
||||
/// the number of series in their preprocess.
|
||||
RangeBandConfig.styleAssignedPercent([int seriesCount = 1])
|
||||
: type = RangeBandType.styleAssignedPercentOfStep,
|
||||
// TODO: retrieve value from the stylepack once available.
|
||||
size = 0.65;
|
||||
|
||||
/// Creates a config that defines the rangeBand as the stepSize - pixels.
|
||||
///
|
||||
/// Where fixedPixels() gave you a constant rangBand in pixels, this will give
|
||||
/// you a constant space between rangeBands in pixels.
|
||||
const RangeBandConfig.fixedPixelSpaceBetweenStep(double pixels)
|
||||
: type = RangeBandType.fixedPixelSpaceFromStep,
|
||||
size = pixels;
|
||||
}
|
||||
|
||||
/// Type of step size calculation to use.
|
||||
enum StepSizeType { autoDetect, fixedDomain, fixedPixels }
|
||||
|
||||
/// Defines the method for calculating the stepSize between points.
|
||||
///
|
||||
/// Typically auto will work fine in most cases, but if your data is
|
||||
/// irregular or you only have one data point, then you may want to override the
|
||||
/// stepSize detection specifying the exact expected stepSize.
|
||||
class StepSizeConfig {
|
||||
final StepSizeType type;
|
||||
final double size;
|
||||
|
||||
/// Creates a StepSizeConfig that calculates step size based on incoming data.
|
||||
///
|
||||
/// The stepSize is determined is calculated by detecting the smallest
|
||||
/// distance between two adjacent data points. This may not be suitable if
|
||||
/// you have irregular data or just a single data point.
|
||||
const StepSizeConfig.auto()
|
||||
: type = StepSizeType.autoDetect,
|
||||
size = 0.0;
|
||||
|
||||
/// Creates a StepSizeConfig specifying the exact step size in pixel units.
|
||||
const StepSizeConfig.fixedPixels(double pixels)
|
||||
: type = StepSizeType.fixedPixels,
|
||||
size = pixels;
|
||||
|
||||
/// Creates a StepSizeConfig specifying the exact step size in domain units.
|
||||
const StepSizeConfig.fixedDomain(double domainSize)
|
||||
: type = StepSizeType.fixedDomain,
|
||||
size = domainSize;
|
||||
}
|
||||
|
||||
// TODO: make other extent subclasses plural.
|
||||
abstract class Extents<D> {}
|
||||
@@ -0,0 +1,344 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:math' show min, max;
|
||||
|
||||
import 'ordinal_scale.dart' show OrdinalScale;
|
||||
import 'ordinal_scale_domain_info.dart' show OrdinalScaleDomainInfo;
|
||||
import 'scale.dart'
|
||||
show
|
||||
RangeBandConfig,
|
||||
RangeBandType,
|
||||
StepSizeConfig,
|
||||
StepSizeType,
|
||||
ScaleOutputExtent;
|
||||
|
||||
/// Scale that converts ordinal values of type [D] to a given range output.
|
||||
///
|
||||
/// A `SimpleOrdinalScale` is used to map values from its domain to the
|
||||
/// available pixel range of the chart. Typically used for bar charts where the
|
||||
/// width of the bar is [rangeBand] and the position of the bar is retrieved
|
||||
/// by [[]].
|
||||
class SimpleOrdinalScale implements OrdinalScale {
|
||||
final _stepSizeConfig = new StepSizeConfig.auto();
|
||||
OrdinalScaleDomainInfo _domain;
|
||||
ScaleOutputExtent _range = new ScaleOutputExtent(0, 1);
|
||||
double _viewportScale = 1.0;
|
||||
double _viewportTranslatePx = 0.0;
|
||||
RangeBandConfig _rangeBandConfig = new RangeBandConfig.styleAssignedPercent();
|
||||
|
||||
bool _scaleChanged = true;
|
||||
double _cachedStepSizePixels;
|
||||
double _cachedRangeBandShift;
|
||||
double _cachedRangeBandSize;
|
||||
|
||||
int _viewportDataSize;
|
||||
String _viewportStartingDomain;
|
||||
|
||||
SimpleOrdinalScale() : _domain = new OrdinalScaleDomainInfo();
|
||||
|
||||
SimpleOrdinalScale._copy(SimpleOrdinalScale other)
|
||||
: _domain = other._domain.copy(),
|
||||
_range = new ScaleOutputExtent(other._range.start, other._range.end),
|
||||
_viewportScale = other._viewportScale,
|
||||
_viewportTranslatePx = other._viewportTranslatePx,
|
||||
_rangeBandConfig = other._rangeBandConfig;
|
||||
|
||||
@override
|
||||
double get rangeBand {
|
||||
if (_scaleChanged) {
|
||||
_updateScale();
|
||||
}
|
||||
|
||||
return _cachedRangeBandSize;
|
||||
}
|
||||
|
||||
@override
|
||||
double get stepSize {
|
||||
if (_scaleChanged) {
|
||||
_updateScale();
|
||||
}
|
||||
|
||||
return _cachedStepSizePixels;
|
||||
}
|
||||
|
||||
@override
|
||||
double get domainStepSize => 1.0;
|
||||
|
||||
@override
|
||||
set rangeBandConfig(RangeBandConfig barGroupWidthConfig) {
|
||||
if (barGroupWidthConfig == null) {
|
||||
throw new ArgumentError.notNull('RangeBandConfig must not be null.');
|
||||
}
|
||||
|
||||
if (barGroupWidthConfig.type == RangeBandType.fixedDomain ||
|
||||
barGroupWidthConfig.type == RangeBandType.none) {
|
||||
throw new ArgumentError(
|
||||
'barGroupWidthConfig must not be NONE or FIXED_DOMAIN');
|
||||
}
|
||||
|
||||
_rangeBandConfig = barGroupWidthConfig;
|
||||
_scaleChanged = true;
|
||||
}
|
||||
|
||||
@override
|
||||
RangeBandConfig get rangeBandConfig => _rangeBandConfig;
|
||||
|
||||
@override
|
||||
set stepSizeConfig(StepSizeConfig config) {
|
||||
if (config != null && config.type != StepSizeType.autoDetect) {
|
||||
throw new ArgumentError(
|
||||
'Ordinal scales only support StepSizeConfig of type Auto');
|
||||
}
|
||||
// Nothing is set because only auto is supported.
|
||||
}
|
||||
|
||||
@override
|
||||
StepSizeConfig get stepSizeConfig => _stepSizeConfig;
|
||||
|
||||
/// Converts [domainValue] to the position to place the band/bar.
|
||||
///
|
||||
/// Returns 0 if not found.
|
||||
@override
|
||||
num operator [](String domainValue) {
|
||||
if (_scaleChanged) {
|
||||
_updateScale();
|
||||
}
|
||||
|
||||
final i = _domain.indexOf(domainValue);
|
||||
if (i != null) {
|
||||
return viewportTranslatePx +
|
||||
_range.start +
|
||||
_cachedRangeBandShift +
|
||||
(_cachedStepSizePixels * i);
|
||||
}
|
||||
// If it wasn't found
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
String reverse(double pixelLocation) {
|
||||
final index = ((pixelLocation -
|
||||
viewportTranslatePx -
|
||||
_range.start -
|
||||
_cachedRangeBandShift) /
|
||||
_cachedStepSizePixels);
|
||||
|
||||
// The last pixel belongs in the last step even if it tries to round up.
|
||||
//
|
||||
// Index may be less than 0 when [pixelLocation] is less than the width of
|
||||
// the range band shift. This may happen on the far left side of the chart,
|
||||
// where we want the first datum anyways. Wrapping the result in "max(0, x)"
|
||||
// cuts off these negative values.
|
||||
return _domain
|
||||
.getDomainAtIndex(max(0, min(index.round(), domain.size - 1)));
|
||||
}
|
||||
|
||||
@override
|
||||
bool canTranslate(String domainValue) =>
|
||||
(_domain.indexOf(domainValue) != null);
|
||||
|
||||
@override
|
||||
OrdinalScaleDomainInfo get domain => _domain;
|
||||
|
||||
/// Update the scale to include [domainValue].
|
||||
@override
|
||||
void addDomain(String domainValue) {
|
||||
_domain.add(domainValue);
|
||||
_scaleChanged = true;
|
||||
}
|
||||
|
||||
@override
|
||||
set range(ScaleOutputExtent extent) {
|
||||
_range = extent;
|
||||
_scaleChanged = true;
|
||||
}
|
||||
|
||||
@override
|
||||
ScaleOutputExtent get range => _range;
|
||||
|
||||
@override
|
||||
resetDomain() {
|
||||
_domain.clear();
|
||||
_scaleChanged = true;
|
||||
}
|
||||
|
||||
@override
|
||||
resetViewportSettings() {
|
||||
_viewportScale = 1.0;
|
||||
_viewportTranslatePx = 0.0;
|
||||
_scaleChanged = true;
|
||||
}
|
||||
|
||||
@override
|
||||
int get rangeWidth => (range.start - range.end).abs().toInt();
|
||||
|
||||
@override
|
||||
double get viewportScalingFactor => _viewportScale;
|
||||
|
||||
@override
|
||||
double get viewportTranslatePx => _viewportTranslatePx;
|
||||
|
||||
@override
|
||||
void setViewportSettings(double viewportScale, double viewportTranslatePx) {
|
||||
_viewportScale = viewportScale;
|
||||
_viewportTranslatePx =
|
||||
min(0.0, max(rangeWidth * (1.0 - viewportScale), viewportTranslatePx));
|
||||
|
||||
_scaleChanged = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void setViewport(int viewportDataSize, String startingDomain) {
|
||||
if (startingDomain != null &&
|
||||
viewportDataSize != null &&
|
||||
viewportDataSize <= 0) {
|
||||
throw new ArgumentError('viewportDataSize can' 't be less than 1.');
|
||||
}
|
||||
|
||||
_scaleChanged = true;
|
||||
_viewportDataSize = viewportDataSize;
|
||||
_viewportStartingDomain = startingDomain;
|
||||
}
|
||||
|
||||
/// Update this scale's viewport using settings [_viewportDataSize] and
|
||||
/// [_viewportStartingDomain].
|
||||
void _updateViewport() {
|
||||
setViewportSettings(1.0, 0.0);
|
||||
_recalculateScale();
|
||||
if (_domain.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the scale with zoom level to help find the correct translate.
|
||||
setViewportSettings(
|
||||
_domain.size / min(_viewportDataSize, _domain.size), 0.0);
|
||||
_recalculateScale();
|
||||
final domainIndex = _domain.indexOf(_viewportStartingDomain);
|
||||
if (domainIndex != null) {
|
||||
// Update the translate so that the scale starts half a step before the
|
||||
// chosen domain.
|
||||
final viewportTranslatePx = -(_cachedStepSizePixels * domainIndex);
|
||||
setViewportSettings(_viewportScale, viewportTranslatePx);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get viewportDataSize {
|
||||
if (_scaleChanged) {
|
||||
_updateScale();
|
||||
}
|
||||
|
||||
return _domain.isEmpty ? 0 : (rangeWidth ~/ _cachedStepSizePixels);
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewportStartingDomain {
|
||||
if (_scaleChanged) {
|
||||
_updateScale();
|
||||
}
|
||||
if (_domain.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return _domain.getDomainAtIndex(
|
||||
(-_viewportTranslatePx / _cachedStepSizePixels).ceil().toInt());
|
||||
}
|
||||
|
||||
@override
|
||||
bool isRangeValueWithinViewport(double rangeValue) {
|
||||
return range != null && rangeValue >= range.min && rangeValue <= range.max;
|
||||
}
|
||||
|
||||
@override
|
||||
int compareDomainValueToViewport(String domainValue) {
|
||||
// TODO: This currently works because range defaults to 0-1
|
||||
// This needs to be looked into further.
|
||||
var i = _domain.indexOf(domainValue);
|
||||
if (i != null && range != null) {
|
||||
var domainPx = this[domainValue];
|
||||
if (domainPx < range.min) {
|
||||
return -1;
|
||||
}
|
||||
if (domainPx > range.max) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@override
|
||||
SimpleOrdinalScale copy() => new SimpleOrdinalScale._copy(this);
|
||||
|
||||
void _updateCachedFields(
|
||||
double stepSizePixels, double rangeBandPixels, double rangeBandShift) {
|
||||
_cachedStepSizePixels = stepSizePixels;
|
||||
_cachedRangeBandSize = rangeBandPixels;
|
||||
_cachedRangeBandShift = rangeBandShift;
|
||||
|
||||
// TODO: When there are horizontal bars increasing from where
|
||||
// the domain and measure axis intersects but the desired behavior is
|
||||
// flipped. The plan is to fix this by fixing code to flip the range in the
|
||||
// code.
|
||||
|
||||
// If range start is less than range end, then the domain is calculated by
|
||||
// adding the band width. If range start is greater than range end, then the
|
||||
// domain is calculated by subtracting from the band width (ex. horizontal
|
||||
// bar charts where first series is at the bottom of the chart).
|
||||
if (range.start > range.end) {
|
||||
_cachedStepSizePixels *= -1;
|
||||
_cachedRangeBandShift *= -1;
|
||||
}
|
||||
|
||||
_scaleChanged = false;
|
||||
}
|
||||
|
||||
void _updateScale() {
|
||||
if (_viewportStartingDomain != null && _viewportDataSize != null) {
|
||||
// Update viewport recalculates the scale.
|
||||
_updateViewport();
|
||||
}
|
||||
_recalculateScale();
|
||||
}
|
||||
|
||||
void _recalculateScale() {
|
||||
final stepSizePixels = _domain.isEmpty
|
||||
? 0.0
|
||||
: _viewportScale * (rangeWidth.toDouble() / _domain.size.toDouble());
|
||||
double rangeBandPixels;
|
||||
|
||||
switch (rangeBandConfig.type) {
|
||||
case RangeBandType.fixedPixel:
|
||||
rangeBandPixels = rangeBandConfig.size.toDouble();
|
||||
break;
|
||||
case RangeBandType.fixedPixelSpaceFromStep:
|
||||
var spaceInPixels = rangeBandConfig.size.toDouble();
|
||||
rangeBandPixels = max(0.0, stepSizePixels - spaceInPixels);
|
||||
break;
|
||||
case RangeBandType.styleAssignedPercentOfStep:
|
||||
case RangeBandType.fixedPercentOfStep:
|
||||
var percent = rangeBandConfig.size.toDouble();
|
||||
rangeBandPixels = stepSizePixels * percent;
|
||||
break;
|
||||
case RangeBandType.fixedDomain:
|
||||
case RangeBandType.none:
|
||||
default:
|
||||
throw new StateError('RangeBandType must not be NONE or FIXED_DOMAIN');
|
||||
break;
|
||||
}
|
||||
|
||||
_updateCachedFields(stepSizePixels, rangeBandPixels, stepSizePixels / 2.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import '../../../../common/color.dart' show Color;
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../axis.dart' show Axis;
|
||||
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import '../tick_formatter.dart' show TickFormatter;
|
||||
import '../tick_provider.dart' show TickProvider;
|
||||
|
||||
@immutable
|
||||
class AxisSpec<D> {
|
||||
final bool showAxisLine;
|
||||
final RenderSpec<D> renderSpec;
|
||||
final TickProviderSpec<D> tickProviderSpec;
|
||||
final TickFormatterSpec<D> tickFormatterSpec;
|
||||
|
||||
const AxisSpec({
|
||||
this.renderSpec,
|
||||
this.tickProviderSpec,
|
||||
this.tickFormatterSpec,
|
||||
this.showAxisLine,
|
||||
});
|
||||
|
||||
factory AxisSpec.from(
|
||||
AxisSpec other, {
|
||||
RenderSpec<D> renderSpec,
|
||||
TickProviderSpec<D> tickProviderSpec,
|
||||
TickFormatterSpec<D> tickFormatterSpec,
|
||||
bool showAxisLine,
|
||||
}) {
|
||||
return new AxisSpec(
|
||||
renderSpec: renderSpec ?? other.renderSpec,
|
||||
tickProviderSpec: tickProviderSpec ?? other.tickProviderSpec,
|
||||
tickFormatterSpec: tickFormatterSpec ?? other.tickFormatterSpec,
|
||||
showAxisLine: showAxisLine ?? other.showAxisLine,
|
||||
);
|
||||
}
|
||||
|
||||
configure(
|
||||
Axis<D> axis, ChartContext context, GraphicsFactory graphicsFactory) {
|
||||
if (showAxisLine != null) {
|
||||
axis.forceDrawAxisLine = showAxisLine;
|
||||
}
|
||||
|
||||
if (renderSpec != null) {
|
||||
axis.tickDrawStrategy =
|
||||
renderSpec.createDrawStrategy(context, graphicsFactory);
|
||||
}
|
||||
|
||||
if (tickProviderSpec != null) {
|
||||
axis.tickProvider = tickProviderSpec.createTickProvider(context);
|
||||
}
|
||||
|
||||
if (tickFormatterSpec != null) {
|
||||
axis.tickFormatter = tickFormatterSpec.createTickFormatter(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an appropriately typed [Axis].
|
||||
Axis<D> createAxis() => null;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is AxisSpec &&
|
||||
renderSpec == other.renderSpec &&
|
||||
tickProviderSpec == other.tickProviderSpec &&
|
||||
tickFormatterSpec == other.tickFormatterSpec &&
|
||||
showAxisLine == other.showAxisLine);
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = renderSpec?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + tickProviderSpec.hashCode;
|
||||
hashcode = (hashcode * 37) + tickFormatterSpec.hashCode;
|
||||
hashcode = (hashcode * 37) + showAxisLine.hashCode;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
abstract class TickProviderSpec<D> {
|
||||
TickProvider<D> createTickProvider(ChartContext context);
|
||||
}
|
||||
|
||||
@immutable
|
||||
abstract class TickFormatterSpec<D> {
|
||||
TickFormatter<D> createTickFormatter(ChartContext context);
|
||||
}
|
||||
|
||||
@immutable
|
||||
abstract class RenderSpec<D> {
|
||||
const RenderSpec();
|
||||
|
||||
TickDrawStrategy<D> createDrawStrategy(
|
||||
ChartContext context, GraphicsFactory graphicFactory);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class TextStyleSpec {
|
||||
final String fontFamily;
|
||||
final int fontSize;
|
||||
final Color color;
|
||||
|
||||
const TextStyleSpec({this.fontFamily, this.fontSize, this.color});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is TextStyleSpec &&
|
||||
fontFamily == other.fontFamily &&
|
||||
fontSize == other.fontSize &&
|
||||
color == other.color);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = fontFamily?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + fontSize?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + color?.hashCode ?? 0;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class LineStyleSpec {
|
||||
final Color color;
|
||||
final List<int> dashPattern;
|
||||
final int thickness;
|
||||
|
||||
const LineStyleSpec({this.color, this.dashPattern, this.thickness});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is LineStyleSpec &&
|
||||
color == other.color &&
|
||||
dashPattern == other.dashPattern &&
|
||||
thickness == other.thickness);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = color?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + dashPattern?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + thickness?.hashCode ?? 0;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
enum TickLabelAnchor {
|
||||
before,
|
||||
centered,
|
||||
after,
|
||||
|
||||
/// The top most tick draws all text under the location.
|
||||
/// The bottom most tick draws all text above the location.
|
||||
/// The rest of the ticks are centered.
|
||||
inside,
|
||||
}
|
||||
|
||||
enum TickLabelJustification {
|
||||
inside,
|
||||
outside,
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../axis.dart' show Axis, NumericAxis;
|
||||
import '../linear/bucketing_numeric_axis.dart' show BucketingNumericAxis;
|
||||
import '../linear/bucketing_numeric_tick_provider.dart'
|
||||
show BucketingNumericTickProvider;
|
||||
import '../numeric_extents.dart' show NumericExtents;
|
||||
import 'axis_spec.dart' show AxisSpec, RenderSpec;
|
||||
import 'numeric_axis_spec.dart'
|
||||
show
|
||||
BasicNumericTickFormatterSpec,
|
||||
BasicNumericTickProviderSpec,
|
||||
NumericAxisSpec,
|
||||
NumericTickProviderSpec,
|
||||
NumericTickFormatterSpec;
|
||||
|
||||
/// A numeric [AxisSpec] that positions all values beneath a certain [threshold]
|
||||
/// into a reserved space on the axis range. The label for the bucket line will
|
||||
/// be drawn in the middle of the bucket range, rather than aligned with the
|
||||
/// gridline for that value's position on the scale.
|
||||
///
|
||||
/// An example illustration of a bucketing measure axis on a point chart
|
||||
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
|
||||
/// of the axis, since they are less than the [threshold] value of 10%.
|
||||
///
|
||||
/// 100% ┠─────────────────────────
|
||||
/// ┃ *
|
||||
/// ┃ *
|
||||
/// 50% ┠──────*──────────────────
|
||||
/// ┃
|
||||
/// ┠─────────────────────────
|
||||
/// < 10% ┃ * *
|
||||
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
|
||||
/// 0 50 100
|
||||
///
|
||||
/// This axis will format numbers as percents by default.
|
||||
@immutable
|
||||
class BucketingAxisSpec extends NumericAxisSpec {
|
||||
/// All values smaller than the threshold will be bucketed into the same
|
||||
/// position in the reserved space on the axis.
|
||||
final num threshold;
|
||||
|
||||
/// Whether or not measure values bucketed below the [threshold] should be
|
||||
/// visible on the chart, or collapsed.
|
||||
///
|
||||
/// If this is false, then any data with measure values smaller than
|
||||
/// [threshold] will not be rendered on the chart.
|
||||
final bool showBucket;
|
||||
|
||||
/// Creates a [NumericAxisSpec] that is specialized for percentage data.
|
||||
BucketingAxisSpec({
|
||||
RenderSpec<num> renderSpec,
|
||||
NumericTickProviderSpec tickProviderSpec,
|
||||
NumericTickFormatterSpec tickFormatterSpec,
|
||||
bool showAxisLine,
|
||||
bool showBucket,
|
||||
this.threshold,
|
||||
NumericExtents viewport,
|
||||
}) : this.showBucket = showBucket ?? true,
|
||||
super(
|
||||
renderSpec: renderSpec,
|
||||
tickProviderSpec:
|
||||
tickProviderSpec ?? const BucketingNumericTickProviderSpec(),
|
||||
tickFormatterSpec: tickFormatterSpec ??
|
||||
new BasicNumericTickFormatterSpec.fromNumberFormat(
|
||||
new NumberFormat.percentPattern()),
|
||||
showAxisLine: showAxisLine,
|
||||
viewport: viewport ?? const NumericExtents(0.0, 1.0));
|
||||
|
||||
@override
|
||||
configure(
|
||||
Axis<num> axis, ChartContext context, GraphicsFactory graphicsFactory) {
|
||||
super.configure(axis, context, graphicsFactory);
|
||||
|
||||
if (axis is NumericAxis && viewport != null) {
|
||||
axis.setScaleViewport(viewport);
|
||||
}
|
||||
|
||||
if (axis is BucketingNumericAxis && threshold != null) {
|
||||
axis.threshold = threshold;
|
||||
}
|
||||
|
||||
if (axis is BucketingNumericAxis && showBucket != null) {
|
||||
axis.showBucket = showBucket;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
BucketingNumericAxis createAxis() => new BucketingNumericAxis();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is BucketingAxisSpec &&
|
||||
showBucket == other.showBucket &&
|
||||
threshold == other.threshold &&
|
||||
super == (other));
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = super.hashCode;
|
||||
hashcode = (hashcode * 37) + showBucket.hashCode;
|
||||
hashcode = (hashcode * 37) + threshold.hashCode;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class BucketingNumericTickProviderSpec extends BasicNumericTickProviderSpec {
|
||||
/// Creates a [TickProviderSpec] that generates ticks for a bucketing axis.
|
||||
///
|
||||
/// [zeroBound] automatically include zero in the data range.
|
||||
/// [dataIsInWholeNumbers] skip over ticks that would produce
|
||||
/// fractional ticks that don't make sense for the domain (ie: headcount).
|
||||
/// [desiredTickCount] the fixed number of ticks to try to make. Convenience
|
||||
/// that sets [desiredMinTickCount] and [desiredMaxTickCount] the same.
|
||||
/// Both min and max win out if they are set along with
|
||||
/// [desiredTickCount].
|
||||
/// [desiredMinTickCount] automatically choose the best tick
|
||||
/// count to produce the 'nicest' ticks but make sure we have this many.
|
||||
/// [desiredMaxTickCount] automatically choose the best tick
|
||||
/// count to produce the 'nicest' ticks but make sure we don't have more
|
||||
/// than this many.
|
||||
const BucketingNumericTickProviderSpec(
|
||||
{bool zeroBound,
|
||||
bool dataIsInWholeNumbers,
|
||||
int desiredTickCount,
|
||||
int desiredMinTickCount,
|
||||
int desiredMaxTickCount})
|
||||
: super(
|
||||
zeroBound: zeroBound ?? true,
|
||||
dataIsInWholeNumbers: dataIsInWholeNumbers ?? false,
|
||||
desiredTickCount: desiredTickCount,
|
||||
desiredMinTickCount: desiredMinTickCount,
|
||||
desiredMaxTickCount: desiredMaxTickCount,
|
||||
);
|
||||
|
||||
@override
|
||||
BucketingNumericTickProvider createTickProvider(ChartContext context) {
|
||||
final provider = new BucketingNumericTickProvider()
|
||||
..zeroBound = zeroBound
|
||||
..dataIsInWholeNumbers = dataIsInWholeNumbers;
|
||||
|
||||
if (desiredMinTickCount != null ||
|
||||
desiredMaxTickCount != null ||
|
||||
desiredTickCount != null) {
|
||||
provider.setTickCount(desiredMaxTickCount ?? desiredTickCount ?? 10,
|
||||
desiredMinTickCount ?? desiredTickCount ?? 2);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../axis.dart' show Axis;
|
||||
import '../end_points_tick_provider.dart' show EndPointsTickProvider;
|
||||
import '../static_tick_provider.dart' show StaticTickProvider;
|
||||
import '../time/auto_adjusting_date_time_tick_provider.dart'
|
||||
show AutoAdjustingDateTimeTickProvider;
|
||||
import '../time/date_time_axis.dart' show DateTimeAxis;
|
||||
import '../time/date_time_extents.dart' show DateTimeExtents;
|
||||
import '../time/date_time_tick_formatter.dart' show DateTimeTickFormatter;
|
||||
import '../time/day_time_stepper.dart' show DayTimeStepper;
|
||||
import '../time/hour_tick_formatter.dart' show HourTickFormatter;
|
||||
import '../time/time_range_tick_provider_impl.dart'
|
||||
show TimeRangeTickProviderImpl;
|
||||
import '../time/time_tick_formatter.dart' show TimeTickFormatter;
|
||||
import '../time/time_tick_formatter_impl.dart'
|
||||
show CalendarField, TimeTickFormatterImpl;
|
||||
import 'axis_spec.dart'
|
||||
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
|
||||
import 'tick_spec.dart' show TickSpec;
|
||||
|
||||
/// Generic [AxisSpec] specialized for Timeseries charts.
|
||||
@immutable
|
||||
class DateTimeAxisSpec extends AxisSpec<DateTime> {
|
||||
/// Sets viewport for this Axis.
|
||||
///
|
||||
/// If pan / zoom behaviors are set, this is the initial viewport.
|
||||
final DateTimeExtents viewport;
|
||||
|
||||
/// Creates a [AxisSpec] that specialized for timeseries charts.
|
||||
///
|
||||
/// [renderSpec] spec used to configure how the ticks and labels
|
||||
/// actually render. Possible values are [GridlineRendererSpec],
|
||||
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
|
||||
/// given to the RenderSpec is of type [DateTime] for Timeseries.
|
||||
/// [tickProviderSpec] spec used to configure what ticks are generated.
|
||||
/// [tickFormatterSpec] spec used to configure how the tick labels
|
||||
/// are formatted.
|
||||
/// [showAxisLine] override to force the axis to draw the axis
|
||||
/// line.
|
||||
const DateTimeAxisSpec({
|
||||
RenderSpec<DateTime> renderSpec,
|
||||
DateTimeTickProviderSpec tickProviderSpec,
|
||||
DateTimeTickFormatterSpec tickFormatterSpec,
|
||||
bool showAxisLine,
|
||||
this.viewport,
|
||||
}) : super(
|
||||
renderSpec: renderSpec,
|
||||
tickProviderSpec: tickProviderSpec,
|
||||
tickFormatterSpec: tickFormatterSpec,
|
||||
showAxisLine: showAxisLine);
|
||||
|
||||
@override
|
||||
configure(Axis<DateTime> axis, ChartContext context,
|
||||
GraphicsFactory graphicsFactory) {
|
||||
super.configure(axis, context, graphicsFactory);
|
||||
|
||||
if (axis is DateTimeAxis && viewport != null) {
|
||||
axis.setScaleViewport(viewport);
|
||||
}
|
||||
}
|
||||
|
||||
Axis<DateTime> createAxis() {
|
||||
assert(false, 'Call createDateTimeAxis() to create a DateTimeAxis.');
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Creates a [DateTimeAxis]. This should be called in place of createAxis.
|
||||
DateTimeAxis createDateTimeAxis(DateTimeFactory dateTimeFactory) =>
|
||||
new DateTimeAxis(dateTimeFactory);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is DateTimeAxisSpec &&
|
||||
viewport == other.viewport &&
|
||||
super == (other);
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = super.hashCode;
|
||||
hashcode = (hashcode * 37) + viewport.hashCode;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class DateTimeTickProviderSpec extends TickProviderSpec<DateTime> {}
|
||||
|
||||
abstract class DateTimeTickFormatterSpec extends TickFormatterSpec<DateTime> {}
|
||||
|
||||
/// [TickProviderSpec] that sets up the automatically assigned time ticks based
|
||||
/// on the extents of your data.
|
||||
@immutable
|
||||
class AutoDateTimeTickProviderSpec implements DateTimeTickProviderSpec {
|
||||
final bool includeTime;
|
||||
|
||||
/// Creates a [TickProviderSpec] that dynamically chooses ticks based on the
|
||||
/// extents of the data.
|
||||
///
|
||||
/// [includeTime] - flag that indicates whether the time should be
|
||||
/// included when choosing appropriate tick intervals.
|
||||
const AutoDateTimeTickProviderSpec({this.includeTime = true});
|
||||
|
||||
@override
|
||||
AutoAdjustingDateTimeTickProvider createTickProvider(ChartContext context) {
|
||||
if (includeTime) {
|
||||
return new AutoAdjustingDateTimeTickProvider.createDefault(
|
||||
context.dateTimeFactory);
|
||||
} else {
|
||||
return new AutoAdjustingDateTimeTickProvider.createWithoutTime(
|
||||
context.dateTimeFactory);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is AutoDateTimeTickProviderSpec && includeTime == other.includeTime;
|
||||
|
||||
@override
|
||||
int get hashCode => includeTime?.hashCode ?? 0;
|
||||
}
|
||||
|
||||
/// [TickProviderSpec] that sets up time ticks with days increments only.
|
||||
@immutable
|
||||
class DayTickProviderSpec implements DateTimeTickProviderSpec {
|
||||
final List<int> increments;
|
||||
|
||||
const DayTickProviderSpec({this.increments});
|
||||
|
||||
/// Creates a [TickProviderSpec] that dynamically chooses ticks based on the
|
||||
/// extents of the data, limited to day increments.
|
||||
///
|
||||
/// [increments] specify the number of day increments that can be chosen from
|
||||
/// when searching for the appropriate tick intervals.
|
||||
@override
|
||||
AutoAdjustingDateTimeTickProvider createTickProvider(ChartContext context) {
|
||||
return new AutoAdjustingDateTimeTickProvider.createWith([
|
||||
new TimeRangeTickProviderImpl(new DayTimeStepper(context.dateTimeFactory,
|
||||
allowedTickIncrements: increments))
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is DayTickProviderSpec && increments == other.increments;
|
||||
|
||||
@override
|
||||
int get hashCode => increments?.hashCode ?? 0;
|
||||
}
|
||||
|
||||
/// [TickProviderSpec] that sets up time ticks at the two end points of the axis
|
||||
/// range.
|
||||
@immutable
|
||||
class DateTimeEndPointsTickProviderSpec implements DateTimeTickProviderSpec {
|
||||
const DateTimeEndPointsTickProviderSpec();
|
||||
|
||||
/// Creates a [TickProviderSpec] that dynamically chooses time ticks at the
|
||||
/// two end points of the axis range
|
||||
@override
|
||||
EndPointsTickProvider<DateTime> createTickProvider(ChartContext context) {
|
||||
return new EndPointsTickProvider<DateTime>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is DateTimeEndPointsTickProviderSpec;
|
||||
}
|
||||
|
||||
/// [TickProviderSpec] that allows you to specific the ticks to be used.
|
||||
@immutable
|
||||
class StaticDateTimeTickProviderSpec implements DateTimeTickProviderSpec {
|
||||
final List<TickSpec<DateTime>> tickSpecs;
|
||||
|
||||
const StaticDateTimeTickProviderSpec(this.tickSpecs);
|
||||
|
||||
@override
|
||||
StaticTickProvider<DateTime> createTickProvider(ChartContext context) =>
|
||||
new StaticTickProvider<DateTime>(tickSpecs);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is StaticDateTimeTickProviderSpec && tickSpecs == other.tickSpecs;
|
||||
|
||||
@override
|
||||
int get hashCode => tickSpecs.hashCode;
|
||||
}
|
||||
|
||||
/// Formatters for a single level of the [DateTimeTickFormatterSpec].
|
||||
@immutable
|
||||
class TimeFormatterSpec {
|
||||
final String format;
|
||||
final String transitionFormat;
|
||||
final String noonFormat;
|
||||
|
||||
/// Creates a formatter for a particular granularity of data.
|
||||
///
|
||||
/// [format] [DateFormat] format string used to format non-transition ticks.
|
||||
/// The string is given to the dateTimeFactory to support i18n formatting.
|
||||
/// [transitionFormat] [DateFormat] format string used to format transition
|
||||
/// ticks. Examples of transition ticks:
|
||||
/// Day ticks would have a transition tick at month boundaries.
|
||||
/// Hour ticks would have a transition tick at day boundaries.
|
||||
/// The first tick is typically a transition tick.
|
||||
/// [noonFormat] [DateFormat] format string used only for formatting hours
|
||||
/// in the event that you want to format noon differently than other
|
||||
/// hours (ie: [10, 11, 12p, 1, 2, 3]).
|
||||
const TimeFormatterSpec(
|
||||
{this.format, this.transitionFormat, this.noonFormat});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is TimeFormatterSpec &&
|
||||
format == other.format &&
|
||||
transitionFormat == other.transitionFormat &&
|
||||
noonFormat == other.noonFormat;
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = format?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + transitionFormat?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + noonFormat?.hashCode ?? 0;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
/// [TickFormatterSpec] that automatically chooses the appropriate level of
|
||||
/// formatting based on the tick stepSize. Each level of date granularity has
|
||||
/// its own [TimeFormatterSpec] used to specify the formatting strings at that
|
||||
/// level.
|
||||
@immutable
|
||||
class AutoDateTimeTickFormatterSpec implements DateTimeTickFormatterSpec {
|
||||
final TimeFormatterSpec minute;
|
||||
final TimeFormatterSpec hour;
|
||||
final TimeFormatterSpec day;
|
||||
final TimeFormatterSpec month;
|
||||
final TimeFormatterSpec year;
|
||||
|
||||
/// Creates a [TickFormatterSpec] that automatically chooses the formatting
|
||||
/// given the individual [TimeFormatterSpec] formatters that are set.
|
||||
///
|
||||
/// There is a default formatter for each level that is configurable, but
|
||||
/// by specifying a level here it replaces the default for that particular
|
||||
/// granularity. This is useful for swapping out one or all of the formatters.
|
||||
const AutoDateTimeTickFormatterSpec(
|
||||
{this.minute, this.hour, this.day, this.month, this.year});
|
||||
|
||||
@override
|
||||
DateTimeTickFormatter createTickFormatter(ChartContext context) {
|
||||
final Map<int, TimeTickFormatter> map = {};
|
||||
|
||||
if (minute != null) {
|
||||
map[DateTimeTickFormatter.MINUTE] =
|
||||
_makeFormatter(minute, CalendarField.hourOfDay, context);
|
||||
}
|
||||
if (hour != null) {
|
||||
map[DateTimeTickFormatter.HOUR] =
|
||||
_makeFormatter(hour, CalendarField.date, context);
|
||||
}
|
||||
if (day != null) {
|
||||
map[23 * DateTimeTickFormatter.HOUR] =
|
||||
_makeFormatter(day, CalendarField.month, context);
|
||||
}
|
||||
if (month != null) {
|
||||
map[28 * DateTimeTickFormatter.DAY] =
|
||||
_makeFormatter(month, CalendarField.year, context);
|
||||
}
|
||||
if (year != null) {
|
||||
map[364 * DateTimeTickFormatter.DAY] =
|
||||
_makeFormatter(year, CalendarField.year, context);
|
||||
}
|
||||
|
||||
return new DateTimeTickFormatter(context.dateTimeFactory, overrides: map);
|
||||
}
|
||||
|
||||
TimeTickFormatterImpl _makeFormatter(TimeFormatterSpec spec,
|
||||
CalendarField transitionField, ChartContext context) {
|
||||
if (spec.noonFormat != null) {
|
||||
return new HourTickFormatter(
|
||||
dateTimeFactory: context.dateTimeFactory,
|
||||
simpleFormat: spec.format,
|
||||
transitionFormat: spec.transitionFormat,
|
||||
noonFormat: spec.noonFormat);
|
||||
} else {
|
||||
return new TimeTickFormatterImpl(
|
||||
dateTimeFactory: context.dateTimeFactory,
|
||||
simpleFormat: spec.format,
|
||||
transitionFormat: spec.transitionFormat,
|
||||
transitionField: transitionField);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is AutoDateTimeTickFormatterSpec &&
|
||||
minute == other.minute &&
|
||||
hour == other.hour &&
|
||||
day == other.day &&
|
||||
month == other.month &&
|
||||
year == other.year);
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = minute?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + hour?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + day?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + month?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + year?.hashCode ?? 0;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import '../draw_strategy/small_tick_draw_strategy.dart'
|
||||
show SmallTickRendererSpec;
|
||||
import '../time/date_time_extents.dart' show DateTimeExtents;
|
||||
import 'axis_spec.dart' show AxisSpec, RenderSpec, TickLabelAnchor;
|
||||
import 'date_time_axis_spec.dart'
|
||||
show
|
||||
DateTimeAxisSpec,
|
||||
DateTimeEndPointsTickProviderSpec,
|
||||
DateTimeTickFormatterSpec,
|
||||
DateTimeTickProviderSpec;
|
||||
|
||||
/// Default [AxisSpec] used for Timeseries charts.
|
||||
@immutable
|
||||
class EndPointsTimeAxisSpec extends DateTimeAxisSpec {
|
||||
/// Creates a [AxisSpec] that specialized for timeseries charts.
|
||||
///
|
||||
/// [renderSpec] spec used to configure how the ticks and labels
|
||||
/// actually render. Possible values are [GridlineRendererSpec],
|
||||
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
|
||||
/// given to the RenderSpec is of type [DateTime] for Timeseries.
|
||||
/// [tickProviderSpec] spec used to configure what ticks are generated.
|
||||
/// [tickFormatterSpec] spec used to configure how the tick labels
|
||||
/// are formatted.
|
||||
/// [showAxisLine] override to force the axis to draw the axis
|
||||
/// line.
|
||||
const EndPointsTimeAxisSpec({
|
||||
RenderSpec<DateTime> renderSpec,
|
||||
DateTimeTickProviderSpec tickProviderSpec,
|
||||
DateTimeTickFormatterSpec tickFormatterSpec,
|
||||
bool showAxisLine,
|
||||
DateTimeExtents viewport,
|
||||
bool usingBarRenderer = false,
|
||||
}) : super(
|
||||
renderSpec: renderSpec ??
|
||||
const SmallTickRendererSpec<DateTime>(
|
||||
labelAnchor: TickLabelAnchor.inside,
|
||||
labelOffsetFromTickPx: 0),
|
||||
tickProviderSpec:
|
||||
tickProviderSpec ?? const DateTimeEndPointsTickProviderSpec(),
|
||||
tickFormatterSpec: tickFormatterSpec,
|
||||
showAxisLine: showAxisLine,
|
||||
viewport: viewport);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is EndPointsTimeAxisSpec && super == (other));
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart';
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../../../common/datum_details.dart' show MeasureFormatter;
|
||||
import '../axis.dart' show Axis, NumericAxis;
|
||||
import '../end_points_tick_provider.dart' show EndPointsTickProvider;
|
||||
import '../numeric_extents.dart' show NumericExtents;
|
||||
import '../numeric_tick_provider.dart' show NumericTickProvider;
|
||||
import '../static_tick_provider.dart' show StaticTickProvider;
|
||||
import '../tick_formatter.dart' show NumericTickFormatter;
|
||||
import 'axis_spec.dart'
|
||||
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
|
||||
import 'tick_spec.dart' show TickSpec;
|
||||
|
||||
/// [AxisSpec] specialized for numeric/continuous axes like the measure axis.
|
||||
@immutable
|
||||
class NumericAxisSpec extends AxisSpec<num> {
|
||||
/// Sets viewport for this Axis.
|
||||
///
|
||||
/// If pan / zoom behaviors are set, this is the initial viewport.
|
||||
final NumericExtents viewport;
|
||||
|
||||
/// Creates a [AxisSpec] that specialized for numeric data.
|
||||
///
|
||||
/// [renderSpec] spec used to configure how the ticks and labels
|
||||
/// actually render. Possible values are [GridlineRendererSpec],
|
||||
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
|
||||
/// given to the RenderSpec is of type [num] when using this spec.
|
||||
/// [tickProviderSpec] spec used to configure what ticks are generated.
|
||||
/// [tickFormatterSpec] spec used to configure how the tick labels are
|
||||
/// formatted.
|
||||
/// [showAxisLine] override to force the axis to draw the axis line.
|
||||
const NumericAxisSpec({
|
||||
RenderSpec<num> renderSpec,
|
||||
NumericTickProviderSpec tickProviderSpec,
|
||||
NumericTickFormatterSpec tickFormatterSpec,
|
||||
bool showAxisLine,
|
||||
this.viewport,
|
||||
}) : super(
|
||||
renderSpec: renderSpec,
|
||||
tickProviderSpec: tickProviderSpec,
|
||||
tickFormatterSpec: tickFormatterSpec,
|
||||
showAxisLine: showAxisLine);
|
||||
|
||||
factory NumericAxisSpec.from(
|
||||
NumericAxisSpec other, {
|
||||
RenderSpec<num> renderSpec,
|
||||
TickProviderSpec tickProviderSpec,
|
||||
TickFormatterSpec tickFormatterSpec,
|
||||
bool showAxisLine,
|
||||
NumericExtents viewport,
|
||||
}) {
|
||||
return new NumericAxisSpec(
|
||||
renderSpec: renderSpec ?? other.renderSpec,
|
||||
tickProviderSpec: tickProviderSpec ?? other.tickProviderSpec,
|
||||
tickFormatterSpec: tickFormatterSpec ?? other.tickFormatterSpec,
|
||||
showAxisLine: showAxisLine ?? other.showAxisLine,
|
||||
viewport: viewport ?? other.viewport,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
configure(
|
||||
Axis<num> axis, ChartContext context, GraphicsFactory graphicsFactory) {
|
||||
super.configure(axis, context, graphicsFactory);
|
||||
|
||||
if (axis is NumericAxis && viewport != null) {
|
||||
axis.setScaleViewport(viewport);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
NumericAxis createAxis() => new NumericAxis();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is NumericAxisSpec &&
|
||||
viewport == other.viewport &&
|
||||
super == (other);
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = super.hashCode;
|
||||
hashcode = (hashcode * 37) + viewport.hashCode;
|
||||
hashcode = (hashcode * 37) + super.hashCode;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class NumericTickProviderSpec extends TickProviderSpec<num> {}
|
||||
|
||||
abstract class NumericTickFormatterSpec extends TickFormatterSpec<num> {}
|
||||
|
||||
@immutable
|
||||
class BasicNumericTickProviderSpec implements NumericTickProviderSpec {
|
||||
final bool zeroBound;
|
||||
final bool dataIsInWholeNumbers;
|
||||
final int desiredTickCount;
|
||||
final int desiredMinTickCount;
|
||||
final int desiredMaxTickCount;
|
||||
|
||||
/// Creates a [TickProviderSpec] that dynamically chooses the number of
|
||||
/// ticks based on the extents of the data.
|
||||
///
|
||||
/// [zeroBound] automatically include zero in the data range.
|
||||
/// [dataIsInWholeNumbers] skip over ticks that would produce
|
||||
/// fractional ticks that don't make sense for the domain (ie: headcount).
|
||||
/// [desiredTickCount] the fixed number of ticks to try to make. Convenience
|
||||
/// that sets [desiredMinTickCount] and [desiredMaxTickCount] the same.
|
||||
/// Both min and max win out if they are set along with
|
||||
/// [desiredTickCount].
|
||||
/// [desiredMinTickCount] automatically choose the best tick
|
||||
/// count to produce the 'nicest' ticks but make sure we have this many.
|
||||
/// [desiredMaxTickCount] automatically choose the best tick
|
||||
/// count to produce the 'nicest' ticks but make sure we don't have more
|
||||
/// than this many.
|
||||
const BasicNumericTickProviderSpec(
|
||||
{this.zeroBound,
|
||||
this.dataIsInWholeNumbers,
|
||||
this.desiredTickCount,
|
||||
this.desiredMinTickCount,
|
||||
this.desiredMaxTickCount});
|
||||
|
||||
@override
|
||||
NumericTickProvider createTickProvider(ChartContext context) {
|
||||
final provider = new NumericTickProvider();
|
||||
if (zeroBound != null) {
|
||||
provider.zeroBound = zeroBound;
|
||||
}
|
||||
if (dataIsInWholeNumbers != null) {
|
||||
provider.dataIsInWholeNumbers = dataIsInWholeNumbers;
|
||||
}
|
||||
|
||||
if (desiredMinTickCount != null ||
|
||||
desiredMaxTickCount != null ||
|
||||
desiredTickCount != null) {
|
||||
provider.setTickCount(desiredMaxTickCount ?? desiredTickCount ?? 10,
|
||||
desiredMinTickCount ?? desiredTickCount ?? 2);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is BasicNumericTickProviderSpec &&
|
||||
zeroBound == other.zeroBound &&
|
||||
dataIsInWholeNumbers == other.dataIsInWholeNumbers &&
|
||||
desiredTickCount == other.desiredTickCount &&
|
||||
desiredMinTickCount == other.desiredMinTickCount &&
|
||||
desiredMaxTickCount == other.desiredMaxTickCount;
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = zeroBound?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + dataIsInWholeNumbers?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + desiredTickCount?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + desiredMinTickCount?.hashCode ?? 0;
|
||||
hashcode = (hashcode * 37) + desiredMaxTickCount?.hashCode ?? 0;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
/// [TickProviderSpec] that sets up numeric ticks at the two end points of the
|
||||
/// axis range.
|
||||
@immutable
|
||||
class NumericEndPointsTickProviderSpec implements NumericTickProviderSpec {
|
||||
/// Creates a [TickProviderSpec] that dynamically chooses numeric ticks at the
|
||||
/// two end points of the axis range
|
||||
const NumericEndPointsTickProviderSpec();
|
||||
|
||||
@override
|
||||
EndPointsTickProvider<num> createTickProvider(ChartContext context) {
|
||||
return new EndPointsTickProvider<num>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is NumericEndPointsTickProviderSpec;
|
||||
}
|
||||
|
||||
/// [TickProviderSpec] that allows you to specific the ticks to be used.
|
||||
@immutable
|
||||
class StaticNumericTickProviderSpec implements NumericTickProviderSpec {
|
||||
final List<TickSpec<num>> tickSpecs;
|
||||
|
||||
const StaticNumericTickProviderSpec(this.tickSpecs);
|
||||
|
||||
@override
|
||||
StaticTickProvider<num> createTickProvider(ChartContext context) =>
|
||||
new StaticTickProvider<num>(tickSpecs);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is StaticNumericTickProviderSpec && tickSpecs == other.tickSpecs);
|
||||
|
||||
@override
|
||||
int get hashCode => tickSpecs.hashCode;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class BasicNumericTickFormatterSpec implements NumericTickFormatterSpec {
|
||||
final MeasureFormatter formatter;
|
||||
final NumberFormat numberFormat;
|
||||
|
||||
/// Simple [TickFormatterSpec] that delegates formatting to the given
|
||||
/// [NumberFormat].
|
||||
const BasicNumericTickFormatterSpec(this.formatter) : numberFormat = null;
|
||||
|
||||
const BasicNumericTickFormatterSpec.fromNumberFormat(this.numberFormat)
|
||||
: formatter = null;
|
||||
|
||||
/// A formatter will be created with the number format if it is not null.
|
||||
/// Otherwise, it will create one with the [MeasureFormatter] callback.
|
||||
@override
|
||||
NumericTickFormatter createTickFormatter(ChartContext context) {
|
||||
return numberFormat != null
|
||||
? new NumericTickFormatter.fromNumberFormat(numberFormat)
|
||||
: new NumericTickFormatter(formatter: formatter);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is BasicNumericTickFormatterSpec &&
|
||||
formatter == other.formatter &&
|
||||
numberFormat == other.numberFormat);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = formatter.hashCode;
|
||||
hashcode = (hashcode * 37) * numberFormat.hashCode;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../axis.dart' show Axis, OrdinalAxis, OrdinalViewport;
|
||||
import '../ordinal_tick_provider.dart' show OrdinalTickProvider;
|
||||
import '../static_tick_provider.dart' show StaticTickProvider;
|
||||
import '../tick_formatter.dart' show OrdinalTickFormatter;
|
||||
import 'axis_spec.dart'
|
||||
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
|
||||
import 'tick_spec.dart' show TickSpec;
|
||||
|
||||
/// [AxisSpec] specialized for ordinal/non-continuous axes typically for bars.
|
||||
@immutable
|
||||
class OrdinalAxisSpec extends AxisSpec<String> {
|
||||
/// Sets viewport for this Axis.
|
||||
///
|
||||
/// If pan / zoom behaviors are set, this is the initial viewport.
|
||||
final OrdinalViewport viewport;
|
||||
|
||||
/// Creates a [AxisSpec] that specialized for ordinal domain charts.
|
||||
///
|
||||
/// [renderSpec] spec used to configure how the ticks and labels
|
||||
/// actually render. Possible values are [GridlineRendererSpec],
|
||||
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
|
||||
/// given to the RenderSpec is of type [String] when using this spec.
|
||||
/// [tickProviderSpec] spec used to configure what ticks are generated.
|
||||
/// [tickFormatterSpec] spec used to configure how the tick labels are
|
||||
/// formatted.
|
||||
/// [showAxisLine] override to force the axis to draw the axis line.
|
||||
const OrdinalAxisSpec({
|
||||
RenderSpec<String> renderSpec,
|
||||
OrdinalTickProviderSpec tickProviderSpec,
|
||||
OrdinalTickFormatterSpec tickFormatterSpec,
|
||||
bool showAxisLine,
|
||||
this.viewport,
|
||||
}) : super(
|
||||
renderSpec: renderSpec,
|
||||
tickProviderSpec: tickProviderSpec,
|
||||
tickFormatterSpec: tickFormatterSpec,
|
||||
showAxisLine: showAxisLine);
|
||||
|
||||
@override
|
||||
configure(Axis<String> axis, ChartContext context,
|
||||
GraphicsFactory graphicsFactory) {
|
||||
super.configure(axis, context, graphicsFactory);
|
||||
|
||||
if (axis is OrdinalAxis && viewport != null) {
|
||||
axis.setScaleViewport(viewport);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
OrdinalAxis createAxis() => new OrdinalAxis();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is OrdinalAxisSpec &&
|
||||
viewport == other.viewport &&
|
||||
super == (other));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hashcode = super.hashCode;
|
||||
hashcode = (hashcode * 37) + viewport.hashCode;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OrdinalTickProviderSpec extends TickProviderSpec<String> {}
|
||||
|
||||
abstract class OrdinalTickFormatterSpec extends TickFormatterSpec<String> {}
|
||||
|
||||
@immutable
|
||||
class BasicOrdinalTickProviderSpec implements OrdinalTickProviderSpec {
|
||||
const BasicOrdinalTickProviderSpec();
|
||||
|
||||
@override
|
||||
OrdinalTickProvider createTickProvider(ChartContext context) =>
|
||||
new OrdinalTickProvider();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is BasicOrdinalTickProviderSpec;
|
||||
|
||||
@override
|
||||
int get hashCode => 37;
|
||||
}
|
||||
|
||||
/// [TickProviderSpec] that allows you to specific the ticks to be used.
|
||||
@immutable
|
||||
class StaticOrdinalTickProviderSpec implements OrdinalTickProviderSpec {
|
||||
final List<TickSpec<String>> tickSpecs;
|
||||
|
||||
const StaticOrdinalTickProviderSpec(this.tickSpecs);
|
||||
|
||||
@override
|
||||
StaticTickProvider<String> createTickProvider(ChartContext context) =>
|
||||
new StaticTickProvider<String>(tickSpecs);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is StaticOrdinalTickProviderSpec && tickSpecs == other.tickSpecs);
|
||||
|
||||
@override
|
||||
int get hashCode => tickSpecs.hashCode;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class BasicOrdinalTickFormatterSpec implements OrdinalTickFormatterSpec {
|
||||
const BasicOrdinalTickFormatterSpec();
|
||||
|
||||
@override
|
||||
OrdinalTickFormatter createTickFormatter(ChartContext context) =>
|
||||
new OrdinalTickFormatter();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is BasicOrdinalTickFormatterSpec;
|
||||
|
||||
@override
|
||||
int get hashCode => 37;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../numeric_extents.dart' show NumericExtents;
|
||||
import 'axis_spec.dart' show AxisSpec, RenderSpec;
|
||||
import 'numeric_axis_spec.dart'
|
||||
show
|
||||
BasicNumericTickFormatterSpec,
|
||||
BasicNumericTickProviderSpec,
|
||||
NumericAxisSpec,
|
||||
NumericTickProviderSpec,
|
||||
NumericTickFormatterSpec;
|
||||
|
||||
/// Convenience [AxisSpec] specialized for numeric percentage axes.
|
||||
@immutable
|
||||
class PercentAxisSpec extends NumericAxisSpec {
|
||||
/// Creates a [NumericAxisSpec] that is specialized for percentage data.
|
||||
PercentAxisSpec({
|
||||
RenderSpec<num> renderSpec,
|
||||
NumericTickProviderSpec tickProviderSpec,
|
||||
NumericTickFormatterSpec tickFormatterSpec,
|
||||
bool showAxisLine,
|
||||
NumericExtents viewport,
|
||||
}) : super(
|
||||
renderSpec: renderSpec,
|
||||
tickProviderSpec: tickProviderSpec ??
|
||||
const BasicNumericTickProviderSpec(dataIsInWholeNumbers: false),
|
||||
tickFormatterSpec: tickFormatterSpec ??
|
||||
new BasicNumericTickFormatterSpec.fromNumberFormat(
|
||||
new NumberFormat.percentPattern()),
|
||||
showAxisLine: showAxisLine,
|
||||
viewport: viewport ?? const NumericExtents(0.0, 1.0));
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is PercentAxisSpec &&
|
||||
viewport == other.viewport &&
|
||||
super == (other);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'axis_spec.dart' show TextStyleSpec;
|
||||
|
||||
/// Definition for a tick.
|
||||
///
|
||||
/// Used to define a tick that is used by static tick provider.
|
||||
class TickSpec<D> {
|
||||
final D value;
|
||||
final String label;
|
||||
final TextStyleSpec style;
|
||||
|
||||
/// [value] the value of this tick
|
||||
/// [label] optional label for this tick. If not set, uses the tick formatter
|
||||
/// of the axis.
|
||||
/// [style] optional style for this tick. If not set, uses the style of the
|
||||
/// axis.
|
||||
const TickSpec(this.value, {this.label, this.style});
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../common/chart_context.dart' show ChartContext;
|
||||
import 'axis.dart' show AxisOrientation;
|
||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import 'numeric_scale.dart' show NumericScale;
|
||||
import 'scale.dart' show MutableScale;
|
||||
import 'spec/tick_spec.dart' show TickSpec;
|
||||
import 'tick.dart' show Tick;
|
||||
import 'tick_formatter.dart' show TickFormatter;
|
||||
import 'tick_provider.dart' show TickProvider, TickHint;
|
||||
import 'time/date_time_scale.dart' show DateTimeScale;
|
||||
|
||||
/// A strategy that uses the ticks provided and only assigns positioning.
|
||||
///
|
||||
/// The [TextStyle] is not overridden during tick draw strategy decorateTicks.
|
||||
/// If it is null, then the default is used.
|
||||
class StaticTickProvider<D> extends TickProvider<D> {
|
||||
final List<TickSpec<D>> tickSpec;
|
||||
|
||||
StaticTickProvider(this.tickSpec);
|
||||
|
||||
@override
|
||||
List<Tick<D>> getTicks({
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required MutableScale<D> scale,
|
||||
@required TickFormatter<D> formatter,
|
||||
@required Map<D, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
@required AxisOrientation orientation,
|
||||
bool viewportExtensionEnabled = false,
|
||||
TickHint<D> tickHint,
|
||||
}) {
|
||||
final ticks = <Tick<D>>[];
|
||||
|
||||
bool allTicksHaveLabels = true;
|
||||
|
||||
for (TickSpec<D> spec in tickSpec) {
|
||||
// When static ticks are being used with a numeric axis, extend the axis
|
||||
// with the values specified.
|
||||
if (scale is NumericScale || scale is DateTimeScale) {
|
||||
scale.addDomain(spec.value);
|
||||
}
|
||||
|
||||
// Save off whether all ticks have labels.
|
||||
allTicksHaveLabels = allTicksHaveLabels && (spec.label != null);
|
||||
}
|
||||
|
||||
// Use the formatter's label if the tick spec does not provide one.
|
||||
List<String> formattedValues;
|
||||
if (allTicksHaveLabels == false) {
|
||||
formattedValues = formatter.format(
|
||||
tickSpec.map((spec) => spec.value).toList(), formatterValueCache,
|
||||
stepSize: scale.domainStepSize);
|
||||
}
|
||||
|
||||
for (var i = 0; i < tickSpec.length; i++) {
|
||||
final spec = tickSpec[i];
|
||||
// We still check if the spec is within the viewport because we do not
|
||||
// extend the axis for OrdinalScale.
|
||||
if (scale.compareDomainValueToViewport(spec.value) == 0) {
|
||||
final tick = new Tick<D>(
|
||||
value: spec.value,
|
||||
textElement: graphicsFactory
|
||||
.createTextElement(spec.label ?? formattedValues[i]),
|
||||
locationPx: scale[spec.value]);
|
||||
if (spec.style != null) {
|
||||
tick.textElement.textStyle = graphicsFactory.createTextPaint()
|
||||
..fontFamily = spec.style.fontFamily
|
||||
..fontSize = spec.style.fontSize
|
||||
..color = spec.style.color;
|
||||
}
|
||||
ticks.add(tick);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow draw strategy to decorate the ticks.
|
||||
tickDrawStrategy.decorateTicks(ticks);
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is StaticTickProvider && tickSpec == other.tickSpec;
|
||||
|
||||
@override
|
||||
int get hashCode => tickSpec.hashCode;
|
||||
}
|
||||
47
web/charts/common/lib/src/chart/cartesian/axis/tick.dart
Normal file
47
web/charts/common/lib/src/chart/cartesian/axis/tick.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import '../../../common/text_element.dart';
|
||||
|
||||
/// A labeled point on an axis.
|
||||
///
|
||||
/// [D] is the type of the value this tick is associated with.
|
||||
class Tick<D> {
|
||||
/// The value that this tick represents
|
||||
final D value;
|
||||
|
||||
/// [TextElement] for this tick.
|
||||
TextElement textElement;
|
||||
|
||||
/// Location on the axis where this tick is rendered (in canvas coordinates).
|
||||
double locationPx;
|
||||
|
||||
/// Offset of the label for this tick from its location.
|
||||
///
|
||||
/// This is a vertical offset for ticks on a vertical axis, or horizontal
|
||||
/// offset for ticks on a horizontal axis.
|
||||
double labelOffsetPx;
|
||||
|
||||
Tick(
|
||||
{@required this.value,
|
||||
@required this.textElement,
|
||||
this.locationPx,
|
||||
this.labelOffsetPx});
|
||||
|
||||
@override
|
||||
String toString() => 'Tick(value: $value, locationPx: $locationPx, '
|
||||
'labelOffsetPx: $labelOffsetPx)';
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../common/datum_details.dart' show MeasureFormatter;
|
||||
|
||||
// TODO: Break out into separate files.
|
||||
|
||||
/// A strategy used for converting domain values of the ticks into Strings.
|
||||
///
|
||||
/// [D] is the domain type.
|
||||
abstract class TickFormatter<D> {
|
||||
const TickFormatter();
|
||||
|
||||
/// Formats a list of tick values.
|
||||
List<String> format(List<D> tickValues, Map<D, String> cache, {num stepSize});
|
||||
}
|
||||
|
||||
abstract class SimpleTickFormatterBase<D> implements TickFormatter<D> {
|
||||
const SimpleTickFormatterBase();
|
||||
|
||||
@override
|
||||
List<String> format(List<D> tickValues, Map<D, String> cache,
|
||||
{num stepSize}) =>
|
||||
tickValues.map((D value) {
|
||||
// Try to use the cached formats first.
|
||||
String formattedString = cache[value];
|
||||
if (formattedString == null) {
|
||||
formattedString = formatValue(value);
|
||||
cache[value] = formattedString;
|
||||
}
|
||||
return formattedString;
|
||||
}).toList();
|
||||
|
||||
/// Formats a single tick value.
|
||||
String formatValue(D value);
|
||||
}
|
||||
|
||||
/// A strategy that converts tick labels using toString().
|
||||
class OrdinalTickFormatter extends SimpleTickFormatterBase<String> {
|
||||
const OrdinalTickFormatter();
|
||||
|
||||
@override
|
||||
String formatValue(String value) => value;
|
||||
|
||||
@override
|
||||
bool operator ==(other) => other is OrdinalTickFormatter;
|
||||
|
||||
@override
|
||||
int get hashCode => 31;
|
||||
}
|
||||
|
||||
/// A strategy for formatting the labels on numeric ticks using [NumberFormat].
|
||||
///
|
||||
/// The default format is [NumberFormat.decimalPattern].
|
||||
class NumericTickFormatter extends SimpleTickFormatterBase<num> {
|
||||
final MeasureFormatter formatter;
|
||||
|
||||
NumericTickFormatter._internal(this.formatter);
|
||||
|
||||
/// Construct a a new [NumericTickFormatter].
|
||||
///
|
||||
/// [formatter] optionally specify a formatter to be used. Defaults to using
|
||||
/// [NumberFormat.decimalPattern] if none is specified.
|
||||
factory NumericTickFormatter({MeasureFormatter formatter}) {
|
||||
formatter ??= _getFormatter(new NumberFormat.decimalPattern());
|
||||
return new NumericTickFormatter._internal(formatter);
|
||||
}
|
||||
|
||||
/// Constructs a new [NumericTickFormatter] that formats using [numberFormat].
|
||||
factory NumericTickFormatter.fromNumberFormat(NumberFormat numberFormat) {
|
||||
return new NumericTickFormatter._internal(_getFormatter(numberFormat));
|
||||
}
|
||||
|
||||
/// Constructs a new formatter that uses [NumberFormat.compactCurrency].
|
||||
factory NumericTickFormatter.compactSimpleCurrency() {
|
||||
return new NumericTickFormatter._internal(
|
||||
_getFormatter(new NumberFormat.compactCurrency()));
|
||||
}
|
||||
|
||||
/// Returns a [MeasureFormatter] that calls format on [numberFormat].
|
||||
static MeasureFormatter _getFormatter(NumberFormat numberFormat) {
|
||||
return (num value) => numberFormat.format(value);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatValue(num value) => formatter(value);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is NumericTickFormatter && formatter == other.formatter;
|
||||
|
||||
@override
|
||||
int get hashCode => formatter.hashCode;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../common/chart_context.dart' show ChartContext;
|
||||
import 'axis.dart' show AxisOrientation;
|
||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import 'scale.dart' show MutableScale;
|
||||
import 'tick.dart' show Tick;
|
||||
import 'tick_formatter.dart' show TickFormatter;
|
||||
|
||||
/// A strategy for selecting values for axis ticks based on the domain values.
|
||||
///
|
||||
/// [D] is the domain type.
|
||||
abstract class TickProvider<D> {
|
||||
/// Returns a list of ticks in value order that should be displayed.
|
||||
///
|
||||
/// This method should not return null. If no ticks are desired an empty list
|
||||
/// should be returned.
|
||||
///
|
||||
/// [graphicsFactory] The graphics factory used for text measurement.
|
||||
/// [scale] The scale of the data.
|
||||
/// [formatter] The formatter to use for generating tick labels.
|
||||
/// [orientation] Orientation of this axis ticks.
|
||||
/// [tickDrawStrategy] Draw strategy for ticks.
|
||||
/// [viewportExtensionEnabled] allow extending the viewport for 'niced' ticks.
|
||||
/// [tickHint] tick values for provider to calculate a desired tick range.
|
||||
List<Tick<D>> getTicks({
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required covariant MutableScale<D> scale,
|
||||
@required TickFormatter<D> formatter,
|
||||
@required Map<D, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
@required AxisOrientation orientation,
|
||||
bool viewportExtensionEnabled = false,
|
||||
TickHint<D> tickHint,
|
||||
});
|
||||
}
|
||||
|
||||
/// A base tick provider.
|
||||
abstract class BaseTickProvider<D> implements TickProvider<D> {
|
||||
const BaseTickProvider();
|
||||
|
||||
/// Create ticks from [domainValues].
|
||||
List<Tick<D>> createTicks(
|
||||
List<D> domainValues, {
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required MutableScale<D> scale,
|
||||
@required TickFormatter<D> formatter,
|
||||
@required Map<D, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
num stepSize,
|
||||
}) {
|
||||
final ticks = <Tick<D>>[];
|
||||
final labels =
|
||||
formatter.format(domainValues, formatterValueCache, stepSize: stepSize);
|
||||
|
||||
for (var i = 0; i < domainValues.length; i++) {
|
||||
final value = domainValues[i];
|
||||
final tick = new Tick(
|
||||
value: value,
|
||||
textElement: graphicsFactory.createTextElement(labels[i]),
|
||||
locationPx: scale[value]);
|
||||
|
||||
ticks.add(tick);
|
||||
}
|
||||
|
||||
// Allow draw strategy to decorate the ticks.
|
||||
tickDrawStrategy.decorateTicks(ticks);
|
||||
|
||||
return ticks;
|
||||
}
|
||||
}
|
||||
|
||||
/// A hint for the tick provider to determine step size and tick count.
|
||||
class TickHint<D> {
|
||||
/// The starting hint tick value.
|
||||
final D start;
|
||||
|
||||
/// The ending hint tick value.
|
||||
final D end;
|
||||
|
||||
/// Number of ticks.
|
||||
final int tickCount;
|
||||
|
||||
TickHint(this.start, this.end, {this.tickCount});
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../axis.dart' show AxisOrientation;
|
||||
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import '../tick.dart' show Tick;
|
||||
import '../tick_formatter.dart' show TickFormatter;
|
||||
import '../tick_provider.dart' show TickProvider, TickHint;
|
||||
import 'date_time_scale.dart' show DateTimeScale;
|
||||
import 'day_time_stepper.dart' show DayTimeStepper;
|
||||
import 'hour_time_stepper.dart' show HourTimeStepper;
|
||||
import 'minute_time_stepper.dart' show MinuteTimeStepper;
|
||||
import 'month_time_stepper.dart' show MonthTimeStepper;
|
||||
import 'time_range_tick_provider.dart' show TimeRangeTickProvider;
|
||||
import 'time_range_tick_provider_impl.dart' show TimeRangeTickProviderImpl;
|
||||
import 'year_time_stepper.dart' show YearTimeStepper;
|
||||
|
||||
/// Tick provider for date and time.
|
||||
///
|
||||
/// When determining the ticks for a given domain, the provider will use choose
|
||||
/// one of the internal tick providers appropriate to the size of the data's
|
||||
/// domain range. It does this in an attempt to ensure there are at least 3
|
||||
/// ticks, before jumping to the next more fine grain provider. The 3 tick
|
||||
/// minimum is not a hard rule as some of the ticks might be eliminated because
|
||||
/// of collisions, but the data was within the targeted range.
|
||||
///
|
||||
/// Once a tick provider is chosen the selection of ticks is done by the child
|
||||
/// tick provider.
|
||||
class AutoAdjustingDateTimeTickProvider implements TickProvider<DateTime> {
|
||||
/// List of tick providers to be selected from.
|
||||
final List<TimeRangeTickProvider> _potentialTickProviders;
|
||||
|
||||
AutoAdjustingDateTimeTickProvider._internal(
|
||||
List<TimeRangeTickProvider> tickProviders)
|
||||
: _potentialTickProviders = tickProviders;
|
||||
|
||||
/// Creates a default [AutoAdjustingDateTimeTickProvider] for day and time.
|
||||
factory AutoAdjustingDateTimeTickProvider.createDefault(
|
||||
DateTimeFactory dateTimeFactory) {
|
||||
return new AutoAdjustingDateTimeTickProvider._internal([
|
||||
createYearTickProvider(dateTimeFactory),
|
||||
createMonthTickProvider(dateTimeFactory),
|
||||
createDayTickProvider(dateTimeFactory),
|
||||
createHourTickProvider(dateTimeFactory),
|
||||
createMinuteTickProvider(dateTimeFactory)
|
||||
]);
|
||||
}
|
||||
|
||||
/// Creates a default [AutoAdjustingDateTimeTickProvider] for day only.
|
||||
factory AutoAdjustingDateTimeTickProvider.createWithoutTime(
|
||||
DateTimeFactory dateTimeFactory) {
|
||||
return new AutoAdjustingDateTimeTickProvider._internal([
|
||||
createYearTickProvider(dateTimeFactory),
|
||||
createMonthTickProvider(dateTimeFactory),
|
||||
createDayTickProvider(dateTimeFactory)
|
||||
]);
|
||||
}
|
||||
|
||||
/// Creates [AutoAdjustingDateTimeTickProvider] with custom tick providers.
|
||||
///
|
||||
/// [potentialTickProviders] must have at least one [TimeRangeTickProvider]
|
||||
/// and this list of tick providers are used in the order they are provided.
|
||||
factory AutoAdjustingDateTimeTickProvider.createWith(
|
||||
List<TimeRangeTickProvider> potentialTickProviders) {
|
||||
if (potentialTickProviders == null || potentialTickProviders.isEmpty) {
|
||||
throw new ArgumentError('At least one TimeRangeTickProvider is required');
|
||||
}
|
||||
|
||||
return new AutoAdjustingDateTimeTickProvider._internal(
|
||||
potentialTickProviders);
|
||||
}
|
||||
|
||||
/// Generates a list of ticks for the given data which should not collide
|
||||
/// unless the range is not large enough.
|
||||
@override
|
||||
List<Tick<DateTime>> getTicks({
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required DateTimeScale scale,
|
||||
@required TickFormatter<DateTime> formatter,
|
||||
@required Map<DateTime, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
@required AxisOrientation orientation,
|
||||
bool viewportExtensionEnabled = false,
|
||||
TickHint<DateTime> tickHint,
|
||||
}) {
|
||||
List<TimeRangeTickProvider> tickProviders;
|
||||
|
||||
/// If tick hint is provided, use the closest tick provider, otherwise
|
||||
/// look through the tick providers for one that provides sufficient ticks
|
||||
/// for the viewport.
|
||||
if (tickHint != null) {
|
||||
tickProviders = [_getClosestTickProvider(tickHint)];
|
||||
} else {
|
||||
tickProviders = _potentialTickProviders;
|
||||
}
|
||||
|
||||
final lastTickProvider = tickProviders.last;
|
||||
|
||||
final viewport = scale.viewportDomain;
|
||||
for (final tickProvider in tickProviders) {
|
||||
final isLastProvider = (tickProvider == lastTickProvider);
|
||||
if (isLastProvider ||
|
||||
tickProvider.providesSufficientTicksForRange(viewport)) {
|
||||
return tickProvider.getTicks(
|
||||
context: context,
|
||||
graphicsFactory: graphicsFactory,
|
||||
scale: scale,
|
||||
formatter: formatter,
|
||||
formatterValueCache: formatterValueCache,
|
||||
tickDrawStrategy: tickDrawStrategy,
|
||||
orientation: orientation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <Tick<DateTime>>[];
|
||||
}
|
||||
|
||||
/// Find the closest tick provider based on the tick hint.
|
||||
TimeRangeTickProvider _getClosestTickProvider(TickHint<DateTime> tickHint) {
|
||||
final stepSize = ((tickHint.end.difference(tickHint.start).inMilliseconds) /
|
||||
(tickHint.tickCount - 1))
|
||||
.round();
|
||||
|
||||
int minDifference;
|
||||
TimeRangeTickProvider closestTickProvider;
|
||||
|
||||
for (final tickProvider in _potentialTickProviders) {
|
||||
final difference =
|
||||
(stepSize - tickProvider.getClosestStepSize(stepSize)).abs();
|
||||
if (minDifference == null || minDifference > difference) {
|
||||
minDifference = difference;
|
||||
closestTickProvider = tickProvider;
|
||||
}
|
||||
}
|
||||
|
||||
return closestTickProvider;
|
||||
}
|
||||
|
||||
static TimeRangeTickProvider createYearTickProvider(
|
||||
DateTimeFactory dateTimeFactory) =>
|
||||
new TimeRangeTickProviderImpl(new YearTimeStepper(dateTimeFactory));
|
||||
|
||||
static TimeRangeTickProvider createMonthTickProvider(
|
||||
DateTimeFactory dateTimeFactory) =>
|
||||
new TimeRangeTickProviderImpl(new MonthTimeStepper(dateTimeFactory));
|
||||
|
||||
static TimeRangeTickProvider createDayTickProvider(
|
||||
DateTimeFactory dateTimeFactory) =>
|
||||
new TimeRangeTickProviderImpl(new DayTimeStepper(dateTimeFactory));
|
||||
|
||||
static TimeRangeTickProvider createHourTickProvider(
|
||||
DateTimeFactory dateTimeFactory) =>
|
||||
new TimeRangeTickProviderImpl(new HourTimeStepper(dateTimeFactory));
|
||||
|
||||
static TimeRangeTickProvider createMinuteTickProvider(
|
||||
DateTimeFactory dateTimeFactory) =>
|
||||
new TimeRangeTickProviderImpl(new MinuteTimeStepper(dateTimeFactory));
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../../../../common/date_time_factory.dart';
|
||||
import 'date_time_extents.dart' show DateTimeExtents;
|
||||
import 'time_stepper.dart'
|
||||
show TimeStepper, TimeStepIteratorFactory, TimeStepIterator;
|
||||
|
||||
/// A base stepper for operating with DateTimeFactory and time range steps.
|
||||
abstract class BaseTimeStepper implements TimeStepper {
|
||||
/// The factory to generate a DateTime object.
|
||||
///
|
||||
/// This is needed because Dart's DateTime does not handle time zone.
|
||||
/// There is a time zone aware library that we could use that implements the
|
||||
/// DateTime interface.
|
||||
final DateTimeFactory dateTimeFactory;
|
||||
|
||||
_TimeStepIteratorFactoryImpl _stepsIterable;
|
||||
|
||||
BaseTimeStepper(this.dateTimeFactory);
|
||||
|
||||
/// Get the step time before or on the given [time] from [tickIncrement].
|
||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement);
|
||||
|
||||
/// Get the next step time after [time] from [tickIncrement].
|
||||
DateTime getNextStepTime(DateTime time, int tickIncrement);
|
||||
|
||||
@override
|
||||
int getStepCountBetween(DateTimeExtents timeExtent, int tickIncrement) {
|
||||
checkTickIncrement(tickIncrement);
|
||||
final min = timeExtent.start;
|
||||
final max = timeExtent.end;
|
||||
var time = getStepTimeAfterInclusive(min, tickIncrement);
|
||||
|
||||
var cnt = 0;
|
||||
while (time.compareTo(max) <= 0) {
|
||||
cnt++;
|
||||
time = getNextStepTime(time, tickIncrement);
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
@override
|
||||
TimeStepIteratorFactory getSteps(DateTimeExtents timeExtent) {
|
||||
// Keep the steps iterable unless time extent changes, so the same iterator
|
||||
// can be used and reset for different increments.
|
||||
if (_stepsIterable == null || _stepsIterable.timeExtent != timeExtent) {
|
||||
_stepsIterable = new _TimeStepIteratorFactoryImpl(timeExtent, this);
|
||||
}
|
||||
return _stepsIterable;
|
||||
}
|
||||
|
||||
@override
|
||||
DateTimeExtents updateBoundingSteps(DateTimeExtents timeExtent) {
|
||||
final stepBefore = getStepTimeBeforeInclusive(timeExtent.start, 1);
|
||||
final stepAfter = getStepTimeAfterInclusive(timeExtent.end, 1);
|
||||
|
||||
return new DateTimeExtents(start: stepBefore, end: stepAfter);
|
||||
}
|
||||
|
||||
DateTime getStepTimeAfterInclusive(DateTime time, int tickIncrement) {
|
||||
final boundedStart = getStepTimeBeforeInclusive(time, tickIncrement);
|
||||
if (boundedStart == time) {
|
||||
return boundedStart;
|
||||
}
|
||||
return getNextStepTime(boundedStart, tickIncrement);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimeStepIteratorImpl implements TimeStepIterator {
|
||||
final DateTime extentStartTime;
|
||||
final DateTime extentEndTime;
|
||||
final BaseTimeStepper stepper;
|
||||
DateTime _current;
|
||||
int _tickIncrement = 1;
|
||||
|
||||
_TimeStepIteratorImpl(
|
||||
this.extentStartTime, this.extentEndTime, this.stepper) {
|
||||
reset(_tickIncrement);
|
||||
}
|
||||
|
||||
@override
|
||||
bool moveNext() {
|
||||
if (_current == null) {
|
||||
_current =
|
||||
stepper.getStepTimeAfterInclusive(extentStartTime, _tickIncrement);
|
||||
} else {
|
||||
_current = stepper.getNextStepTime(_current, _tickIncrement);
|
||||
}
|
||||
|
||||
return _current.compareTo(extentEndTime) <= 0;
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime get current => _current;
|
||||
|
||||
@override
|
||||
TimeStepIterator reset(int tickIncrement) {
|
||||
checkTickIncrement(tickIncrement);
|
||||
_tickIncrement = tickIncrement;
|
||||
_current = null;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class _TimeStepIteratorFactoryImpl extends TimeStepIteratorFactory {
|
||||
final DateTimeExtents timeExtent;
|
||||
final _TimeStepIteratorImpl _timeStepIterator;
|
||||
|
||||
_TimeStepIteratorFactoryImpl._internal(
|
||||
_TimeStepIteratorImpl timeStepIterator, this.timeExtent)
|
||||
: _timeStepIterator = timeStepIterator;
|
||||
|
||||
factory _TimeStepIteratorFactoryImpl(
|
||||
DateTimeExtents timeExtent, BaseTimeStepper stepper) {
|
||||
final startTime = timeExtent.start;
|
||||
final endTime = timeExtent.end;
|
||||
return new _TimeStepIteratorFactoryImpl._internal(
|
||||
new _TimeStepIteratorImpl(startTime, endTime, stepper), timeExtent);
|
||||
}
|
||||
|
||||
@override
|
||||
TimeStepIterator get iterator => _timeStepIterator;
|
||||
}
|
||||
|
||||
void checkTickIncrement(int tickIncrement) {
|
||||
/// tickIncrement must be greater than 0
|
||||
assert(tickIncrement > 0);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import '../axis.dart' show Axis;
|
||||
import '../tick_formatter.dart' show TickFormatter;
|
||||
import '../tick_provider.dart' show TickProvider;
|
||||
import 'auto_adjusting_date_time_tick_provider.dart'
|
||||
show AutoAdjustingDateTimeTickProvider;
|
||||
import 'date_time_extents.dart' show DateTimeExtents;
|
||||
import 'date_time_scale.dart' show DateTimeScale;
|
||||
import 'date_time_tick_formatter.dart' show DateTimeTickFormatter;
|
||||
|
||||
class DateTimeAxis extends Axis<DateTime> {
|
||||
DateTimeAxis(DateTimeFactory dateTimeFactory,
|
||||
{TickProvider tickProvider, TickFormatter tickFormatter})
|
||||
: super(
|
||||
tickProvider: tickProvider ??
|
||||
new AutoAdjustingDateTimeTickProvider.createDefault(
|
||||
dateTimeFactory),
|
||||
tickFormatter:
|
||||
tickFormatter ?? new DateTimeTickFormatter(dateTimeFactory),
|
||||
scale: new DateTimeScale(dateTimeFactory),
|
||||
);
|
||||
|
||||
void setScaleViewport(DateTimeExtents viewport) {
|
||||
(mutableScale as DateTimeScale).viewportDomain = viewport;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../scale.dart' show Extents;
|
||||
|
||||
class DateTimeExtents extends Extents<DateTime> {
|
||||
final DateTime start;
|
||||
final DateTime end;
|
||||
|
||||
DateTimeExtents({@required this.start, @required this.end});
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
return other is DateTimeExtents && start == other.start && end == other.end;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => (start.hashCode + (end.hashCode * 37));
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import '../linear/linear_scale.dart' show LinearScale;
|
||||
import '../numeric_extents.dart' show NumericExtents;
|
||||
import '../scale.dart'
|
||||
show MutableScale, StepSizeConfig, RangeBandConfig, ScaleOutputExtent;
|
||||
import 'date_time_extents.dart' show DateTimeExtents;
|
||||
|
||||
/// [DateTimeScale] is a wrapper for [LinearScale].
|
||||
/// [DateTime] values are converted to millisecondsSinceEpoch and passed to the
|
||||
/// [LinearScale].
|
||||
class DateTimeScale extends MutableScale<DateTime> {
|
||||
final DateTimeFactory dateTimeFactory;
|
||||
final LinearScale _linearScale;
|
||||
|
||||
DateTimeScale(this.dateTimeFactory) : _linearScale = new LinearScale();
|
||||
|
||||
DateTimeScale._copy(DateTimeScale other)
|
||||
: dateTimeFactory = other.dateTimeFactory,
|
||||
_linearScale = other._linearScale.copy();
|
||||
|
||||
@override
|
||||
num operator [](DateTime domainValue) =>
|
||||
_linearScale[domainValue.millisecondsSinceEpoch];
|
||||
|
||||
@override
|
||||
DateTime reverse(double pixelLocation) =>
|
||||
dateTimeFactory.createDateTimeFromMilliSecondsSinceEpoch(
|
||||
_linearScale.reverse(pixelLocation).round());
|
||||
|
||||
@override
|
||||
void resetDomain() {
|
||||
_linearScale.resetDomain();
|
||||
}
|
||||
|
||||
@override
|
||||
set stepSizeConfig(StepSizeConfig config) {
|
||||
_linearScale.stepSizeConfig = config;
|
||||
}
|
||||
|
||||
@override
|
||||
StepSizeConfig get stepSizeConfig => _linearScale.stepSizeConfig;
|
||||
|
||||
@override
|
||||
set rangeBandConfig(RangeBandConfig barGroupWidthConfig) {
|
||||
_linearScale.rangeBandConfig = barGroupWidthConfig;
|
||||
}
|
||||
|
||||
@override
|
||||
void setViewportSettings(double viewportScale, double viewportTranslatePx) {
|
||||
_linearScale.setViewportSettings(viewportScale, viewportTranslatePx);
|
||||
}
|
||||
|
||||
@override
|
||||
set range(ScaleOutputExtent extent) {
|
||||
_linearScale.range = extent;
|
||||
}
|
||||
|
||||
@override
|
||||
void addDomain(DateTime domainValue) {
|
||||
_linearScale.addDomain(domainValue.millisecondsSinceEpoch);
|
||||
}
|
||||
|
||||
@override
|
||||
void resetViewportSettings() {
|
||||
_linearScale.resetViewportSettings();
|
||||
}
|
||||
|
||||
DateTimeExtents get viewportDomain {
|
||||
final extents = _linearScale.viewportDomain;
|
||||
return new DateTimeExtents(
|
||||
start: dateTimeFactory
|
||||
.createDateTimeFromMilliSecondsSinceEpoch(extents.min.toInt()),
|
||||
end: dateTimeFactory
|
||||
.createDateTimeFromMilliSecondsSinceEpoch(extents.max.toInt()));
|
||||
}
|
||||
|
||||
set viewportDomain(DateTimeExtents extents) {
|
||||
_linearScale.viewportDomain = new NumericExtents(
|
||||
extents.start.millisecondsSinceEpoch,
|
||||
extents.end.millisecondsSinceEpoch);
|
||||
}
|
||||
|
||||
@override
|
||||
DateTimeScale copy() => new DateTimeScale._copy(this);
|
||||
|
||||
@override
|
||||
double get viewportTranslatePx => _linearScale.viewportTranslatePx;
|
||||
|
||||
@override
|
||||
double get viewportScalingFactor => _linearScale.viewportScalingFactor;
|
||||
|
||||
@override
|
||||
bool isRangeValueWithinViewport(double rangeValue) =>
|
||||
_linearScale.isRangeValueWithinViewport(rangeValue);
|
||||
|
||||
@override
|
||||
int compareDomainValueToViewport(DateTime domainValue) => _linearScale
|
||||
.compareDomainValueToViewport(domainValue.millisecondsSinceEpoch);
|
||||
|
||||
@override
|
||||
double get rangeBand => _linearScale.rangeBand;
|
||||
|
||||
@override
|
||||
double get stepSize => _linearScale.stepSize;
|
||||
|
||||
@override
|
||||
double get domainStepSize => _linearScale.domainStepSize;
|
||||
|
||||
@override
|
||||
RangeBandConfig get rangeBandConfig => _linearScale.rangeBandConfig;
|
||||
|
||||
@override
|
||||
int get rangeWidth => _linearScale.rangeWidth;
|
||||
|
||||
@override
|
||||
ScaleOutputExtent get range => _linearScale.range;
|
||||
|
||||
@override
|
||||
bool canTranslate(DateTime domainValue) =>
|
||||
_linearScale.canTranslate(domainValue.millisecondsSinceEpoch);
|
||||
|
||||
NumericExtents get dataExtent => _linearScale.dataExtent;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import '../tick_formatter.dart' show TickFormatter;
|
||||
import 'hour_tick_formatter.dart' show HourTickFormatter;
|
||||
import 'time_tick_formatter.dart' show TimeTickFormatter;
|
||||
import 'time_tick_formatter_impl.dart'
|
||||
show CalendarField, TimeTickFormatterImpl;
|
||||
|
||||
/// A [TickFormatter] that formats date/time values based on minimum difference
|
||||
/// between subsequent ticks.
|
||||
///
|
||||
/// This formatter assumes that the Tick values passed in are sorted in
|
||||
/// increasing order.
|
||||
///
|
||||
/// This class is setup with a list of formatters that format the input ticks at
|
||||
/// a given time resolution. The time resolution which will accurately display
|
||||
/// the difference between 2 subsequent ticks is picked. Each time resolution
|
||||
/// can be setup with a [TimeTickFormatter], which is used to format ticks as
|
||||
/// regular or transition ticks based on whether the tick has crossed the time
|
||||
/// boundary defined in the [TimeTickFormatter].
|
||||
class DateTimeTickFormatter implements TickFormatter<DateTime> {
|
||||
static const int SECOND = 1000;
|
||||
static const int MINUTE = 60 * SECOND;
|
||||
static const int HOUR = 60 * MINUTE;
|
||||
static const int DAY = 24 * HOUR;
|
||||
|
||||
/// Used for the case when there is only one formatter.
|
||||
static const int ANY = -1;
|
||||
|
||||
final Map<int, TimeTickFormatter> _timeFormatters;
|
||||
|
||||
/// Creates a [DateTimeTickFormatter] that works well with time tick provider
|
||||
/// classes.
|
||||
///
|
||||
/// The default formatter makes assumptions on border cases that time tick
|
||||
/// providers will still provide ticks that make sense. Example: Tick provider
|
||||
/// does not provide ticks with 23 hour intervals. For custom tick providers
|
||||
/// where these assumptions are not correct, please create a custom
|
||||
/// [TickFormatter].
|
||||
factory DateTimeTickFormatter(DateTimeFactory dateTimeFactory,
|
||||
{Map<int, TimeTickFormatter> overrides}) {
|
||||
final Map<int, TimeTickFormatter> map = {
|
||||
MINUTE: new TimeTickFormatterImpl(
|
||||
dateTimeFactory: dateTimeFactory,
|
||||
simpleFormat: 'mm',
|
||||
transitionFormat: 'h mm',
|
||||
transitionField: CalendarField.hourOfDay),
|
||||
HOUR: new HourTickFormatter(
|
||||
dateTimeFactory: dateTimeFactory,
|
||||
simpleFormat: 'h',
|
||||
transitionFormat: 'MMM d ha',
|
||||
noonFormat: 'ha'),
|
||||
23 * HOUR: new TimeTickFormatterImpl(
|
||||
dateTimeFactory: dateTimeFactory,
|
||||
simpleFormat: 'd',
|
||||
transitionFormat: 'MMM d',
|
||||
transitionField: CalendarField.month),
|
||||
28 * DAY: new TimeTickFormatterImpl(
|
||||
dateTimeFactory: dateTimeFactory,
|
||||
simpleFormat: 'MMM',
|
||||
transitionFormat: 'MMM yyyy',
|
||||
transitionField: CalendarField.year),
|
||||
364 * DAY: new TimeTickFormatterImpl(
|
||||
dateTimeFactory: dateTimeFactory,
|
||||
simpleFormat: 'yyyy',
|
||||
transitionFormat: 'yyyy',
|
||||
transitionField: CalendarField.year),
|
||||
};
|
||||
|
||||
// Allow the user to override some of the defaults.
|
||||
if (overrides != null) {
|
||||
map.addAll(overrides);
|
||||
}
|
||||
|
||||
return new DateTimeTickFormatter._internal(map);
|
||||
}
|
||||
|
||||
/// Creates a [DateTimeTickFormatter] without the time component.
|
||||
factory DateTimeTickFormatter.withoutTime(DateTimeFactory dateTimeFactory) {
|
||||
return new DateTimeTickFormatter._internal({
|
||||
23 * HOUR: new TimeTickFormatterImpl(
|
||||
dateTimeFactory: dateTimeFactory,
|
||||
simpleFormat: 'd',
|
||||
transitionFormat: 'MMM d',
|
||||
transitionField: CalendarField.month),
|
||||
28 * DAY: new TimeTickFormatterImpl(
|
||||
dateTimeFactory: dateTimeFactory,
|
||||
simpleFormat: 'MMM',
|
||||
transitionFormat: 'MMM yyyy',
|
||||
transitionField: CalendarField.year),
|
||||
365 * DAY: new TimeTickFormatterImpl(
|
||||
dateTimeFactory: dateTimeFactory,
|
||||
simpleFormat: 'yyyy',
|
||||
transitionFormat: 'yyyy',
|
||||
transitionField: CalendarField.year),
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates a [DateTimeTickFormatter] that formats all ticks the same.
|
||||
///
|
||||
/// Only use this formatter for data with fixed intervals, otherwise use the
|
||||
/// default, or build from scratch.
|
||||
///
|
||||
/// [formatter] The format for all ticks.
|
||||
factory DateTimeTickFormatter.uniform(TimeTickFormatter formatter) {
|
||||
return new DateTimeTickFormatter._internal({ANY: formatter});
|
||||
}
|
||||
|
||||
/// Creates a [DateTimeTickFormatter] that formats ticks with [formatters].
|
||||
///
|
||||
/// The formatters are expected to be provided with keys in increasing order.
|
||||
factory DateTimeTickFormatter.withFormatters(
|
||||
Map<int, TimeTickFormatter> formatters) {
|
||||
// Formatters must be non empty.
|
||||
if (formatters == null || formatters.isEmpty) {
|
||||
throw new ArgumentError('At least one TimeTickFormatter is required.');
|
||||
}
|
||||
|
||||
return new DateTimeTickFormatter._internal(formatters);
|
||||
}
|
||||
|
||||
DateTimeTickFormatter._internal(this._timeFormatters) {
|
||||
// If there is only one formatter, just use this one and skip this check.
|
||||
if (_timeFormatters.length == 1) {
|
||||
return;
|
||||
}
|
||||
_checkPositiveAndSorted(_timeFormatters.keys);
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> format(List<DateTime> tickValues, Map<DateTime, String> cache,
|
||||
{@required num stepSize}) {
|
||||
final tickLabels = <String>[];
|
||||
if (tickValues.isEmpty) {
|
||||
return tickLabels;
|
||||
}
|
||||
|
||||
// Find the formatter that is the largest interval that has enough
|
||||
// resolution to describe the difference between ticks. If no such formatter
|
||||
// exists pick the highest res one.
|
||||
var formatter = _timeFormatters[_timeFormatters.keys.first];
|
||||
var formatterFound = false;
|
||||
if (_timeFormatters.keys.first == ANY) {
|
||||
formatterFound = true;
|
||||
} else {
|
||||
int minTimeBetweenTicks = stepSize.toInt();
|
||||
|
||||
// TODO: Skip the formatter if the formatter's step size is
|
||||
// smaller than the minimum step size of the data.
|
||||
|
||||
var keys = _timeFormatters.keys.iterator;
|
||||
while (keys.moveNext() && !formatterFound) {
|
||||
if (keys.current > minTimeBetweenTicks) {
|
||||
formatterFound = true;
|
||||
} else {
|
||||
formatter = _timeFormatters[keys.current];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format the ticks.
|
||||
final tickValuesIt = tickValues.iterator;
|
||||
|
||||
var tickValue = (tickValuesIt..moveNext()).current;
|
||||
var prevTickValue = tickValue;
|
||||
tickLabels.add(formatter.formatFirstTick(tickValue));
|
||||
|
||||
while (tickValuesIt.moveNext()) {
|
||||
tickValue = tickValuesIt.current;
|
||||
if (formatter.isTransition(tickValue, prevTickValue)) {
|
||||
tickLabels.add(formatter.formatTransitionTick(tickValue));
|
||||
} else {
|
||||
tickLabels.add(formatter.formatSimpleTick(tickValue));
|
||||
}
|
||||
prevTickValue = tickValue;
|
||||
}
|
||||
|
||||
return tickLabels;
|
||||
}
|
||||
|
||||
static void _checkPositiveAndSorted(Iterable<int> values) {
|
||||
final valuesIterator = values.iterator;
|
||||
var prev = (valuesIterator..moveNext()).current;
|
||||
var isSorted = true;
|
||||
|
||||
// Only need to check the first value, because the values after are expected
|
||||
// to be greater.
|
||||
if (prev <= 0) {
|
||||
throw new ArgumentError('Formatter keys must be positive');
|
||||
}
|
||||
|
||||
while (valuesIterator.moveNext() && isSorted) {
|
||||
isSorted = prev < valuesIterator.current;
|
||||
prev = valuesIterator.current;
|
||||
}
|
||||
|
||||
if (!isSorted) {
|
||||
throw new ArgumentError(
|
||||
'Formatters must be sorted with keys in increasing order');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import 'base_time_stepper.dart' show BaseTimeStepper;
|
||||
|
||||
/// Day stepper.
|
||||
class DayTimeStepper extends BaseTimeStepper {
|
||||
// TODO: Remove the 14 day increment if we add week stepper.
|
||||
static const _defaultIncrements = const [1, 2, 3, 7, 14];
|
||||
static const _hoursInDay = 24;
|
||||
|
||||
final List<int> _allowedTickIncrements;
|
||||
|
||||
DayTimeStepper._internal(
|
||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
||||
: _allowedTickIncrements = increments,
|
||||
super(dateTimeFactory);
|
||||
|
||||
factory DayTimeStepper(DateTimeFactory dateTimeFactory,
|
||||
{List<int> allowedTickIncrements}) {
|
||||
// Set the default increments if null.
|
||||
allowedTickIncrements ??= _defaultIncrements;
|
||||
|
||||
// Must have at least one increment option.
|
||||
assert(allowedTickIncrements.isNotEmpty);
|
||||
// All increments must be > 0.
|
||||
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
|
||||
|
||||
return new DayTimeStepper._internal(dateTimeFactory, allowedTickIncrements);
|
||||
}
|
||||
|
||||
@override
|
||||
int get typicalStepSizeMs => _hoursInDay * 3600 * 1000;
|
||||
|
||||
@override
|
||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
||||
|
||||
/// Get the step time before or on the given [time] from [tickIncrement].
|
||||
///
|
||||
/// Increments are based off the beginning of the month.
|
||||
/// Ex. 5 day increments in a month is 1,6,11,16,21,26,31
|
||||
/// Ex. Time is Aug 20, increment is 1 day. Returns Aug 20.
|
||||
/// Ex. Time is Aug 20, increment is 2 days. Returns Aug 19 because 2 day
|
||||
/// increments in a month is 1,3,5,7,9,11,13,15,17,19,21....
|
||||
@override
|
||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
||||
final dayRemainder = (time.day - 1) % tickIncrement;
|
||||
// Subtract an extra hour in case stepping through a daylight saving change.
|
||||
final dayBefore = dayRemainder > 0
|
||||
? time.subtract(new Duration(hours: (_hoursInDay * dayRemainder) - 1))
|
||||
: time;
|
||||
// Explicitly leaving off hours and beyond to truncate to start of day.
|
||||
final stepBefore = dateTimeFactory.createDateTime(
|
||||
dayBefore.year, dayBefore.month, dayBefore.day);
|
||||
|
||||
return stepBefore;
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
||||
// Add an extra hour in case stepping through a daylight saving change.
|
||||
final stepAfter =
|
||||
time.add(new Duration(hours: (_hoursInDay * tickIncrement) + 1));
|
||||
// Explicitly leaving off hours and beyond to truncate to start of day.
|
||||
return dateTimeFactory.createDateTime(
|
||||
stepAfter.year, stepAfter.month, stepAfter.day);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:meta/meta.dart' show required;
|
||||
import '../../../../common/date_time_factory.dart';
|
||||
import 'time_tick_formatter_impl.dart'
|
||||
show CalendarField, TimeTickFormatterImpl;
|
||||
|
||||
/// Hour specific tick formatter which will format noon differently.
|
||||
class HourTickFormatter extends TimeTickFormatterImpl {
|
||||
DateFormat _noonFormat;
|
||||
|
||||
HourTickFormatter(
|
||||
{@required DateTimeFactory dateTimeFactory,
|
||||
@required String simpleFormat,
|
||||
@required String transitionFormat,
|
||||
@required String noonFormat})
|
||||
: super(
|
||||
dateTimeFactory: dateTimeFactory,
|
||||
simpleFormat: simpleFormat,
|
||||
transitionFormat: transitionFormat,
|
||||
transitionField: CalendarField.date) {
|
||||
_noonFormat = dateTimeFactory.createDateFormat(noonFormat);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatSimpleTick(DateTime date) {
|
||||
return (date.hour == 12)
|
||||
? _noonFormat.format(date)
|
||||
: super.formatSimpleTick(date);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import 'base_time_stepper.dart' show BaseTimeStepper;
|
||||
|
||||
/// Hour stepper.
|
||||
class HourTimeStepper extends BaseTimeStepper {
|
||||
static const _defaultIncrements = const [1, 2, 3, 4, 6, 12, 24];
|
||||
static const _hoursInDay = 24;
|
||||
static const _millisecondsInHour = 3600 * 1000;
|
||||
|
||||
final List<int> _allowedTickIncrements;
|
||||
|
||||
HourTimeStepper._internal(
|
||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
||||
: _allowedTickIncrements = increments,
|
||||
super(dateTimeFactory);
|
||||
|
||||
factory HourTimeStepper(DateTimeFactory dateTimeFactory,
|
||||
{List<int> allowedTickIncrements}) {
|
||||
// Set the default increments if null.
|
||||
allowedTickIncrements ??= _defaultIncrements;
|
||||
|
||||
// Must have at least one increment option.
|
||||
assert(allowedTickIncrements.isNotEmpty);
|
||||
// All increments must be between 1 and 24 inclusive.
|
||||
assert(allowedTickIncrements
|
||||
.any((increment) => increment <= 0 || increment > 24) ==
|
||||
false);
|
||||
|
||||
return new HourTimeStepper._internal(
|
||||
dateTimeFactory, allowedTickIncrements);
|
||||
}
|
||||
|
||||
@override
|
||||
int get typicalStepSizeMs => _millisecondsInHour;
|
||||
|
||||
@override
|
||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
||||
|
||||
/// Get the step time before or on the given [time] from [tickIncrement].
|
||||
///
|
||||
/// Guarantee a step at the start of the next day.
|
||||
/// Ex. Time is Aug 20 10 AM, increment is 1 hour. Returns 10 AM.
|
||||
/// Ex. Time is Aug 20 6 AM, increment is 4 hours. Returns 4 AM.
|
||||
@override
|
||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
||||
final nextDay = dateTimeFactory
|
||||
.createDateTime(time.year, time.month, time.day)
|
||||
.add(new Duration(hours: _hoursInDay + 1));
|
||||
final nextDayStart = dateTimeFactory.createDateTime(
|
||||
nextDay.year, nextDay.month, nextDay.day);
|
||||
|
||||
final hoursToNextDay =
|
||||
((nextDayStart.millisecondsSinceEpoch - time.millisecondsSinceEpoch) /
|
||||
_millisecondsInHour)
|
||||
.ceil();
|
||||
|
||||
final hoursRemainder = hoursToNextDay % tickIncrement;
|
||||
final rewindHours =
|
||||
hoursRemainder == 0 ? 0 : tickIncrement - hoursRemainder;
|
||||
final stepBefore = dateTimeFactory.createDateTime(
|
||||
time.year, time.month, time.day, time.hour - rewindHours);
|
||||
|
||||
return stepBefore;
|
||||
}
|
||||
|
||||
/// Get next step time.
|
||||
///
|
||||
/// [time] is expected to be a [DateTime] with the hour at start of the hour.
|
||||
@override
|
||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
||||
return time.add(new Duration(hours: tickIncrement));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import 'base_time_stepper.dart';
|
||||
|
||||
/// Minute stepper where ticks generated aligns with the hour.
|
||||
class MinuteTimeStepper extends BaseTimeStepper {
|
||||
static const _defaultIncrements = const [5, 10, 15, 20, 30];
|
||||
static const _millisecondsInMinute = 60 * 1000;
|
||||
|
||||
final List<int> _allowedTickIncrements;
|
||||
|
||||
MinuteTimeStepper._internal(
|
||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
||||
: _allowedTickIncrements = increments,
|
||||
super(dateTimeFactory);
|
||||
|
||||
factory MinuteTimeStepper(DateTimeFactory dateTimeFactory,
|
||||
{List<int> allowedTickIncrements}) {
|
||||
// Set the default increments if null.
|
||||
allowedTickIncrements ??= _defaultIncrements;
|
||||
|
||||
// Must have at least one increment
|
||||
assert(allowedTickIncrements.isNotEmpty);
|
||||
// Increment must be between 1 and 60 inclusive.
|
||||
assert(allowedTickIncrements
|
||||
.any((increment) => increment <= 0 || increment > 60) ==
|
||||
false);
|
||||
|
||||
return new MinuteTimeStepper._internal(
|
||||
dateTimeFactory, allowedTickIncrements);
|
||||
}
|
||||
|
||||
@override
|
||||
int get typicalStepSizeMs => _millisecondsInMinute;
|
||||
|
||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
||||
|
||||
/// Picks a tick start time that guarantees the start of the hour is included.
|
||||
///
|
||||
/// Ex. Time is 3:46, increments is 5 minutes, step before is 3:45, because
|
||||
/// we can guarantee a step at 4:00.
|
||||
@override
|
||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
||||
final nextHourStart = time.millisecondsSinceEpoch +
|
||||
(60 - time.minute) * _millisecondsInMinute;
|
||||
|
||||
final minutesToNextHour =
|
||||
((nextHourStart - time.millisecondsSinceEpoch) / _millisecondsInMinute)
|
||||
.ceil();
|
||||
|
||||
final minRemainder = minutesToNextHour % tickIncrement;
|
||||
final rewindMinutes = minRemainder == 0 ? 0 : tickIncrement - minRemainder;
|
||||
|
||||
final stepBefore = dateTimeFactory.createDateTimeFromMilliSecondsSinceEpoch(
|
||||
time.millisecondsSinceEpoch - rewindMinutes * _millisecondsInMinute);
|
||||
|
||||
return stepBefore;
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
||||
return time.add(new Duration(minutes: tickIncrement));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import 'base_time_stepper.dart' show BaseTimeStepper;
|
||||
|
||||
/// Month stepper.
|
||||
class MonthTimeStepper extends BaseTimeStepper {
|
||||
static const _defaultIncrements = const [1, 2, 3, 4, 6, 12];
|
||||
|
||||
final List<int> _allowedTickIncrements;
|
||||
|
||||
MonthTimeStepper._internal(
|
||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
||||
: _allowedTickIncrements = increments,
|
||||
super(dateTimeFactory);
|
||||
|
||||
factory MonthTimeStepper(DateTimeFactory dateTimeFactory,
|
||||
{List<int> allowedTickIncrements}) {
|
||||
// Set the default increments if null.
|
||||
allowedTickIncrements ??= _defaultIncrements;
|
||||
|
||||
// Must have at least one increment option.
|
||||
assert(allowedTickIncrements.isNotEmpty);
|
||||
// All increments must be > 0.
|
||||
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
|
||||
|
||||
return new MonthTimeStepper._internal(
|
||||
dateTimeFactory, allowedTickIncrements);
|
||||
}
|
||||
|
||||
@override
|
||||
int get typicalStepSizeMs => 30 * 24 * 3600 * 1000;
|
||||
|
||||
@override
|
||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
||||
|
||||
/// Guarantee a step ending in the last month of the year.
|
||||
///
|
||||
/// If date is 2017 Oct and increments is 6, the step before is 2017 June.
|
||||
@override
|
||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
||||
final monthRemainder = time.month % tickIncrement;
|
||||
var newMonth = (time.month - monthRemainder) % DateTime.monthsPerYear;
|
||||
// Handles the last month of the year (December) edge case.
|
||||
// Ex. When month is December and increment is 1
|
||||
if (time.month == DateTime.monthsPerYear && newMonth == 0) {
|
||||
newMonth = DateTime.monthsPerYear;
|
||||
}
|
||||
final newYear =
|
||||
time.year - (monthRemainder / DateTime.monthsPerYear).floor();
|
||||
|
||||
return dateTimeFactory.createDateTime(newYear, newMonth);
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
||||
final incrementedMonth = time.month + tickIncrement;
|
||||
final newMonth = incrementedMonth % DateTime.monthsPerYear;
|
||||
final newYear =
|
||||
time.year + (incrementedMonth / DateTime.monthsPerYear).floor();
|
||||
|
||||
return dateTimeFactory.createDateTime(newYear, newMonth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../tick_provider.dart' show BaseTickProvider;
|
||||
import '../time/date_time_extents.dart' show DateTimeExtents;
|
||||
|
||||
/// Provides ticks for a particular time unit.
|
||||
///
|
||||
/// Used by [AutoAdjustingDateTimeTickProvider].
|
||||
abstract class TimeRangeTickProvider extends BaseTickProvider<DateTime> {
|
||||
/// Returns if this tick provider will produce a sufficient number of ticks
|
||||
/// for [domainExtents].
|
||||
bool providesSufficientTicksForRange(DateTimeExtents domainExtents);
|
||||
|
||||
/// Find the closet step size, from provided step size, in milliseconds.
|
||||
int getClosestStepSize(int stepSize);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../../common/chart_context.dart' show ChartContext;
|
||||
import '../axis.dart' show AxisOrientation;
|
||||
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
||||
import '../tick.dart' show Tick;
|
||||
import '../tick_formatter.dart' show TickFormatter;
|
||||
import '../tick_provider.dart' show TickHint;
|
||||
import 'date_time_extents.dart' show DateTimeExtents;
|
||||
import 'date_time_scale.dart' show DateTimeScale;
|
||||
import 'time_range_tick_provider.dart' show TimeRangeTickProvider;
|
||||
import 'time_stepper.dart' show TimeStepper;
|
||||
|
||||
// Contains all the common code for the time range tick providers.
|
||||
class TimeRangeTickProviderImpl extends TimeRangeTickProvider {
|
||||
final int requiredMinimumTicks;
|
||||
final TimeStepper timeStepper;
|
||||
|
||||
TimeRangeTickProviderImpl(this.timeStepper, {this.requiredMinimumTicks = 3});
|
||||
|
||||
@override
|
||||
bool providesSufficientTicksForRange(DateTimeExtents domainExtents) {
|
||||
final cnt = timeStepper.getStepCountBetween(domainExtents, 1);
|
||||
return cnt >= requiredMinimumTicks;
|
||||
}
|
||||
|
||||
/// Find the closet step size, from provided step size, in milliseconds.
|
||||
@override
|
||||
int getClosestStepSize(int stepSize) {
|
||||
return timeStepper.typicalStepSizeMs *
|
||||
_getClosestIncrementFromStepSize(stepSize);
|
||||
}
|
||||
|
||||
// Find the increment that is closest to the step size.
|
||||
int _getClosestIncrementFromStepSize(int stepSize) {
|
||||
int minDifference;
|
||||
int closestIncrement;
|
||||
|
||||
for (int increment in timeStepper.allowedTickIncrements) {
|
||||
final difference =
|
||||
(stepSize - (timeStepper.typicalStepSizeMs * increment)).abs();
|
||||
if (minDifference == null || minDifference > difference) {
|
||||
minDifference = difference;
|
||||
closestIncrement = increment;
|
||||
}
|
||||
}
|
||||
|
||||
return closestIncrement;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Tick<DateTime>> getTicks({
|
||||
@required ChartContext context,
|
||||
@required GraphicsFactory graphicsFactory,
|
||||
@required DateTimeScale scale,
|
||||
@required TickFormatter<DateTime> formatter,
|
||||
@required Map<DateTime, String> formatterValueCache,
|
||||
@required TickDrawStrategy tickDrawStrategy,
|
||||
@required AxisOrientation orientation,
|
||||
bool viewportExtensionEnabled = false,
|
||||
TickHint<DateTime> tickHint,
|
||||
}) {
|
||||
List<Tick<DateTime>> currentTicks;
|
||||
final tickValues = <DateTime>[];
|
||||
final timeStepIt = timeStepper.getSteps(scale.viewportDomain).iterator;
|
||||
|
||||
// Try different tickIncrements and choose the first that has no collisions.
|
||||
// If none exist use the last one which should have the fewest ticks and
|
||||
// hope that the renderer will resolve collisions.
|
||||
//
|
||||
// If a tick hint was provided, use the tick hint to search for the closest
|
||||
// increment and use that.
|
||||
List<int> allowedTickIncrements;
|
||||
if (tickHint != null) {
|
||||
final stepSize = tickHint.end.difference(tickHint.start).inMilliseconds;
|
||||
allowedTickIncrements = [_getClosestIncrementFromStepSize(stepSize)];
|
||||
} else {
|
||||
allowedTickIncrements = timeStepper.allowedTickIncrements;
|
||||
}
|
||||
|
||||
for (int i = 0; i < allowedTickIncrements.length; i++) {
|
||||
// Create tick values with a specified increment.
|
||||
final tickIncrement = allowedTickIncrements[i];
|
||||
tickValues.clear();
|
||||
timeStepIt.reset(tickIncrement);
|
||||
while (timeStepIt.moveNext()) {
|
||||
tickValues.add(timeStepIt.current);
|
||||
}
|
||||
|
||||
// Create ticks
|
||||
currentTicks = createTicks(tickValues,
|
||||
context: context,
|
||||
graphicsFactory: graphicsFactory,
|
||||
scale: scale,
|
||||
formatter: formatter,
|
||||
formatterValueCache: formatterValueCache,
|
||||
tickDrawStrategy: tickDrawStrategy,
|
||||
stepSize: timeStepper.typicalStepSizeMs * tickIncrement);
|
||||
|
||||
// Request collision check from draw strategy.
|
||||
final collisionReport =
|
||||
tickDrawStrategy.collides(currentTicks, orientation);
|
||||
|
||||
if (!collisionReport.ticksCollide) {
|
||||
// Return the first non colliding ticks.
|
||||
return currentTicks;
|
||||
}
|
||||
}
|
||||
|
||||
// If all ticks collide, return the last generated ticks.
|
||||
return currentTicks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'date_time_extents.dart' show DateTimeExtents;
|
||||
|
||||
/// Represents the step/tick information for the given time range.
|
||||
abstract class TimeStepper {
|
||||
/// Get new bounding extents to the ticks that would contain the given
|
||||
/// timeExtents.
|
||||
DateTimeExtents updateBoundingSteps(DateTimeExtents timeExtents);
|
||||
|
||||
/// Returns the number steps/ticks are between the given extents inclusive.
|
||||
///
|
||||
/// Does not extend the extents to the bounding ticks.
|
||||
int getStepCountBetween(DateTimeExtents timeExtents, int tickIncrement);
|
||||
|
||||
/// Generates an Iterable for iterating over the time steps bounded by the
|
||||
/// given timeExtents. The desired tickIncrement can be set on the returned
|
||||
/// [TimeStepIteratorFactory].
|
||||
TimeStepIteratorFactory getSteps(DateTimeExtents timeExtents);
|
||||
|
||||
/// Returns the typical stepSize for this stepper assuming increment by 1.
|
||||
int get typicalStepSizeMs;
|
||||
|
||||
/// An ordered list of step increments that makes sense given the step.
|
||||
///
|
||||
/// Example: hours may increment by 1, 2, 3, 4, 6, 12. It doesn't make sense
|
||||
/// to increment hours by 7.
|
||||
List<int> get allowedTickIncrements;
|
||||
}
|
||||
|
||||
/// Iterator with a reset function that can be used multiple times to avoid
|
||||
/// object instantiation during the Android layout/draw phases.
|
||||
abstract class TimeStepIterator extends Iterator<DateTime> {
|
||||
/// Reset the iterator and set the tickIncrement to the specified value.
|
||||
///
|
||||
/// This method is provided so that the same iterator instance can be used for
|
||||
/// different tick increments, avoiding object allocation during Android
|
||||
/// layout/draw phases.
|
||||
TimeStepIterator reset(int tickIncrement);
|
||||
}
|
||||
|
||||
/// Factory that creates TimeStepIterator with the set tickIncrement value.
|
||||
abstract class TimeStepIteratorFactory extends Iterable {
|
||||
/// Get iterator and optionally set the tickIncrement.
|
||||
@override
|
||||
TimeStepIterator get iterator;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// Formatter of [DateTime] ticks
|
||||
abstract class TimeTickFormatter {
|
||||
/// Format for tick that is the first in a set of ticks.
|
||||
String formatFirstTick(DateTime date);
|
||||
|
||||
/// Format for a 'simple' tick.
|
||||
///
|
||||
/// Ex. Not a first tick or transition tick.
|
||||
String formatSimpleTick(DateTime date);
|
||||
|
||||
/// Format for a transitional tick.
|
||||
String formatTransitionTick(DateTime date);
|
||||
|
||||
/// Returns true if tick is a transitional tick.
|
||||
bool isTransition(DateTime tickValue, DateTime prevTickValue);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:meta/meta.dart' show required;
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import 'time_tick_formatter.dart' show TimeTickFormatter;
|
||||
|
||||
/// Formatter that can format simple and transition time ticks differently.
|
||||
class TimeTickFormatterImpl implements TimeTickFormatter {
|
||||
DateFormat _simpleFormat;
|
||||
DateFormat _transitionFormat;
|
||||
final CalendarField transitionField;
|
||||
|
||||
/// Create time tick formatter.
|
||||
///
|
||||
/// [dateTimeFactory] factory to use to generate the [DateFormat].
|
||||
/// [simpleFormat] format to use for most ticks.
|
||||
/// [transitionFormat] format to use when the time unit transitions.
|
||||
/// For example showing the month with the date for Jan 1.
|
||||
/// [transitionField] the calendar field that indicates transition.
|
||||
TimeTickFormatterImpl(
|
||||
{@required DateTimeFactory dateTimeFactory,
|
||||
@required String simpleFormat,
|
||||
@required String transitionFormat,
|
||||
this.transitionField}) {
|
||||
_simpleFormat = dateTimeFactory.createDateFormat(simpleFormat);
|
||||
_transitionFormat = dateTimeFactory.createDateFormat(transitionFormat);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatFirstTick(DateTime date) => _transitionFormat.format(date);
|
||||
|
||||
@override
|
||||
String formatSimpleTick(DateTime date) => _simpleFormat.format(date);
|
||||
|
||||
@override
|
||||
String formatTransitionTick(DateTime date) => _transitionFormat.format(date);
|
||||
|
||||
@override
|
||||
bool isTransition(DateTime tickValue, DateTime prevTickValue) {
|
||||
// Transition is always false if no transition field is specified.
|
||||
if (transitionField == null) {
|
||||
return false;
|
||||
}
|
||||
final prevTransitionFieldValue =
|
||||
getCalendarField(prevTickValue, transitionField);
|
||||
final transitionFieldValue = getCalendarField(tickValue, transitionField);
|
||||
return prevTransitionFieldValue != transitionFieldValue;
|
||||
}
|
||||
|
||||
/// Gets the calendar field for [dateTime].
|
||||
int getCalendarField(DateTime dateTime, CalendarField field) {
|
||||
int value;
|
||||
|
||||
switch (field) {
|
||||
case CalendarField.year:
|
||||
value = dateTime.year;
|
||||
break;
|
||||
case CalendarField.month:
|
||||
value = dateTime.month;
|
||||
break;
|
||||
case CalendarField.date:
|
||||
value = dateTime.day;
|
||||
break;
|
||||
case CalendarField.hourOfDay:
|
||||
value = dateTime.hour;
|
||||
break;
|
||||
case CalendarField.minute:
|
||||
value = dateTime.minute;
|
||||
break;
|
||||
case CalendarField.second:
|
||||
value = dateTime.second;
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
enum CalendarField {
|
||||
year,
|
||||
month,
|
||||
date,
|
||||
hourOfDay,
|
||||
minute,
|
||||
second,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
||||
import 'base_time_stepper.dart' show BaseTimeStepper;
|
||||
|
||||
/// Year stepper.
|
||||
class YearTimeStepper extends BaseTimeStepper {
|
||||
static const _defaultIncrements = const [1, 2, 5, 10, 50, 100, 500, 1000];
|
||||
|
||||
final List<int> _allowedTickIncrements;
|
||||
|
||||
YearTimeStepper._internal(
|
||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
||||
: _allowedTickIncrements = increments,
|
||||
super(dateTimeFactory);
|
||||
|
||||
factory YearTimeStepper(DateTimeFactory dateTimeFactory,
|
||||
{List<int> allowedTickIncrements}) {
|
||||
// Set the default increments if null.
|
||||
allowedTickIncrements ??= _defaultIncrements;
|
||||
|
||||
// Must have at least one increment option.
|
||||
assert(allowedTickIncrements.isNotEmpty);
|
||||
// All increments must be > 0.
|
||||
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
|
||||
|
||||
return new YearTimeStepper._internal(
|
||||
dateTimeFactory, allowedTickIncrements);
|
||||
}
|
||||
|
||||
@override
|
||||
int get typicalStepSizeMs => 365 * 24 * 3600 * 1000;
|
||||
|
||||
@override
|
||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
||||
|
||||
/// Guarantees the increment is a factor of the tick value.
|
||||
///
|
||||
/// Example: 2017, tick increment of 10, step before is 2010.
|
||||
@override
|
||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
||||
final yearRemainder = time.year % tickIncrement;
|
||||
return dateTimeFactory.createDateTime(time.year - yearRemainder);
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
||||
return dateTimeFactory.createDateTime(time.year + tickIncrement);
|
||||
}
|
||||
}
|
||||
468
web/charts/common/lib/src/chart/cartesian/cartesian_chart.dart
Normal file
468
web/charts/common/lib/src/chart/cartesian/cartesian_chart.dart
Normal file
@@ -0,0 +1,468 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:collection' show LinkedHashMap;
|
||||
import 'dart:math' show Point;
|
||||
|
||||
import 'package:meta/meta.dart' show protected;
|
||||
|
||||
import '../../common/graphics_factory.dart' show GraphicsFactory;
|
||||
import '../../data/series.dart' show Series;
|
||||
import '../bar/bar_renderer.dart' show BarRenderer;
|
||||
import '../common/base_chart.dart' show BaseChart;
|
||||
import '../common/chart_context.dart' show ChartContext;
|
||||
import '../common/datum_details.dart' show DatumDetails;
|
||||
import '../common/processed_series.dart' show MutableSeries;
|
||||
import '../common/selection_model/selection_model.dart' show SelectionModelType;
|
||||
import '../common/series_renderer.dart' show SeriesRenderer;
|
||||
import '../layout/layout_config.dart' show LayoutConfig, MarginSpec;
|
||||
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
|
||||
import 'axis/axis.dart'
|
||||
show
|
||||
Axis,
|
||||
AxisOrientation,
|
||||
OrdinalAxis,
|
||||
NumericAxis,
|
||||
domainAxisKey,
|
||||
measureAxisIdKey,
|
||||
measureAxisKey;
|
||||
import 'axis/draw_strategy/gridline_draw_strategy.dart'
|
||||
show GridlineRendererSpec;
|
||||
import 'axis/draw_strategy/none_draw_strategy.dart' show NoneDrawStrategy;
|
||||
import 'axis/draw_strategy/small_tick_draw_strategy.dart'
|
||||
show SmallTickRendererSpec;
|
||||
import 'axis/spec/axis_spec.dart' show AxisSpec;
|
||||
|
||||
class NumericCartesianChart extends CartesianChart<num> {
|
||||
NumericCartesianChart(
|
||||
{bool vertical,
|
||||
LayoutConfig layoutConfig,
|
||||
NumericAxis primaryMeasureAxis,
|
||||
NumericAxis secondaryMeasureAxis,
|
||||
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
|
||||
: super(
|
||||
vertical: vertical,
|
||||
layoutConfig: layoutConfig,
|
||||
domainAxis: new NumericAxis(),
|
||||
primaryMeasureAxis: primaryMeasureAxis,
|
||||
secondaryMeasureAxis: secondaryMeasureAxis,
|
||||
disjointMeasureAxes: disjointMeasureAxes);
|
||||
|
||||
@protected
|
||||
void initDomainAxis() {
|
||||
_domainAxis.tickDrawStrategy = new SmallTickRendererSpec<num>()
|
||||
.createDrawStrategy(context, graphicsFactory);
|
||||
}
|
||||
}
|
||||
|
||||
class OrdinalCartesianChart extends CartesianChart<String> {
|
||||
OrdinalCartesianChart(
|
||||
{bool vertical,
|
||||
LayoutConfig layoutConfig,
|
||||
NumericAxis primaryMeasureAxis,
|
||||
NumericAxis secondaryMeasureAxis,
|
||||
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
|
||||
: super(
|
||||
vertical: vertical,
|
||||
layoutConfig: layoutConfig,
|
||||
domainAxis: new OrdinalAxis(),
|
||||
primaryMeasureAxis: primaryMeasureAxis,
|
||||
secondaryMeasureAxis: secondaryMeasureAxis,
|
||||
disjointMeasureAxes: disjointMeasureAxes);
|
||||
|
||||
@protected
|
||||
void initDomainAxis() {
|
||||
_domainAxis
|
||||
..tickDrawStrategy = new SmallTickRendererSpec<String>()
|
||||
.createDrawStrategy(context, graphicsFactory);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CartesianChart<D> extends BaseChart<D> {
|
||||
static final _defaultLayoutConfig = new LayoutConfig(
|
||||
topSpec: new MarginSpec.fromPixel(minPixel: 20),
|
||||
bottomSpec: new MarginSpec.fromPixel(minPixel: 20),
|
||||
leftSpec: new MarginSpec.fromPixel(minPixel: 20),
|
||||
rightSpec: new MarginSpec.fromPixel(minPixel: 20),
|
||||
);
|
||||
|
||||
bool vertical;
|
||||
|
||||
/// The current domain axis for this chart.
|
||||
Axis<D> _domainAxis;
|
||||
|
||||
/// Temporarily stores the new domain axis that is passed in the constructor
|
||||
/// and the new domain axis created when [domainAxisSpec] is set to a new
|
||||
/// spec.
|
||||
///
|
||||
/// This step is necessary because the axis cannot be fully configured until
|
||||
/// [context] is available. [configurationChanged] is called after [context]
|
||||
/// is available and [_newDomainAxis] will be set to [_domainAxis] and then
|
||||
/// reset back to null.
|
||||
Axis<D> _newDomainAxis;
|
||||
|
||||
/// The current domain axis spec that was used to configure [_domainAxis].
|
||||
///
|
||||
/// This is kept to check if the axis spec has changed when [domainAxisSpec]
|
||||
/// is set.
|
||||
AxisSpec<D> _domainAxisSpec;
|
||||
|
||||
/// Temporarily stores the new domain axis spec that is passed in when
|
||||
/// [domainAxisSpec] is set and is different from [_domainAxisSpec]. This spec
|
||||
/// is then applied to the new domain axis when [configurationChanged] is
|
||||
/// called.
|
||||
AxisSpec<D> _newDomainAxisSpec;
|
||||
|
||||
final Axis<num> _primaryMeasureAxis;
|
||||
|
||||
final Axis<num> _secondaryMeasureAxis;
|
||||
|
||||
final LinkedHashMap<String, NumericAxis> _disjointMeasureAxes;
|
||||
|
||||
/// If set to true, the vertical axis will render the opposite of the default
|
||||
/// direction.
|
||||
bool flipVerticalAxisOutput = false;
|
||||
|
||||
bool _usePrimaryMeasureAxis = false;
|
||||
bool _useSecondaryMeasureAxis = false;
|
||||
|
||||
CartesianChart(
|
||||
{bool vertical,
|
||||
LayoutConfig layoutConfig,
|
||||
Axis<D> domainAxis,
|
||||
NumericAxis primaryMeasureAxis,
|
||||
NumericAxis secondaryMeasureAxis,
|
||||
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
|
||||
: vertical = vertical ?? true,
|
||||
// [domainAxis] will be set to the new axis in [configurationChanged].
|
||||
_newDomainAxis = domainAxis,
|
||||
_primaryMeasureAxis = primaryMeasureAxis ?? new NumericAxis(),
|
||||
_secondaryMeasureAxis = secondaryMeasureAxis ?? new NumericAxis(),
|
||||
_disjointMeasureAxes = disjointMeasureAxes ?? <String, NumericAxis>{},
|
||||
super(layoutConfig: layoutConfig ?? _defaultLayoutConfig) {
|
||||
// As a convenience for chart configuration, set the paint order on any axis
|
||||
// that is missing one.
|
||||
_primaryMeasureAxis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
|
||||
_secondaryMeasureAxis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
|
||||
|
||||
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
|
||||
axis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
|
||||
});
|
||||
}
|
||||
|
||||
void init(ChartContext context, GraphicsFactory graphicsFactory) {
|
||||
super.init(context, graphicsFactory);
|
||||
|
||||
_primaryMeasureAxis.context = context;
|
||||
_primaryMeasureAxis.tickDrawStrategy = new GridlineRendererSpec<num>()
|
||||
.createDrawStrategy(context, graphicsFactory);
|
||||
|
||||
_secondaryMeasureAxis.context = context;
|
||||
_secondaryMeasureAxis.tickDrawStrategy = new GridlineRendererSpec<num>()
|
||||
.createDrawStrategy(context, graphicsFactory);
|
||||
|
||||
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
|
||||
axis.context = context;
|
||||
axis.tickDrawStrategy =
|
||||
new NoneDrawStrategy<num>(context, graphicsFactory);
|
||||
});
|
||||
}
|
||||
|
||||
Axis get domainAxis => _domainAxis;
|
||||
|
||||
/// Allows the chart to configure the domain axis when it is created.
|
||||
@protected
|
||||
void initDomainAxis();
|
||||
|
||||
/// Create a new domain axis and save the new spec to be applied during
|
||||
/// [configurationChanged].
|
||||
set domainAxisSpec(AxisSpec axisSpec) {
|
||||
if (_domainAxisSpec != axisSpec) {
|
||||
_newDomainAxis = createDomainAxisFromSpec(axisSpec);
|
||||
_newDomainAxisSpec = axisSpec;
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the domain axis spec from provided axis spec.
|
||||
@protected
|
||||
Axis<D> createDomainAxisFromSpec(AxisSpec<D> axisSpec) {
|
||||
return axisSpec.createAxis();
|
||||
}
|
||||
|
||||
@override
|
||||
void configurationChanged() {
|
||||
if (_newDomainAxis != null) {
|
||||
if (_domainAxis != null) {
|
||||
removeView(_domainAxis);
|
||||
}
|
||||
|
||||
_domainAxis = _newDomainAxis;
|
||||
_domainAxis
|
||||
..context = context
|
||||
..layoutPaintOrder = LayoutViewPaintOrder.domainAxis;
|
||||
|
||||
initDomainAxis();
|
||||
|
||||
addView(_domainAxis);
|
||||
|
||||
_newDomainAxis = null;
|
||||
}
|
||||
|
||||
if (_newDomainAxisSpec != null) {
|
||||
_domainAxisSpec = _newDomainAxisSpec;
|
||||
_newDomainAxisSpec.configure(_domainAxis, context, graphicsFactory);
|
||||
_newDomainAxisSpec = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the measure axis matching the provided id.
|
||||
///
|
||||
/// If none is provided, this returns the primary measure axis.
|
||||
Axis getMeasureAxis({String axisId}) {
|
||||
Axis axis;
|
||||
if (axisId == Axis.secondaryMeasureAxisId) {
|
||||
axis = _secondaryMeasureAxis;
|
||||
} else if (axisId == Axis.primaryMeasureAxisId) {
|
||||
axis = _primaryMeasureAxis;
|
||||
} else if (_disjointMeasureAxes[axisId] != null) {
|
||||
axis = _disjointMeasureAxes[axisId];
|
||||
}
|
||||
|
||||
// If no valid axisId was provided, fall back to primary axis.
|
||||
axis ??= _primaryMeasureAxis;
|
||||
|
||||
return axis;
|
||||
}
|
||||
|
||||
// TODO: Change measure axis spec to create new measure axis.
|
||||
/// Sets the primary measure axis for the chart, rendered on the start side of
|
||||
/// the domain axis.
|
||||
set primaryMeasureAxisSpec(AxisSpec axisSpec) {
|
||||
axisSpec.configure(_primaryMeasureAxis, context, graphicsFactory);
|
||||
}
|
||||
|
||||
/// Sets the secondary measure axis for the chart, rendered on the end side of
|
||||
/// the domain axis.
|
||||
set secondaryMeasureAxisSpec(AxisSpec axisSpec) {
|
||||
axisSpec.configure(_secondaryMeasureAxis, context, graphicsFactory);
|
||||
}
|
||||
|
||||
/// Sets a map of disjoint measure axes for the chart.
|
||||
///
|
||||
/// Disjoint measure axes can be used to scale a sub-set of series on the
|
||||
/// chart independently from the primary and secondary axes. The general use
|
||||
/// case for this type of chart is to show differences in the trends of the
|
||||
/// data, without comparing their absolute values.
|
||||
///
|
||||
/// Disjoint axes will not render any tick or gridline elements. With
|
||||
/// independent scales, there would be a lot of collision in labels were they
|
||||
/// to do so.
|
||||
///
|
||||
/// If any series is rendered with a disjoint axis, it is highly recommended
|
||||
/// to render all series with disjoint axes. Otherwise, the chart may be
|
||||
/// visually misleading.
|
||||
///
|
||||
/// A [LinkedHashMap] is used to ensure consistent ordering when painting the
|
||||
/// axes.
|
||||
set disjointMeasureAxisSpecs(LinkedHashMap<String, AxisSpec> axisSpecs) {
|
||||
axisSpecs.forEach((String axisId, AxisSpec axisSpec) {
|
||||
axisSpec.configure(
|
||||
_disjointMeasureAxes[axisId], context, graphicsFactory);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
MutableSeries<D> makeSeries(Series<dynamic, D> series) {
|
||||
MutableSeries<D> s = super.makeSeries(series);
|
||||
|
||||
s.measureOffsetFn ??= (_) => 0;
|
||||
|
||||
// Setup the Axes
|
||||
s.setAttr(domainAxisKey, domainAxis);
|
||||
s.setAttr(measureAxisKey,
|
||||
getMeasureAxis(axisId: series.getAttribute(measureAxisIdKey)));
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@override
|
||||
SeriesRenderer<D> makeDefaultRenderer() {
|
||||
return new BarRenderer()..rendererId = SeriesRenderer.defaultRendererId;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, List<MutableSeries<D>>> preprocessSeries(
|
||||
List<MutableSeries<D>> seriesList) {
|
||||
var rendererToSeriesList = super.preprocessSeries(seriesList);
|
||||
|
||||
// Check if primary or secondary measure axis is being used.
|
||||
for (final series in seriesList) {
|
||||
final measureAxisId = series.getAttr(measureAxisIdKey);
|
||||
_usePrimaryMeasureAxis = _usePrimaryMeasureAxis ||
|
||||
(measureAxisId == null || measureAxisId == Axis.primaryMeasureAxisId);
|
||||
_useSecondaryMeasureAxis = _useSecondaryMeasureAxis ||
|
||||
(measureAxisId == Axis.secondaryMeasureAxisId);
|
||||
}
|
||||
|
||||
// Add or remove the primary axis view.
|
||||
if (_usePrimaryMeasureAxis) {
|
||||
addView(_primaryMeasureAxis);
|
||||
} else {
|
||||
removeView(_primaryMeasureAxis);
|
||||
}
|
||||
|
||||
// Add or remove the secondary axis view.
|
||||
if (_useSecondaryMeasureAxis) {
|
||||
addView(_secondaryMeasureAxis);
|
||||
} else {
|
||||
removeView(_secondaryMeasureAxis);
|
||||
}
|
||||
|
||||
// Add all disjoint axis views so that their range will be configured.
|
||||
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
|
||||
addView(axis);
|
||||
});
|
||||
|
||||
// Reset stale values from previous draw cycles.
|
||||
domainAxis.resetDomains();
|
||||
_primaryMeasureAxis.resetDomains();
|
||||
_secondaryMeasureAxis.resetDomains();
|
||||
|
||||
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
|
||||
axis.resetDomains();
|
||||
});
|
||||
|
||||
final reverseAxisDirection = context != null && context.isRtl;
|
||||
|
||||
if (vertical) {
|
||||
domainAxis
|
||||
..axisOrientation = AxisOrientation.bottom
|
||||
..reverseOutputRange = reverseAxisDirection;
|
||||
|
||||
_primaryMeasureAxis
|
||||
..axisOrientation = (reverseAxisDirection
|
||||
? AxisOrientation.right
|
||||
: AxisOrientation.left)
|
||||
..reverseOutputRange = flipVerticalAxisOutput;
|
||||
|
||||
_secondaryMeasureAxis
|
||||
..axisOrientation = (reverseAxisDirection
|
||||
? AxisOrientation.left
|
||||
: AxisOrientation.right)
|
||||
..reverseOutputRange = flipVerticalAxisOutput;
|
||||
|
||||
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
|
||||
axis
|
||||
..axisOrientation = (reverseAxisDirection
|
||||
? AxisOrientation.left
|
||||
: AxisOrientation.right)
|
||||
..reverseOutputRange = flipVerticalAxisOutput;
|
||||
});
|
||||
} else {
|
||||
domainAxis
|
||||
..axisOrientation = (reverseAxisDirection
|
||||
? AxisOrientation.right
|
||||
: AxisOrientation.left)
|
||||
..reverseOutputRange = flipVerticalAxisOutput;
|
||||
|
||||
_primaryMeasureAxis
|
||||
..axisOrientation = AxisOrientation.bottom
|
||||
..reverseOutputRange = reverseAxisDirection;
|
||||
|
||||
_secondaryMeasureAxis
|
||||
..axisOrientation = AxisOrientation.top
|
||||
..reverseOutputRange = reverseAxisDirection;
|
||||
|
||||
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
|
||||
axis
|
||||
..axisOrientation = AxisOrientation.top
|
||||
..reverseOutputRange = reverseAxisDirection;
|
||||
});
|
||||
}
|
||||
|
||||
// Have each renderer configure the axes with their domain and measure
|
||||
// values.
|
||||
rendererToSeriesList
|
||||
.forEach((String rendererId, List<MutableSeries<D>> seriesList) {
|
||||
getSeriesRenderer(rendererId).configureDomainAxes(seriesList);
|
||||
getSeriesRenderer(rendererId).configureMeasureAxes(seriesList);
|
||||
});
|
||||
|
||||
return rendererToSeriesList;
|
||||
}
|
||||
|
||||
@override
|
||||
void onSkipLayout() {
|
||||
// Update ticks only when skipping layout.
|
||||
domainAxis.updateTicks();
|
||||
|
||||
if (_usePrimaryMeasureAxis) {
|
||||
_primaryMeasureAxis.updateTicks();
|
||||
}
|
||||
|
||||
if (_useSecondaryMeasureAxis) {
|
||||
_secondaryMeasureAxis.updateTicks();
|
||||
}
|
||||
|
||||
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
|
||||
axis.updateTicks();
|
||||
});
|
||||
|
||||
super.onSkipLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void onPostLayout(Map<String, List<MutableSeries<D>>> rendererToSeriesList) {
|
||||
fireOnAxisConfigured();
|
||||
|
||||
super.onPostLayout(rendererToSeriesList);
|
||||
}
|
||||
|
||||
/// Returns a list of datum details from selection model of [type].
|
||||
@override
|
||||
List<DatumDetails<D>> getDatumDetails(SelectionModelType type) {
|
||||
final entries = <DatumDetails<D>>[];
|
||||
|
||||
getSelectionModel(type).selectedDatum.forEach((seriesDatum) {
|
||||
final series = seriesDatum.series;
|
||||
final datum = seriesDatum.datum;
|
||||
final datumIndex = seriesDatum.index;
|
||||
|
||||
final domain = series.domainFn(datumIndex);
|
||||
final measure = series.measureFn(datumIndex);
|
||||
final rawMeasure = series.rawMeasureFn(datumIndex);
|
||||
final color = series.colorFn(datumIndex);
|
||||
|
||||
final domainPosition = series.getAttr(domainAxisKey).getLocation(domain);
|
||||
final measurePosition =
|
||||
series.getAttr(measureAxisKey).getLocation(measure);
|
||||
|
||||
final chartPosition = new Point<double>(
|
||||
vertical ? domainPosition : measurePosition,
|
||||
vertical ? measurePosition : domainPosition);
|
||||
|
||||
entries.add(new DatumDetails(
|
||||
datum: datum,
|
||||
domain: domain,
|
||||
measure: measure,
|
||||
rawMeasure: rawMeasure,
|
||||
series: series,
|
||||
color: color,
|
||||
chartPosition: chartPosition));
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
||||
// for details.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../common/symbol_renderer.dart' show SymbolRenderer;
|
||||
import '../../data/series.dart' show AccessorFn;
|
||||
import '../common/base_chart.dart' show BaseChart;
|
||||
import '../common/processed_series.dart' show MutableSeries;
|
||||
import '../common/series_renderer.dart' show BaseSeriesRenderer, SeriesRenderer;
|
||||
import 'axis/axis.dart' show Axis, domainAxisKey, measureAxisKey;
|
||||
import 'cartesian_chart.dart' show CartesianChart;
|
||||
|
||||
abstract class CartesianRenderer<D> extends SeriesRenderer<D> {
|
||||
void configureDomainAxes(List<MutableSeries<D>> seriesList);
|
||||
|
||||
void configureMeasureAxes(List<MutableSeries<D>> seriesList);
|
||||
}
|
||||
|
||||
abstract class BaseCartesianRenderer<D> extends BaseSeriesRenderer<D>
|
||||
implements CartesianRenderer<D> {
|
||||
bool _renderingVertically = true;
|
||||
|
||||
BaseCartesianRenderer(
|
||||
{@required String rendererId,
|
||||
@required int layoutPaintOrder,
|
||||
SymbolRenderer symbolRenderer})
|
||||
: super(
|
||||
rendererId: rendererId,
|
||||
layoutPaintOrder: layoutPaintOrder,
|
||||
symbolRenderer: symbolRenderer);
|
||||
|
||||
@override
|
||||
void onAttach(BaseChart<D> chart) {
|
||||
super.onAttach(chart);
|
||||
_renderingVertically = (chart as CartesianChart).vertical;
|
||||
}
|
||||
|
||||
bool get renderingVertically => _renderingVertically;
|
||||
|
||||
@override
|
||||
void configureDomainAxes(List<MutableSeries<D>> seriesList) {
|
||||
seriesList.forEach((MutableSeries<D> series) {
|
||||
if (series.data.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final domainAxis = series.getAttr(domainAxisKey);
|
||||
final domainFn = series.domainFn;
|
||||
final domainLowerBoundFn = series.domainLowerBoundFn;
|
||||
final domainUpperBoundFn = series.domainUpperBoundFn;
|
||||
|
||||
if (domainAxis == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderingVertically) {
|
||||
for (int i = 0; i < series.data.length; i++) {
|
||||
domainAxis.addDomainValue(domainFn(i));
|
||||
|
||||
if (domainLowerBoundFn != null && domainUpperBoundFn != null) {
|
||||
final domainLowerBound = domainLowerBoundFn(i);
|
||||
final domainUpperBound = domainUpperBoundFn(i);
|
||||
if (domainLowerBound != null && domainUpperBound != null) {
|
||||
domainAxis.addDomainValue(domainLowerBound);
|
||||
domainAxis.addDomainValue(domainUpperBound);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// When rendering horizontally, domains are displayed from top to bottom
|
||||
// in order to match visual display in legend.
|
||||
for (int i = series.data.length - 1; i >= 0; i--) {
|
||||
domainAxis.addDomainValue(domainFn(i));
|
||||
|
||||
if (domainLowerBoundFn != null && domainUpperBoundFn != null) {
|
||||
final domainLowerBound = domainLowerBoundFn(i);
|
||||
final domainUpperBound = domainUpperBoundFn(i);
|
||||
if (domainLowerBound != null && domainUpperBound != null) {
|
||||
domainAxis.addDomainValue(domainLowerBound);
|
||||
domainAxis.addDomainValue(domainUpperBound);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void configureMeasureAxes(List<MutableSeries<D>> seriesList) {
|
||||
seriesList.forEach((MutableSeries<D> series) {
|
||||
if (series.data.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final domainAxis = series.getAttr(domainAxisKey);
|
||||
final domainFn = series.domainFn;
|
||||
|
||||
if (domainAxis == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final measureAxis = series.getAttr(measureAxisKey);
|
||||
if (measureAxis == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add the measure values for datum who's domain is within the
|
||||
// domainAxis viewport.
|
||||
int startIndex =
|
||||
findNearestViewportStart(domainAxis, domainFn, series.data);
|
||||
int endIndex = findNearestViewportEnd(domainAxis, domainFn, series.data);
|
||||
|
||||
addMeasureValuesFor(series, measureAxis, startIndex, endIndex);
|
||||
});
|
||||
}
|
||||
|
||||
void addMeasureValuesFor(
|
||||
MutableSeries<D> series, Axis measureAxis, int startIndex, int endIndex) {
|
||||
final measureFn = series.measureFn;
|
||||
final measureOffsetFn = series.measureOffsetFn;
|
||||
final measureLowerBoundFn = series.measureLowerBoundFn;
|
||||
final measureUpperBoundFn = series.measureUpperBoundFn;
|
||||
|
||||
for (int i = startIndex; i <= endIndex; i++) {
|
||||
final measure = measureFn(i);
|
||||
final measureOffset = measureOffsetFn(i);
|
||||
|
||||
if (measure != null && measureOffset != null) {
|
||||
measureAxis.addDomainValue(measure + measureOffset);
|
||||
|
||||
if (measureLowerBoundFn != null && measureUpperBoundFn != null) {
|
||||
measureAxis.addDomainValue(measureLowerBoundFn(i) + measureOffset);
|
||||
measureAxis.addDomainValue(measureUpperBoundFn(i) + measureOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
int findNearestViewportStart(
|
||||
Axis domainAxis, AccessorFn<D> domainFn, List data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Quick optimization for full viewport (likely).
|
||||
if (domainAxis.compareDomainValueToViewport(domainFn(0)) == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var start = 1; // Index zero was already checked for above.
|
||||
var end = data.length - 1;
|
||||
|
||||
// Binary search for the start of the viewport.
|
||||
while (end >= start) {
|
||||
int searchIndex = ((end - start) / 2).floor() + start;
|
||||
int prevIndex = searchIndex - 1;
|
||||
|
||||
var comparisonValue =
|
||||
domainAxis.compareDomainValueToViewport(domainFn(searchIndex));
|
||||
var prevComparisonValue =
|
||||
domainAxis.compareDomainValueToViewport(domainFn(prevIndex));
|
||||
|
||||
// Found start?
|
||||
if (prevComparisonValue == -1 && comparisonValue == 0) {
|
||||
return searchIndex;
|
||||
}
|
||||
|
||||
// Straddling viewport?
|
||||
// Return previous index as the nearest start of the viewport.
|
||||
if (comparisonValue == 1 && prevComparisonValue == -1) {
|
||||
return (searchIndex - 1);
|
||||
}
|
||||
|
||||
// Before start? Update startIndex
|
||||
if (comparisonValue == -1) {
|
||||
start = searchIndex + 1;
|
||||
} else {
|
||||
// Middle or after viewport? Update endIndex
|
||||
end = searchIndex - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Binary search would reach this point for the edge cases where the domain
|
||||
// specified is prior or after the domain viewport.
|
||||
// If domain is prior to the domain viewport, return the first index as the
|
||||
// nearest viewport start.
|
||||
// If domain is after the domain viewport, return the last index as the
|
||||
// nearest viewport start.
|
||||
final lastComparison =
|
||||
domainAxis.compareDomainValueToViewport(domainFn(data.length - 1));
|
||||
return lastComparison == 1 ? (data.length - 1) : 0;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
int findNearestViewportEnd(
|
||||
Axis domainAxis, AccessorFn<D> domainFn, List data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var start = 1;
|
||||
var end = data.length - 1;
|
||||
|
||||
// Quick optimization for full viewport (likely).
|
||||
if (domainAxis.compareDomainValueToViewport(domainFn(end)) == 0) {
|
||||
return end;
|
||||
}
|
||||
end = end - 1; // Last index was already checked for above.
|
||||
|
||||
// Binary search for the start of the viewport.
|
||||
while (end >= start) {
|
||||
int searchIndex = ((end - start) / 2).floor() + start;
|
||||
int prevIndex = searchIndex - 1;
|
||||
|
||||
int comparisonValue =
|
||||
domainAxis.compareDomainValueToViewport(domainFn(searchIndex));
|
||||
int prevComparisonValue =
|
||||
domainAxis.compareDomainValueToViewport(domainFn(prevIndex));
|
||||
|
||||
// Found end?
|
||||
if (prevComparisonValue == 0 && comparisonValue == 1) {
|
||||
return prevIndex;
|
||||
}
|
||||
|
||||
// Straddling viewport?
|
||||
// Return the current index as the start of the viewport.
|
||||
if (comparisonValue == 1 && prevComparisonValue == -1) {
|
||||
return searchIndex;
|
||||
}
|
||||
|
||||
// After end? Update endIndex
|
||||
if (comparisonValue == 1) {
|
||||
end = searchIndex - 1;
|
||||
} else {
|
||||
// Middle or before viewport? Update startIndex
|
||||
start = searchIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Binary search would reach this point for the edge cases where the domain
|
||||
// specified is prior or after the domain viewport.
|
||||
// If domain is prior to the domain viewport, return the first index as the
|
||||
// nearest viewport end.
|
||||
// If domain is after the domain viewport, return the last index as the
|
||||
// nearest viewport end.
|
||||
final lastComparison =
|
||||
domainAxis.compareDomainValueToViewport(domainFn(data.length - 1));
|
||||
return lastComparison == 1 ? (data.length - 1) : 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user