1
0
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:
Kevin Moore
2019-05-07 13:32:08 -07:00
committed by Andrew Brogdon
parent 42f2dce01b
commit 3fe927cb29
697 changed files with 241026 additions and 0 deletions

View 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();
}

View 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);
}
}

View File

@@ -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;
}

View File

@@ -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,
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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];
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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> {}

View File

@@ -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);
}
}

View File

@@ -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,
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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});
}

View File

@@ -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;
}

View 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)';
}

View File

@@ -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;
}

View File

@@ -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});
}

View File

@@ -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));
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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));
}

View File

@@ -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;
}

View File

@@ -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');
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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,
}

View File

@@ -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);
}
}

View 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;
}
}

View File

@@ -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;
}
}