1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-11 15:28:44 +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,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 'package:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/tick_draw_strategy.dart';
import 'package:charts_common/src/chart/cartesian/axis/scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/spec/tick_spec.dart';
import 'package:charts_common/src/chart/cartesian/axis/static_tick_provider.dart';
import 'package:charts_common/src/common/graphics_factory.dart';
import 'package:charts_common/src/common/text_element.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockTickDrawStrategy extends Mock implements TickDrawStrategy<num> {}
class MockGraphicsFactory extends Mock implements GraphicsFactory {
TextElement createTextElement(String _) {
return MockTextElement();
}
}
class MockTextElement extends Mock implements TextElement {}
StaticTickProvider<num> _createProvider(List<num> values) =>
StaticTickProvider<num>(values.map((v) => TickSpec(v)).toList());
void main() {
test('changing first tick only', () {
var axis = NumericAxis(
tickProvider: _createProvider([1, 10]),
);
var tester = AxisTester(axis);
axis.tickDrawStrategy = MockTickDrawStrategy();
axis.graphicsFactory = MockGraphicsFactory();
tester.scale.range = new ScaleOutputExtent(0, 300);
axis.updateTicks();
axis.tickProvider = _createProvider([5, 10]);
axis.updateTicks();
// The old value should still be there as it gets animated out, but the
// values should be sorted by their position.
expect(tester.axisValues, equals([1, 5, 10]));
});
}

View File

@@ -0,0 +1,186 @@
// 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/common/text_element.dart';
import 'package:charts_common/src/common/text_measurement.dart';
import 'package:charts_common/src/common/text_style.dart';
import 'package:charts_common/src/chart/cartesian/axis/axis_tick.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick.dart';
import 'package:test/test.dart';
/// Fake [TextElement] for testing.
class FakeTextElement implements TextElement {
final String text;
double opacity;
TextMeasurement measurement;
TextStyle textStyle;
int maxWidth;
MaxWidthStrategy maxWidthStrategy;
TextDirection textDirection;
FakeTextElement(this.text);
}
/// Helper to create a tick for testing.
Tick<String> _createTestTick(String value, double locationPx) {
return new Tick(
value: value,
textElement: new FakeTextElement(value),
locationPx: locationPx);
}
void _verify(Tick<String> tick, {double location, double opacity}) {
expect(tick.locationPx, equals(location));
expect((tick.textElement as FakeTextElement).opacity, equals(opacity));
}
void main() {
// Tick first render.
test('tick created for the first time', () {
final tick = new AxisTicks(_createTestTick('a', 100.0));
// Animate in the tick, there was no previous position to animated in from
// so the tick appears in the target immediately.
tick.setCurrentTick(0.0);
_verify(tick, location: 100.0, opacity: 1.0);
tick.setCurrentTick(0.25);
_verify(tick, location: 100.0, opacity: 1.0);
tick.setCurrentTick(0.75);
_verify(tick, location: 100.0, opacity: 1.0);
tick.setCurrentTick(1.0);
_verify(tick, location: 100.0, opacity: 1.0);
});
// Tick that is animated in.
test('tick created with a previous location', () {
final tick = new AxisTicks(_createTestTick('a', 200.0))
..animateInFrom(100.0);
tick.setCurrentTick(0.0);
_verify(tick, location: 100.0, opacity: 0.0);
tick.setCurrentTick(0.25);
_verify(tick, location: 125.0, opacity: 0.25);
tick.setCurrentTick(0.75);
_verify(tick, location: 175.0, opacity: 0.75);
tick.setCurrentTick(1.0);
_verify(tick, location: 200.0, opacity: 1.0);
});
// Tick that is being animated out.
test('tick is animated in and then out', () {
final tick = new AxisTicks(_createTestTick('a', 100.0));
// Animate in the tick, there was no previous position to animated in from
// so the tick appears in the target immediately.
tick.setCurrentTick(0.25);
_verify(tick, location: 100.0, opacity: 1.0);
// Change to animate the tick out.
tick.animateOut(0.0);
expect(tick.markedForRemoval, isTrue);
tick.setCurrentTick(0.0);
_verify(tick, location: 100.0, opacity: 1.0);
tick.setCurrentTick(0.25);
_verify(tick, location: 75.0, opacity: 0.75);
tick.setCurrentTick(0.75);
_verify(tick, location: 25.0, opacity: 0.25);
tick.setCurrentTick(1.0);
_verify(tick, location: 0.0, opacity: 0.0);
});
test('tick target change after reaching target', () {
final tick = new AxisTicks(_createTestTick('a', 100.0));
// Animate in the tick.
tick.setCurrentTick(1.0);
_verify(tick, location: 100.0, opacity: 1.0);
tick.setNewTarget(200.0);
expect(tick.markedForRemoval, isFalse);
tick.setCurrentTick(0.0);
_verify(tick, location: 100.0, opacity: 1.0);
tick.setCurrentTick(0.25);
_verify(tick, location: 125.0, opacity: 1.0);
tick.setCurrentTick(0.75);
_verify(tick, location: 175.0, opacity: 1.0);
tick.setCurrentTick(1.0);
_verify(tick, location: 200.0, opacity: 1.0);
});
test('tick target change before reaching initial target', () {
final tick = new AxisTicks(_createTestTick('a', 400.0))..animateInFrom(0.0);
// Animate in the tick.
tick.setCurrentTick(0.25);
_verify(tick, location: 100.0, opacity: 0.25);
tick.setNewTarget(200.0);
expect(tick.markedForRemoval, isFalse);
tick.setCurrentTick(0.0);
_verify(tick, location: 100.0, opacity: 0.25);
tick.setCurrentTick(0.25);
_verify(tick, location: 125.0, opacity: 0.4375);
tick.setCurrentTick(0.75);
_verify(tick, location: 175.0, opacity: 0.8125);
tick.setCurrentTick(1.0);
_verify(tick, location: 200.0, opacity: 1.0);
});
test('tick target animate out before reaching initial target', () {
final tick = new AxisTicks(_createTestTick('a', 400.0))..animateInFrom(0.0);
// Animate in the tick.
tick.setCurrentTick(0.25);
_verify(tick, location: 100.0, opacity: 0.25);
tick.animateOut(200.0);
expect(tick.markedForRemoval, isTrue);
tick.setCurrentTick(0.0);
_verify(tick, location: 100.0, opacity: 0.25);
tick.setCurrentTick(0.25);
_verify(tick, location: 125.0, opacity: 0.1875);
tick.setCurrentTick(0.75);
_verify(tick, location: 175.0, opacity: 0.0625);
tick.setCurrentTick(1.0);
_verify(tick, location: 200.0, opacity: 0.0);
});
}

View File

@@ -0,0 +1,180 @@
// 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:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart';
import 'package:charts_common/src/chart/cartesian/axis/collision_report.dart';
import 'package:charts_common/src/chart/cartesian/axis/numeric_scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart';
import 'package:charts_common/src/chart/cartesian/axis/numeric_extents.dart';
import 'package:charts_common/src/chart/cartesian/axis/linear/bucketing_numeric_tick_provider.dart';
import 'package:charts_common/src/chart/common/chart_canvas.dart';
import 'package:charts_common/src/chart/common/chart_context.dart';
import 'package:charts_common/src/chart/common/unitconverter/unit_converter.dart';
import 'package:charts_common/src/common/graphics_factory.dart';
import 'package:charts_common/src/common/line_style.dart';
import 'package:charts_common/src/common/text_style.dart';
import 'package:charts_common/src/common/text_element.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockNumericScale extends Mock implements NumericScale {}
/// A fake draw strategy that reports collision and alternate ticks
///
/// Reports collision when the tick count is greater than or equal to
/// [collidesAfterTickCount].
///
/// Reports alternate rendering after tick count is greater than or equal to
/// [alternateRenderingAfterTickCount].
class FakeDrawStrategy extends BaseTickDrawStrategy<num> {
final int collidesAfterTickCount;
final int alternateRenderingAfterTickCount;
FakeDrawStrategy(
this.collidesAfterTickCount, this.alternateRenderingAfterTickCount)
: super(null, new FakeGraphicsFactory());
@override
CollisionReport collides(List<Tick<num>> ticks, _) {
final ticksCollide = ticks.length >= collidesAfterTickCount;
final alternateTicksUsed = ticks.length >= alternateRenderingAfterTickCount;
return new CollisionReport(
ticksCollide: ticksCollide,
ticks: ticks,
alternateTicksUsed: alternateTicksUsed);
}
@override
void draw(ChartCanvas canvas, Tick<num> tick,
{AxisOrientation orientation,
Rectangle<int> axisBounds,
Rectangle<int> drawAreaBounds,
bool isFirst,
bool isLast}) {}
}
/// A fake [GraphicsFactory] that returns [MockTextStyle] and [MockTextElement].
class FakeGraphicsFactory extends GraphicsFactory {
@override
TextStyle createTextPaint() => new MockTextStyle();
@override
TextElement createTextElement(String text) => new MockTextElement(text);
@override
LineStyle createLinePaint() => new MockLinePaint();
}
class MockTextStyle extends Mock implements TextStyle {}
class MockTextElement extends Mock implements TextElement {
String text;
MockTextElement(this.text);
}
class MockLinePaint extends Mock implements LineStyle {}
class MockChartContext extends Mock implements ChartContext {}
/// A celsius to fahrenheit converter for testing axis with unit converter.
class CelsiusToFahrenheitConverter implements UnitConverter<num, num> {
const CelsiusToFahrenheitConverter();
@override
num convert(num value) => (value * 1.8) + 32.0;
@override
num invert(num value) => (value - 32.0) / 1.8;
}
void main() {
FakeGraphicsFactory graphicsFactory;
MockNumericScale scale;
BucketingNumericTickProvider tickProvider;
TickFormatter<num> formatter;
ChartContext context;
setUp(() {
graphicsFactory = new FakeGraphicsFactory();
scale = new MockNumericScale();
tickProvider = new BucketingNumericTickProvider();
formatter = new NumericTickFormatter();
context = new MockChartContext();
});
group('threshold', () {
test('tick generated correctly with no ticks between it and zero', () {
tickProvider
..dataIsInWholeNumbers = false
..threshold = 0.1
..showBucket = true
..setFixedTickCount(21)
..allowedSteps = [1.0, 2.5, 5.0];
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(0.1, 0.7));
when(scale.rangeWidth).thenReturn(1000);
when(scale[0.1]).thenReturn(90.0);
when(scale[0]).thenReturn(100.0);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
// Verify.
// We expect to have 20 ticks, because the expected tick at 0.05 should be
// removed from the list.
expect(ticks, hasLength(20));
// Verify that we still have a 0 tick with an empty label.
expect(ticks[0].labelOffsetPx, isNull);
expect(ticks[0].locationPx, equals(100.0));
expect(ticks[0].value, equals(0.0));
expect(ticks[0].textElement.text, equals(''));
// Verify that we have a threshold tick.
expect(ticks[1].labelOffsetPx, equals(5.0));
expect(ticks[1].locationPx, equals(90.0));
expect(ticks[1].value, equals(0.10));
expect(ticks[1].textElement.text, equals('< 0.1'));
// Verify that the rest of the ticks are all above the threshold in value
// and have normal labels.
var aboveThresholdTicks = ticks.sublist(2);
aboveThresholdTicks.retainWhere((Tick tick) => tick.value > 0.1);
expect(aboveThresholdTicks, hasLength(18));
aboveThresholdTicks = ticks.sublist(2);
aboveThresholdTicks.retainWhere((Tick tick) =>
tick.textElement.text != '' && !tick.textElement.text.contains('<'));
expect(aboveThresholdTicks, hasLength(18));
aboveThresholdTicks = ticks.sublist(2);
aboveThresholdTicks
.retainWhere((Tick tick) => tick.labelOffsetPx == null);
expect(aboveThresholdTicks, hasLength(18));
});
});
}

View File

@@ -0,0 +1,408 @@
// 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:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart';
import 'package:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/cartesian/axis/spec/axis_spec.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick.dart';
import 'package:charts_common/src/chart/common/chart_canvas.dart';
import 'package:charts_common/src/chart/common/chart_context.dart';
import 'package:charts_common/src/common/graphics_factory.dart';
import 'package:charts_common/src/common/line_style.dart';
import 'package:charts_common/src/common/text_element.dart';
import 'package:charts_common/src/common/text_measurement.dart';
import 'package:charts_common/src/common/text_style.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockContext extends Mock implements ChartContext {}
/// Implementation of [BaseTickDrawStrategy] that does nothing on draw.
class BaseTickDrawStrategyImpl<D> extends BaseTickDrawStrategy<D> {
BaseTickDrawStrategyImpl(
ChartContext chartContext, GraphicsFactory graphicsFactory,
{TextStyleSpec labelStyleSpec,
LineStyleSpec axisLineStyleSpec,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
int minimumPaddingBetweenLabelsPx})
: super(chartContext, graphicsFactory,
labelStyleSpec: labelStyleSpec,
axisLineStyleSpec: axisLineStyleSpec,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx);
void draw(ChartCanvas canvas, Tick<D> tick,
{AxisOrientation orientation,
Rectangle<int> axisBounds,
Rectangle<int> drawAreaBounds,
bool isFirst,
bool isLast}) {}
}
/// Fake [TextElement] for testing.
///
/// [baseline] returns the same value as the [verticalSliceWidth] specified.
class FakeTextElement implements TextElement {
final String text;
final TextMeasurement measurement;
TextStyle textStyle;
int maxWidth;
MaxWidthStrategy maxWidthStrategy;
TextDirection textDirection;
double opacity;
FakeTextElement(
this.text,
this.textDirection,
double horizontalSliceWidth,
double verticalSliceWidth,
) : measurement = new TextMeasurement(
horizontalSliceWidth: horizontalSliceWidth,
verticalSliceWidth: verticalSliceWidth);
}
class MockGraphicsFactory extends Mock implements GraphicsFactory {}
class MockLineStyle extends Mock implements LineStyle {}
class MockTextStyle extends Mock implements TextStyle {}
/// Helper function to create [Tick] for testing.
Tick<String> createTick(String value, double locationPx,
{double horizontalWidth,
double verticalWidth,
TextDirection textDirection}) {
return new Tick<String>(
value: value,
locationPx: locationPx,
textElement: new FakeTextElement(
value, textDirection, horizontalWidth, verticalWidth));
}
void main() {
GraphicsFactory graphicsFactory;
ChartContext chartContext;
setUpAll(() {
graphicsFactory = new MockGraphicsFactory();
when(graphicsFactory.createLinePaint()).thenReturn(new MockLineStyle());
when(graphicsFactory.createTextPaint()).thenReturn(new MockTextStyle());
chartContext = new MockContext();
when(chartContext.chartContainerIsRtl).thenReturn(false);
when(chartContext.isRtl).thenReturn(false);
});
group('collision detection - vertically drawn axis', () {
test('ticks do not collide', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 2);
final ticks = [
createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2)
createTick('B', 20.0, verticalWidth: 8.0), // 20.0 - 30.0 (28.0 + 2)
createTick('C', 30.0, verticalWidth: 8.0), // 30.0 - 40.0 (38.0 + 2)
];
final report = drawStrategy.collides(ticks, AxisOrientation.left);
expect(report.ticksCollide, isFalse);
});
test('ticks collide because it does not have minimum padding', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 2);
final ticks = [
createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2)
createTick('B', 20.0, verticalWidth: 9.0), // 20.0 - 31.0 (28.0 + 3)
createTick('C', 30.0, verticalWidth: 8.0), // 30.0 - 40.0 (38.0 + 2)
];
final report = drawStrategy.collides(ticks, AxisOrientation.left);
expect(report.ticksCollide, isTrue);
});
test('first tick causes collision', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 0);
final ticks = [
createTick('A', 10.0, verticalWidth: 11.0), // 10.0 - 21.0
createTick('B', 20.0, verticalWidth: 10.0), // 20.0 - 30.0
createTick('C', 30.0, verticalWidth: 10.0), // 30.0 - 40.0
];
final report = drawStrategy.collides(ticks, AxisOrientation.left);
expect(report.ticksCollide, isTrue);
});
test('last tick causes collision', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 0);
final ticks = [
createTick('A', 10.0, verticalWidth: 10.0), // 10.0 - 20.0
createTick('B', 20.0, verticalWidth: 10.0), // 20.0 - 30.0
createTick('C', 29.0, verticalWidth: 10.0), // 29.0 - 40.0
];
final report = drawStrategy.collides(ticks, AxisOrientation.left);
expect(report.ticksCollide, isTrue);
});
test('ticks do not collide for inside tick label anchor', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 2,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2)
createTick('B', 25.0, verticalWidth: 8.0), // 20.0 - 30.0 (25 + 2 + 1)
createTick('C', 40.0, verticalWidth: 8.0), // 30.0 - 40.0 (40-8-2)
];
final report = drawStrategy.collides(ticks, AxisOrientation.left);
expect(report.ticksCollide, isFalse);
});
test('ticks collide for inside anchor - first tick too large', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 2,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('A', 10.0, verticalWidth: 9.0), // 10.0 - 21.0 (19.0 + 2)
createTick('B', 25.0, verticalWidth: 8.0), // 20.0 - 30.0 (25 + 2 + 1)
createTick('C', 40.0, verticalWidth: 8.0), // 30.0 - 40.0 (40-8-2)
];
final report = drawStrategy.collides(ticks, AxisOrientation.left);
expect(report.ticksCollide, isTrue);
});
test('ticks collide for inside anchor - center tick too large', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 2,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2)
createTick('B', 25.0, verticalWidth: 9.0), // 19.5 - 30.5 (25 + 2.5 + 1)
createTick('C', 40.0, verticalWidth: 8.0), // 30.0 - 40.0 (40-8-2)
];
final report = drawStrategy.collides(ticks, AxisOrientation.left);
expect(report.ticksCollide, isTrue);
});
test('ticks collide for inside anchor - last tick too large', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 2,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2)
createTick('B', 25.0, verticalWidth: 8.0), // 20.0 - 30.0 (25 + 2 + 1)
createTick('C', 40.0, verticalWidth: 9.0), // 29.0 - 40.0 (40-9-2)
];
final report = drawStrategy.collides(ticks, AxisOrientation.left);
expect(report.ticksCollide, isTrue);
});
});
group('collision detection - horizontally drawn axis', () {
test('ticks do not collide for TickLabelAnchor.before', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 2,
labelAnchor: TickLabelAnchor.before);
final ticks = [
createTick('A', 10.0, horizontalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2)
createTick('B', 20.0, horizontalWidth: 8.0), // 20.0 - 30.0 (28.0 + 2)
createTick('C', 30.0, horizontalWidth: 8.0), // 30.0 - 40.0 (38.0 + 2)
];
final report = drawStrategy.collides(ticks, AxisOrientation.bottom);
expect(report.ticksCollide, isFalse);
});
test('ticks do not collide for TickLabelAnchor.inside', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 0,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('A', 10.0,
horizontalWidth: 10.0,
textDirection: TextDirection.ltr), // 10.0 - 20.0
createTick('B', 25.0,
horizontalWidth: 10.0,
textDirection: TextDirection.center), // 20.0 - 30.0
createTick('C', 40.0,
horizontalWidth: 10.0,
textDirection: TextDirection.rtl), // 30.0 - 40.0
];
final report = drawStrategy.collides(ticks, AxisOrientation.bottom);
expect(report.ticksCollide, isFalse);
});
test('ticks collide - first tick too large', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 0,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('A', 10.0, horizontalWidth: 11.0), // 10.0 - 21.0
createTick('B', 25.0, horizontalWidth: 10.0), // 20.0 - 30.0
createTick('C', 40.0, horizontalWidth: 10.0), // 30.0 - 40.0
];
final report = drawStrategy.collides(ticks, AxisOrientation.bottom);
expect(report.ticksCollide, isTrue);
});
test('ticks collide - middle tick too large', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 0,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('A', 10.0, horizontalWidth: 10.0), // 10.0 - 20.0
createTick('B', 25.0, horizontalWidth: 11.0), // 19.5 - 30.5
createTick('C', 40.0, horizontalWidth: 10.0), // 30.0 - 40.0
];
final report = drawStrategy.collides(ticks, AxisOrientation.bottom);
expect(report.ticksCollide, isTrue);
});
test('ticks collide - last tick too large', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 0,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('A', 10.0, horizontalWidth: 10.0), // 10.0 - 20.0
createTick('B', 25.0, horizontalWidth: 10.0), // 20.0 - 30.0
createTick('C', 40.0, horizontalWidth: 11.0), // 29.0 - 40.0
];
final report = drawStrategy.collides(ticks, AxisOrientation.bottom);
expect(report.ticksCollide, isTrue);
});
});
group('collision detection - unsorted ticks', () {
test('ticks do not collide', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 0,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('C', 40.0, horizontalWidth: 10.0), // 30.0 - 40.0
createTick('B', 25.0, horizontalWidth: 10.0), // 20.0 - 30.0
createTick('A', 10.0, horizontalWidth: 10.0), // 10.0 - 20.0
];
final report = drawStrategy.collides(ticks, AxisOrientation.bottom);
expect(report.ticksCollide, isFalse);
});
test('ticks collide - tick B is too large', () {
final drawStrategy = new BaseTickDrawStrategyImpl(
chartContext, graphicsFactory,
minimumPaddingBetweenLabelsPx: 0,
labelAnchor: TickLabelAnchor.inside);
final ticks = [
createTick('A', 10.0, horizontalWidth: 10.0), // 10.0 - 20.0
createTick('C', 40.0, horizontalWidth: 10.0), // 30.0 - 40.0
createTick('B', 25.0, horizontalWidth: 11.0), // 19.5 - 30.5
];
final report = drawStrategy.collides(ticks, AxisOrientation.bottom);
expect(report.ticksCollide, isTrue);
});
});
group('collision detection - corner cases', () {
test('null ticks do not collide', () {
final drawStrategy =
new BaseTickDrawStrategyImpl(chartContext, graphicsFactory);
final report = drawStrategy.collides(null, AxisOrientation.left);
expect(report.ticksCollide, isFalse);
});
test('empty tick list do not collide', () {
final drawStrategy =
new BaseTickDrawStrategyImpl(chartContext, graphicsFactory);
final report = drawStrategy.collides([], AxisOrientation.left);
expect(report.ticksCollide, isFalse);
});
test('single tick does not collide', () {
final drawStrategy =
new BaseTickDrawStrategyImpl(chartContext, graphicsFactory);
final report = drawStrategy.collides(
[createTick('A', 10.0, horizontalWidth: 10.0)],
AxisOrientation.bottom);
expect(report.ticksCollide, isFalse);
});
});
}

View File

@@ -0,0 +1,237 @@
// 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:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart';
import 'package:charts_common/src/common/graphics_factory.dart';
import 'package:charts_common/src/common/line_style.dart';
import 'package:charts_common/src/common/text_style.dart';
import 'package:charts_common/src/common/text_element.dart';
import 'package:charts_common/src/chart/common/chart_canvas.dart';
import 'package:charts_common/src/chart/common/chart_context.dart';
import 'package:charts_common/src/chart/cartesian/axis/collision_report.dart';
import 'package:charts_common/src/chart/cartesian/axis/end_points_tick_provider.dart';
import 'package:charts_common/src/chart/cartesian/axis/numeric_scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/simple_ordinal_scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart';
import 'package:charts_common/src/chart/cartesian/axis/numeric_extents.dart';
import 'package:charts_common/src/chart/cartesian/axis/time/date_time_extents.dart';
import 'package:charts_common/src/chart/cartesian/axis/time/date_time_scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/time/date_time_tick_formatter.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'time/simple_date_time_factory.dart' show SimpleDateTimeFactory;
class MockDateTimeScale extends Mock implements DateTimeScale {}
class MockNumericScale extends Mock implements NumericScale {}
class MockOrdinalScale extends Mock implements SimpleOrdinalScale {}
/// A fake draw strategy that reports collision and alternate ticks
///
/// Reports collision when the tick count is greater than or equal to
/// [collidesAfterTickCount].
///
/// Reports alternate rendering after tick count is greater than or equal to
/// [alternateRenderingAfterTickCount].
class FakeDrawStrategy<D> extends BaseTickDrawStrategy<D> {
final int collidesAfterTickCount;
final int alternateRenderingAfterTickCount;
FakeDrawStrategy(
this.collidesAfterTickCount, this.alternateRenderingAfterTickCount)
: super(null, new FakeGraphicsFactory());
@override
CollisionReport collides(List<Tick<D>> ticks, _) {
final ticksCollide = ticks.length >= collidesAfterTickCount;
final alternateTicksUsed = ticks.length >= alternateRenderingAfterTickCount;
return new CollisionReport(
ticksCollide: ticksCollide,
ticks: ticks,
alternateTicksUsed: alternateTicksUsed);
}
@override
void draw(ChartCanvas canvas, Tick<D> tick,
{AxisOrientation orientation,
Rectangle<int> axisBounds,
Rectangle<int> drawAreaBounds,
bool isFirst,
bool isLast}) {}
}
/// A fake [GraphicsFactory] that returns [MockTextStyle] and [MockTextElement].
class FakeGraphicsFactory extends GraphicsFactory {
@override
TextStyle createTextPaint() => new MockTextStyle();
@override
TextElement createTextElement(String text) => new MockTextElement();
@override
LineStyle createLinePaint() => new MockLinePaint();
}
class MockTextStyle extends Mock implements TextStyle {}
class MockTextElement extends Mock implements TextElement {}
class MockLinePaint extends Mock implements LineStyle {}
class MockChartContext extends Mock implements ChartContext {}
void main() {
const dateTimeFactory = const SimpleDateTimeFactory();
FakeGraphicsFactory graphicsFactory;
EndPointsTickProvider tickProvider;
ChartContext context;
setUp(() {
graphicsFactory = new FakeGraphicsFactory();
context = new MockChartContext();
});
test('dateTime_choosesEndPointTicks', () {
final formatter = new DateTimeTickFormatter(dateTimeFactory);
final scale = new MockDateTimeScale();
tickProvider = new EndPointsTickProvider<DateTime>();
final drawStrategy = new FakeDrawStrategy<DateTime>(10, 10);
when(scale.viewportDomain).thenReturn(new DateTimeExtents(
start: new DateTime(2018, 8, 1), end: new DateTime(2018, 8, 11)));
when(scale.rangeWidth).thenReturn(1000);
when(scale.domainStepSize).thenReturn(1000.0);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <DateTime, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks, hasLength(2));
expect(ticks[0].value, equals(new DateTime(2018, 8, 1)));
expect(ticks[1].value, equals(new DateTime(2018, 8, 11)));
});
test('numeric_choosesEndPointTicks', () {
final formatter = new NumericTickFormatter();
final scale = new MockNumericScale();
tickProvider = new EndPointsTickProvider<num>();
final drawStrategy = new FakeDrawStrategy<num>(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 70.0));
when(scale.rangeWidth).thenReturn(1000);
when(scale.domainStepSize).thenReturn(1000.0);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks, hasLength(2));
expect(ticks[0].value, equals(10));
expect(ticks[1].value, equals(70));
});
test('ordinal_choosesEndPointTicks', () {
final formatter = new OrdinalTickFormatter();
final scale = new SimpleOrdinalScale();
scale.addDomain('A');
scale.addDomain('B');
scale.addDomain('C');
scale.addDomain('D');
tickProvider = new EndPointsTickProvider<String>();
final drawStrategy = new FakeDrawStrategy<String>(10, 10);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <String, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks, hasLength(2));
expect(ticks[0].value, equals('A'));
expect(ticks[1].value, equals('D'));
});
test('dateTime_emptySeriesChoosesNoTicks', () {
final formatter = new DateTimeTickFormatter(dateTimeFactory);
final scale = new MockDateTimeScale();
tickProvider = new EndPointsTickProvider<DateTime>();
final drawStrategy = new FakeDrawStrategy<DateTime>(10, 10);
when(scale.viewportDomain).thenReturn(new DateTimeExtents(
start: new DateTime(2018, 8, 1), end: new DateTime(2018, 8, 11)));
when(scale.rangeWidth).thenReturn(1000);
// An un-configured axis has no domain step size, and its scale defaults to
// infinity.
when(scale.domainStepSize).thenReturn(double.infinity);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <DateTime, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks, hasLength(0));
});
test('numeric_emptySeriesChoosesNoTicks', () {
final formatter = new NumericTickFormatter();
final scale = new MockNumericScale();
tickProvider = new EndPointsTickProvider<num>();
final drawStrategy = new FakeDrawStrategy<num>(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 70.0));
when(scale.rangeWidth).thenReturn(1000);
// An un-configured axis has no domain step size, and its scale defaults to
// infinity.
when(scale.domainStepSize).thenReturn(double.infinity);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks, hasLength(0));
});
}

View File

@@ -0,0 +1,307 @@
// 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/numeric_extents.dart'
show NumericExtents;
import 'package:charts_common/src/chart/cartesian/axis/linear/linear_scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/scale.dart'
show RangeBandConfig, ScaleOutputExtent, StepSizeConfig;
import 'package:test/test.dart';
const EPSILON = 0.001;
void main() {
group('Stacking bars', () {
test('basic apply survives copy and reset', () {
LinearScale scale = new LinearScale();
scale.addDomain(100.0);
scale.addDomain(130.0);
scale.addDomain(200.0);
scale.addDomain(170.0);
scale.range = new ScaleOutputExtent(2000, 1000);
expect(scale.range.start, equals(2000));
expect(scale.range.end, equals(1000));
expect(scale.range.diff, equals(-1000));
expect(scale.dataExtent.min, equals(100.0));
expect(scale.dataExtent.max, equals(200.0));
expect(scale[100.0], closeTo(2000, EPSILON));
expect(scale[200.0], closeTo(1000, EPSILON));
expect(scale[166.0], closeTo(1340, EPSILON));
expect(scale[0.0], closeTo(3000, EPSILON));
expect(scale[300.0], closeTo(0, EPSILON));
// test copy
LinearScale other = scale.copy();
expect(other[166.0], closeTo(1340, EPSILON));
expect(other.range.start, equals(2000));
expect(other.range.end, equals(1000));
// test reset
other.resetDomain();
other.resetViewportSettings();
other.addDomain(10.0);
other.addDomain(20.0);
expect(other.dataExtent.min, equals(10.0));
expect(other.dataExtent.max, equals(20.0));
expect(other.viewportDomain.min, equals(10.0));
expect(other.viewportDomain.max, equals(20.0));
expect(other[15.0], closeTo(1500, EPSILON));
// original scale shouldn't have been touched.
expect(scale[166.0], closeTo(1340, EPSILON));
// should always return true.
expect(scale.canTranslate(3.14), isTrue);
});
test('viewport assigned domain extent applies to scale', () {
LinearScale scale = new LinearScale()..keepViewportWithinData = false;
scale.addDomain(50.0);
scale.addDomain(70.0);
scale.viewportDomain = new NumericExtents(100.0, 200.0);
scale.range = new ScaleOutputExtent(0, 200);
expect(scale[200.0], closeTo(200, EPSILON));
expect(scale[100.0], closeTo(0, EPSILON));
expect(scale[50.0], closeTo(-100, EPSILON));
expect(scale[150.0], closeTo(100, EPSILON));
scale.resetDomain();
scale.resetViewportSettings();
scale.addDomain(50.0);
scale.addDomain(100.0);
scale.viewportDomain = new NumericExtents(0.0, 100.0);
scale.range = new ScaleOutputExtent(0, 200);
expect(scale[0.0], closeTo(0, EPSILON));
expect(scale[100.0], closeTo(200, EPSILON));
expect(scale[50.0], closeTo(100, EPSILON));
expect(scale[200.0], closeTo(400, EPSILON));
});
test('comparing domain and range to viewport handles extent edges', () {
LinearScale scale = new LinearScale();
scale.range = new ScaleOutputExtent(1000, 1400);
scale.domainOverride = new NumericExtents(100.0, 300.0);
scale.viewportDomain = new NumericExtents(200.0, 300.0);
expect(scale.viewportDomain, equals(new NumericExtents(200.0, 300.0)));
expect(scale[210.0], closeTo(1040, EPSILON));
expect(scale[400.0], closeTo(1800, EPSILON));
expect(scale[100.0], closeTo(600, EPSILON));
expect(scale.compareDomainValueToViewport(199.0), equals(-1));
expect(scale.compareDomainValueToViewport(200.0), equals(0));
expect(scale.compareDomainValueToViewport(201.0), equals(0));
expect(scale.compareDomainValueToViewport(299.0), equals(0));
expect(scale.compareDomainValueToViewport(300.0), equals(0));
expect(scale.compareDomainValueToViewport(301.0), equals(1));
expect(scale.isRangeValueWithinViewport(999.0), isFalse);
expect(scale.isRangeValueWithinViewport(1100.0), isTrue);
expect(scale.isRangeValueWithinViewport(1401.0), isFalse);
});
test('scale applies in reverse', () {
LinearScale scale = new LinearScale();
scale.range = new ScaleOutputExtent(1000, 1400);
scale.domainOverride = new NumericExtents(100.0, 300.0);
scale.viewportDomain = new NumericExtents(200.0, 300.0);
expect(scale.reverse(1040.0), closeTo(210.0, EPSILON));
expect(scale.reverse(1800.0), closeTo(400.0, EPSILON));
expect(scale.reverse(600.0), closeTo(100.0, EPSILON));
});
test('scale works with a range from larger to smaller', () {
LinearScale scale = new LinearScale();
scale.range = new ScaleOutputExtent(1400, 1000);
scale.domainOverride = new NumericExtents(100.0, 300.0);
scale.viewportDomain = new NumericExtents(200.0, 300.0);
expect(scale[200.0], closeTo(1400.0, EPSILON));
expect(scale[250.0], closeTo(1200.0, EPSILON));
expect(scale[300.0], closeTo(1000.0, EPSILON));
});
test('scaleFactor and translate applies to scale', () {
LinearScale scale = new LinearScale();
scale.range = new ScaleOutputExtent(1000, 1200);
scale.domainOverride = new NumericExtents(100.0, 200.0);
scale.setViewportSettings(4.0, -50.0);
expect(scale[100.0], closeTo(950.0, EPSILON));
expect(scale[200.0], closeTo(1750.0, EPSILON));
expect(scale[150.0], closeTo(1350.0, EPSILON));
expect(scale[106.25], closeTo(1000.0, EPSILON));
expect(scale[131.25], closeTo(1200.0, EPSILON));
expect(scale.compareDomainValueToViewport(106.0), equals(-1));
expect(scale.compareDomainValueToViewport(106.25), equals(0));
expect(scale.compareDomainValueToViewport(107.0), equals(0));
expect(scale.compareDomainValueToViewport(131.0), equals(0));
expect(scale.compareDomainValueToViewport(131.25), equals(0));
expect(scale.compareDomainValueToViewport(132.0), equals(1));
expect(scale.isRangeValueWithinViewport(999.0), isFalse);
expect(scale.isRangeValueWithinViewport(1100.0), isTrue);
expect(scale.isRangeValueWithinViewport(1201.0), isFalse);
});
test('scale handles single point', () {
LinearScale domainScale = new LinearScale();
domainScale.range = new ScaleOutputExtent(1000, 1200);
domainScale.addDomain(50.0);
// A single point should render in the middle of the scale.
expect(domainScale[50.0], closeTo(1100.0, EPSILON));
});
test('testAllZeros', () {
LinearScale measureScale = new LinearScale();
measureScale.range = new ScaleOutputExtent(1000, 1200);
measureScale.addDomain(0.0);
expect(measureScale[0.0], closeTo(1100.0, EPSILON));
});
test('scale calculates step size', () {
LinearScale scale = new LinearScale();
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0);
scale.addDomain(1.0);
scale.addDomain(3.0);
scale.addDomain(11.0);
scale.range = new ScaleOutputExtent(100, 200);
// 1 - 11 has 6 steps of size 2, 0 - 12
expect(scale.rangeBand, closeTo(100.0 / 6.0, EPSILON));
});
test('scale applies rangeBand to detected step size', () {
LinearScale scale = new LinearScale();
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(0.5);
scale.addDomain(1.0);
scale.addDomain(2.0);
scale.addDomain(10.0);
scale.range = new ScaleOutputExtent(100, 200);
// 100 range / 10 steps * 0.5percentStep = 5
expect(scale.rangeBand, closeTo(5.0, EPSILON));
});
test('scale stepSize calculation survives copy', () {
LinearScale scale = new LinearScale();
scale.stepSizeConfig = new StepSizeConfig.fixedDomain(1.0);
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0);
scale.addDomain(1.0);
scale.addDomain(3.0);
scale.range = new ScaleOutputExtent(100, 200);
expect(scale.copy().rangeBand, closeTo(100.0 / 3.0, EPSILON));
});
test('scale rangeBand calculation survives copy', () {
LinearScale scale = new LinearScale();
scale.rangeBandConfig = new RangeBandConfig.fixedPixel(123.0);
scale.addDomain(1.0);
scale.addDomain(3.0);
scale.range = new ScaleOutputExtent(100, 200);
expect(scale.copy().rangeBand, closeTo(123, EPSILON));
});
test('scale rangeBand works for single domain value', () {
LinearScale scale = new LinearScale();
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0);
scale.addDomain(1.0);
scale.range = new ScaleOutputExtent(100, 200);
expect(scale.rangeBand, closeTo(100, EPSILON));
});
test('scale rangeBand works for multiple domains of the same value', () {
LinearScale scale = new LinearScale();
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0);
scale.addDomain(1.0);
scale.addDomain(1.0);
scale.range = new ScaleOutputExtent(100, 200);
expect(scale.rangeBand, closeTo(100.0, EPSILON));
});
test('scale rangeBand is zero when no domains are added', () {
LinearScale scale = new LinearScale();
scale.range = new ScaleOutputExtent(100, 200);
expect(scale.rangeBand, closeTo(0.0, EPSILON));
});
test('scale domain info reset on resetDomain', () {
LinearScale scale = new LinearScale();
scale.addDomain(1.0);
scale.addDomain(3.0);
scale.range = new ScaleOutputExtent(100, 200);
scale.setViewportSettings(1000.0, 2000.0);
scale.resetDomain();
scale.resetViewportSettings();
expect(scale.viewportScalingFactor, closeTo(1.0, EPSILON));
expect(scale.viewportTranslatePx, closeTo(0, EPSILON));
expect(scale.range, equals(new ScaleOutputExtent(100, 200)));
});
test('scale handles null domain values', () {
LinearScale scale = new LinearScale();
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0);
scale.addDomain(1.0);
scale.addDomain(null);
scale.addDomain(3.0);
scale.addDomain(11.0);
scale.range = new ScaleOutputExtent(100, 200);
expect(scale.rangeBand, closeTo(100.0 / 6.0, EPSILON));
});
test('scale domainOverride survives copy', () {
LinearScale scale = new LinearScale()..keepViewportWithinData = false;
scale.addDomain(1.0);
scale.addDomain(3.0);
scale.range = new ScaleOutputExtent(100, 200);
scale.setViewportSettings(2.0, 10.0);
scale.domainOverride = new NumericExtents(0.0, 100.0);
LinearScale other = scale.copy();
expect(other.domainOverride, equals(new NumericExtents(0.0, 100.0)));
expect(other[5.0], closeTo(120.0, EPSILON));
});
test('scale calculates a scaleFactor given a domain window', () {
LinearScale scale = new LinearScale();
scale.addDomain(100.0);
scale.addDomain(130.0);
scale.addDomain(200.0);
scale.addDomain(170.0);
expect(scale.computeViewportScaleFactor(10.0), closeTo(10, EPSILON));
expect(scale.computeViewportScaleFactor(100.0), closeTo(1, EPSILON));
});
});
}

View File

@@ -0,0 +1,498 @@
// 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:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart';
import 'package:charts_common/src/common/graphics_factory.dart';
import 'package:charts_common/src/common/line_style.dart';
import 'package:charts_common/src/common/text_style.dart';
import 'package:charts_common/src/common/text_element.dart';
import 'package:charts_common/src/chart/common/chart_canvas.dart';
import 'package:charts_common/src/chart/common/chart_context.dart';
import 'package:charts_common/src/chart/common/unitconverter/unit_converter.dart';
import 'package:charts_common/src/chart/cartesian/axis/collision_report.dart';
import 'package:charts_common/src/chart/cartesian/axis/numeric_scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick_provider.dart';
import 'package:charts_common/src/chart/cartesian/axis/numeric_extents.dart';
import 'package:charts_common/src/chart/cartesian/axis/numeric_tick_provider.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockNumericScale extends Mock implements NumericScale {}
/// A fake draw strategy that reports collision and alternate ticks
///
/// Reports collision when the tick count is greater than or equal to
/// [collidesAfterTickCount].
///
/// Reports alternate rendering after tick count is greater than or equal to
/// [alternateRenderingAfterTickCount].
class FakeDrawStrategy extends BaseTickDrawStrategy<num> {
final int collidesAfterTickCount;
final int alternateRenderingAfterTickCount;
FakeDrawStrategy(
this.collidesAfterTickCount, this.alternateRenderingAfterTickCount)
: super(null, new FakeGraphicsFactory());
@override
CollisionReport collides(List<Tick<num>> ticks, _) {
final ticksCollide = ticks.length >= collidesAfterTickCount;
final alternateTicksUsed = ticks.length >= alternateRenderingAfterTickCount;
return new CollisionReport(
ticksCollide: ticksCollide,
ticks: ticks,
alternateTicksUsed: alternateTicksUsed);
}
@override
void draw(ChartCanvas canvas, Tick<num> tick,
{AxisOrientation orientation,
Rectangle<int> axisBounds,
Rectangle<int> drawAreaBounds,
bool isFirst,
bool isLast}) {}
}
/// A fake [GraphicsFactory] that returns [MockTextStyle] and [MockTextElement].
class FakeGraphicsFactory extends GraphicsFactory {
@override
TextStyle createTextPaint() => new MockTextStyle();
@override
TextElement createTextElement(String text) => new MockTextElement();
@override
LineStyle createLinePaint() => new MockLinePaint();
}
class MockTextStyle extends Mock implements TextStyle {}
class MockTextElement extends Mock implements TextElement {}
class MockLinePaint extends Mock implements LineStyle {}
class MockChartContext extends Mock implements ChartContext {}
/// A celsius to fahrenheit converter for testing axis with unit converter.
class CelsiusToFahrenheitConverter implements UnitConverter<num, num> {
const CelsiusToFahrenheitConverter();
@override
num convert(num value) => (value * 1.8) + 32.0;
@override
num invert(num value) => (value - 32.0) / 1.8;
}
void main() {
FakeGraphicsFactory graphicsFactory;
MockNumericScale scale;
NumericTickProvider tickProvider;
TickFormatter<num> formatter;
ChartContext context;
setUp(() {
graphicsFactory = new FakeGraphicsFactory();
scale = new MockNumericScale();
tickProvider = new NumericTickProvider();
formatter = new NumericTickFormatter();
context = new MockChartContext();
});
test('singleTickCount_choosesTicksWithSmallestStepCoveringDomain', () {
tickProvider
..zeroBound = false
..dataIsInWholeNumbers = false
..setFixedTickCount(4)
..allowedSteps = [1.0, 2.5, 5.0];
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 70.0));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks, hasLength(4));
expect(ticks[0].value, equals(0));
expect(ticks[1].value, equals(25));
expect(ticks[2].value, equals(50));
expect(ticks[3].value, equals(75));
});
test(
'tickCountRangeChoosesTicksWithMostTicksAndSmallestIntervalCoveringDomain',
() {
tickProvider
..zeroBound = false
..dataIsInWholeNumbers = false
..setTickCount(5, 3)
..allowedSteps = [1.0, 2.5, 5.0];
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 80.0));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks, hasLength(5));
expect(ticks[0].value, equals(0));
expect(ticks[1].value, equals(25));
expect(ticks[2].value, equals(50));
expect(ticks[3].value, equals(75));
expect(ticks[4].value, equals(100));
});
test('choosesNonAlternateRenderingTicksEvenIfIntervalIsLarger', () {
tickProvider
..zeroBound = false
..dataIsInWholeNumbers = false
..setTickCount(5, 3)
..allowedSteps = [1.0, 2.5, 6.0];
final drawStrategy = new FakeDrawStrategy(10, 5);
when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 80.0));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks, hasLength(3));
expect(ticks[0].value, equals(0));
expect(ticks[1].value, equals(60));
expect(ticks[2].value, equals(120));
});
test('choosesNonCollidingTicksEvenIfIntervalIsLarger', () {
tickProvider
..zeroBound = false
..dataIsInWholeNumbers = false
..setTickCount(5, 3)
..allowedSteps = [1.0, 2.5, 6.0];
final drawStrategy = new FakeDrawStrategy(5, 5);
when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 80.0));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks, hasLength(3));
expect(ticks[0].value, equals(0));
expect(ticks[1].value, equals(60));
expect(ticks[2].value, equals(120));
});
test('zeroBound_alwaysReturnsZeroTick', () {
tickProvider
..zeroBound = true
..dataIsInWholeNumbers = false
..setFixedTickCount(3)
..allowedSteps = [1.0, 2.5, 5.0];
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(55.0, 135.0));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
final tickValues = ticks.map((tick) => tick.value).toList();
expect(tickValues, contains(0.0));
});
test('boundsCrossOrigin_alwaysReturnsZeroTick', () {
tickProvider
..zeroBound = false
..dataIsInWholeNumbers = false
..setFixedTickCount(3)
..allowedSteps = [1.0, 2.5, 5.0];
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(-55.0, 135.0));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
final tickValues = ticks.map((tick) => tick.value).toList();
expect(tickValues, contains(0.0));
});
test('boundsCrossOrigin_returnsValidTickRange', () {
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(-55.0, 135.0));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
final tickValues = ticks.map((tick) => tick.value).toList();
// We expect to see a range of ticks that crosses zero.
expect(tickValues,
equals([-60.0, -30.0, 0.0, 30.0, 60.0, 90.0, 120.0, 150.0]));
});
test('dataIsWholeNumbers_returnsWholeNumberTicks', () {
tickProvider
..zeroBound = false
..dataIsInWholeNumbers = true
..setFixedTickCount(3)
..allowedSteps = [1.0, 2.5, 5.0];
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(0.25, 0.75));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks[0].value, equals(0));
expect(ticks[1].value, equals(1));
expect(ticks[2].value, equals(2));
});
test('choosesTicksBasedOnPreferredAxisUnits', () {
tickProvider
..zeroBound = true
..dataIsInWholeNumbers = false
..setFixedTickCount(3)
..allowedSteps = [5.0]
..dataToAxisUnitConverter = const CelsiusToFahrenheitConverter();
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(0.0, 20.0));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks[0].value, closeTo(-17.8, 0.1)); // 0 in axis units
expect(ticks[1].value, closeTo(10, 0.1)); // 50 in axis units
expect(ticks[2].value, closeTo(37.8, 0.1)); // 100 in axis units
});
test('handlesVerySmallMeasures', () {
tickProvider
..zeroBound = true
..dataIsInWholeNumbers = false
..setFixedTickCount(5);
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain)
.thenReturn(new NumericExtents(0.000001, 0.000002));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks.length, equals(5));
expect(ticks[0].value, equals(0));
expect(ticks[1].value, equals(0.0000005));
expect(ticks[2].value, equals(0.0000010));
expect(ticks[3].value, equals(0.0000015));
expect(ticks[4].value, equals(0.000002));
});
test('handlesVerySmallMeasuresForWholeNumbers', () {
tickProvider
..zeroBound = true
..dataIsInWholeNumbers = true
..setFixedTickCount(5);
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain)
.thenReturn(new NumericExtents(0.000001, 0.000002));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks.length, equals(5));
expect(ticks[0].value, equals(0));
expect(ticks[1].value, equals(1));
expect(ticks[2].value, equals(2));
expect(ticks[3].value, equals(3));
expect(ticks[4].value, equals(4));
});
test('handlesVerySmallMeasuresForWholeNumbersWithoutZero', () {
tickProvider
..zeroBound = false
..dataIsInWholeNumbers = true
..setFixedTickCount(5);
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain)
.thenReturn(new NumericExtents(101.000001, 101.000002));
when(scale.rangeWidth).thenReturn(1000);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(ticks.length, equals(5));
expect(ticks[0].value, equals(101));
expect(ticks[1].value, equals(102));
expect(ticks[2].value, equals(103));
expect(ticks[3].value, equals(104));
expect(ticks[4].value, equals(105));
});
test('handles tick hint for non zero ticks', () {
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(20.0, 35.0));
when(scale.rangeWidth).thenReturn(1000);
// Step Size: 3,
// Previous start tick: 10
// Previous window: 10 - 25
// Previous ticks: 10, 13, 16, 19, 22, 25
final tickHint = new TickHint(10, 25, tickCount: 6);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null,
tickHint: tickHint,
);
// New adjusted ticks for window 20 - 35
// Should have ticks 22, 25, 28, 31, 34, 37
expect(ticks, hasLength(6));
expect(ticks[0].value, equals(22));
expect(ticks[1].value, equals(25));
expect(ticks[2].value, equals(28));
expect(ticks[3].value, equals(31));
expect(ticks[4].value, equals(34));
expect(ticks[5].value, equals(37));
});
test('handles tick hint for negative starting ticks', () {
final drawStrategy = new FakeDrawStrategy(10, 10);
when(scale.viewportDomain).thenReturn(new NumericExtents(-35.0, -20.0));
when(scale.rangeWidth).thenReturn(1000);
// Step Size: 3,
// Previous start tick: -25
// Previous window: -25 to -10
// Previous ticks: -25, -22, -19, -16, -13, -10
final tickHint = new TickHint(-25, -10, tickCount: 6);
final ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null,
tickHint: tickHint,
);
// New adjusted ticks for window -35 to -20
// Should have ticks -34, -31, -28, -25, -22, -19
expect(ticks, hasLength(6));
expect(ticks[0].value, equals(-34));
expect(ticks[1].value, equals(-31));
expect(ticks[2].value, equals(-28));
expect(ticks[3].value, equals(-25));
expect(ticks[4].value, equals(-22));
expect(ticks[5].value, equals(-19));
});
}

View File

@@ -0,0 +1,250 @@
// 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/scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/simple_ordinal_scale.dart';
import 'package:test/test.dart';
const EPSILON = 0.001;
void main() {
SimpleOrdinalScale scale;
setUp(() {
scale = new SimpleOrdinalScale();
scale.addDomain('a');
scale.addDomain('b');
scale.addDomain('c');
scale.addDomain('d');
scale.range = new ScaleOutputExtent(2000, 1000);
});
group('conversion', () {
test('with duplicate keys', () {
scale.addDomain('c');
scale.addDomain('a');
// Current RangeBandConfig.styleAssignedPercent sets size to 0.65 percent.
expect(scale.rangeBand, closeTo(250 * 0.65, EPSILON));
expect(scale['a'], closeTo(2000 - 125, EPSILON));
expect(scale['b'], closeTo(2000 - 375, EPSILON));
expect(scale['c'], closeTo(2000 - 625, EPSILON));
});
test('invalid domain does not throw exception', () {
expect(scale['e'], 0);
});
test('invalid domain can translate is false', () {
expect(scale.canTranslate('e'), isFalse);
});
});
group('copy', () {
test('can convert domain', () {
final copied = scale.copy();
expect(copied['c'], closeTo(2000 - 625, EPSILON));
});
test('does not affect original', () {
final copied = scale.copy();
copied.addDomain('bar');
expect(copied.canTranslate('bar'), isTrue);
expect(scale.canTranslate('bar'), isFalse);
});
});
group('reset', () {
test('clears domains', () {
scale.resetDomain();
scale.addDomain('foo');
scale.addDomain('bar');
expect(scale['foo'], closeTo(2000 - 250, EPSILON));
});
});
group('set RangeBandConfig', () {
test('fixed pixel range band changes range band', () {
scale.rangeBandConfig = new RangeBandConfig.fixedPixel(123.0);
expect(scale.rangeBand, closeTo(123.0, EPSILON));
// Adding another domain to ensure it still doesn't change.
scale.addDomain('foo');
expect(scale.rangeBand, closeTo(123.0, EPSILON));
});
test('percent range band changes range band', () {
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(0.5);
// 125 = 0.5f * 1000pixels / 4domains
expect(scale.rangeBand, closeTo(125.0, EPSILON));
});
test('space from step changes range band', () {
scale.rangeBandConfig =
new RangeBandConfig.fixedPixelSpaceBetweenStep(50.0);
// 200 = 1000pixels / 4domains) - 50
expect(scale.rangeBand, closeTo(200.0, EPSILON));
});
test('fixed domain throws argument exception', () {
expect(() => scale.rangeBandConfig = new RangeBandConfig.fixedDomain(5.0),
throwsArgumentError);
});
test('type of none throws argument exception', () {
expect(() => scale.rangeBandConfig = new RangeBandConfig.none(),
throwsArgumentError);
});
test('set to null throws argument exception', () {
expect(() => scale.rangeBandConfig = null, throwsArgumentError);
});
});
group('set step size config', () {
test('to null does not throw', () {
scale.stepSizeConfig = null;
});
test('to auto does not throw', () {
scale.stepSizeConfig = new StepSizeConfig.auto();
});
test('to fixed domain throw arugment exception', () {
expect(() => scale.stepSizeConfig = new StepSizeConfig.fixedDomain(1.0),
throwsArgumentError);
});
test('to fixed pixel throw arugment exception', () {
expect(() => scale.stepSizeConfig = new StepSizeConfig.fixedPixels(1.0),
throwsArgumentError);
});
});
group('set range persists', () {
test('', () {
expect(scale.range.start, equals(2000));
expect(scale.range.end, equals(1000));
expect(scale.range.min, equals(1000));
expect(scale.range.max, equals(2000));
expect(scale.rangeWidth, equals(1000));
expect(scale.isRangeValueWithinViewport(1500.0), isTrue);
expect(scale.isRangeValueWithinViewport(1000.0), isTrue);
expect(scale.isRangeValueWithinViewport(2000.0), isTrue);
expect(scale.isRangeValueWithinViewport(500.0), isFalse);
expect(scale.isRangeValueWithinViewport(2500.0), isFalse);
});
});
group('scale factor', () {
test('sets', () {
scale.setViewportSettings(2.0, -700.0);
expect(scale.viewportScalingFactor, closeTo(2.0, EPSILON));
expect(scale.viewportTranslatePx, closeTo(-700.0, EPSILON));
});
test('rangeband is scaled', () {
scale.setViewportSettings(2.0, -700.0);
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0);
expect(scale.rangeBand, closeTo(500.0, EPSILON));
});
test('translate to pixels is scaled', () {
scale.setViewportSettings(2.0, -700.0);
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0);
scale.range = new ScaleOutputExtent(1000, 2000);
final scaledStepWidth = 500.0;
final scaledInitialShift = 250.0;
expect(scale['a'], closeTo(1000 + scaledInitialShift - 700, EPSILON));
expect(scale['b'],
closeTo(1000 + scaledInitialShift - 700 + scaledStepWidth, EPSILON));
});
test('only b and c should be within the viewport', () {
scale.setViewportSettings(2.0, -700.0);
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0);
scale.range = new ScaleOutputExtent(1000, 2000);
expect(scale.compareDomainValueToViewport('a'), equals(-1));
expect(scale.compareDomainValueToViewport('c'), equals(0));
expect(scale.compareDomainValueToViewport('d'), equals(1));
expect(scale.compareDomainValueToViewport('f'), isNot(0));
});
});
group('viewport', () {
test('set adjust scale to show viewport', () {
scale.range = new ScaleOutputExtent(1000, 2000);
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(0.5);
scale.setViewport(2, 'b');
expect(scale['a'], closeTo(750, EPSILON));
expect(scale['b'], closeTo(1250, EPSILON));
expect(scale['c'], closeTo(1750, EPSILON));
expect(scale['d'], closeTo(2250, EPSILON));
expect(scale.compareDomainValueToViewport('a'), equals(-1));
expect(scale.compareDomainValueToViewport('b'), equals(0));
expect(scale.compareDomainValueToViewport('c'), equals(0));
expect(scale.compareDomainValueToViewport('d'), equals(1));
});
test('illegal to set window size less than one', () {
expect(() => scale.setViewport(0, 'b'), throwsArgumentError);
});
test('set starting value if starting domain is not in domain list', () {
scale.range = new ScaleOutputExtent(1000, 2000);
scale.rangeBandConfig = new RangeBandConfig.percentOfStep(0.5);
scale.setViewport(2, 'f');
expect(scale['a'], closeTo(1250, EPSILON));
expect(scale['b'], closeTo(1750, EPSILON));
expect(scale['c'], closeTo(2250, EPSILON));
expect(scale['d'], closeTo(2750, EPSILON));
});
test('get size returns number of full steps that fit scale range', () {
scale.range = new ScaleOutputExtent(1000, 2000);
scale.setViewportSettings(2.0, 0.0);
expect(scale.viewportDataSize, equals(2));
scale.setViewportSettings(5.0, 0.0);
expect(scale.viewportDataSize, equals(0));
});
test('get starting viewport gets first fully visible domain', () {
scale.range = new ScaleOutputExtent(1000, 2000);
scale.setViewportSettings(2.0, -500.0);
expect(scale.viewportStartingDomain, equals('b'));
scale.setViewportSettings(2.0, -100.0);
expect(scale.viewportStartingDomain, equals('b'));
});
});
}

View File

@@ -0,0 +1,180 @@
// 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/static_tick_provider.dart';
import 'package:charts_common/src/chart/cartesian/axis/linear/linear_scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart';
import 'package:charts_common/src/common/graphics_factory.dart';
import 'package:charts_common/src/chart/common/chart_context.dart';
import 'package:charts_common/src/chart/cartesian/axis/scale.dart';
import 'package:charts_common/src/chart/cartesian/axis/spec/tick_spec.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChartContext extends Mock implements ChartContext {}
class MockGraphicsFactory extends Mock implements GraphicsFactory {}
class MockNumericTickFormatter extends Mock implements TickFormatter<num> {}
class FakeNumericTickFormatter implements TickFormatter<num> {
int calledTimes = 0;
@override
List<String> format(List<num> tickValues, Map<num, String> cache,
{num stepSize}) {
calledTimes += 1;
return tickValues.map((value) => value.toString()).toList();
}
}
class MockDrawStrategy extends Mock implements BaseTickDrawStrategy {}
void main() {
ChartContext context;
GraphicsFactory graphicsFactory;
TickFormatter formatter;
BaseTickDrawStrategy drawStrategy;
LinearScale scale;
setUp(() {
context = new MockChartContext();
graphicsFactory = new MockGraphicsFactory();
formatter = new MockNumericTickFormatter();
drawStrategy = new MockDrawStrategy();
scale = new LinearScale()..range = new ScaleOutputExtent(0, 300);
});
group('scale is extended with static tick values', () {
test('values extend existing domain values', () {
final tickProvider = new StaticTickProvider<num>([
new TickSpec<num>(50, label: '50'),
new TickSpec<num>(75, label: '75'),
new TickSpec<num>(100, label: '100'),
]);
scale.addDomain(60);
scale.addDomain(80);
expect(scale.dataExtent.min, equals(60));
expect(scale.dataExtent.max, equals(80));
tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(scale.dataExtent.min, equals(50));
expect(scale.dataExtent.max, equals(100));
});
test('values within data extent', () {
final tickProvider = new StaticTickProvider<num>([
new TickSpec<num>(50, label: '50'),
new TickSpec<num>(75, label: '75'),
new TickSpec<num>(100, label: '100'),
]);
scale.addDomain(0);
scale.addDomain(150);
expect(scale.dataExtent.min, equals(0));
expect(scale.dataExtent.max, equals(150));
tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(scale.dataExtent.min, equals(0));
expect(scale.dataExtent.max, equals(150));
});
});
group('formatter', () {
test('is not called when all ticks have labels', () {
final tickProvider = new StaticTickProvider<num>([
new TickSpec<num>(50, label: '50'),
new TickSpec<num>(75, label: '75'),
new TickSpec<num>(100, label: '100'),
]);
final fakeFormatter = new FakeNumericTickFormatter();
tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: fakeFormatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(fakeFormatter.calledTimes, equals(0));
});
test('is called when one ticks does not have label', () {
final tickProvider = new StaticTickProvider<num>([
new TickSpec<num>(50, label: '50'),
new TickSpec<num>(75),
new TickSpec<num>(100, label: '100'),
]);
final fakeFormatter = new FakeNumericTickFormatter();
tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: fakeFormatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(fakeFormatter.calledTimes, equals(1));
});
test('is called when all ticks do not have labels', () {
final tickProvider = new StaticTickProvider<num>([
new TickSpec<num>(50),
new TickSpec<num>(75),
new TickSpec<num>(100),
]);
final fakeFormatter = new FakeNumericTickFormatter();
tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: fakeFormatter,
formatterValueCache: <num, String>{},
tickDrawStrategy: drawStrategy,
orientation: null);
expect(fakeFormatter.calledTimes, equals(1));
});
});
}

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/time/time_tick_formatter.dart';
import 'package:charts_common/src/chart/cartesian/axis/time/date_time_tick_formatter.dart';
import 'package:test/test.dart';
const EPSILON = 0.001;
typedef bool IsTransitionFunction(DateTime tickValue, DateTime prevTickValue);
class FakeTimeTickFormatter implements TimeTickFormatter {
static const firstTick = '-firstTick-';
static const simpleTick = '-simpleTick-';
static const transitionTick = '-transitionTick-';
static final transitionAlwaysFalse = (_, __) => false;
final String id;
final IsTransitionFunction isTransitionFunction;
FakeTimeTickFormatter(this.id, {IsTransitionFunction isTransitionFunction})
: isTransitionFunction = isTransitionFunction ?? transitionAlwaysFalse;
@override
String formatFirstTick(DateTime date) =>
id + firstTick + date.millisecondsSinceEpoch.toString();
@override
String formatSimpleTick(DateTime date) =>
id + simpleTick + date.millisecondsSinceEpoch.toString();
@override
String formatTransitionTick(DateTime date) =>
id + transitionTick + date.millisecondsSinceEpoch.toString();
@override
bool isTransition(DateTime tickValue, DateTime prevTickValue) =>
isTransitionFunction(tickValue, prevTickValue);
}
void main() {
TimeTickFormatter timeFormatter1;
TimeTickFormatter timeFormatter2;
TimeTickFormatter timeFormatter3;
setUp(() {
timeFormatter1 = new FakeTimeTickFormatter('fake1');
timeFormatter2 = new FakeTimeTickFormatter('fake2');
timeFormatter3 = new FakeTimeTickFormatter('fake3');
});
group('Uses formatter', () {
test('with largest interval less than diff between tickValues', () {
final formatter = new DateTimeTickFormatter.withFormatters(
{10: timeFormatter1, 100: timeFormatter2, 1000: timeFormatter3});
final formatterCache = <DateTime, String>{};
final ticksWith10Diff = [
new DateTime.fromMillisecondsSinceEpoch(0),
new DateTime.fromMillisecondsSinceEpoch(10),
new DateTime.fromMillisecondsSinceEpoch(20)
];
final ticksWith20Diff = [
new DateTime.fromMillisecondsSinceEpoch(0),
new DateTime.fromMillisecondsSinceEpoch(20),
new DateTime.fromMillisecondsSinceEpoch(40)
];
final ticksWith100Diff = [
new DateTime.fromMillisecondsSinceEpoch(0),
new DateTime.fromMillisecondsSinceEpoch(100),
new DateTime.fromMillisecondsSinceEpoch(200)
];
final ticksWith200Diff = [
new DateTime.fromMillisecondsSinceEpoch(0),
new DateTime.fromMillisecondsSinceEpoch(200),
new DateTime.fromMillisecondsSinceEpoch(400)
];
final ticksWith1000Diff = [
new DateTime.fromMillisecondsSinceEpoch(0),
new DateTime.fromMillisecondsSinceEpoch(1000),
new DateTime.fromMillisecondsSinceEpoch(2000)
];
final expectedLabels10Diff = [
'fake1-firstTick-0',
'fake1-simpleTick-10',
'fake1-simpleTick-20'
];
final expectedLabels20Diff = [
'fake1-firstTick-0',
'fake1-simpleTick-20',
'fake1-simpleTick-40'
];
final expectedLabels100Diff = [
'fake2-firstTick-0',
'fake2-simpleTick-100',
'fake2-simpleTick-200'
];
final expectedLabels200Diff = [
'fake2-firstTick-0',
'fake2-simpleTick-200',
'fake2-simpleTick-400'
];
final expectedLabels1000Diff = [
'fake3-firstTick-0',
'fake3-simpleTick-1000',
'fake3-simpleTick-2000'
];
final actualLabelsWith10Diff =
formatter.format(ticksWith10Diff, formatterCache, stepSize: 10);
final actualLabelsWith20Diff =
formatter.format(ticksWith20Diff, formatterCache, stepSize: 20);
final actualLabelsWith100Diff =
formatter.format(ticksWith100Diff, formatterCache, stepSize: 100);
final actualLabelsWith200Diff =
formatter.format(ticksWith200Diff, formatterCache, stepSize: 200);
final actualLabelsWith1000Diff =
formatter.format(ticksWith1000Diff, formatterCache, stepSize: 1000);
expect(actualLabelsWith10Diff, equals(expectedLabels10Diff));
expect(actualLabelsWith20Diff, equals(expectedLabels20Diff));
expect(actualLabelsWith100Diff, equals(expectedLabels100Diff));
expect(actualLabelsWith200Diff, equals(expectedLabels200Diff));
expect(actualLabelsWith1000Diff, equals(expectedLabels1000Diff));
});
test('with smallest interval when no smaller one exists', () {
final formatter = new DateTimeTickFormatter.withFormatters(
{10: timeFormatter1, 100: timeFormatter2});
final formatterCache = <DateTime, String>{};
final ticks = [
new DateTime.fromMillisecondsSinceEpoch(0),
new DateTime.fromMillisecondsSinceEpoch(1),
new DateTime.fromMillisecondsSinceEpoch(2)
];
final expectedLabels = [
'fake1-firstTick-0',
'fake1-simpleTick-1',
'fake1-simpleTick-2'
];
final actualLabels = formatter.format(ticks, formatterCache, stepSize: 1);
expect(actualLabels, equals(expectedLabels));
});
test('with smallest interval for single tick input', () {
final formatter = new DateTimeTickFormatter.withFormatters(
{10: timeFormatter1, 100: timeFormatter2});
final formatterCache = <DateTime, String>{};
final ticks = [new DateTime.fromMillisecondsSinceEpoch(5000)];
final expectedLabels = ['fake1-firstTick-5000'];
final actualLabels = formatter.format(ticks, formatterCache, stepSize: 0);
expect(actualLabels, equals(expectedLabels));
});
test('on empty input doesnt break', () {
final formatter =
new DateTimeTickFormatter.withFormatters({10: timeFormatter1});
final formatterCache = <DateTime, String>{};
final actualLabels =
formatter.format(<DateTime>[], formatterCache, stepSize: 10);
expect(actualLabels, isEmpty);
});
test('that formats transition tick with transition format', () {
final timeFormatter = new FakeTimeTickFormatter('fake',
isTransitionFunction: (DateTime tickValue, _) =>
tickValue.millisecondsSinceEpoch == 20);
final formatterCache = <DateTime, String>{};
final formatter =
new DateTimeTickFormatter.withFormatters({10: timeFormatter});
final ticks = [
new DateTime.fromMillisecondsSinceEpoch(0),
new DateTime.fromMillisecondsSinceEpoch(10),
new DateTime.fromMillisecondsSinceEpoch(20),
new DateTime.fromMillisecondsSinceEpoch(30)
];
final expectedLabels = [
'fake-firstTick-0',
'fake-simpleTick-10',
'fake-transitionTick-20',
'fake-simpleTick-30'
];
final actualLabels =
formatter.format(ticks, formatterCache, stepSize: 10);
expect(actualLabels, equals(expectedLabels));
});
});
group('check custom time tick formatters', () {
test('throws arugment error if time resolution key is not positive', () {
// -1 is reserved for any, if there is only one formatter, -1 is allowed.
expect(
() => new DateTimeTickFormatter.withFormatters(
{-1: timeFormatter1, 2: timeFormatter2}),
throwsArgumentError);
});
test('throws argument error if formatters is null or empty', () {
expect(() => new DateTimeTickFormatter.withFormatters(null),
throwsArgumentError);
expect(() => new DateTimeTickFormatter.withFormatters({}),
throwsArgumentError);
});
test('throws arugment error if formatters are not sorted', () {
expect(
() => new DateTimeTickFormatter.withFormatters({
3: timeFormatter1,
1: timeFormatter2,
2: timeFormatter3,
}),
throwsArgumentError);
expect(
() => new DateTimeTickFormatter.withFormatters({
1: timeFormatter1,
3: timeFormatter2,
2: timeFormatter3,
}),
throwsArgumentError);
expect(
() => new DateTimeTickFormatter.withFormatters({
2: timeFormatter1,
3: timeFormatter2,
1: timeFormatter3,
}),
throwsArgumentError);
});
});
}

View File

@@ -0,0 +1,42 @@
// 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/common/date_time_factory.dart';
import 'package:intl/intl.dart' show DateFormat;
/// Returns DateTime for testing.
class SimpleDateTimeFactory implements DateTimeFactory {
const SimpleDateTimeFactory();
@override
DateTime createDateTimeFromMilliSecondsSinceEpoch(
int millisecondsSinceEpoch) =>
new DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch);
@override
DateTime createDateTime(int year,
[int month = 1,
int day = 1,
int hour = 0,
int minute = 0,
int second = 0,
int millisecond = 0,
int microsecond = 0]) =>
new DateTime(
year, month, day, hour, minute, second, millisecond, microsecond);
@override
DateFormat createDateFormat(String pattern) => new DateFormat(pattern);
}

View File

@@ -0,0 +1,484 @@
// 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/time/date_time_extents.dart';
import 'package:charts_common/src/chart/cartesian/axis/time/day_time_stepper.dart';
import 'package:charts_common/src/chart/cartesian/axis/time/hour_time_stepper.dart';
import 'package:charts_common/src/chart/cartesian/axis/time/minute_time_stepper.dart';
import 'package:charts_common/src/chart/cartesian/axis/time/month_time_stepper.dart';
import 'package:charts_common/src/chart/cartesian/axis/time/year_time_stepper.dart';
import 'package:test/test.dart';
import 'simple_date_time_factory.dart' show SimpleDateTimeFactory;
const EPSILON = 0.001;
void main() {
const dateTimeFactory = const SimpleDateTimeFactory();
const millisecondsInHour = 3600 * 1000;
setUp(() {});
group('Day time stepper', () {
test('get steps with 1 day increments', () {
final stepper = new DayTimeStepper(dateTimeFactory);
final extent = new DateTimeExtents(
start: new DateTime(2017, 8, 20), end: new DateTime(2017, 8, 25));
final stepIterable = stepper.getSteps(extent)..iterator.reset(1);
final steps = stepIterable.toList();
expect(steps.length, equals(6));
expect(
steps,
equals([
new DateTime(2017, 8, 20),
new DateTime(2017, 8, 21),
new DateTime(2017, 8, 22),
new DateTime(2017, 8, 23),
new DateTime(2017, 8, 24),
new DateTime(2017, 8, 25),
]));
});
test('get steps with 5 day increments', () {
final stepper = new DayTimeStepper(dateTimeFactory);
final extent = new DateTimeExtents(
start: new DateTime(2017, 8, 10),
end: new DateTime(2017, 8, 26),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(5);
final steps = stepIterable.toList();
expect(steps.length, equals(4));
// Note, this is because 5 day increments in a month is 1,6,11,16,21,26,31
expect(
steps,
equals([
new DateTime(2017, 8, 11),
new DateTime(2017, 8, 16),
new DateTime(2017, 8, 21),
new DateTime(2017, 8, 26),
]));
});
test('step through daylight saving forward change', () {
final stepper = new DayTimeStepper(dateTimeFactory);
// DST for PST 2017 begin on March 12
final extent = new DateTimeExtents(
start: new DateTime(2017, 3, 11),
end: new DateTime(2017, 3, 13),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(1);
final steps = stepIterable.toList();
expect(steps.length, equals(3));
expect(
steps,
equals([
new DateTime(2017, 3, 11),
new DateTime(2017, 3, 12),
new DateTime(2017, 3, 13),
]));
});
test('step through daylight saving backward change', () {
final stepper = new DayTimeStepper(dateTimeFactory);
// DST for PST 2017 end on November 5
final extent = new DateTimeExtents(
start: new DateTime(2017, 11, 4),
end: new DateTime(2017, 11, 6),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(1);
final steps = stepIterable.toList();
expect(steps.length, equals(3));
expect(
steps,
equals([
new DateTime(2017, 11, 4),
new DateTime(2017, 11, 5),
new DateTime(2017, 11, 6),
]));
});
});
group('Hour time stepper', () {
test('gets steps in 1 hour increments', () {
final stepper = new HourTimeStepper(dateTimeFactory);
final extent = new DateTimeExtents(
start: new DateTime(2017, 8, 20, 10),
end: new DateTime(2017, 8, 20, 15),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(1);
final steps = stepIterable.toList();
expect(steps.length, equals(6));
expect(
steps,
equals([
new DateTime(2017, 8, 20, 10),
new DateTime(2017, 8, 20, 11),
new DateTime(2017, 8, 20, 12),
new DateTime(2017, 8, 20, 13),
new DateTime(2017, 8, 20, 14),
new DateTime(2017, 8, 20, 15),
]));
});
test('gets steps in 4 hour increments', () {
final stepper = new HourTimeStepper(dateTimeFactory);
final extent = new DateTimeExtents(
start: new DateTime(2017, 8, 20, 10),
end: new DateTime(2017, 8, 21, 10),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(4);
final steps = stepIterable.toList();
expect(steps.length, equals(6));
expect(
steps,
equals([
new DateTime(2017, 8, 20, 12),
new DateTime(2017, 8, 20, 16),
new DateTime(2017, 8, 20, 20),
new DateTime(2017, 8, 21, 0),
new DateTime(2017, 8, 21, 4),
new DateTime(2017, 8, 21, 8),
]));
});
test('step through daylight saving forward change in 1 hour increments',
() {
final stepper = new HourTimeStepper(dateTimeFactory);
// DST for PST 2017 begin on March 12. At 2am clocks are turned to 3am.
final extent = new DateTimeExtents(
start: new DateTime(2017, 3, 12, 0),
end: new DateTime(2017, 3, 12, 5),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(1);
final steps = stepIterable.toList();
expect(steps.length, equals(5));
expect(
steps,
equals([
new DateTime(2017, 3, 12, 0),
new DateTime(2017, 3, 12, 1),
new DateTime(2017, 3, 12, 3),
new DateTime(2017, 3, 12, 4),
new DateTime(2017, 3, 12, 5),
]));
});
test('step through daylight saving backward change in 1 hour increments',
() {
final stepper = new HourTimeStepper(dateTimeFactory);
// DST for PST 2017 end on November 5. At 2am, clocks are turned to 1am.
final extent = new DateTimeExtents(
start: new DateTime(2017, 11, 5, 0),
end: new DateTime(2017, 11, 5, 4),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(1);
final steps = stepIterable.toList();
expect(steps.length, equals(6));
expect(
steps,
equals([
new DateTime(2017, 11, 5, 0),
new DateTime(2017, 11, 5, 0)
.add(new Duration(milliseconds: millisecondsInHour)),
new DateTime(2017, 11, 5, 0)
.add(new Duration(milliseconds: millisecondsInHour * 2)),
new DateTime(2017, 11, 5, 2),
new DateTime(2017, 11, 5, 3),
new DateTime(2017, 11, 5, 4),
]));
});
test('step through daylight saving forward change in 4 hour increments',
() {
final stepper = new HourTimeStepper(dateTimeFactory);
// DST for PST 2017 begin on March 12. At 2am clocks are turned to 3am.
final extent = new DateTimeExtents(
start: new DateTime(2017, 3, 12, 0),
end: new DateTime(2017, 3, 13, 0),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(4);
final steps = stepIterable.toList();
expect(steps.length, equals(6));
expect(
steps,
equals([
new DateTime(2017, 3, 12, 4),
new DateTime(2017, 3, 12, 8),
new DateTime(2017, 3, 12, 12),
new DateTime(2017, 3, 12, 16),
new DateTime(2017, 3, 12, 20),
new DateTime(2017, 3, 13, 0),
]));
});
test('step through daylight saving backward change in 4 hour increments',
() {
final stepper = new HourTimeStepper(dateTimeFactory);
// DST for PST 2017 end on November 5.
// At 2am, clocks are turned to 1am.
final extent = new DateTimeExtents(
start: new DateTime(2017, 11, 5, 0),
end: new DateTime(2017, 11, 6, 0),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(4);
final steps = stepIterable.toList();
expect(steps.length, equals(7));
expect(
steps,
equals([
new DateTime(2017, 11, 5, 0)
.add(new Duration(milliseconds: millisecondsInHour)),
new DateTime(2017, 11, 5, 4),
new DateTime(2017, 11, 5, 8),
new DateTime(2017, 11, 5, 12),
new DateTime(2017, 11, 5, 16),
new DateTime(2017, 11, 5, 20),
new DateTime(2017, 11, 6, 0),
]));
});
});
group('Minute time stepper', () {
test('gets steps with 5 minute increments', () {
final stepper = new MinuteTimeStepper(dateTimeFactory);
final extent = new DateTimeExtents(
start: new DateTime(2017, 8, 20, 3, 46),
end: new DateTime(2017, 8, 20, 4, 02),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(5);
final steps = stepIterable.toList();
expect(steps.length, equals(3));
expect(
steps,
equals([
new DateTime(2017, 8, 20, 3, 50),
new DateTime(2017, 8, 20, 3, 55),
new DateTime(2017, 8, 20, 4),
]));
});
test('step through daylight saving forward change', () {
final stepper = new MinuteTimeStepper(dateTimeFactory);
// DST for PST 2017 begin on March 12. At 2am clocks are turned to 3am.
final extent = new DateTimeExtents(
start: new DateTime(2017, 3, 12, 1, 40),
end: new DateTime(2017, 3, 12, 4, 02),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(15);
final steps = stepIterable.toList();
expect(steps.length, equals(6));
expect(
steps,
equals([
new DateTime(2017, 3, 12, 1, 45),
new DateTime(2017, 3, 12, 3),
new DateTime(2017, 3, 12, 3, 15),
new DateTime(2017, 3, 12, 3, 30),
new DateTime(2017, 3, 12, 3, 45),
new DateTime(2017, 3, 12, 4),
]));
});
test('steps correctly after daylight saving forward change', () {
final stepper = new MinuteTimeStepper(dateTimeFactory);
// DST for PST 2017 begin on March 12. At 2am clocks are turned to 3am.
final extent = new DateTimeExtents(
start: new DateTime(2017, 3, 12, 3, 02),
end: new DateTime(2017, 3, 12, 4, 02),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(30);
final steps = stepIterable.toList();
expect(steps.length, equals(2));
expect(
steps,
equals([
new DateTime(2017, 3, 12, 3, 30),
new DateTime(2017, 3, 12, 4),
]));
});
test('step through daylight saving backward change', () {
final stepper = new MinuteTimeStepper(dateTimeFactory);
// DST for PST 2017 end on November 5.
// At 2am, clocks are turned to 1am.
final extent = new DateTimeExtents(
start: new DateTime(2017, 11, 5)
.add(new Duration(hours: 1, minutes: 29)),
end: new DateTime(2017, 11, 5, 3, 02));
final stepIterable = stepper.getSteps(extent)..iterator.reset(30);
final steps = stepIterable.toList();
expect(steps.length, equals(6));
expect(
steps,
equals([
// The first 1:30am
new DateTime(2017, 11, 5).add(new Duration(hours: 1, minutes: 30)),
// The 2nd 1am.
new DateTime(2017, 11, 5).add(new Duration(hours: 2)),
// The 2nd 1:30am
new DateTime(2017, 11, 5).add(new Duration(hours: 2, minutes: 30)),
// 2am
new DateTime(2017, 11, 5).add(new Duration(hours: 3)),
// 2:30am
new DateTime(2017, 11, 5).add(new Duration(hours: 3, minutes: 30)),
// 3am
new DateTime(2017, 11, 5, 3)
]));
});
});
group('Month time stepper', () {
test('steps crosses the year', () {
final stepper = new MonthTimeStepper(dateTimeFactory);
final extent = new DateTimeExtents(
start: new DateTime(2017, 5),
end: new DateTime(2018, 9),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(4);
final steps = stepIterable.toList();
expect(steps.length, equals(4));
expect(
steps,
equals([
new DateTime(2017, 8),
new DateTime(2017, 12),
new DateTime(2018, 4),
new DateTime(2018, 8),
]));
});
test('steps within one year', () {
final stepper = new MonthTimeStepper(dateTimeFactory);
final extent = new DateTimeExtents(
start: new DateTime(2017, 1),
end: new DateTime(2017, 5),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(2);
final steps = stepIterable.toList();
expect(steps.length, equals(2));
expect(
steps,
equals([
new DateTime(2017, 2),
new DateTime(2017, 4),
]));
});
test('step before would allow ticks to include last month of the year', () {
final stepper = new MonthTimeStepper(dateTimeFactory);
final time = new DateTime(2017, 10);
expect(stepper.getStepTimeBeforeInclusive(time, 1),
equals(new DateTime(2017, 10)));
// Months - 3, 6, 9, 12
expect(stepper.getStepTimeBeforeInclusive(time, 3),
equals(new DateTime(2017, 9)));
// Months - 6, 12
expect(stepper.getStepTimeBeforeInclusive(time, 6),
equals(new DateTime(2017, 6)));
});
test('step before for January', () {
final stepper = new MonthTimeStepper(dateTimeFactory);
final time = new DateTime(2017, 1);
expect(stepper.getStepTimeBeforeInclusive(time, 1),
equals(new DateTime(2017, 1)));
// Months - 3, 6, 9, 12
expect(stepper.getStepTimeBeforeInclusive(time, 3),
equals(new DateTime(2016, 12)));
// Months - 6, 12
expect(stepper.getStepTimeBeforeInclusive(time, 6),
equals(new DateTime(2016, 12)));
});
test('step before for December', () {
final stepper = new MonthTimeStepper(dateTimeFactory);
final time = new DateTime(2017, 12);
expect(stepper.getStepTimeBeforeInclusive(time, 1),
equals(new DateTime(2017, 12)));
// Months - 3, 6, 9, 12
expect(stepper.getStepTimeBeforeInclusive(time, 3),
equals(new DateTime(2017, 12)));
// Months - 6, 12
expect(stepper.getStepTimeBeforeInclusive(time, 6),
equals(new DateTime(2017, 12)));
});
});
group('Year stepper', () {
test('steps in 10 year increments', () {
final stepper = new YearTimeStepper(dateTimeFactory);
final extent = new DateTimeExtents(
start: new DateTime(2017),
end: new DateTime(2042),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(10);
final steps = stepIterable.toList();
expect(steps.length, equals(3));
expect(
steps,
equals([
new DateTime(2020),
new DateTime(2030),
new DateTime(2040),
]));
});
test('steps through negative year', () {
final stepper = new YearTimeStepper(dateTimeFactory);
final extent = new DateTimeExtents(
start: new DateTime(-420),
end: new DateTime(240),
);
final stepIterable = stepper.getSteps(extent)..iterator.reset(200);
final steps = stepIterable.toList();
expect(steps.length, equals(4));
expect(
steps,
equals([
new DateTime(-400),
new DateTime(-200),
new DateTime(0),
new DateTime(200),
]));
});
});
}

View File

@@ -0,0 +1,67 @@
// 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/time/auto_adjusting_date_time_tick_provider.dart';
import 'package:test/test.dart';
import 'simple_date_time_factory.dart' show SimpleDateTimeFactory;
const EPSILON = 0.001;
void main() {
const dateTimeFactory = const SimpleDateTimeFactory();
group('Find closest step size from stepper', () {
test('from exactly matching step size', () {
final stepper = AutoAdjustingDateTimeTickProvider.createHourTickProvider(
dateTimeFactory);
final oneHourMs = (new Duration(hours: 1)).inMilliseconds;
final closestStepSize = stepper.getClosestStepSize(oneHourMs);
expect(closestStepSize, equals(oneHourMs));
});
test('choose smallest increment if step is smaller than smallest increment',
() {
final stepper = AutoAdjustingDateTimeTickProvider.createHourTickProvider(
dateTimeFactory);
final oneHourMs = (new Duration(hours: 1)).inMilliseconds;
final closestStepSize = stepper
.getClosestStepSize((new Duration(minutes: 56)).inMilliseconds);
expect(closestStepSize, equals(oneHourMs));
});
test('choose largest increment if step is larger than largest increment',
() {
final stepper = AutoAdjustingDateTimeTickProvider.createHourTickProvider(
dateTimeFactory);
final oneDayMs = (new Duration(hours: 24)).inMilliseconds;
final closestStepSize =
stepper.getClosestStepSize((new Duration(hours: 25)).inMilliseconds);
expect(closestStepSize, equals(oneDayMs));
});
test('choose closest increment if exact not found', () {
final stepper = AutoAdjustingDateTimeTickProvider.createHourTickProvider(
dateTimeFactory);
final threeHoursMs = (new Duration(hours: 3)).inMilliseconds;
final closestStepSize = stepper.getClosestStepSize(
(new Duration(hours: 3, minutes: 28)).inMilliseconds);
expect(closestStepSize, equals(threeHoursMs));
});
});
}

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 'package:charts_common/src/chart/cartesian/cartesian_chart.dart';
import 'package:charts_common/src/chart/cartesian/axis/spec/date_time_axis_spec.dart';
import 'package:charts_common/src/chart/cartesian/axis/spec/ordinal_axis_spec.dart';
import 'package:charts_common/src/chart/cartesian/axis/spec/numeric_axis_spec.dart';
import 'package:charts_common/src/chart/common/chart_context.dart';
import 'package:charts_common/src/chart/time_series/time_series_chart.dart';
import 'package:charts_common/src/common/graphics_factory.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockContext extends Mock implements ChartContext {}
class MockGraphicsFactory extends Mock implements GraphicsFactory {}
class FakeNumericChart extends NumericCartesianChart {
FakeNumericChart() {
context = new MockContext();
graphicsFactory = new MockGraphicsFactory();
}
@override
void initDomainAxis() {
// Purposely bypass the renderer code.
}
}
class FakeOrdinalChart extends OrdinalCartesianChart {
FakeOrdinalChart() {
context = new MockContext();
graphicsFactory = new MockGraphicsFactory();
}
@override
void initDomainAxis() {
// Purposely bypass the renderer code.
}
}
class FakeTimeSeries extends TimeSeriesChart {
FakeTimeSeries() {
context = new MockContext();
graphicsFactory = new MockGraphicsFactory();
}
@override
void initDomainAxis() {
// Purposely bypass the renderer code.
}
}
void main() {
group('Axis reset with new axis spec', () {
test('for ordinal chart', () {
final chart = new FakeOrdinalChart();
chart.configurationChanged();
final domainAxis = chart.domainAxis;
expect(domainAxis, isNotNull);
chart.domainAxisSpec = new OrdinalAxisSpec();
chart.configurationChanged();
expect(domainAxis, isNot(chart.domainAxis));
});
test('for numeric chart', () {
final chart = new FakeNumericChart();
chart.configurationChanged();
final domainAxis = chart.domainAxis;
expect(domainAxis, isNotNull);
chart.domainAxisSpec = new NumericAxisSpec();
chart.configurationChanged();
expect(domainAxis, isNot(chart.domainAxis));
});
test('for time series chart', () {
final chart = new FakeTimeSeries();
chart.configurationChanged();
final domainAxis = chart.domainAxis;
expect(domainAxis, isNotNull);
chart.domainAxisSpec = new DateTimeAxisSpec();
chart.configurationChanged();
expect(domainAxis, isNot(chart.domainAxis));
});
});
}

View File

@@ -0,0 +1,295 @@
// 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:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/cartesian/cartesian_renderer.dart';
import 'package:charts_common/src/chart/common/chart_canvas.dart';
import 'package:charts_common/src/chart/common/datum_details.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/common/series_datum.dart';
import 'package:charts_common/src/common/symbol_renderer.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
/// For testing viewport start / end.
class FakeCartesianRenderer extends BaseCartesianRenderer {
@override
List<DatumDetails> getNearestDatumDetailPerSeries(Point<double> chartPoint,
bool byDomain, Rectangle<int> boundsOverride) =>
null;
@override
void paint(ChartCanvas canvas, double animationPercent) {}
@override
void update(List<ImmutableSeries> seriesList, bool isAnimating) {}
@override
SymbolRenderer get symbolRenderer => null;
DatumDetails addPositionToDetailsForSeriesDatum(
DatumDetails details, SeriesDatum seriesDatum) {
return details;
}
}
class MockAxis extends Mock implements Axis {}
void main() {
BaseCartesianRenderer renderer;
setUp(() {
renderer = new FakeCartesianRenderer();
});
group('find viewport start', () {
test('several domains are in the viewport', () {
final data = [0, 1, 2, 3, 4, 5, 6];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(-1);
when(axis.compareDomainValueToViewport(1)).thenReturn(-1);
when(axis.compareDomainValueToViewport(2)).thenReturn(0);
when(axis.compareDomainValueToViewport(3)).thenReturn(0);
when(axis.compareDomainValueToViewport(4)).thenReturn(0);
when(axis.compareDomainValueToViewport(5)).thenReturn(1);
when(axis.compareDomainValueToViewport(6)).thenReturn(1);
final start = renderer.findNearestViewportStart(axis, domainFn, data);
expect(start, equals(2));
});
test('extents are all in the viewport, use the first domain', () {
// Start of viewport is the same as the start of the domain.
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(any)).thenReturn(0);
final start = renderer.findNearestViewportStart(axis, domainFn, data);
expect(start, equals(0));
});
test('is the first domain', () {
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(0);
when(axis.compareDomainValueToViewport(1)).thenReturn(1);
when(axis.compareDomainValueToViewport(2)).thenReturn(1);
when(axis.compareDomainValueToViewport(3)).thenReturn(1);
final start = renderer.findNearestViewportStart(axis, domainFn, data);
expect(start, equals(0));
});
test('is the last domain', () {
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(-1);
when(axis.compareDomainValueToViewport(1)).thenReturn(-1);
when(axis.compareDomainValueToViewport(2)).thenReturn(-1);
when(axis.compareDomainValueToViewport(3)).thenReturn(0);
final start = renderer.findNearestViewportStart(axis, domainFn, data);
expect(start, equals(3));
});
test('is the middle', () {
final data = [0, 1, 2, 3, 4, 5, 6];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(-1);
when(axis.compareDomainValueToViewport(1)).thenReturn(-1);
when(axis.compareDomainValueToViewport(2)).thenReturn(-1);
when(axis.compareDomainValueToViewport(3)).thenReturn(0);
when(axis.compareDomainValueToViewport(4)).thenReturn(1);
when(axis.compareDomainValueToViewport(5)).thenReturn(1);
when(axis.compareDomainValueToViewport(6)).thenReturn(1);
final start = renderer.findNearestViewportStart(axis, domainFn, data);
expect(start, equals(3));
});
test('viewport is between data', () {
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(-1);
when(axis.compareDomainValueToViewport(1)).thenReturn(-1);
when(axis.compareDomainValueToViewport(2)).thenReturn(1);
when(axis.compareDomainValueToViewport(3)).thenReturn(1);
final start = renderer.findNearestViewportStart(axis, domainFn, data);
expect(start, equals(1));
});
// Error case, viewport shouldn't be set to the outside of the extents.
// We still want to provide a value.
test('all extents greater than viewport ', () {
// Return the right most value as start of viewport.
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(any)).thenReturn(1);
final start = renderer.findNearestViewportStart(axis, domainFn, data);
expect(start, equals(3));
});
// Error case, viewport shouldn't be set to the outside of the extents.
// We still want to provide a value.
test('all extents less than viewport ', () {
// Return the left most value as the start of the viewport.
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(any)).thenReturn(-1);
final start = renderer.findNearestViewportStart(axis, domainFn, data);
expect(start, equals(0));
});
});
group('find viewport end', () {
test('several domains are in the viewport', () {
final data = [0, 1, 2, 3, 4, 5, 6];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(-1);
when(axis.compareDomainValueToViewport(1)).thenReturn(-1);
when(axis.compareDomainValueToViewport(2)).thenReturn(0);
when(axis.compareDomainValueToViewport(3)).thenReturn(0);
when(axis.compareDomainValueToViewport(4)).thenReturn(0);
when(axis.compareDomainValueToViewport(5)).thenReturn(1);
when(axis.compareDomainValueToViewport(6)).thenReturn(1);
final start = renderer.findNearestViewportEnd(axis, domainFn, data);
expect(start, equals(4));
});
test('extents are all in the viewport, use the last domain', () {
// Start of viewport is the same as the end of the domain.
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(any)).thenReturn(0);
final start = renderer.findNearestViewportEnd(axis, domainFn, data);
expect(start, equals(3));
});
test('is the first domain', () {
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(0);
when(axis.compareDomainValueToViewport(1)).thenReturn(1);
when(axis.compareDomainValueToViewport(2)).thenReturn(1);
when(axis.compareDomainValueToViewport(3)).thenReturn(1);
final start = renderer.findNearestViewportEnd(axis, domainFn, data);
expect(start, equals(0));
});
test('is the last domain', () {
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(-1);
when(axis.compareDomainValueToViewport(1)).thenReturn(-1);
when(axis.compareDomainValueToViewport(2)).thenReturn(-1);
when(axis.compareDomainValueToViewport(3)).thenReturn(0);
final start = renderer.findNearestViewportEnd(axis, domainFn, data);
expect(start, equals(3));
});
test('is the middle', () {
final data = [0, 1, 2, 3, 4, 5, 6];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(-1);
when(axis.compareDomainValueToViewport(1)).thenReturn(-1);
when(axis.compareDomainValueToViewport(2)).thenReturn(-1);
when(axis.compareDomainValueToViewport(3)).thenReturn(0);
when(axis.compareDomainValueToViewport(4)).thenReturn(1);
when(axis.compareDomainValueToViewport(5)).thenReturn(1);
when(axis.compareDomainValueToViewport(6)).thenReturn(1);
final start = renderer.findNearestViewportEnd(axis, domainFn, data);
expect(start, equals(3));
});
test('viewport is between data', () {
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(0)).thenReturn(-1);
when(axis.compareDomainValueToViewport(1)).thenReturn(-1);
when(axis.compareDomainValueToViewport(2)).thenReturn(1);
when(axis.compareDomainValueToViewport(3)).thenReturn(1);
final start = renderer.findNearestViewportEnd(axis, domainFn, data);
expect(start, equals(2));
});
// Error case, viewport shouldn't be set to the outside of the extents.
// We still want to provide a value.
test('all extents greater than viewport ', () {
// Return the right most value as start of viewport.
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(any)).thenReturn(1);
final start = renderer.findNearestViewportEnd(axis, domainFn, data);
expect(start, equals(3));
});
// Error case, viewport shouldn't be set to the outside of the extents.
// We still want to provide a value.
test('all extents less than viewport ', () {
// Return the left most value as the start of the viewport.
final data = [0, 1, 2, 3];
final domainFn = (int index) => data[index];
final axis = new MockAxis();
when(axis.compareDomainValueToViewport(any)).thenReturn(-1);
final start = renderer.findNearestViewportEnd(axis, domainFn, data);
expect(start, equals(0));
});
});
}