1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 14:58:34 +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,404 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:charts_common/src/chart/common/processed_series.dart'
show ImmutableSeries;
import 'package:charts_common/src/common/color.dart' show Color;
import 'package:charts_common/src/common/graphics_factory.dart'
show GraphicsFactory;
import 'package:charts_common/src/common/line_style.dart' show LineStyle;
import 'package:charts_common/src/common/text_element.dart'
show TextDirection, TextElement, MaxWidthStrategy;
import 'package:charts_common/src/common/text_measurement.dart'
show TextMeasurement;
import 'package:charts_common/src/common/text_style.dart' show TextStyle;
import 'package:charts_common/src/chart/bar/bar_renderer.dart'
show ImmutableBarRendererElement;
import 'package:charts_common/src/chart/cartesian/axis/spec/axis_spec.dart'
show TextStyleSpec;
import 'package:charts_common/src/chart/common/chart_canvas.dart'
show ChartCanvas;
import 'package:charts_common/src/chart/bar/bar_label_decorator.dart'
show BarLabelDecorator, BarLabelAnchor, BarLabelPosition;
import 'package:charts_common/src/data/series.dart' show AccessorFn;
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockCanvas extends Mock implements ChartCanvas {}
/// A fake [GraphicsFactory] that returns [FakeTextStyle] and [FakeTextElement].
class FakeGraphicsFactory extends GraphicsFactory {
@override
TextStyle createTextPaint() => new FakeTextStyle();
@override
TextElement createTextElement(String text) => new FakeTextElement(text);
@override
LineStyle createLinePaint() => new MockLinePaint();
}
/// Stores [TextStyle] properties for test to verify.
class FakeTextStyle implements TextStyle {
Color color;
int fontSize;
String fontFamily;
}
/// Fake [TextElement] which returns text length as [horizontalSliceWidth].
///
/// Font size is returned for [verticalSliceWidth] and [baseline].
class FakeTextElement implements TextElement {
final String text;
TextStyle textStyle;
int maxWidth;
MaxWidthStrategy maxWidthStrategy;
TextDirection textDirection;
double opacity;
FakeTextElement(this.text);
TextMeasurement get measurement => new TextMeasurement(
horizontalSliceWidth: text.length.toDouble(),
verticalSliceWidth: textStyle.fontSize.toDouble(),
baseline: textStyle.fontSize.toDouble());
}
class MockLinePaint extends Mock implements LineStyle {}
class FakeBarRendererElement implements ImmutableBarRendererElement<String> {
final _series = new MockImmutableSeries<String>();
final AccessorFn<String> labelAccessor;
final String datum;
final Rectangle<int> bounds;
final List<String> data;
int index;
FakeBarRendererElement(
this.datum, this.bounds, this.labelAccessor, this.data) {
index = data.indexOf(datum);
when(_series.labelAccessorFn).thenReturn(labelAccessor);
when(_series.data).thenReturn(data);
}
ImmutableSeries<String> get series => _series;
}
class MockImmutableSeries<D> extends Mock implements ImmutableSeries<D> {}
void main() {
ChartCanvas canvas;
GraphicsFactory graphicsFactory;
Rectangle<int> drawBounds;
setUpAll(() {
canvas = new MockCanvas();
graphicsFactory = new FakeGraphicsFactory();
drawBounds = new Rectangle(0, 0, 200, 100);
});
group('horizontal bar chart', () {
test('Paint labels with default settings', () {
final data = ['A', 'B'];
final barElements = [
// 'LabelA' and 'LabelB' both have lengths of 6.
// 'LabelB' would not fit inside the bar in auto setting because it has
// width of 5.
new FakeBarRendererElement(
'A', new Rectangle(0, 20, 50, 20), (_) => 'LabelA', data),
new FakeBarRendererElement(
'B', new Rectangle(0, 70, 5, 20), (_) => 'LabelB', data)
];
final decorator = new BarLabelDecorator();
decorator.decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
// Draw text is called twice (once for each bar) and all 3 parameters were
// captured. Total parameters captured expected to be 6.
expect(captured, hasLength(6));
// For bar 'A'.
expect(captured[0].maxWidth, equals(50 - decorator.labelPadding * 2));
expect(captured[0].textDirection, equals(TextDirection.ltr));
expect(captured[1], equals(decorator.labelPadding));
expect(captured[2],
equals(30 - decorator.insideLabelStyleSpec.fontSize ~/ 2));
// For bar 'B'.
expect(
captured[3].maxWidth, equals(200 - 5 - decorator.labelPadding * 2));
expect(captured[3].textDirection, equals(TextDirection.ltr));
expect(captured[4], equals(5 + decorator.labelPadding));
expect(captured[5],
equals(80 - decorator.outsideLabelStyleSpec.fontSize ~/ 2));
});
test('LabelPosition.auto paints inside bar if outside bar has less width',
() {
final barElements = [
// 'LabelABC' would not fit inside the bar in auto setting because it
// has a width of 8.
new FakeBarRendererElement(
'A', new Rectangle(0, 0, 6, 20), (_) => 'LabelABC', ['A']),
];
// Draw bounds with width of 10 means that space inside the bar is larger.
final smallDrawBounds = new Rectangle(0, 0, 10, 20);
new BarLabelDecorator(
labelPadding: 0, // Turn off label padding for testing.
insideLabelStyleSpec: new TextStyleSpec(fontSize: 10))
.decorate(barElements, canvas, graphicsFactory,
drawBounds: smallDrawBounds,
animationPercent: 1.0,
renderingVertically: false);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
expect(captured, hasLength(3));
expect(captured[0].maxWidth, equals(6));
expect(captured[0].textDirection, equals(TextDirection.ltr));
expect(captured[1], equals(0));
expect(captured[2], equals(5));
});
test('LabelPosition.inside always paints inside the bar', () {
final barElements = [
// 'LabelABC' would not fit inside the bar in auto setting because it
// has a width of 8.
new FakeBarRendererElement(
'A', new Rectangle(0, 0, 6, 20), (_) => 'LabelABC', ['A']),
];
new BarLabelDecorator(
labelPosition: BarLabelPosition.inside,
labelPadding: 0, // Turn off label padding for testing.
insideLabelStyleSpec: new TextStyleSpec(fontSize: 10))
.decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
expect(captured, hasLength(3));
expect(captured[0].maxWidth, equals(6));
expect(captured[0].textDirection, equals(TextDirection.ltr));
expect(captured[1], equals(0));
expect(captured[2], equals(5));
});
test('LabelPosition.outside always paints outside the bar', () {
final barElements = [
new FakeBarRendererElement(
'A', new Rectangle(0, 0, 10, 20), (_) => 'Label', ['A']),
];
new BarLabelDecorator(
labelPosition: BarLabelPosition.outside,
labelPadding: 0, // Turn off label padding for testing.
outsideLabelStyleSpec: new TextStyleSpec(fontSize: 10))
.decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
expect(captured, hasLength(3));
expect(captured[0].maxWidth, equals(190));
expect(captured[0].textDirection, equals(TextDirection.ltr));
expect(captured[1], equals(10));
expect(captured[2], equals(5));
});
test('Inside and outside label styles are applied', () {
final data = ['A', 'B'];
final barElements = [
// 'LabelA' and 'LabelB' both have lengths of 6.
// 'LabelB' would not fit inside the bar in auto setting because it has
// width of 5.
new FakeBarRendererElement(
'A', new Rectangle(0, 20, 50, 20), (_) => 'LabelA', data),
new FakeBarRendererElement(
'B', new Rectangle(0, 70, 5, 20), (_) => 'LabelB', data)
];
final insideColor = new Color(r: 0, g: 0, b: 0);
final outsideColor = new Color(r: 255, g: 255, b: 255);
final decorator = new BarLabelDecorator(
labelPadding: 0,
insideLabelStyleSpec: new TextStyleSpec(
fontSize: 10, fontFamily: 'insideFont', color: insideColor),
outsideLabelStyleSpec: new TextStyleSpec(
fontSize: 8, fontFamily: 'outsideFont', color: outsideColor));
decorator.decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
// Draw text is called twice (once for each bar) and all 3 parameters were
// captured. Total parameters captured expected to be 6.
expect(captured, hasLength(6));
// For bar 'A'.
expect(captured[0].maxWidth, equals(50));
expect(captured[0].textDirection, equals(TextDirection.ltr));
expect(captured[0].textStyle.fontFamily, equals('insideFont'));
expect(captured[0].textStyle.color, equals(insideColor));
expect(captured[1], equals(0));
expect(captured[2], equals(30 - 5));
// For bar 'B'.
expect(captured[3].maxWidth, equals(200 - 5));
expect(captured[3].textDirection, equals(TextDirection.ltr));
expect(captured[3].textStyle.fontFamily, equals('outsideFont'));
expect(captured[3].textStyle.color, equals(outsideColor));
expect(captured[4], equals(5));
expect(captured[5], equals(80 - 4));
});
test('TextAnchor.end starts on the right most of bar', () {
final barElements = [
new FakeBarRendererElement(
'A', new Rectangle(0, 0, 10, 20), (_) => 'LabelA', ['A'])
];
new BarLabelDecorator(
labelAnchor: BarLabelAnchor.end,
labelPosition: BarLabelPosition.inside,
labelPadding: 0, // Turn off label padding for testing.
insideLabelStyleSpec: new TextStyleSpec(fontSize: 10))
.decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
expect(captured, hasLength(3));
expect(captured[0].maxWidth, equals(10));
expect(captured[0].textDirection, equals(TextDirection.rtl));
expect(captured[1], equals(10));
expect(captured[2], equals(5));
});
test('RTL TextAnchor.start starts on the right', () {
final barElements = [
new FakeBarRendererElement(
'A', new Rectangle(0, 0, 10, 20), (_) => 'LabelA', ['A'])
];
new BarLabelDecorator(
labelAnchor: BarLabelAnchor.start,
labelPosition: BarLabelPosition.inside,
labelPadding: 0, // Turn off label padding for testing.
insideLabelStyleSpec: new TextStyleSpec(fontSize: 10))
.decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false,
rtl: true);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
expect(captured, hasLength(3));
expect(captured[0].maxWidth, equals(10));
expect(captured[0].textDirection, equals(TextDirection.rtl));
expect(captured[1], equals(10));
expect(captured[2], equals(5));
});
test('RTL TextAnchor.end starts on the left', () {
final barElements = [
new FakeBarRendererElement(
'A', new Rectangle(0, 0, 10, 20), (_) => 'LabelA', ['A'])
];
new BarLabelDecorator(
labelAnchor: BarLabelAnchor.end,
labelPosition: BarLabelPosition.inside,
labelPadding: 0, // Turn off label padding for testing.
insideLabelStyleSpec: new TextStyleSpec(fontSize: 10))
.decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false,
rtl: true);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
expect(captured, hasLength(3));
expect(captured[0].maxWidth, equals(10));
expect(captured[0].textDirection, equals(TextDirection.ltr));
expect(captured[1], equals(0));
expect(captured[2], equals(5));
});
});
group('Null and empty label scenarios', () {
test('Skip label if label accessor does not exist', () {
final barElements = [
new FakeBarRendererElement(
'A', new Rectangle(0, 0, 10, 20), null, ['A'])
];
new BarLabelDecorator().decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false);
verifyNever(canvas.drawText(any, any, any));
});
test('Skip label if label is null or empty', () {
final data = ['A', 'B'];
final barElements = [
new FakeBarRendererElement(
'A', new Rectangle(0, 0, 10, 20), null, data),
new FakeBarRendererElement(
'B', new Rectangle(0, 50, 10, 20), (_) => '', data),
];
new BarLabelDecorator().decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false);
verifyNever(canvas.drawText(any, any, any));
});
test('Skip label if no width available', () {
final barElements = [
new FakeBarRendererElement(
'A', new Rectangle(0, 0, 200, 20), (_) => 'a', ['A'])
];
new BarLabelDecorator(
labelPadding: 0,
labelPosition: BarLabelPosition.outside,
).decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: 1.0,
renderingVertically: false);
verifyNever(canvas.drawText(any, any, any));
});
});
}

View File

@@ -0,0 +1,882 @@
// 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/bar/bar_renderer.dart';
import 'package:charts_common/src/chart/bar/bar_renderer_config.dart';
import 'package:charts_common/src/chart/bar/base_bar_renderer.dart';
import 'package:charts_common/src/chart/bar/base_bar_renderer_config.dart';
import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart';
import 'package:charts_common/src/chart/cartesian/axis/axis.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/processed_series.dart'
show MutableSeries;
import 'package:charts_common/src/common/material_palette.dart'
show MaterialPalette;
import 'package:charts_common/src/common/color.dart';
import 'package:charts_common/src/data/series.dart' show Series;
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
/// Datum/Row for the chart.
class MyRow {
final String campaign;
final int clickCount;
MyRow(this.campaign, this.clickCount);
}
class MockAxis<D> extends Mock implements Axis<D> {}
class MockCanvas extends Mock implements ChartCanvas {}
class MockContext extends Mock implements ChartContext {}
class MockChart extends Mock implements CartesianChart {}
class FakeBarRenderer<D> extends BarRenderer<D> {
int paintBarCallCount = 0;
factory FakeBarRenderer({BarRendererConfig config, String rendererId}) {
return new FakeBarRenderer._internal(
config: config, rendererId: rendererId);
}
FakeBarRenderer._internal({BarRendererConfig config, String rendererId})
: super.internal(config: config, rendererId: rendererId);
@override
void paintBar(ChartCanvas canvas, double animationPercent,
Iterable<BarRendererElement<D>> barElements) {
paintBarCallCount += 1;
}
}
void main() {
BarRenderer renderer;
List<MutableSeries<String>> seriesList;
List<MutableSeries<String>> groupedStackedSeriesList;
/////////////////////////////////////////
// Convenience methods for creating mocks.
/////////////////////////////////////////
_configureBaseRenderer(BaseBarRenderer renderer, bool vertical) {
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
final verticalChart = new MockChart();
when(verticalChart.vertical).thenReturn(vertical);
when(verticalChart.context).thenReturn(context);
renderer.onAttach(verticalChart);
return renderer;
}
BarRenderer makeRenderer({BarRendererConfig config}) {
final renderer = new BarRenderer(config: config);
_configureBaseRenderer(renderer, true);
return renderer;
}
FakeBarRenderer makeFakeRenderer({BarRendererConfig config}) {
final renderer = new FakeBarRenderer(config: config);
_configureBaseRenderer(renderer, true);
return renderer;
}
setUp(() {
var myFakeDesktopAData = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 25),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
var myFakeTabletAData = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 25),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
var myFakeMobileAData = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 25),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
var myFakeDesktopBData = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 25),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
var myFakeTabletBData = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 25),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
var myFakeMobileBData = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 25),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
seriesList = [
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop',
colorFn: (_, __) => MaterialPalette.blue.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeDesktopAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Tablet',
colorFn: (_, __) => MaterialPalette.red.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeTabletAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Mobile',
colorFn: (_, __) => MaterialPalette.green.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeMobileAData))
];
groupedStackedSeriesList = [
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop A',
seriesCategory: 'A',
colorFn: (_, __) => MaterialPalette.blue.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeDesktopAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Tablet A',
seriesCategory: 'A',
colorFn: (_, __) => MaterialPalette.red.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeTabletAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Mobile A',
seriesCategory: 'A',
colorFn: (_, __) => MaterialPalette.green.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeMobileAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop B',
seriesCategory: 'B',
colorFn: (_, __) => MaterialPalette.blue.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeDesktopBData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Tablet B',
seriesCategory: 'B',
colorFn: (_, __) => MaterialPalette.red.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeTabletBData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Mobile B',
seriesCategory: 'B',
colorFn: (_, __) => MaterialPalette.green.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeMobileBData))
];
});
group('preprocess', () {
test('with grouped bars', () {
renderer = makeRenderer(
config: new BarRendererConfig(groupingType: BarGroupingType.grouped));
renderer.preprocessSeries(seriesList);
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
// Validate Tablet series.
series = seriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(1 / 3));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
// Validate Mobile series.
series = seriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(2));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
});
test('with grouped stacked bars', () {
renderer = makeRenderer(
config: new BarRendererConfig(
groupingType: BarGroupingType.groupedStacked));
renderer.preprocessSeries(groupedStackedSeriesList);
expect(groupedStackedSeriesList.length, equals(6));
// Validate Desktop A series.
var series = groupedStackedSeriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(0.5));
expect(series.getAttr(stackKeyKey), equals('A'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(10));
expect(element.measureOffsetPlusMeasure, equals(15));
expect(series.measureOffsetFn(0), equals(10));
// Validate Tablet A series.
series = groupedStackedSeriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(0.5));
expect(series.getAttr(stackKeyKey), equals('A'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
// Validate Mobile A series.
series = groupedStackedSeriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(0.5));
expect(series.getAttr(stackKeyKey), equals('A'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
// Validate Desktop B series.
series = groupedStackedSeriesList[3];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.5));
expect(series.getAttr(barGroupWeightKey), equals(0.5));
expect(series.getAttr(stackKeyKey), equals('B'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(10));
expect(element.measureOffsetPlusMeasure, equals(15));
expect(series.measureOffsetFn(0), equals(10));
// Validate Tablet B series.
series = groupedStackedSeriesList[4];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.5));
expect(series.getAttr(barGroupWeightKey), equals(0.5));
expect(series.getAttr(stackKeyKey), equals('B'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
// Validate Mobile B series.
series = groupedStackedSeriesList[5];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.5));
expect(series.getAttr(barGroupWeightKey), equals(0.5));
expect(series.getAttr(stackKeyKey), equals('B'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
});
test('with stacked bars', () {
renderer = makeRenderer(
config: new BarRendererConfig(groupingType: BarGroupingType.stacked));
renderer.preprocessSeries(seriesList);
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(10));
expect(element.measureOffsetPlusMeasure, equals(15));
expect(series.measureOffsetFn(0), equals(10));
// Validate Tablet series.
series = seriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
// Validate Mobile series.
series = seriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
});
test('with stacked bars containing zero and null', () {
// Set up some nulls and zeros in the data.
seriesList[2].data[0] = new MyRow('MyCampaign1', null);
seriesList[2].data[2] = new MyRow('MyCampaign3', 0);
seriesList[1].data[1] = new MyRow('MyCampaign2', null);
seriesList[1].data[3] = new MyRow('MyOtherCampaign', 0);
seriesList[0].data[2] = new MyRow('MyCampaign3', 0);
renderer = makeRenderer(
config: new BarRendererConfig(groupingType: BarGroupingType.stacked));
renderer.preprocessSeries(seriesList);
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
var elementsList = series.getAttr(barElementsKey);
var element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
element = elementsList[1];
expect(element.measureOffset, equals(25));
expect(element.measureOffsetPlusMeasure, equals(50));
expect(series.measureOffsetFn(1), equals(25));
element = elementsList[2];
expect(element.measureOffset, equals(100));
expect(element.measureOffsetPlusMeasure, equals(100));
expect(series.measureOffsetFn(2), equals(100));
element = elementsList[3];
expect(element.measureOffset, equals(75));
expect(element.measureOffsetPlusMeasure, equals(150));
expect(series.measureOffsetFn(3), equals(75));
// Validate Tablet series.
series = seriesList[1];
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
element = elementsList[1];
expect(element.measureOffset, equals(25));
expect(element.measureOffsetPlusMeasure, equals(25));
expect(series.measureOffsetFn(1), equals(25));
element = elementsList[2];
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(100));
expect(series.measureOffsetFn(2), equals(0));
element = elementsList[3];
expect(element.measureOffset, equals(75));
expect(element.measureOffsetPlusMeasure, equals(75));
expect(series.measureOffsetFn(3), equals(75));
// Validate Mobile series.
series = seriesList[2];
elementsList = series.getAttr(barElementsKey);
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(0));
expect(series.measureOffsetFn(0), equals(0));
element = elementsList[1];
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(25));
expect(series.measureOffsetFn(1), equals(0));
element = elementsList[2];
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(0));
expect(series.measureOffsetFn(2), equals(0));
element = elementsList[3];
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(75));
expect(series.measureOffsetFn(3), equals(0));
});
});
group('preprocess weight pattern', () {
test('with grouped bars', () {
renderer = makeRenderer(
config: new BarRendererConfig(
groupingType: BarGroupingType.grouped, weightPattern: [3, 2, 1]));
renderer.preprocessSeries(seriesList);
// Verify that bar group weights are proportional to the sum of the used
// segments of weightPattern. The weightPattern should be distributed
// amongst bars that share the same domain value.
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(0.5));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
// Validate Tablet series.
series = seriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.5));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
// Validate Mobile series.
series = seriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(2));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.5 + 1 / 3));
expect(series.getAttr(barGroupWeightKey), equals(1 / 6));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
});
test('with grouped stacked bars', () {
renderer = makeRenderer(
config: new BarRendererConfig(
groupingType: BarGroupingType.groupedStacked,
weightPattern: [2, 1]));
renderer.preprocessSeries(groupedStackedSeriesList);
// Verify that bar group weights are proportional to the sum of the used
// segments of weightPattern. The weightPattern should be distributed
// amongst bars that share the same domain and series category values.
expect(groupedStackedSeriesList.length, equals(6));
// Validate Desktop A series.
var series = groupedStackedSeriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(2 / 3));
expect(series.getAttr(stackKeyKey), equals('A'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(10));
expect(element.measureOffsetPlusMeasure, equals(15));
expect(series.measureOffsetFn(0), equals(10));
// Validate Tablet A series.
series = groupedStackedSeriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(2 / 3));
expect(series.getAttr(stackKeyKey), equals('A'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
// Validate Mobile A series.
series = groupedStackedSeriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(2 / 3));
expect(series.getAttr(stackKeyKey), equals('A'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
// Validate Desktop B series.
series = groupedStackedSeriesList[3];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('B'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(10));
expect(element.measureOffsetPlusMeasure, equals(15));
expect(series.measureOffsetFn(0), equals(10));
// Validate Tablet B series.
series = groupedStackedSeriesList[4];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('B'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
// Validate Mobile B series.
series = groupedStackedSeriesList[5];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(2));
expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('B'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
});
test('with stacked bars - weightPattern not used', () {
renderer = makeRenderer(
config: new BarRendererConfig(
groupingType: BarGroupingType.stacked, weightPattern: [2, 1]));
renderer.preprocessSeries(seriesList);
// Verify that weightPattern is not used, since stacked bars have only a
// single group per domain value.
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(10));
expect(element.measureOffsetPlusMeasure, equals(15));
expect(series.measureOffsetFn(0), equals(10));
// Validate Tablet series.
series = seriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
// Validate Mobile series.
series = seriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
});
});
group('null measure', () {
test('only include null in draw if animating from a non null measure', () {
// Helper to create series list for this test only.
List<MutableSeries<String>> _createSeriesList(List<MyRow> data) {
final domainAxis = new MockAxis<dynamic>();
when(domainAxis.rangeBand).thenReturn(100.0);
when(domainAxis.getLocation('MyCampaign1')).thenReturn(20.0);
when(domainAxis.getLocation('MyCampaign2')).thenReturn(40.0);
when(domainAxis.getLocation('MyCampaign3')).thenReturn(60.0);
when(domainAxis.getLocation('MyOtherCampaign')).thenReturn(80.0);
final measureAxis = new MockAxis<num>();
when(measureAxis.getLocation(0)).thenReturn(0.0);
when(measureAxis.getLocation(5)).thenReturn(5.0);
when(measureAxis.getLocation(75)).thenReturn(75.0);
when(measureAxis.getLocation(100)).thenReturn(100.0);
final color = new Color.fromHex(code: '#000000');
final series = new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
colorFn: (_, __) => color,
fillColorFn: (_, __) => color,
dashPatternFn: (_, __) => [1],
data: data))
..setAttr(domainAxisKey, domainAxis)
..setAttr(measureAxisKey, measureAxis);
return [series];
}
final canvas = new MockCanvas();
final myDataWithNull = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', null),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
final seriesListWithNull = _createSeriesList(myDataWithNull);
final myDataWithMeasures = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 0),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
final seriesListWithMeasures = _createSeriesList(myDataWithMeasures);
final renderer = makeFakeRenderer(
config: new BarRendererConfig(groupingType: BarGroupingType.grouped));
// Verify that only 3 bars are drawn for an initial draw with null data.
renderer.preprocessSeries(seriesListWithNull);
renderer.update(seriesListWithNull, true);
renderer.paintBarCallCount = 0;
renderer.paint(canvas, 0.5);
expect(renderer.paintBarCallCount, equals(3));
// On animation complete, verify that only 3 bars are drawn.
renderer.paintBarCallCount = 0;
renderer.paint(canvas, 1.0);
expect(renderer.paintBarCallCount, equals(3));
// Change series list where there are measures on all values, verify all
// 4 bars were drawn
renderer.preprocessSeries(seriesListWithMeasures);
renderer.update(seriesListWithMeasures, true);
renderer.paintBarCallCount = 0;
renderer.paint(canvas, 0.5);
expect(renderer.paintBarCallCount, equals(4));
// Change series to one with null measures, verifies all 4 bars drawn
renderer.preprocessSeries(seriesListWithNull);
renderer.update(seriesListWithNull, true);
renderer.paintBarCallCount = 0;
renderer.paint(canvas, 0.5);
expect(renderer.paintBarCallCount, equals(4));
// On animation complete, verify that only 3 bars are drawn.
renderer.paintBarCallCount = 0;
renderer.paint(canvas, 1.0);
expect(renderer.paintBarCallCount, equals(3));
});
});
}

View File

@@ -0,0 +1,653 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point, Rectangle;
import 'package:charts_common/src/chart/bar/bar_target_line_renderer.dart';
import 'package:charts_common/src/chart/bar/bar_target_line_renderer_config.dart';
import 'package:charts_common/src/chart/bar/base_bar_renderer.dart';
import 'package:charts_common/src/chart/bar/base_bar_renderer_config.dart';
import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart';
import 'package:charts_common/src/chart/cartesian/axis/axis.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/processed_series.dart'
show MutableSeries;
import 'package:charts_common/src/common/color.dart';
import 'package:charts_common/src/data/series.dart' show Series;
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
/// Datum/Row for the chart.
class MyRow {
final String campaign;
final int clickCount;
MyRow(this.campaign, this.clickCount);
}
class MockAxis<D> extends Mock implements Axis<D> {}
class MockCanvas extends Mock implements ChartCanvas {
final drawLinePointsList = <List<Point>>[];
void drawLine(
{List<Point> points,
Rectangle<num> clipBounds,
Color fill,
Color stroke,
bool roundEndCaps,
double strokeWidthPx,
List<int> dashPattern}) {
drawLinePointsList.add(points);
}
}
class MockContext extends Mock implements ChartContext {}
class MockChart extends Mock implements CartesianChart {}
void main() {
BarTargetLineRenderer renderer;
List<MutableSeries<String>> seriesList;
/////////////////////////////////////////
// Convenience methods for creating mocks.
/////////////////////////////////////////
_configureBaseRenderer(BaseBarRenderer renderer, bool vertical) {
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
final verticalChart = new MockChart();
when(verticalChart.vertical).thenReturn(vertical);
when(verticalChart.context).thenReturn(context);
renderer.onAttach(verticalChart);
return renderer;
}
BarTargetLineRenderer makeRenderer({BarTargetLineRendererConfig config}) {
final renderer = new BarTargetLineRenderer(config: config);
_configureBaseRenderer(renderer, true);
return renderer;
}
setUp(() {
var myFakeDesktopData = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 25),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
var myFakeTabletData = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 25),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
var myFakeMobileData = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 25),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
seriesList = [
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeDesktopData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Tablet',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeTabletData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Mobile',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeMobileData))
];
});
group('preprocess', () {
test('with grouped bar target lines', () {
renderer = makeRenderer(
config: new BarTargetLineRendererConfig(
groupingType: BarGroupingType.grouped));
renderer.preprocessSeries(seriesList);
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
// Validate Tablet series.
series = seriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(1 / 3));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
// Validate Mobile series.
series = seriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(2));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
});
test('with stacked bar target lines', () {
renderer = makeRenderer(
config: new BarTargetLineRendererConfig(
groupingType: BarGroupingType.stacked));
renderer.preprocessSeries(seriesList);
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(10));
expect(element.measureOffsetPlusMeasure, equals(15));
expect(series.measureOffsetFn(0), equals(10));
expect(element.strokeWidthPx, equals(3));
// Validate Tablet series.
series = seriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
expect(element.strokeWidthPx, equals(3));
// Validate Mobile series.
series = seriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
});
test('with stacked bar target lines containing zero and null', () {
// Set up some nulls and zeros in the data.
seriesList[2].data[0] = new MyRow('MyCampaign1', null);
seriesList[2].data[2] = new MyRow('MyCampaign3', 0);
seriesList[1].data[1] = new MyRow('MyCampaign2', null);
seriesList[1].data[3] = new MyRow('MyOtherCampaign', 0);
seriesList[0].data[2] = new MyRow('MyCampaign3', 0);
renderer = makeRenderer(
config: new BarTargetLineRendererConfig(
groupingType: BarGroupingType.stacked));
renderer.preprocessSeries(seriesList);
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
var elementsList = series.getAttr(barElementsKey);
var element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
expect(element.strokeWidthPx, equals(3));
element = elementsList[1];
expect(element.measureOffset, equals(25));
expect(element.measureOffsetPlusMeasure, equals(50));
expect(series.measureOffsetFn(1), equals(25));
expect(element.strokeWidthPx, equals(3));
element = elementsList[2];
expect(element.measureOffset, equals(100));
expect(element.measureOffsetPlusMeasure, equals(100));
expect(series.measureOffsetFn(2), equals(100));
expect(element.strokeWidthPx, equals(3));
element = elementsList[3];
expect(element.measureOffset, equals(75));
expect(element.measureOffsetPlusMeasure, equals(150));
expect(series.measureOffsetFn(3), equals(75));
expect(element.strokeWidthPx, equals(3));
// Validate Tablet series.
series = seriesList[1];
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
element = elementsList[1];
expect(element.measureOffset, equals(25));
expect(element.measureOffsetPlusMeasure, equals(25));
expect(series.measureOffsetFn(1), equals(25));
expect(element.strokeWidthPx, equals(3));
element = elementsList[2];
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(100));
expect(series.measureOffsetFn(2), equals(0));
expect(element.strokeWidthPx, equals(3));
element = elementsList[3];
expect(element.measureOffset, equals(75));
expect(element.measureOffsetPlusMeasure, equals(75));
expect(series.measureOffsetFn(3), equals(75));
expect(element.strokeWidthPx, equals(3));
// Validate Mobile series.
series = seriesList[2];
elementsList = series.getAttr(barElementsKey);
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(0));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
element = elementsList[1];
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(25));
expect(series.measureOffsetFn(1), equals(0));
expect(element.strokeWidthPx, equals(3));
element = elementsList[2];
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(0));
expect(series.measureOffsetFn(2), equals(0));
expect(element.strokeWidthPx, equals(3));
element = elementsList[3];
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(75));
expect(series.measureOffsetFn(3), equals(0));
expect(element.strokeWidthPx, equals(3));
});
});
test('with stroke width target lines', () {
renderer = makeRenderer(
config: new BarTargetLineRendererConfig(
groupingType: BarGroupingType.grouped, strokeWidthPx: 5.0));
renderer.preprocessSeries(seriesList);
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
var elementsList = series.getAttr(barElementsKey);
var element = elementsList[0];
expect(element.strokeWidthPx, equals(5));
element = elementsList[1];
expect(element.strokeWidthPx, equals(5));
element = elementsList[2];
expect(element.strokeWidthPx, equals(5));
element = elementsList[3];
expect(element.strokeWidthPx, equals(5));
// Validate Tablet series.
series = seriesList[1];
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.strokeWidthPx, equals(5));
element = elementsList[1];
expect(element.strokeWidthPx, equals(5));
element = elementsList[2];
expect(element.strokeWidthPx, equals(5));
element = elementsList[3];
expect(element.strokeWidthPx, equals(5));
// Validate Mobile series.
series = seriesList[2];
elementsList = series.getAttr(barElementsKey);
element = elementsList[0];
expect(element.strokeWidthPx, equals(5));
element = elementsList[1];
expect(element.strokeWidthPx, equals(5));
element = elementsList[2];
expect(element.strokeWidthPx, equals(5));
element = elementsList[3];
expect(element.strokeWidthPx, equals(5));
});
group('preprocess with weight pattern', () {
test('with grouped bar target lines', () {
renderer = makeRenderer(
config: new BarTargetLineRendererConfig(
groupingType: BarGroupingType.grouped, weightPattern: [3, 2, 1]));
renderer.preprocessSeries(seriesList);
// Verify that bar group weights are proportional to the sum of the used
// segments of weightPattern. The weightPattern should be distributed
// amongst bars that share the same domain value.
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(0.5));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
// Validate Tablet series.
series = seriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(1));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.5));
expect(series.getAttr(barGroupWeightKey), equals(1 / 3));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
// Validate Mobile series.
series = seriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(2));
expect(series.getAttr(barGroupCountKey), equals(3));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.5 + 1 / 3));
expect(series.getAttr(barGroupWeightKey), equals(1 / 6));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(null));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
});
test('with stacked bar target lines - weightPattern not used', () {
renderer = makeRenderer(
config: new BarTargetLineRendererConfig(
groupingType: BarGroupingType.stacked, weightPattern: [2, 1]));
renderer.preprocessSeries(seriesList);
// Verify that weightPattern is not used, since stacked bars have only a
// single group per domain value.
expect(seriesList.length, equals(3));
// Validate Desktop series.
var series = seriesList[0];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
var elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
var element = elementsList[0];
expect(element.barStackIndex, equals(2));
expect(element.measureOffset, equals(10));
expect(element.measureOffsetPlusMeasure, equals(15));
expect(series.measureOffsetFn(0), equals(10));
expect(element.strokeWidthPx, equals(3));
// Validate Tablet series.
series = seriesList[1];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(1));
expect(element.measureOffset, equals(5));
expect(element.measureOffsetPlusMeasure, equals(10));
expect(series.measureOffsetFn(0), equals(5));
expect(element.strokeWidthPx, equals(3));
// Validate Mobile series.
series = seriesList[2];
expect(series.getAttr(barGroupIndexKey), equals(0));
expect(series.getAttr(barGroupCountKey), equals(1));
expect(series.getAttr(previousBarGroupWeightKey), equals(0.0));
expect(series.getAttr(barGroupWeightKey), equals(1));
expect(series.getAttr(stackKeyKey), equals('__defaultKey__'));
elementsList = series.getAttr(barElementsKey);
expect(elementsList.length, equals(4));
element = elementsList[0];
expect(element.barStackIndex, equals(0));
expect(element.measureOffset, equals(0));
expect(element.measureOffsetPlusMeasure, equals(5));
expect(series.measureOffsetFn(0), equals(0));
expect(element.strokeWidthPx, equals(3));
});
});
group('null measure', () {
test('only include null in draw if animating from a non null measure', () {
// Helper to create series list for this test only.
List<MutableSeries<String>> _createSeriesList(List<MyRow> data) {
final domainAxis = new MockAxis<dynamic>();
when(domainAxis.rangeBand).thenReturn(100.0);
when(domainAxis.getLocation('MyCampaign1')).thenReturn(20.0);
when(domainAxis.getLocation('MyCampaign2')).thenReturn(40.0);
when(domainAxis.getLocation('MyCampaign3')).thenReturn(60.0);
when(domainAxis.getLocation('MyOtherCampaign')).thenReturn(80.0);
final measureAxis = new MockAxis<num>();
when(measureAxis.getLocation(0)).thenReturn(0.0);
when(measureAxis.getLocation(5)).thenReturn(5.0);
when(measureAxis.getLocation(75)).thenReturn(75.0);
when(measureAxis.getLocation(100)).thenReturn(100.0);
final color = new Color.fromHex(code: '#000000');
final series = new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
colorFn: (_, __) => color,
fillColorFn: (_, __) => color,
dashPatternFn: (_, __) => [1],
data: data))
..setAttr(domainAxisKey, domainAxis)
..setAttr(measureAxisKey, measureAxis);
return [series];
}
final canvas = new MockCanvas();
final myDataWithNull = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', null),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
final seriesListWithNull = _createSeriesList(myDataWithNull);
final myDataWithMeasures = [
new MyRow('MyCampaign1', 5),
new MyRow('MyCampaign2', 0),
new MyRow('MyCampaign3', 100),
new MyRow('MyOtherCampaign', 75),
];
final seriesListWithMeasures = _createSeriesList(myDataWithMeasures);
renderer = makeRenderer(
config: new BarTargetLineRendererConfig(
groupingType: BarGroupingType.grouped));
// Verify that only 3 lines are drawn for an initial draw with null data.
renderer.preprocessSeries(seriesListWithNull);
renderer.update(seriesListWithNull, true);
canvas.drawLinePointsList.clear();
renderer.paint(canvas, 0.5);
expect(canvas.drawLinePointsList, hasLength(3));
// On animation complete, verify that only 3 lines are drawn.
canvas.drawLinePointsList.clear();
renderer.paint(canvas, 1.0);
expect(canvas.drawLinePointsList, hasLength(3));
// Change series list where there are measures on all values, verify all
// 4 lines were drawn
renderer.preprocessSeries(seriesListWithMeasures);
renderer.update(seriesListWithMeasures, true);
canvas.drawLinePointsList.clear();
renderer.paint(canvas, 0.5);
expect(canvas.drawLinePointsList, hasLength(4));
// Change series to one with null measures, verifies all 4 lines drawn
renderer.preprocessSeries(seriesListWithNull);
renderer.update(seriesListWithNull, true);
canvas.drawLinePointsList.clear();
renderer.paint(canvas, 0.5);
expect(canvas.drawLinePointsList, hasLength(4));
// On animation complete, verify that only 3 lines are drawn.
canvas.drawLinePointsList.clear();
renderer.paint(canvas, 1.0);
expect(canvas.drawLinePointsList, hasLength(3));
});
});
}

File diff suppressed because it is too large Load Diff

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

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 'dart:math' show Rectangle;
import 'package:charts_common/src/chart/common/chart_context.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/common/behavior/a11y/domain_a11y_explore_behavior.dart';
import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockContext extends Mock implements ChartContext {}
class MockAxis extends Mock implements Axis<String> {}
class FakeCartesianChart extends CartesianChart<String> {
@override
Rectangle<int> drawAreaBounds;
void callFireOnPostprocess(List<MutableSeries<String>> seriesList) {
fireOnPostprocess(seriesList);
}
@override
initDomainAxis() {}
}
void main() {
FakeCartesianChart chart;
DomainA11yExploreBehavior<String> behavior;
MockAxis domainAxis;
MutableSeries<String> _series1;
final _s1D1 = new MyRow('s1d1', 11, 'a11yd1');
final _s1D2 = new MyRow('s1d2', 12, 'a11yd2');
final _s1D3 = new MyRow('s1d3', 13, 'a11yd3');
setUp(() {
chart = new FakeCartesianChart()
..drawAreaBounds = new Rectangle(50, 20, 150, 80);
behavior = new DomainA11yExploreBehavior<String>(
vocalizationCallback: domainVocalization);
behavior.attachTo(chart);
domainAxis = new MockAxis();
_series1 = new MutableSeries(new Series<MyRow, String>(
id: 's1',
data: [_s1D1, _s1D2, _s1D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
))
..setAttr(domainAxisKey, domainAxis);
});
test('creates nodes for vertically drawn charts', () {
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
chart.context = context;
// Drawn vertically
chart.vertical = true;
// Set step size of 50, which should be the width of the bounding box
when(domainAxis.stepSize).thenReturn(50.0);
when(domainAxis.getLocation('s1d1')).thenReturn(75.0);
when(domainAxis.getLocation('s1d2')).thenReturn(125.0);
when(domainAxis.getLocation('s1d3')).thenReturn(175.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 50, 80)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(150, 20, 50, 80)));
});
test('creates nodes for vertically drawn RTL charts', () {
// A RTL chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(true);
when(context.isRtl).thenReturn(true);
chart.context = context;
// Drawn vertically
chart.vertical = true;
// Set step size of 50, which should be the width of the bounding box
when(domainAxis.stepSize).thenReturn(50.0);
when(domainAxis.getLocation('s1d1')).thenReturn(175.0);
when(domainAxis.getLocation('s1d2')).thenReturn(125.0);
when(domainAxis.getLocation('s1d3')).thenReturn(75.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(150, 20, 50, 80)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(50, 20, 50, 80)));
});
test('creates nodes for horizontally drawn charts', () {
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
chart.context = context;
// Drawn horizontally
chart.vertical = false;
// Set step size of 20, which should be the height of the bounding box
when(domainAxis.stepSize).thenReturn(20.0);
when(domainAxis.getLocation('s1d1')).thenReturn(30.0);
when(domainAxis.getLocation('s1d2')).thenReturn(50.0);
when(domainAxis.getLocation('s1d3')).thenReturn(70.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 150, 20)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(50, 40, 150, 20)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(50, 60, 150, 20)));
});
test('creates nodes for horizontally drawn RTL charts', () {
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(true);
when(context.isRtl).thenReturn(true);
chart.context = context;
// Drawn horizontally
chart.vertical = false;
// Set step size of 20, which should be the height of the bounding box
when(domainAxis.stepSize).thenReturn(20.0);
when(domainAxis.getLocation('s1d1')).thenReturn(30.0);
when(domainAxis.getLocation('s1d2')).thenReturn(50.0);
when(domainAxis.getLocation('s1d3')).thenReturn(70.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 150, 20)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(50, 40, 150, 20)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(50, 60, 150, 20)));
});
test('nodes ordered correctly with a series missing a domain', () {
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
chart.context = context;
// Drawn vertically
chart.vertical = true;
// Set step size of 50, which should be the width of the bounding box
when(domainAxis.stepSize).thenReturn(50.0);
when(domainAxis.getLocation('s1d1')).thenReturn(75.0);
when(domainAxis.getLocation('s1d2')).thenReturn(125.0);
when(domainAxis.getLocation('s1d3')).thenReturn(175.0);
// Create a series with a missing domain
final seriesWithMissingDomain = new MutableSeries(new Series<MyRow, String>(
id: 'm1',
data: [_s1D1, _s1D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
))
..setAttr(domainAxisKey, domainAxis);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([seriesWithMissingDomain, _series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 50, 80)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(150, 20, 50, 80)));
});
test('creates nodes with minimum width', () {
// A behavior with minimum width of 50
final behaviorWithMinWidth =
new DomainA11yExploreBehavior<String>(minimumWidth: 50.0);
behaviorWithMinWidth.attachTo(chart);
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
chart.context = context;
// Drawn vertically
chart.vertical = true;
// Return a step size of 20, which is less than the minimum width.
// Expect the results to use the minimum width of 50 instead.
when(domainAxis.stepSize).thenReturn(20.0);
when(domainAxis.getLocation('s1d1')).thenReturn(75.0);
when(domainAxis.getLocation('s1d2')).thenReturn(125.0);
when(domainAxis.getLocation('s1d3')).thenReturn(175.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behaviorWithMinWidth.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 50, 80)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(150, 20, 50, 80)));
});
}
class MyRow {
final String campaign;
final int count;
final String a11yDescription;
MyRow(this.campaign, this.count, this.a11yDescription);
}

View File

@@ -0,0 +1,593 @@
// 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/common/base_chart.dart';
import 'package:charts_common/src/chart/common/processed_series.dart'
show MutableSeries;
import 'package:charts_common/src/chart/common/behavior/calculation/percent_injector.dart';
import 'package:charts_common/src/data/series.dart' show Series;
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
/// Datum/Row for the chart.
class MyRow {
final String campaign;
final int clickCount;
final int clickCountLower;
final int clickCountUpper;
MyRow(this.campaign, this.clickCount, this.clickCountLower,
this.clickCountUpper);
}
class MockChart extends Mock implements CartesianChart {
LifecycleListener lastLifecycleListener;
bool vertical = true;
@override
addLifecycleListener(LifecycleListener listener) =>
lastLifecycleListener = listener;
@override
removeLifecycleListener(LifecycleListener listener) {
expect(listener, equals(lastLifecycleListener));
lastLifecycleListener = null;
return true;
}
}
void main() {
MockChart _chart;
List<MutableSeries<String>> seriesList;
PercentInjector _makeBehavior(
{PercentInjectorTotalType totalType = PercentInjectorTotalType.domain}) {
final behavior = new PercentInjector(totalType: totalType);
behavior.attachTo(_chart);
return behavior;
}
setUp(() {
_chart = new MockChart();
final myFakeDesktopAData = [
new MyRow('MyCampaign1', 1, 1, 1),
new MyRow('MyCampaign2', 2, 2, 2),
new MyRow('MyCampaign3', 3, 3, 3),
];
final myFakeTabletAData = [
new MyRow('MyCampaign1', 2, 2, 2),
new MyRow('MyCampaign2', 3, 3, 3),
new MyRow('MyCampaign3', 4, 4, 4),
];
final myFakeMobileAData = [
new MyRow('MyCampaign1', 3, 3, 3),
new MyRow('MyCampaign2', 4, 4, 4),
new MyRow('MyCampaign3', 5, 5, 5),
];
final myFakeDesktopBData = [
new MyRow('MyCampaign1', 10, 8, 12),
new MyRow('MyCampaign2', 20, 18, 22),
new MyRow('MyCampaign3', 30, 28, 32),
];
final myFakeTabletBData = [
new MyRow('MyCampaign1', 20, 18, 22),
new MyRow('MyCampaign2', 30, 28, 32),
new MyRow('MyCampaign3', 40, 38, 42),
];
final myFakeMobileBData = [
new MyRow('MyCampaign1', 30, 28, 32),
new MyRow('MyCampaign2', 40, 38, 42),
new MyRow('MyCampaign3', 50, 48, 52),
];
seriesList = [
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop A',
seriesCategory: 'A',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeDesktopAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Tablet A',
seriesCategory: 'A',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeTabletAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Mobile A',
seriesCategory: 'A',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeMobileAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop B',
seriesCategory: 'B',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureLowerBoundFn: (MyRow row, _) => row.clickCountLower,
measureUpperBoundFn: (MyRow row, _) => row.clickCountUpper,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeDesktopBData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Tablet B',
seriesCategory: 'B',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureLowerBoundFn: (MyRow row, _) => row.clickCountLower,
measureUpperBoundFn: (MyRow row, _) => row.clickCountUpper,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeTabletBData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Mobile B',
seriesCategory: 'B',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureLowerBoundFn: (MyRow row, _) => row.clickCountLower,
measureUpperBoundFn: (MyRow row, _) => row.clickCountUpper,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeMobileBData))
];
});
group('Inject', () {
test('percent of domain', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.domain);
// Act
_chart.lastLifecycleListener.onData(seriesList);
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify first series.
var series = seriesList[0];
expect(series.measureFn(0), equals(1 / 66));
expect(series.measureFn(1), equals(2 / 99));
expect(series.measureFn(2), equals(3 / 132));
expect(series.rawMeasureFn(0), equals(1));
expect(series.rawMeasureFn(1), equals(2));
expect(series.rawMeasureFn(2), equals(3));
// Verify second series.
series = seriesList[1];
expect(series.measureFn(0), equals(2 / 66));
expect(series.measureFn(1), equals(3 / 99));
expect(series.measureFn(2), equals(4 / 132));
expect(series.rawMeasureFn(0), equals(2));
expect(series.rawMeasureFn(1), equals(3));
expect(series.rawMeasureFn(2), equals(4));
// Verify third series.
series = seriesList[2];
expect(series.measureFn(0), equals(3 / 66));
expect(series.measureFn(1), equals(4 / 99));
expect(series.measureFn(2), equals(5 / 132));
expect(series.rawMeasureFn(0), equals(3));
expect(series.rawMeasureFn(1), equals(4));
expect(series.rawMeasureFn(2), equals(5));
// Verify fourth series.
series = seriesList[3];
expect(series.measureFn(0), equals(10 / 66));
expect(series.measureFn(1), equals(20 / 99));
expect(series.measureFn(2), equals(30 / 132));
expect(series.rawMeasureFn(0), equals(10));
expect(series.rawMeasureFn(1), equals(20));
expect(series.rawMeasureFn(2), equals(30));
expect(series.measureLowerBoundFn(0), equals(8 / 66));
expect(series.measureLowerBoundFn(1), equals(18 / 99));
expect(series.measureLowerBoundFn(2), equals(28 / 132));
expect(series.rawMeasureLowerBoundFn(0), equals(8));
expect(series.rawMeasureLowerBoundFn(1), equals(18));
expect(series.rawMeasureLowerBoundFn(2), equals(28));
expect(series.measureUpperBoundFn(0), equals(12 / 66));
expect(series.measureUpperBoundFn(1), equals(22 / 99));
expect(series.measureUpperBoundFn(2), equals(32 / 132));
expect(series.rawMeasureUpperBoundFn(0), equals(12));
expect(series.rawMeasureUpperBoundFn(1), equals(22));
expect(series.rawMeasureUpperBoundFn(2), equals(32));
// Verify fifth series.
series = seriesList[4];
expect(series.measureFn(0), equals(20 / 66));
expect(series.measureFn(1), equals(30 / 99));
expect(series.measureFn(2), equals(40 / 132));
expect(series.rawMeasureFn(0), equals(20));
expect(series.rawMeasureFn(1), equals(30));
expect(series.rawMeasureFn(2), equals(40));
expect(series.measureLowerBoundFn(0), equals(18 / 66));
expect(series.measureLowerBoundFn(1), equals(28 / 99));
expect(series.measureLowerBoundFn(2), equals(38 / 132));
expect(series.rawMeasureLowerBoundFn(0), equals(18));
expect(series.rawMeasureLowerBoundFn(1), equals(28));
expect(series.rawMeasureLowerBoundFn(2), equals(38));
expect(series.measureUpperBoundFn(0), equals(22 / 66));
expect(series.measureUpperBoundFn(1), equals(32 / 99));
expect(series.measureUpperBoundFn(2), equals(42 / 132));
expect(series.rawMeasureUpperBoundFn(0), equals(22));
expect(series.rawMeasureUpperBoundFn(1), equals(32));
expect(series.rawMeasureUpperBoundFn(2), equals(42));
// Verify sixth series.
series = seriesList[5];
expect(series.measureFn(0), equals(30 / 66));
expect(series.measureFn(1), equals(40 / 99));
expect(series.measureFn(2), equals(50 / 132));
expect(series.rawMeasureFn(0), equals(30));
expect(series.rawMeasureFn(1), equals(40));
expect(series.rawMeasureFn(2), equals(50));
expect(series.measureLowerBoundFn(0), equals(28 / 66));
expect(series.measureLowerBoundFn(1), equals(38 / 99));
expect(series.measureLowerBoundFn(2), equals(48 / 132));
expect(series.rawMeasureLowerBoundFn(0), equals(28));
expect(series.rawMeasureLowerBoundFn(1), equals(38));
expect(series.rawMeasureLowerBoundFn(2), equals(48));
expect(series.measureUpperBoundFn(0), equals(32 / 66));
expect(series.measureUpperBoundFn(1), equals(42 / 99));
expect(series.measureUpperBoundFn(2), equals(52 / 132));
expect(series.rawMeasureUpperBoundFn(0), equals(32));
expect(series.rawMeasureUpperBoundFn(1), equals(42));
expect(series.rawMeasureUpperBoundFn(2), equals(52));
});
test('percent of domain, grouped by series category', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.domainBySeriesCategory);
// Act
_chart.lastLifecycleListener.onData(seriesList);
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify first series.
var series = seriesList[0];
expect(series.measureFn(0), equals(1 / 6));
expect(series.measureFn(1), equals(2 / 9));
expect(series.measureFn(2), equals(3 / 12));
expect(series.rawMeasureFn(0), equals(1));
expect(series.rawMeasureFn(1), equals(2));
expect(series.rawMeasureFn(2), equals(3));
// Verify second series.
series = seriesList[1];
expect(series.measureFn(0), equals(2 / 6));
expect(series.measureFn(1), equals(3 / 9));
expect(series.measureFn(2), equals(4 / 12));
expect(series.rawMeasureFn(0), equals(2));
expect(series.rawMeasureFn(1), equals(3));
expect(series.rawMeasureFn(2), equals(4));
// Verify third series.
series = seriesList[2];
expect(series.measureFn(0), equals(3 / 6));
expect(series.measureFn(1), equals(4 / 9));
expect(series.measureFn(2), equals(5 / 12));
expect(series.rawMeasureFn(0), equals(3));
expect(series.rawMeasureFn(1), equals(4));
expect(series.rawMeasureFn(2), equals(5));
// Verify fourth series.
series = seriesList[3];
expect(series.measureFn(0), equals(10 / 60));
expect(series.measureFn(1), equals(20 / 90));
expect(series.measureFn(2), equals(30 / 120));
expect(series.rawMeasureFn(0), equals(10));
expect(series.rawMeasureFn(1), equals(20));
expect(series.rawMeasureFn(2), equals(30));
expect(series.measureLowerBoundFn(0), equals(8 / 60));
expect(series.measureLowerBoundFn(1), equals(18 / 90));
expect(series.measureLowerBoundFn(2), equals(28 / 120));
expect(series.rawMeasureLowerBoundFn(0), equals(8));
expect(series.rawMeasureLowerBoundFn(1), equals(18));
expect(series.rawMeasureLowerBoundFn(2), equals(28));
expect(series.measureUpperBoundFn(0), equals(12 / 60));
expect(series.measureUpperBoundFn(1), equals(22 / 90));
expect(series.measureUpperBoundFn(2), equals(32 / 120));
expect(series.rawMeasureUpperBoundFn(0), equals(12));
expect(series.rawMeasureUpperBoundFn(1), equals(22));
expect(series.rawMeasureUpperBoundFn(2), equals(32));
// Verify fifth series.
series = seriesList[4];
expect(series.measureFn(0), equals(20 / 60));
expect(series.measureFn(1), equals(30 / 90));
expect(series.measureFn(2), equals(40 / 120));
expect(series.rawMeasureFn(0), equals(20));
expect(series.rawMeasureFn(1), equals(30));
expect(series.rawMeasureFn(2), equals(40));
expect(series.measureLowerBoundFn(0), equals(18 / 60));
expect(series.measureLowerBoundFn(1), equals(28 / 90));
expect(series.measureLowerBoundFn(2), equals(38 / 120));
expect(series.rawMeasureLowerBoundFn(0), equals(18));
expect(series.rawMeasureLowerBoundFn(1), equals(28));
expect(series.rawMeasureLowerBoundFn(2), equals(38));
expect(series.measureUpperBoundFn(0), equals(22 / 60));
expect(series.measureUpperBoundFn(1), equals(32 / 90));
expect(series.measureUpperBoundFn(2), equals(42 / 120));
expect(series.rawMeasureUpperBoundFn(0), equals(22));
expect(series.rawMeasureUpperBoundFn(1), equals(32));
expect(series.rawMeasureUpperBoundFn(2), equals(42));
// Verify sixth series.
series = seriesList[5];
expect(series.measureFn(0), equals(30 / 60));
expect(series.measureFn(1), equals(40 / 90));
expect(series.measureFn(2), equals(50 / 120));
expect(series.rawMeasureFn(0), equals(30));
expect(series.rawMeasureFn(1), equals(40));
expect(series.rawMeasureFn(2), equals(50));
expect(series.measureLowerBoundFn(0), equals(28 / 60));
expect(series.measureLowerBoundFn(1), equals(38 / 90));
expect(series.measureLowerBoundFn(2), equals(48 / 120));
expect(series.rawMeasureLowerBoundFn(0), equals(28));
expect(series.rawMeasureLowerBoundFn(1), equals(38));
expect(series.rawMeasureLowerBoundFn(2), equals(48));
expect(series.measureUpperBoundFn(0), equals(32 / 60));
expect(series.measureUpperBoundFn(1), equals(42 / 90));
expect(series.measureUpperBoundFn(2), equals(52 / 120));
expect(series.rawMeasureUpperBoundFn(0), equals(32));
expect(series.rawMeasureUpperBoundFn(1), equals(42));
expect(series.rawMeasureUpperBoundFn(2), equals(52));
});
test('percent of series', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.series);
// Act
_chart.lastLifecycleListener.onData(seriesList);
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify that every series has a total measure value. Technically this is
// handled in MutableSeries, but it is a pre-condition for this behavior
// functioning properly.
expect(seriesList[0].seriesMeasureTotal, equals(6));
expect(seriesList[1].seriesMeasureTotal, equals(9));
expect(seriesList[2].seriesMeasureTotal, equals(12));
expect(seriesList[3].seriesMeasureTotal, equals(60));
expect(seriesList[4].seriesMeasureTotal, equals(90));
expect(seriesList[5].seriesMeasureTotal, equals(120));
// Verify first series.
var series = seriesList[0];
expect(series.measureFn(0), equals(1 / 6));
expect(series.measureFn(1), equals(2 / 6));
expect(series.measureFn(2), equals(3 / 6));
expect(series.rawMeasureFn(0), equals(1));
expect(series.rawMeasureFn(1), equals(2));
expect(series.rawMeasureFn(2), equals(3));
// Verify second series.
series = seriesList[1];
expect(series.measureFn(0), equals(2 / 9));
expect(series.measureFn(1), equals(3 / 9));
expect(series.measureFn(2), equals(4 / 9));
expect(series.rawMeasureFn(0), equals(2));
expect(series.rawMeasureFn(1), equals(3));
expect(series.rawMeasureFn(2), equals(4));
// Verify third series.
series = seriesList[2];
expect(series.measureFn(0), equals(3 / 12));
expect(series.measureFn(1), equals(4 / 12));
expect(series.measureFn(2), equals(5 / 12));
expect(series.rawMeasureFn(0), equals(3));
expect(series.rawMeasureFn(1), equals(4));
expect(series.rawMeasureFn(2), equals(5));
// Verify fourth series.
series = seriesList[3];
expect(series.measureFn(0), equals(10 / 60));
expect(series.measureFn(1), equals(20 / 60));
expect(series.measureFn(2), equals(30 / 60));
expect(series.rawMeasureFn(0), equals(10));
expect(series.rawMeasureFn(1), equals(20));
expect(series.rawMeasureFn(2), equals(30));
expect(series.measureLowerBoundFn(0), equals(8 / 60));
expect(series.measureLowerBoundFn(1), equals(18 / 60));
expect(series.measureLowerBoundFn(2), equals(28 / 60));
expect(series.rawMeasureLowerBoundFn(0), equals(8));
expect(series.rawMeasureLowerBoundFn(1), equals(18));
expect(series.rawMeasureLowerBoundFn(2), equals(28));
expect(series.measureUpperBoundFn(0), equals(12 / 60));
expect(series.measureUpperBoundFn(1), equals(22 / 60));
expect(series.measureUpperBoundFn(2), equals(32 / 60));
expect(series.rawMeasureUpperBoundFn(0), equals(12));
expect(series.rawMeasureUpperBoundFn(1), equals(22));
expect(series.rawMeasureUpperBoundFn(2), equals(32));
// Verify fifth series.
series = seriesList[4];
expect(series.measureFn(0), equals(20 / 90));
expect(series.measureFn(1), equals(30 / 90));
expect(series.measureFn(2), equals(40 / 90));
expect(series.rawMeasureFn(0), equals(20));
expect(series.rawMeasureFn(1), equals(30));
expect(series.rawMeasureFn(2), equals(40));
expect(series.measureLowerBoundFn(0), equals(18 / 90));
expect(series.measureLowerBoundFn(1), equals(28 / 90));
expect(series.measureLowerBoundFn(2), equals(38 / 90));
expect(series.rawMeasureLowerBoundFn(0), equals(18));
expect(series.rawMeasureLowerBoundFn(1), equals(28));
expect(series.rawMeasureLowerBoundFn(2), equals(38));
expect(series.measureUpperBoundFn(0), equals(22 / 90));
expect(series.measureUpperBoundFn(1), equals(32 / 90));
expect(series.measureUpperBoundFn(2), equals(42 / 90));
expect(series.rawMeasureUpperBoundFn(0), equals(22));
expect(series.rawMeasureUpperBoundFn(1), equals(32));
expect(series.rawMeasureUpperBoundFn(2), equals(42));
// Verify sixth series.
series = seriesList[5];
expect(series.measureFn(0), equals(30 / 120));
expect(series.measureFn(1), equals(40 / 120));
expect(series.measureFn(2), equals(50 / 120));
expect(series.rawMeasureFn(0), equals(30));
expect(series.rawMeasureFn(1), equals(40));
expect(series.rawMeasureFn(2), equals(50));
expect(series.measureLowerBoundFn(0), equals(28 / 120));
expect(series.measureLowerBoundFn(1), equals(38 / 120));
expect(series.measureLowerBoundFn(2), equals(48 / 120));
expect(series.rawMeasureLowerBoundFn(0), equals(28));
expect(series.rawMeasureLowerBoundFn(1), equals(38));
expect(series.rawMeasureLowerBoundFn(2), equals(48));
expect(series.measureUpperBoundFn(0), equals(32 / 120));
expect(series.measureUpperBoundFn(1), equals(42 / 120));
expect(series.measureUpperBoundFn(2), equals(52 / 120));
expect(series.rawMeasureUpperBoundFn(0), equals(32));
expect(series.rawMeasureUpperBoundFn(1), equals(42));
expect(series.rawMeasureUpperBoundFn(2), equals(52));
});
});
group('Life cycle', () {
test('sets injected flag for percent of domain', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.domain);
// Act
_chart.lastLifecycleListener.onData(seriesList);
// Verify that each series has an initially false flag.
expect(seriesList[0].getAttr(percentInjectedKey), isFalse);
expect(seriesList[1].getAttr(percentInjectedKey), isFalse);
expect(seriesList[2].getAttr(percentInjectedKey), isFalse);
expect(seriesList[3].getAttr(percentInjectedKey), isFalse);
expect(seriesList[4].getAttr(percentInjectedKey), isFalse);
expect(seriesList[5].getAttr(percentInjectedKey), isFalse);
// Act
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify that each series has a true flag.
expect(seriesList[0].getAttr(percentInjectedKey), isTrue);
expect(seriesList[1].getAttr(percentInjectedKey), isTrue);
expect(seriesList[2].getAttr(percentInjectedKey), isTrue);
expect(seriesList[3].getAttr(percentInjectedKey), isTrue);
expect(seriesList[4].getAttr(percentInjectedKey), isTrue);
expect(seriesList[5].getAttr(percentInjectedKey), isTrue);
});
test('sets injected flag for percent of series', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.series);
// Act
_chart.lastLifecycleListener.onData(seriesList);
// Verify that each series has an initially false flag.
expect(seriesList[0].getAttr(percentInjectedKey), isFalse);
expect(seriesList[1].getAttr(percentInjectedKey), isFalse);
expect(seriesList[2].getAttr(percentInjectedKey), isFalse);
expect(seriesList[3].getAttr(percentInjectedKey), isFalse);
expect(seriesList[4].getAttr(percentInjectedKey), isFalse);
expect(seriesList[5].getAttr(percentInjectedKey), isFalse);
// Act
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify that each series has a true flag.
expect(seriesList[0].getAttr(percentInjectedKey), isTrue);
expect(seriesList[1].getAttr(percentInjectedKey), isTrue);
expect(seriesList[2].getAttr(percentInjectedKey), isTrue);
expect(seriesList[3].getAttr(percentInjectedKey), isTrue);
expect(seriesList[4].getAttr(percentInjectedKey), isTrue);
expect(seriesList[5].getAttr(percentInjectedKey), isTrue);
});
});
}

View File

@@ -0,0 +1,153 @@
// 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/common/series_renderer.dart';
import 'package:mockito/mockito.dart';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/behavior/chart_behavior.dart';
import 'package:charts_common/src/chart/common/datum_details.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:test/test.dart';
class MockBehavior extends Mock implements ChartBehavior<String> {}
class ParentBehavior implements ChartBehavior<String> {
final ChartBehavior<String> child;
ParentBehavior(this.child);
String get role => null;
@override
void attachTo(BaseChart chart) {
chart.addBehavior(child);
}
@override
void removeFrom(BaseChart chart) {
chart.removeBehavior(child);
}
}
class ConcreteChart extends BaseChart<String> {
@override
SeriesRenderer<String> makeDefaultRenderer() => null;
@override
List<DatumDetails<String>> getDatumDetails(SelectionModelType _) => null;
}
void main() {
ConcreteChart chart;
MockBehavior namedBehavior;
MockBehavior unnamedBehavior;
setUp(() {
chart = new ConcreteChart();
namedBehavior = new MockBehavior();
when(namedBehavior.role).thenReturn('foo');
unnamedBehavior = new MockBehavior();
when(unnamedBehavior.role).thenReturn(null);
});
group('Attach & Detach', () {
test('attach is called once', () {
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
verify(namedBehavior.role);
verifyNoMoreInteractions(namedBehavior);
});
test('deteach is called once', () {
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
chart.removeBehavior(namedBehavior);
verify(namedBehavior.removeFrom(chart)).called(1);
verify(namedBehavior.role);
verifyNoMoreInteractions(namedBehavior);
});
test('detach is called when name is reused', () {
final otherBehavior = new MockBehavior();
when(otherBehavior.role).thenReturn('foo');
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
chart.addBehavior(otherBehavior);
verify(namedBehavior.removeFrom(chart)).called(1);
verify(otherBehavior.attachTo(chart)).called(1);
verify(namedBehavior.role);
verify(otherBehavior.role);
verifyNoMoreInteractions(namedBehavior);
verifyNoMoreInteractions(otherBehavior);
});
test('detach is not called when name is null', () {
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
chart.addBehavior(unnamedBehavior);
verify(unnamedBehavior.attachTo(chart)).called(1);
verify(namedBehavior.role);
verify(unnamedBehavior.role);
verifyNoMoreInteractions(namedBehavior);
verifyNoMoreInteractions(unnamedBehavior);
});
test('detach is not called when name is different', () {
final otherBehavior = new MockBehavior();
when(otherBehavior.role).thenReturn('bar');
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
chart.addBehavior(otherBehavior);
verify(otherBehavior.attachTo(chart)).called(1);
verify(namedBehavior.role);
verify(otherBehavior.role);
verifyNoMoreInteractions(namedBehavior);
verifyNoMoreInteractions(otherBehavior);
});
test('behaviors are removed when chart is destroyed', () {
final parentBehavior = new ParentBehavior(unnamedBehavior);
chart.addBehavior(parentBehavior);
// The parent should add the child behavoir.
verify(unnamedBehavior.attachTo(chart)).called(1);
chart.destroy();
// The parent should remove the child behavior.
verify(unnamedBehavior.removeFrom(chart)).called(1);
// Remove should only be called once and shouldn't trigger a concurrent
// modification exception.
verify(unnamedBehavior.role);
verifyNoMoreInteractions(unnamedBehavior);
});
});
}

View File

@@ -0,0 +1,185 @@
// 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/common/base_chart.dart';
import 'package:charts_common/src/chart/common/behavior/domain_highlighter.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/common/material_palette.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChart extends Mock implements BaseChart {
LifecycleListener lastListener;
@override
addLifecycleListener(LifecycleListener listener) => lastListener = listener;
@override
removeLifecycleListener(LifecycleListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
return true;
}
}
class MockSelectionModel extends Mock implements MutableSelectionModel {
SelectionModelListener lastListener;
@override
addSelectionChangedListener(SelectionModelListener listener) =>
lastListener = listener;
@override
removeSelectionChangedListener(SelectionModelListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
}
}
void main() {
MockChart _chart;
MockSelectionModel _selectionModel;
MutableSeries<String> _series1;
final _s1D1 = new MyRow('s1d1', 11);
final _s1D2 = new MyRow('s1d2', 12);
final _s1D3 = new MyRow('s1d3', 13);
MutableSeries<String> _series2;
final _s2D1 = new MyRow('s2d1', 21);
final _s2D2 = new MyRow('s2d2', 22);
final _s2D3 = new MyRow('s2d3', 23);
_setupSelection(List<MyRow> selected) {
for (var i = 0; i < _series1.data.length; i++) {
when(_selectionModel.isDatumSelected(_series1, i))
.thenReturn(selected.contains(_series1.data[i]));
}
for (var i = 0; i < _series2.data.length; i++) {
when(_selectionModel.isDatumSelected(_series2, i))
.thenReturn(selected.contains(_series2.data[i]));
}
}
setUp(() {
_chart = new MockChart();
_selectionModel = new MockSelectionModel();
when(_chart.getSelectionModel(SelectionModelType.info))
.thenReturn(_selectionModel);
_series1 = new MutableSeries(new Series<MyRow, String>(
id: 's1',
data: [_s1D1, _s1D2, _s1D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => MaterialPalette.blue.shadeDefault))
..measureFn = (_) => 0.0;
_series2 = new MutableSeries(new Series<MyRow, String>(
id: 's2',
data: [_s2D1, _s2D2, _s2D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => MaterialPalette.red.shadeDefault))
..measureFn = (_) => 0.0;
});
group('DomainHighligher', () {
test('darkens the selected bars', () {
// Setup
final behavior = new DomainHighlighter(SelectionModelType.info);
behavior.attachTo(_chart);
_setupSelection([_s1D2, _s2D2]);
final seriesList = [_series1, _series2];
// Act
_selectionModel.lastListener(_selectionModel);
verify(_chart.redraw(skipAnimation: true, skipLayout: true));
_chart.lastListener.onPostprocess(seriesList);
// Verify
final s1ColorFn = _series1.colorFn;
expect(s1ColorFn(0), equals(MaterialPalette.blue.shadeDefault));
expect(s1ColorFn(1), equals(MaterialPalette.blue.shadeDefault.darker));
expect(s1ColorFn(2), equals(MaterialPalette.blue.shadeDefault));
final s2ColorFn = _series2.colorFn;
expect(s2ColorFn(0), equals(MaterialPalette.red.shadeDefault));
expect(s2ColorFn(1), equals(MaterialPalette.red.shadeDefault.darker));
expect(s2ColorFn(2), equals(MaterialPalette.red.shadeDefault));
});
test('listens to other selection models', () {
// Setup
final behavior = new DomainHighlighter(SelectionModelType.action);
when(_chart.getSelectionModel(SelectionModelType.action))
.thenReturn(_selectionModel);
// Act
behavior.attachTo(_chart);
// Verify
verify(_chart.getSelectionModel(SelectionModelType.action));
verifyNever(_chart.getSelectionModel(SelectionModelType.info));
});
test('leaves everything alone with no selection', () {
// Setup
final behavior = new DomainHighlighter(SelectionModelType.info);
behavior.attachTo(_chart);
_setupSelection([]);
final seriesList = [_series1, _series2];
// Act
_selectionModel.lastListener(_selectionModel);
verify(_chart.redraw(skipAnimation: true, skipLayout: true));
_chart.lastListener.onPostprocess(seriesList);
// Verify
final s1ColorFn = _series1.colorFn;
expect(s1ColorFn(0), equals(MaterialPalette.blue.shadeDefault));
expect(s1ColorFn(1), equals(MaterialPalette.blue.shadeDefault));
expect(s1ColorFn(2), equals(MaterialPalette.blue.shadeDefault));
final s2ColorFn = _series2.colorFn;
expect(s2ColorFn(0), equals(MaterialPalette.red.shadeDefault));
expect(s2ColorFn(1), equals(MaterialPalette.red.shadeDefault));
expect(s2ColorFn(2), equals(MaterialPalette.red.shadeDefault));
});
test('cleans up', () {
// Setup
final behavior = new DomainHighlighter(SelectionModelType.info);
behavior.attachTo(_chart);
_setupSelection([_s1D2, _s2D2]);
// Act
behavior.removeFrom(_chart);
// Verify
expect(_chart.lastListener, isNull);
expect(_selectionModel.lastListener, isNull);
});
});
}
class MyRow {
final String campaign;
final int count;
MyRow(this.campaign, this.count);
}

View File

@@ -0,0 +1,213 @@
// 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/common/base_chart.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/behavior/initial_selection.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/chart/common/series_renderer.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:test/test.dart';
class FakeRenderer extends BaseSeriesRenderer {
@override
DatumDetails addPositionToDetailsForSeriesDatum(
DatumDetails details, SeriesDatum seriesDatum) {
return null;
}
@override
List<DatumDetails> getNearestDatumDetailPerSeries(
Point<double> chartPoint, bool byDomain, Rectangle<int> boundsOverride) {
return null;
}
@override
void paint(ChartCanvas canvas, double animationPercent) {}
@override
void update(List<ImmutableSeries> seriesList, bool isAnimating) {}
}
class FakeChart extends BaseChart {
@override
List<DatumDetails> getDatumDetails(SelectionModelType type) => [];
@override
SeriesRenderer makeDefaultRenderer() => new FakeRenderer();
void requestOnDraw(List<MutableSeries> seriesList) {
fireOnDraw(seriesList);
}
}
void main() {
FakeChart _chart;
ImmutableSeries _series1;
ImmutableSeries _series2;
ImmutableSeries _series3;
ImmutableSeries _series4;
final infoSelectionType = SelectionModelType.info;
InitialSelection _makeBehavior(SelectionModelType selectionModelType,
{List<String> selectedSeries, List<SeriesDatumConfig> selectedData}) {
InitialSelection behavior = new InitialSelection(
selectionModelType: selectionModelType,
selectedSeriesConfig: selectedSeries,
selectedDataConfig: selectedData);
behavior.attachTo(_chart);
return behavior;
}
setUp(() {
_chart = new FakeChart();
_series1 = new MutableSeries(new Series(
id: 'mySeries1',
data: ['A', 'B', 'C', 'D'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {}));
_series2 = new MutableSeries(new Series(
id: 'mySeries2',
data: ['W', 'X', 'Y', 'Z'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {}));
_series3 = new MutableSeries(new Series(
id: 'mySeries3',
data: ['W', 'X', 'Y', 'Z'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {}));
_series4 = new MutableSeries(new Series(
id: 'mySeries4',
data: ['W', 'X', 'Y', 'Z'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {}));
});
test('selects initial datum', () {
_makeBehavior(infoSelectionType,
selectedData: [new SeriesDatumConfig('mySeries1', 'C')]);
_chart.requestOnDraw([_series1, _series2]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(1));
expect(model.selectedSeries[0], equals(_series1));
expect(model.selectedDatum, hasLength(1));
expect(model.selectedDatum[0].series, equals(_series1));
expect(model.selectedDatum[0].datum, equals('C'));
});
test('selects multiple initial data', () {
_makeBehavior(infoSelectionType, selectedData: [
new SeriesDatumConfig('mySeries1', 'C'),
new SeriesDatumConfig('mySeries1', 'D')
]);
_chart.requestOnDraw([_series1, _series2]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(1));
expect(model.selectedSeries[0], equals(_series1));
expect(model.selectedDatum, hasLength(2));
expect(model.selectedDatum[0].series, equals(_series1));
expect(model.selectedDatum[0].datum, equals('C'));
expect(model.selectedDatum[1].series, equals(_series1));
expect(model.selectedDatum[1].datum, equals('D'));
});
test('selects initial series', () {
_makeBehavior(infoSelectionType, selectedSeries: ['mySeries2']);
_chart.requestOnDraw([_series1, _series2, _series3, _series4]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(1));
expect(model.selectedSeries[0], equals(_series2));
expect(model.selectedDatum, isEmpty);
});
test('selects multiple series', () {
_makeBehavior(infoSelectionType,
selectedSeries: ['mySeries2', 'mySeries4']);
_chart.requestOnDraw([_series1, _series2, _series3, _series4]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(2));
expect(model.selectedSeries[0], equals(_series2));
expect(model.selectedSeries[1], equals(_series4));
expect(model.selectedDatum, isEmpty);
});
test('selects series and datum', () {
_makeBehavior(infoSelectionType,
selectedData: [new SeriesDatumConfig('mySeries1', 'C')],
selectedSeries: ['mySeries4']);
_chart.requestOnDraw([_series1, _series2, _series3, _series4]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(2));
expect(model.selectedSeries[0], equals(_series1));
expect(model.selectedSeries[1], equals(_series4));
expect(model.selectedDatum[0].series, equals(_series1));
expect(model.selectedDatum[0].datum, equals('C'));
});
test('selection model is reset when a new series is drawn', () {
_makeBehavior(infoSelectionType, selectedSeries: ['mySeries2']);
_chart.requestOnDraw([_series1, _series2, _series3, _series4]);
final model = _chart.getSelectionModel(infoSelectionType);
// Verify initial selection is selected on first draw
expect(model.selectedSeries, hasLength(1));
expect(model.selectedSeries[0], equals(_series2));
expect(model.selectedDatum, isEmpty);
// Request a draw with a new series list.
_chart.draw(
[
new Series(
id: 'mySeries2',
data: ['W', 'X', 'Y', 'Z'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {})
],
);
// Verify selection is cleared.
expect(model.selectedSeries, isEmpty);
expect(model.selectedDatum, isEmpty);
});
}

View File

@@ -0,0 +1,270 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point, Rectangle;
import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart';
import 'package:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/behavior/line_point_highlighter.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/chart/common/series_renderer.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/common/material_palette.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChart extends Mock implements CartesianChart {
LifecycleListener lastListener;
@override
addLifecycleListener(LifecycleListener listener) => lastListener = listener;
@override
removeLifecycleListener(LifecycleListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
return true;
}
@override
bool get vertical => true;
}
class MockSelectionModel extends Mock implements MutableSelectionModel {
SelectionModelListener lastListener;
@override
addSelectionChangedListener(SelectionModelListener listener) =>
lastListener = listener;
@override
removeSelectionChangedListener(SelectionModelListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
}
}
class MockNumericAxis extends Mock implements NumericAxis {
@override
getLocation(num domain) {
return 10.0;
}
}
class MockSeriesRenderer extends BaseSeriesRenderer {
@override
void update(_, __) {}
@override
void paint(_, __) {}
List<DatumDetails> getNearestDatumDetailPerSeries(
Point<double> chartPoint, bool byDomain, Rectangle<int> boundsOverride) {
return null;
}
DatumDetails addPositionToDetailsForSeriesDatum(
DatumDetails details, SeriesDatum seriesDatum) {
return new DatumDetails.from(details,
chartPosition: new Point<double>(0.0, 0.0));
}
}
void main() {
MockChart _chart;
MockSelectionModel _selectionModel;
MockSeriesRenderer _seriesRenderer;
MutableSeries<int> _series1;
final _s1D1 = new MyRow(1, 11);
final _s1D2 = new MyRow(2, 12);
final _s1D3 = new MyRow(3, 13);
MutableSeries<int> _series2;
final _s2D1 = new MyRow(4, 21);
final _s2D2 = new MyRow(5, 22);
final _s2D3 = new MyRow(6, 23);
List<DatumDetails> _mockGetSelectedDatumDetails(List<SeriesDatum> selection) {
final details = <DatumDetails>[];
for (SeriesDatum seriesDatum in selection) {
details.add(_seriesRenderer.getDetailsForSeriesDatum(seriesDatum));
}
return details;
}
_setupSelection(List<SeriesDatum> selection) {
final selected = <MyRow>[];
for (var i = 0; i < selection.length; i++) {
selected.add(selection[0].datum);
}
for (int i = 0; i < _series1.data.length; i++) {
when(_selectionModel.isDatumSelected(_series1, i))
.thenReturn(selected.contains(_series1.data[i]));
}
for (int i = 0; i < _series2.data.length; i++) {
when(_selectionModel.isDatumSelected(_series2, i))
.thenReturn(selected.contains(_series2.data[i]));
}
when(_selectionModel.selectedDatum).thenReturn(selection);
final selectedDetails = _mockGetSelectedDatumDetails(selection);
when(_chart.getSelectedDatumDetails(SelectionModelType.info))
.thenReturn(selectedDetails);
}
setUp(() {
_chart = new MockChart();
_seriesRenderer = new MockSeriesRenderer();
_selectionModel = new MockSelectionModel();
when(_chart.getSelectionModel(SelectionModelType.info))
.thenReturn(_selectionModel);
_series1 = new MutableSeries(new Series<MyRow, int>(
id: 's1',
data: [_s1D1, _s1D2, _s1D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => MaterialPalette.blue.shadeDefault))
..measureFn = (_) => 0.0;
_series2 = new MutableSeries(new Series<MyRow, int>(
id: 's2',
data: [_s2D1, _s2D2, _s2D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => MaterialPalette.red.shadeDefault))
..measureFn = (_) => 0.0;
});
group('LinePointHighlighter', () {
test('highlights the selected points', () {
// Setup
final behavior =
new LinePointHighlighter(selectionModelType: SelectionModelType.info);
final tester = new LinePointHighlighterTester(behavior);
behavior.attachTo(_chart);
_setupSelection([
new SeriesDatum(_series1, _s1D2),
new SeriesDatum(_series2, _s2D2),
]);
// Mock axes for returning fake domain locations.
Axis domainAxis = new MockNumericAxis();
Axis primaryMeasureAxis = new MockNumericAxis();
_series1.setAttr(domainAxisKey, domainAxis);
_series1.setAttr(measureAxisKey, primaryMeasureAxis);
_series1.measureOffsetFn = (_) => 0.0;
_series2.setAttr(domainAxisKey, domainAxis);
_series2.setAttr(measureAxisKey, primaryMeasureAxis);
_series2.measureOffsetFn = (_) => 0.0;
// Act
_selectionModel.lastListener(_selectionModel);
verify(_chart.redraw(skipAnimation: true, skipLayout: true));
_chart.lastListener.onAxisConfigured();
// Verify
expect(tester.getSelectionLength(), equals(2));
expect(tester.isDatumSelected(_series1.data[0]), equals(false));
expect(tester.isDatumSelected(_series1.data[1]), equals(true));
expect(tester.isDatumSelected(_series1.data[2]), equals(false));
expect(tester.isDatumSelected(_series2.data[0]), equals(false));
expect(tester.isDatumSelected(_series2.data[1]), equals(true));
expect(tester.isDatumSelected(_series2.data[2]), equals(false));
});
test('listens to other selection models', () {
// Setup
final behavior = new LinePointHighlighter(
selectionModelType: SelectionModelType.action);
when(_chart.getSelectionModel(SelectionModelType.action))
.thenReturn(_selectionModel);
// Act
behavior.attachTo(_chart);
// Verify
verify(_chart.getSelectionModel(SelectionModelType.action));
verifyNever(_chart.getSelectionModel(SelectionModelType.info));
});
test('leaves everything alone with no selection', () {
// Setup
final behavior =
new LinePointHighlighter(selectionModelType: SelectionModelType.info);
final tester = new LinePointHighlighterTester(behavior);
behavior.attachTo(_chart);
_setupSelection([]);
// Act
_selectionModel.lastListener(_selectionModel);
verify(_chart.redraw(skipAnimation: true, skipLayout: true));
_chart.lastListener.onAxisConfigured();
// Verify
expect(tester.getSelectionLength(), equals(0));
expect(tester.isDatumSelected(_series1.data[0]), equals(false));
expect(tester.isDatumSelected(_series1.data[1]), equals(false));
expect(tester.isDatumSelected(_series1.data[2]), equals(false));
expect(tester.isDatumSelected(_series2.data[0]), equals(false));
expect(tester.isDatumSelected(_series2.data[1]), equals(false));
expect(tester.isDatumSelected(_series2.data[2]), equals(false));
});
test('cleans up', () {
// Setup
final behavior =
new LinePointHighlighter(selectionModelType: SelectionModelType.info);
behavior.attachTo(_chart);
_setupSelection([
new SeriesDatum(_series1, _s1D2),
new SeriesDatum(_series2, _s2D2),
]);
// Act
behavior.removeFrom(_chart);
// Verify
expect(_chart.lastListener, isNull);
expect(_selectionModel.lastListener, isNull);
});
});
}
class MyRow {
final int campaign;
final int count;
MyRow(this.campaign, this.count);
}

View File

@@ -0,0 +1,351 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/cartesian/axis/numeric_tick_provider.dart';
import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart';
import 'package:charts_common/src/chart/cartesian/axis/linear/linear_scale.dart';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/chart_context.dart';
import 'package:charts_common/src/chart/common/behavior/range_annotation.dart';
import 'package:charts_common/src/chart/line/line_chart.dart';
import 'package:charts_common/src/common/material_palette.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockContext extends Mock implements ChartContext {}
class ConcreteChart extends LineChart {
LifecycleListener<num> lastListener;
final _domainAxis = new ConcreteNumericAxis();
final _primaryMeasureAxis = new ConcreteNumericAxis();
@override
addLifecycleListener(LifecycleListener listener) {
lastListener = listener;
return super.addLifecycleListener(listener);
}
@override
removeLifecycleListener(LifecycleListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
return super.removeLifecycleListener(listener);
}
@override
Axis get domainAxis => _domainAxis;
@override
Axis getMeasureAxis({String axisId}) => _primaryMeasureAxis;
}
class ConcreteNumericAxis extends Axis<num> {
ConcreteNumericAxis()
: super(
tickProvider: new MockTickProvider(),
tickFormatter: new NumericTickFormatter(),
scale: new LinearScale(),
);
}
class MockTickProvider extends Mock implements NumericTickProvider {}
void main() {
Rectangle<int> drawBounds;
Rectangle<int> domainAxisBounds;
Rectangle<int> measureAxisBounds;
ConcreteChart _chart;
Series<MyRow, int> _series1;
final _s1D1 = new MyRow(0, 11);
final _s1D2 = new MyRow(1, 12);
final _s1D3 = new MyRow(2, 13);
Series<MyRow, int> _series2;
final _s2D1 = new MyRow(3, 21);
final _s2D2 = new MyRow(4, 22);
final _s2D3 = new MyRow(5, 23);
const _dashPattern = const <int>[2, 3];
List<RangeAnnotationSegment<num>> _annotations1;
List<RangeAnnotationSegment<num>> _annotations2;
List<LineAnnotationSegment<num>> _annotations3;
ConcreteChart _makeChart() {
final chart = new ConcreteChart();
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
chart.context = context;
return chart;
}
/// Initializes the [chart], draws the [seriesList], and configures mock axis
/// layout bounds.
_drawSeriesList(ConcreteChart chart, List<Series<MyRow, int>> seriesList) {
_chart.domainAxis.autoViewport = true;
_chart.domainAxis.resetDomains();
_chart.getMeasureAxis().autoViewport = true;
_chart.getMeasureAxis().resetDomains();
_chart.draw(seriesList);
_chart.domainAxis.layout(domainAxisBounds, drawBounds);
_chart.getMeasureAxis().layout(measureAxisBounds, drawBounds);
_chart.lastListener.onAxisConfigured();
}
setUpAll(() {
drawBounds = new Rectangle<int>(0, 0, 100, 100);
domainAxisBounds = new Rectangle<int>(0, 0, 100, 100);
measureAxisBounds = new Rectangle<int>(0, 0, 100, 100);
});
setUp(() {
_chart = _makeChart();
_series1 = new Series<MyRow, int>(
id: 's1',
data: [_s1D1, _s1D2, _s1D3],
domainFn: (dynamic row, _) => row.campaign,
measureFn: (dynamic row, _) => row.count,
colorFn: (_, __) => MaterialPalette.blue.shadeDefault);
_series2 = new Series<MyRow, int>(
id: 's2',
data: [_s2D1, _s2D2, _s2D3],
domainFn: (dynamic row, _) => row.campaign,
measureFn: (dynamic row, _) => row.count,
colorFn: (_, __) => MaterialPalette.red.shadeDefault);
_annotations1 = [
new RangeAnnotationSegment(1, 2, RangeAnnotationAxisType.domain,
startLabel: 'Ann 1'),
new RangeAnnotationSegment(4, 5, RangeAnnotationAxisType.domain,
color: MaterialPalette.gray.shade200, endLabel: 'Ann 2'),
new RangeAnnotationSegment(5, 5.5, RangeAnnotationAxisType.measure,
startLabel: 'Really long tick start label',
endLabel: 'Really long tick end label'),
new RangeAnnotationSegment(10, 15, RangeAnnotationAxisType.measure,
startLabel: 'Ann 4 Start', endLabel: 'Ann 4 End'),
new RangeAnnotationSegment(16, 22, RangeAnnotationAxisType.measure,
startLabel: 'Ann 5 Start', endLabel: 'Ann 5 End'),
];
_annotations2 = [
new RangeAnnotationSegment(1, 2, RangeAnnotationAxisType.domain),
new RangeAnnotationSegment(4, 5, RangeAnnotationAxisType.domain,
color: MaterialPalette.gray.shade200),
new RangeAnnotationSegment(8, 10, RangeAnnotationAxisType.domain,
color: MaterialPalette.gray.shade300),
];
_annotations3 = [
new LineAnnotationSegment(1, RangeAnnotationAxisType.measure,
startLabel: 'Ann 1 Start', endLabel: 'Ann 1 End'),
new LineAnnotationSegment(4, RangeAnnotationAxisType.measure,
startLabel: 'Ann 2 Start',
endLabel: 'Ann 2 End',
color: MaterialPalette.gray.shade200,
dashPattern: _dashPattern),
];
});
group('RangeAnnotation', () {
test('renders the annotations', () {
// Setup
final behavior = new RangeAnnotation<num>(_annotations1);
final tester = new RangeAnnotationTester(behavior);
behavior.attachTo(_chart);
final seriesList = [_series1, _series2];
// Act
_drawSeriesList(_chart, seriesList);
// Verify
expect(_chart.domainAxis.getLocation(2), equals(40.0));
expect(
tester.doesAnnotationExist(
startPosition: 20.0,
endPosition: 40.0,
color: MaterialPalette.gray.shade100,
startLabel: 'Ann 1',
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.vertical,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
expect(
tester.doesAnnotationExist(
startPosition: 80.0,
endPosition: 100.0,
color: MaterialPalette.gray.shade200,
endLabel: 'Ann 2',
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.vertical,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
// Verify measure annotations
expect(_chart.getMeasureAxis().getLocation(11).round(), equals(33));
expect(
tester.doesAnnotationExist(
startPosition: 0.0,
endPosition: 2.78,
color: MaterialPalette.gray.shade100,
startLabel: 'Really long tick start label',
endLabel: 'Really long tick end label',
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.horizontal,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
expect(
tester.doesAnnotationExist(
startPosition: 27.78,
endPosition: 55.56,
color: MaterialPalette.gray.shade100,
startLabel: 'Ann 4 Start',
endLabel: 'Ann 4 End',
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.horizontal,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
expect(
tester.doesAnnotationExist(
startPosition: 61.11,
endPosition: 94.44,
color: MaterialPalette.gray.shade100,
startLabel: 'Ann 5 Start',
endLabel: 'Ann 5 End',
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.horizontal,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
});
test('extends the domain axis when annotations fall outside the range', () {
// Setup
final behavior = new RangeAnnotation<num>(_annotations2);
final tester = new RangeAnnotationTester(behavior);
behavior.attachTo(_chart);
final seriesList = [_series1, _series2];
// Act
_drawSeriesList(_chart, seriesList);
// Verify
expect(_chart.domainAxis.getLocation(2), equals(20.0));
expect(
tester.doesAnnotationExist(
startPosition: 10.0,
endPosition: 20.0,
color: MaterialPalette.gray.shade100,
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.vertical,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
expect(
tester.doesAnnotationExist(
startPosition: 40.0,
endPosition: 50.0,
color: MaterialPalette.gray.shade200,
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.vertical,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
expect(
tester.doesAnnotationExist(
startPosition: 80.0,
endPosition: 100.0,
color: MaterialPalette.gray.shade300,
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.vertical,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
});
test('test dash pattern equality', () {
// Setup
final behavior = new RangeAnnotation<num>(_annotations3);
final tester = new RangeAnnotationTester(behavior);
behavior.attachTo(_chart);
final seriesList = [_series1, _series2];
// Act
_drawSeriesList(_chart, seriesList);
// Verify
expect(_chart.domainAxis.getLocation(2), equals(40.0));
expect(
tester.doesAnnotationExist(
startPosition: 0.0,
endPosition: 0.0,
color: MaterialPalette.gray.shade100,
startLabel: 'Ann 1 Start',
endLabel: 'Ann 1 End',
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.horizontal,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
expect(
tester.doesAnnotationExist(
startPosition: 13.64,
endPosition: 13.64,
color: MaterialPalette.gray.shade200,
dashPattern: _dashPattern,
startLabel: 'Ann 2 Start',
endLabel: 'Ann 2 End',
labelAnchor: AnnotationLabelAnchor.end,
labelDirection: AnnotationLabelDirection.horizontal,
labelPosition: AnnotationLabelPosition.auto),
equals(true));
});
test('cleans up', () {
// Setup
final behavior = new RangeAnnotation<num>(_annotations2);
behavior.attachTo(_chart);
// Act
behavior.removeFrom(_chart);
// Verify
expect(_chart.lastListener, isNull);
});
});
}
class MyRow {
final int campaign;
final int count;
MyRow(this.campaign, this.count);
}

View File

@@ -0,0 +1,160 @@
// 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/common/base_chart.dart';
import 'package:charts_common/src/chart/common/behavior/selection/lock_selection.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/common/gesture_listener.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChart extends Mock implements BaseChart {
GestureListener lastListener;
@override
GestureListener addGestureListener(GestureListener listener) {
lastListener = listener;
return listener;
}
@override
void removeGestureListener(GestureListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
}
}
class MockSelectionModel extends Mock implements MutableSelectionModel {
bool locked = false;
}
void main() {
MockChart _chart;
MockSelectionModel _hoverSelectionModel;
MockSelectionModel _clickSelectionModel;
LockSelection _makeLockSelectionBehavior(
SelectionModelType selectionModelType) {
LockSelection behavior =
new LockSelection(selectionModelType: selectionModelType);
behavior.attachTo(_chart);
return behavior;
}
_setupChart({Point<double> forPoint, bool isWithinRenderer}) {
if (isWithinRenderer != null) {
when(_chart.pointWithinRenderer(forPoint)).thenReturn(isWithinRenderer);
}
}
setUp(() {
_hoverSelectionModel = new MockSelectionModel();
_clickSelectionModel = new MockSelectionModel();
_chart = new MockChart();
when(_chart.getSelectionModel(SelectionModelType.info))
.thenReturn(_hoverSelectionModel);
when(_chart.getSelectionModel(SelectionModelType.action))
.thenReturn(_clickSelectionModel);
});
group('LockSelection trigger handling', () {
test('can lock model with a selection', () {
// Setup chart matches point with single domain single series.
_makeLockSelectionBehavior(SelectionModelType.info);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true);
when(_hoverSelectionModel.hasAnySelection).thenReturn(true);
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_hoverSelectionModel.hasAnySelection);
expect(_hoverSelectionModel.locked, equals(true));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('can lock and unlock model', () {
// Setup chart matches point with single domain single series.
_makeLockSelectionBehavior(SelectionModelType.info);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true);
when(_hoverSelectionModel.hasAnySelection).thenReturn(true);
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_hoverSelectionModel.hasAnySelection);
expect(_hoverSelectionModel.locked, equals(true));
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_hoverSelectionModel.clearSelection());
expect(_hoverSelectionModel.locked, equals(false));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('does not lock model with empty selection', () {
// Setup chart matches point with single domain single series.
_makeLockSelectionBehavior(SelectionModelType.info);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true);
when(_hoverSelectionModel.hasAnySelection).thenReturn(false);
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_hoverSelectionModel.hasAnySelection);
expect(_hoverSelectionModel.locked, equals(false));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
});
group('Cleanup', () {
test('detach removes listener', () {
// Setup
final behavior = _makeLockSelectionBehavior(SelectionModelType.info);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true);
expect(_chart.lastListener, isNotNull);
// Act
behavior.removeFrom(_chart);
// Validate
expect(_chart.lastListener, isNull);
});
});
}

View File

@@ -0,0 +1,491 @@
// 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/common/base_chart.dart';
import 'package:charts_common/src/chart/common/behavior/selection/select_nearest.dart';
import 'package:charts_common/src/chart/common/behavior/selection/selection_trigger.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/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/common/gesture_listener.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChart extends Mock implements BaseChart<String> {
GestureListener lastListener;
@override
GestureListener addGestureListener(GestureListener listener) {
lastListener = listener;
return listener;
}
@override
void removeGestureListener(GestureListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
}
}
class MockSelectionModel extends Mock implements MutableSelectionModel<String> {
}
void main() {
MockChart _chart;
MockSelectionModel _hoverSelectionModel;
MockSelectionModel _clickSelectionModel;
List<String> _series1Data;
List<String> _series2Data;
MutableSeries<String> _series1;
MutableSeries<String> _series2;
DatumDetails<String> _details1;
DatumDetails<String> _details1Series2;
DatumDetails<String> _details2;
DatumDetails<String> _details3;
SelectNearest<String> _makeBehavior(
SelectionModelType selectionModelType, SelectionTrigger eventTrigger,
{bool expandToDomain,
bool selectClosestSeries,
int maximumDomainDistancePx}) {
SelectNearest<String> behavior = new SelectNearest<String>(
selectionModelType: selectionModelType,
expandToDomain: expandToDomain,
selectClosestSeries: selectClosestSeries,
eventTrigger: eventTrigger,
maximumDomainDistancePx: maximumDomainDistancePx);
behavior.attachTo(_chart);
return behavior;
}
_setupChart(
{Point<double> forPoint,
bool isWithinRenderer,
List<DatumDetails<String>> respondWithDetails,
List<MutableSeries<String>> seriesList}) {
if (isWithinRenderer != null) {
when(_chart.pointWithinRenderer(forPoint)).thenReturn(isWithinRenderer);
}
if (respondWithDetails != null) {
when(_chart.getNearestDatumDetailPerSeries(forPoint, true))
.thenReturn(respondWithDetails);
}
if (seriesList != null) {
when(_chart.currentSeriesList).thenReturn(seriesList);
}
}
setUp(() {
_hoverSelectionModel = new MockSelectionModel();
_clickSelectionModel = new MockSelectionModel();
_chart = new MockChart();
when(_chart.getSelectionModel(SelectionModelType.info))
.thenReturn(_hoverSelectionModel);
when(_chart.getSelectionModel(SelectionModelType.action))
.thenReturn(_clickSelectionModel);
_series1Data = ['myDomain1', 'myDomain2', 'myDomain3'];
_series1 = new MutableSeries<String>(new Series(
id: 'mySeries1',
data: ['myDatum1', 'myDatum2', 'myDatum3'],
domainFn: (_, int i) => _series1Data[i],
measureFn: (_, __) {}));
_details1 = new DatumDetails(
datum: 'myDatum1',
domain: 'myDomain1',
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_details2 = new DatumDetails(
datum: 'myDatum2',
domain: 'myDomain2',
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_details3 = new DatumDetails(
datum: 'myDatum3',
domain: 'myDomain3',
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_series2Data = ['myDomain1'];
_series2 = new MutableSeries<String>(new Series(
id: 'mySeries2',
data: ['myDatum1s2'],
domainFn: (_, int i) => _series2Data[i],
measureFn: (_, __) {}));
_details1Series2 = new DatumDetails(
datum: 'myDatum1s2',
domain: 'myDomain1',
series: _series2,
domainDistance: 10.0,
measureDistance: 20.0);
});
tearDown(resetMockitoState);
group('SelectNearest trigger handling', () {
test('single series selects detail', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(
forPoint: point,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
// Shouldn't be listening to anything else.
expect(_chart.lastListener.onTap, isNull);
expect(_chart.lastListener.onDragStart, isNull);
});
test('can listen to tap', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.action, SelectionTrigger.tap,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(
forPoint: point,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_clickSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('can listen to drag', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.pressHold,
expandToDomain: true, selectClosestSeries: true);
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
Point<double> updatePoint1 = new Point(200.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
Point<double> updatePoint2 = new Point(300.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details2],
seriesList: [_series1]);
Point<double> endPoint = new Point(400.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3],
seriesList: [_series1]);
// Act
_chart.lastListener.onTapTest(startPoint);
_chart.lastListener.onDragStart(startPoint);
_chart.lastListener.onDragUpdate(updatePoint1, 1.0);
_chart.lastListener.onDragUpdate(updatePoint2, 1.0);
_chart.lastListener.onDragEnd(endPoint, 1.0, 0.0);
// Validate
// details1 was tripped 2 times (startPoint & updatePoint1)
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1])).called(2);
// details2 was tripped for updatePoint2
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details2.datum)], [_series1]));
// dragEnd deselects even though we are over details3.
verify(_hoverSelectionModel.updateSelection([], []));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('can listen to drag after long press', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.longPressHold,
expandToDomain: true, selectClosestSeries: true);
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
Point<double> updatePoint1 = new Point(200.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2],
seriesList: [_series1]);
Point<double> endPoint = new Point(400.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3],
seriesList: [_series1]);
// Act 1
_chart.lastListener.onTapTest(startPoint);
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
// Act 2
// verify no interaction yet.
_chart.lastListener.onLongPress(startPoint);
_chart.lastListener.onDragStart(startPoint);
_chart.lastListener.onDragUpdate(updatePoint1, 1.0);
_chart.lastListener.onDragEnd(endPoint, 1.0, 0.0);
// Validate
// details1 was tripped 2 times (longPress & dragStart)
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1])).called(2);
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details2.datum)], [_series1]));
// dragEnd deselects even though we are over details3.
verify(_hoverSelectionModel.updateSelection([], []));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('no trigger before long press', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.longPressHold,
expandToDomain: true, selectClosestSeries: true);
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
Point<double> updatePoint1 = new Point(200.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2],
seriesList: [_series1]);
Point<double> endPoint = new Point(400.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3],
seriesList: [_series1]);
// Act
_chart.lastListener.onTapTest(startPoint);
_chart.lastListener.onDragStart(startPoint);
_chart.lastListener.onDragUpdate(updatePoint1, 1.0);
_chart.lastListener.onDragEnd(endPoint, 1.0, 0.0);
// Validate
// No interaction, didn't long press first.
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
});
group('Details', () {
test('expands to domain and includes closest series', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection([
new SeriesDatum(_series1, _details1.datum),
new SeriesDatum(_series2, _details1Series2.datum)
], [
_series1
]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('does not expand to domain', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: false, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('does not include closest series', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: false);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection([
new SeriesDatum(_series1, _details1.datum),
new SeriesDatum(_series2, _details1Series2.datum)
], []));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('does not include overlay series', () {
// Setup chart with an overlay series.
_series2.overlaySeries = true;
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection([
new SeriesDatum(_series1, _details1.datum),
], [
_series1
]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('selection does not exceed maximumDomainDistancePx', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true,
selectClosestSeries: true,
maximumDomainDistancePx: 1);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection([], []));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
});
group('Cleanup', () {
test('detach removes listener', () {
// Setup
SelectNearest behavior = _makeBehavior(
SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(
forPoint: point,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
expect(_chart.lastListener, isNotNull);
// Act
behavior.removeFrom(_chart);
// Validate
expect(_chart.lastListener, isNull);
});
});
}

View File

@@ -0,0 +1,474 @@
// 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/common/base_chart.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/chart/common/series_renderer.dart';
import 'package:charts_common/src/chart/common/behavior/legend/legend_entry_generator.dart';
import 'package:charts_common/src/chart/common/behavior/legend/series_legend.dart';
import 'package:charts_common/src/chart/common/datum_details.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/common/color.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:test/test.dart';
class ConcreteChart extends BaseChart<String> {
List<MutableSeries<String>> _seriesList;
ConcreteChart(this._seriesList);
@override
SeriesRenderer<String> makeDefaultRenderer() => null;
@override
List<MutableSeries<String>> get currentSeriesList => _seriesList;
@override
List<DatumDetails<String>> getDatumDetails(SelectionModelType _) => null;
set seriesList(List<MutableSeries<String>> seriesList) {
_seriesList = seriesList;
}
void callOnDraw() {
fireOnDraw(_seriesList);
}
void callOnPreProcess() {
fireOnPreprocess(_seriesList);
}
void callOnPostProcess() {
fireOnPostprocess(_seriesList);
}
}
class ConcreteSeriesLegend<D> extends SeriesLegend<D> {
ConcreteSeriesLegend(
{SelectionModelType selectionModelType,
LegendEntryGenerator<D> legendEntryGenerator})
: super(
selectionModelType: selectionModelType,
legendEntryGenerator: legendEntryGenerator);
@override
bool isSeriesRenderer = false;
@override
void hideSeries(String seriesId) {
super.hideSeries(seriesId);
}
@override
void showSeries(String seriesId) {
super.showSeries(seriesId);
}
@override
bool isSeriesHidden(String seriesId) {
return super.isSeriesHidden(seriesId);
}
}
void main() {
MutableSeries<String> series1;
final s1D1 = new MyRow('s1d1', 11);
final s1D2 = new MyRow('s1d2', 12);
final s1D3 = new MyRow('s1d3', 13);
MutableSeries<String> series2;
final s2D1 = new MyRow('s2d1', 21);
final s2D2 = new MyRow('s2d2', 22);
final s2D3 = new MyRow('s2d3', 23);
final blue = new Color(r: 0x21, g: 0x96, b: 0xF3);
final red = new Color(r: 0xF4, g: 0x43, b: 0x36);
ConcreteChart chart;
setUp(() {
series1 = new MutableSeries(new Series<MyRow, String>(
id: 's1',
data: [s1D1, s1D2, s1D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => blue));
series2 = new MutableSeries(new Series<MyRow, String>(
id: 's2',
data: [s2D1, s2D2, s2D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => red));
});
test('Legend entries created on chart post process', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend = new SeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
});
test('default hidden series are removed from list during pre process', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend =
new ConcreteSeriesLegend<String>(selectionModelType: selectionType);
legend.defaultHiddenSeries = ['s2'];
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isFalse);
expect(legend.isSeriesHidden('s2'), isTrue);
expect(seriesList, hasLength(1));
expect(seriesList[0].id, equals('s1'));
});
test('hidden series are removed from list after chart pre process', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend =
new ConcreteSeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
legend.hideSeries('s1');
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isTrue);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList, hasLength(1));
expect(seriesList[0].id, equals('s2'));
});
test('hidden and re-shown series is in the list after chart pre process', () {
final seriesList = [series1, series2];
final seriesList2 = [series1, series2];
final selectionType = SelectionModelType.info;
final legend =
new ConcreteSeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
// First hide the series.
legend.hideSeries('s1');
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isTrue);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList, hasLength(1));
expect(seriesList[0].id, equals('s2'));
// Then un-hide the series. This second list imitates the behavior of the
// chart, which creates a fresh copy of the original data from the user
// during each draw cycle.
legend.showSeries('s1');
chart.seriesList = seriesList2;
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isFalse);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList2, hasLength(2));
expect(seriesList2[0].id, equals('s1'));
expect(seriesList2[1].id, equals('s2'));
});
test('selected series legend entry is updated', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend = new SeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
chart.getSelectionModel(selectionType).updateSelection([], [series1]);
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isTrue);
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
});
test('hidden series removed from chart and later readded is visible', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend =
new ConcreteSeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
// First hide the series.
legend.hideSeries('s1');
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isTrue);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList, hasLength(1));
expect(seriesList[0].id, equals('s2'));
// Validate that drawing the same set of series again maintains the hidden
// states.
final seriesList2 = [series1, series2];
chart.seriesList = seriesList2;
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isTrue);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList2, hasLength(1));
expect(seriesList2[0].id, equals('s2'));
// Next, redraw the chart with only the visible series2.
final seriesList3 = [series2];
chart.seriesList = seriesList3;
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList3, hasLength(1));
expect(seriesList3[0].id, equals('s2'));
// Finally, add series1 back to the chart, and validate that it is not
// hidden.
final seriesList4 = [series1, series2];
chart.seriesList = seriesList4;
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isFalse);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList4, hasLength(2));
expect(seriesList4[0].id, equals('s1'));
expect(seriesList4[1].id, equals('s2'));
});
test('generated legend entries use provided formatters', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter =
(num value) => 'measure ${value?.toStringAsFixed(0)}';
final secondaryMeasureFormatter =
(num value) => 'secondary ${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter);
series2.setAttr(measureAxisIdKey, 'secondaryMeasureAxisId');
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
chart.getSelectionModel(selectionType).updateSelection(
[new SeriesDatum(series1, s1D1), new SeriesDatum(series2, s2D1)],
[series1, series2]);
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].isSelected, isTrue);
expect(legendEntries[0].value, equals(11.0));
expect(legendEntries[0].formattedValue, equals('measure 11'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].isSelected, isTrue);
expect(legendEntries[1].value, equals(21.0));
expect(legendEntries[1].formattedValue, equals('secondary 21'));
});
test('series legend - show measure sum when there is no selection', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
legendDefaultMeasure: LegendDefaultMeasure.sum,
measureFormatter: measureFormatter);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[0].value, equals(36.0));
expect(legendEntries[0].formattedValue, equals('36'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
expect(legendEntries[1].value, equals(66.0));
expect(legendEntries[1].formattedValue, equals('66'));
});
test('series legend - show measure average when there is no selection', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
legendDefaultMeasure: LegendDefaultMeasure.average,
measureFormatter: measureFormatter);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[0].value, equals(12.0));
expect(legendEntries[0].formattedValue, equals('12'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
expect(legendEntries[1].value, equals(22.0));
expect(legendEntries[1].formattedValue, equals('22'));
});
test('series legend - show first measure when there is no selection', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
legendDefaultMeasure: LegendDefaultMeasure.firstValue,
measureFormatter: measureFormatter);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[0].value, equals(11.0));
expect(legendEntries[0].formattedValue, equals('11'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
expect(legendEntries[1].value, equals(21.0));
expect(legendEntries[1].formattedValue, equals('21'));
});
test('series legend - show last measure when there is no selection', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
legendDefaultMeasure: LegendDefaultMeasure.lastValue,
measureFormatter: measureFormatter);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[0].value, equals(13.0));
expect(legendEntries[0].formattedValue, equals('13'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
expect(legendEntries[1].value, equals(23.0));
expect(legendEntries[1].formattedValue, equals('23'));
});
}
class MyRow {
final String campaign;
final int count;
MyRow(this.campaign, this.count);
}

View File

@@ -0,0 +1,611 @@
// 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/cartesian_chart.dart';
import 'package:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/common/base_chart.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/behavior/slider/slider.dart';
import 'package:charts_common/src/chart/common/behavior/selection/selection_trigger.dart';
import 'package:charts_common/src/common/gesture_listener.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChart extends Mock implements CartesianChart {
GestureListener lastGestureListener;
LifecycleListener lastLifecycleListener;
bool vertical = true;
@override
GestureListener addGestureListener(GestureListener listener) {
lastGestureListener = listener;
return listener;
}
@override
void removeGestureListener(GestureListener listener) {
expect(listener, equals(lastGestureListener));
lastGestureListener = null;
}
@override
addLifecycleListener(LifecycleListener listener) =>
lastLifecycleListener = listener;
@override
removeLifecycleListener(LifecycleListener listener) {
expect(listener, equals(lastLifecycleListener));
lastLifecycleListener = null;
return true;
}
}
class MockDomainAxis extends Mock implements NumericAxis {
@override
double getDomain(num location) {
return (location / 20.0).toDouble();
}
@override
double getLocation(num domain) {
return (domain * 20.0).toDouble();
}
}
void main() {
MockChart _chart;
MockDomainAxis _domainAxis;
ImmutableSeries _series1;
DatumDetails _details1;
DatumDetails _details2;
DatumDetails _details3;
SliderTester tester;
Slider _makeBehavior(SelectionTrigger eventTrigger,
{Point<double> handleOffset,
Rectangle<int> handleSize,
double initialDomainValue,
SliderListenerCallback onChangeCallback,
bool snapToDatum = false,
SliderHandlePosition handlePosition = SliderHandlePosition.middle}) {
Slider behavior = new Slider(
eventTrigger: eventTrigger,
initialDomainValue: initialDomainValue,
onChangeCallback: onChangeCallback,
snapToDatum: snapToDatum,
style: new SliderStyle(
handleOffset: handleOffset, handlePosition: handlePosition));
behavior.attachTo(_chart);
tester = new SliderTester(behavior);
// Mock out chart layout by assigning bounds to the layout view.
tester.layout(
new Rectangle<int>(0, 0, 200, 200), new Rectangle<int>(0, 0, 200, 200));
return behavior;
}
_setupChart(
{Point<double> forPoint,
bool isWithinRenderer,
List<DatumDetails> respondWithDetails}) {
when(_chart.domainAxis).thenReturn(_domainAxis);
if (isWithinRenderer != null) {
when(_chart.pointWithinRenderer(forPoint)).thenReturn(isWithinRenderer);
}
if (respondWithDetails != null) {
when(_chart.getNearestDatumDetailPerSeries(forPoint, true))
.thenReturn(respondWithDetails);
}
}
setUp(() {
_chart = new MockChart();
_domainAxis = new MockDomainAxis();
_series1 = new MutableSeries(new Series(
id: 'mySeries1',
data: [],
domainFn: (_, __) {},
measureFn: (_, __) {}));
_details1 = new DatumDetails(
chartPosition: new Point(20.0, 80.0),
datum: 'myDatum1',
domain: 1.0,
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_details2 = new DatumDetails(
chartPosition: new Point(40.0, 80.0),
datum: 'myDatum2',
domain: 2.0,
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_details3 = new DatumDetails(
chartPosition: new Point(90.0, 80.0),
datum: 'myDatum3',
domain: 4.5,
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
});
group('Slider trigger handling', () {
test('can listen to tap and drag', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.tapAndDrag,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20));
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onTap(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint1));
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint2));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
});
test('slider handle can render at top', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.tapAndDrag,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20),
handlePosition: SliderHandlePosition.top);
Point<double> startPoint = new Point(100.0, 0.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 0.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 0.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 0.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onTap(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, -10, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, -10, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, -10, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, -10, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, -10, 10, 20)));
});
test('can listen to press hold', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.pressHold,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20));
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onLongPress(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint1));
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint2));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
});
test('can listen to long press hold', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.longPressHold,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20));
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onLongPress(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint1));
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint2));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
});
test('no position update before long press', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.longPressHold,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20));
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
});
test('can snap to datum', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.tapAndDrag,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20),
snapToDatum: true);
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onTap(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag to first update point. The slider should follow the mouse during
// each drag update.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint1));
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint2));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
// Simulate onDragEnd. This is where we expect the snap to occur.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(new Point<int>(90, 100)));
expect(tester.domainValue, equals(4.5));
expect(tester.handleBounds, equals(new Rectangle<int>(85, 90, 10, 20)));
});
});
group('Slider manual control', () {
test('can set domain position', () {
// Setup chart matches point with single domain single series.
final slider = _makeBehavior(SelectionTrigger.tapAndDrag,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20),
initialDomainValue: 1.0);
_setupChart();
// Act
_chart.lastLifecycleListener.onAxisConfigured();
// Verify initial position.
expect(tester.domainCenterPoint, equals(new Point(20.0, 100.0)));
expect(tester.domainValue, equals(1.0));
expect(tester.handleBounds, equals(new Rectangle<int>(15, 90, 10, 20)));
// Move to first domain value.
slider.moveSliderToDomain(2);
expect(tester.domainCenterPoint, equals(new Point(40.0, 100.0)));
expect(tester.domainValue, equals(2.0));
expect(tester.handleBounds, equals(new Rectangle<int>(35, 90, 10, 20)));
// Move to second domain value.
slider.moveSliderToDomain(5);
expect(tester.domainCenterPoint, equals(new Point(100.0, 100.0)));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Move to second domain value.
slider.moveSliderToDomain(7.5);
expect(tester.domainCenterPoint, equals(new Point(150.0, 100.0)));
expect(tester.domainValue, equals(7.5));
expect(tester.handleBounds, equals(new Rectangle<int>(145, 90, 10, 20)));
});
});
group('Cleanup', () {
test('detach removes listener', () {
// Setup
Slider behavior = _makeBehavior(SelectionTrigger.tapAndDrag);
Point<double> point = new Point(100.0, 100.0);
_setupChart(
forPoint: point,
isWithinRenderer: true,
respondWithDetails: [_details1]);
expect(_chart.lastGestureListener, isNotNull);
// Act
behavior.removeFrom(_chart);
// Validate
expect(_chart.lastGestureListener, isNull);
});
});
}

View File

@@ -0,0 +1,249 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point;
import 'package:charts_common/src/common/gesture_listener.dart';
import 'package:charts_common/src/common/proxy_gesture_listener.dart';
import 'package:test/test.dart';
void main() {
ProxyGestureListener _proxy;
Point<double> _point;
setUp(() {
_proxy = new ProxyGestureListener();
_point = new Point<double>(10.0, 12.0);
});
group('Tap gesture', () {
test('notified for simple case', () {
// Setup
final tapListener = new MockListener(consumeEvent: true);
_proxy.add(new GestureListener(onTap: tapListener.callback));
// Act
_proxy.onTapTest(_point);
_proxy.onTap(_point);
// Verify
tapListener.verify(arg1: _point);
});
test('notifies new listener for second event', () {
// Setup
final tapListener1 = new MockListener();
_proxy.add(new GestureListener(
onTap: tapListener1.callback,
));
// Act
_proxy.onTapTest(_point);
_proxy.onTap(_point);
// Verify
tapListener1.verify(arg1: _point);
// Setup Another
final tapListener2 = new MockListener();
_proxy.add(new GestureListener(
onTap: tapListener2.callback,
));
// Act
_proxy.onTapTest(_point);
_proxy.onTap(_point);
// Verify
tapListener1.verify(callCount: 2, arg1: _point);
tapListener2.verify(arg1: _point);
});
test('notifies claiming listener registered first', () {
// Setup
final claimingTapDownListener = new MockListener(consumeEvent: true);
final claimingTapListener = new MockListener(consumeEvent: true);
_proxy.add(new GestureListener(
onTapTest: claimingTapDownListener.callback,
onTap: claimingTapListener.callback,
));
final nonclaimingTapDownListener = new MockListener(consumeEvent: false);
final nonclaimingTapListener = new MockListener(consumeEvent: false);
_proxy.add(new GestureListener(
onTapTest: nonclaimingTapDownListener.callback,
onTap: nonclaimingTapListener.callback,
));
// Act
_proxy.onTapTest(_point);
_proxy.onTap(_point);
// Verify
claimingTapDownListener.verify(arg1: _point);
claimingTapListener.verify(arg1: _point);
nonclaimingTapDownListener.verify(arg1: _point);
nonclaimingTapListener.verify(callCount: 0);
});
test('notifies claiming listener registered second', () {
// Setup
final nonclaimingTapDownListener = new MockListener(consumeEvent: false);
final nonclaimingTapListener = new MockListener(consumeEvent: false);
_proxy.add(new GestureListener(
onTapTest: nonclaimingTapDownListener.callback,
onTap: nonclaimingTapListener.callback,
));
final claimingTapDownListener = new MockListener(consumeEvent: true);
final claimingTapListener = new MockListener(consumeEvent: true);
_proxy.add(new GestureListener(
onTapTest: claimingTapDownListener.callback,
onTap: claimingTapListener.callback,
));
// Act
_proxy.onTapTest(_point);
_proxy.onTap(_point);
// Verify
nonclaimingTapDownListener.verify(arg1: _point);
nonclaimingTapListener.verify(callCount: 0);
claimingTapDownListener.verify(arg1: _point);
claimingTapListener.verify(arg1: _point);
});
});
group('LongPress gesture', () {
test('notifies with tap', () {
// Setup
final tapDown = new MockListener(consumeEvent: true);
final tap = new MockListener(consumeEvent: true);
final tapCancel = new MockListener(consumeEvent: true);
_proxy.add(new GestureListener(
onTapTest: tapDown.callback,
onTap: tap.callback,
onTapCancel: tapCancel.callback,
));
final pressTapDown = new MockListener(consumeEvent: true);
final longPress = new MockListener(consumeEvent: true);
final pressCancel = new MockListener(consumeEvent: true);
_proxy.add(new GestureListener(
onTapTest: pressTapDown.callback,
onLongPress: longPress.callback,
onTapCancel: pressCancel.callback,
));
// Act
_proxy.onTapTest(_point);
_proxy.onLongPress(_point);
_proxy.onTap(_point);
// Verify
tapDown.verify(arg1: _point);
tap.verify(callCount: 0);
tapCancel.verify(callCount: 1);
pressTapDown.verify(arg1: _point);
longPress.verify(arg1: _point);
pressCancel.verify(callCount: 0);
});
});
group('Drag gesture', () {
test('wins over tap', () {
// Setup
final tapDown = new MockListener(consumeEvent: true);
final tap = new MockListener(consumeEvent: true);
final tapCancel = new MockListener(consumeEvent: true);
_proxy.add(new GestureListener(
onTapTest: tapDown.callback,
onTap: tap.callback,
onTapCancel: tapCancel.callback,
));
final dragTapDown = new MockListener(consumeEvent: true);
final dragStart = new MockListener(consumeEvent: true);
final dragUpdate = new MockListener(consumeEvent: true);
final dragEnd = new MockListener(consumeEvent: true);
final dragCancel = new MockListener(consumeEvent: true);
_proxy.add(new GestureListener(
onTapTest: dragTapDown.callback,
onDragStart: dragStart.callback,
onDragUpdate: dragUpdate.callback,
onDragEnd: dragEnd.callback,
onTapCancel: dragCancel.callback,
));
// Act
_proxy.onTapTest(_point);
_proxy.onDragStart(_point);
_proxy.onDragUpdate(_point, 1.0);
_proxy.onDragUpdate(_point, 1.0);
_proxy.onDragEnd(_point, 2.0, 3.0);
_proxy.onTap(_point);
// Verify
tapDown.verify(arg1: _point);
tap.verify(callCount: 0);
tapCancel.verify(callCount: 1);
dragTapDown.verify(arg1: _point);
dragStart.verify(arg1: _point);
dragUpdate.verify(callCount: 2, arg1: _point, arg2: 1.0);
dragEnd.verify(arg1: _point, arg2: 2.0, arg3: 3.0);
dragCancel.verify(callCount: 0);
});
});
}
class MockListener {
Object _arg1;
Object _arg2;
Object _arg3;
int _callCount = 0;
final bool consumeEvent;
MockListener({this.consumeEvent = false});
bool callback([Object arg1, Object arg2, Object arg3]) {
_arg1 = arg1;
_arg2 = arg2;
_arg3 = arg3;
_callCount++;
return consumeEvent;
}
verify({int callCount = 1, Object arg1, Object arg2, Object arg3}) {
if (callCount != any) {
expect(_callCount, equals(callCount));
}
expect(_arg1, equals(arg1));
expect(_arg2, equals(arg2));
expect(_arg3, equals(arg3));
}
}
const any = -1;

View File

@@ -0,0 +1,331 @@
// 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/common/selection_model/selection_model.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/data/series.dart';
import 'package:test/test.dart';
void main() {
MutableSelectionModel<String> _selectionModel;
ImmutableSeries<String> _closestSeries;
MyDatum _closestDatumClosestSeries;
SeriesDatum<String> _closestDatumClosestSeriesPair;
MyDatum _otherDatumClosestSeries;
SeriesDatum<String> _otherDatumClosestSeriesPair;
ImmutableSeries<String> _otherSeries;
MyDatum _closestDatumOtherSeries;
SeriesDatum<String> _closestDatumOtherSeriesPair;
MyDatum _otherDatumOtherSeries;
SeriesDatum<String> _otherDatumOtherSeriesPair;
setUp(() {
_selectionModel = new MutableSelectionModel<String>();
_closestDatumClosestSeries = new MyDatum('cDcS');
_otherDatumClosestSeries = new MyDatum('oDcS');
_closestSeries = new MutableSeries<String>(new Series<MyDatum, String>(
id: 'closest',
data: [_closestDatumClosestSeries, _otherDatumClosestSeries],
domainFn: (dynamic d, _) => d.id,
measureFn: (_, __) => 0));
_closestDatumClosestSeriesPair =
new SeriesDatum<String>(_closestSeries, _closestDatumClosestSeries);
_otherDatumClosestSeriesPair =
new SeriesDatum<String>(_closestSeries, _otherDatumClosestSeries);
_closestDatumOtherSeries = new MyDatum('cDoS');
_otherDatumOtherSeries = new MyDatum('oDoS');
_otherSeries = new MutableSeries<String>(new Series<MyDatum, String>(
id: 'other',
data: [_closestDatumOtherSeries, _otherDatumOtherSeries],
domainFn: (dynamic d, _) => d.id,
measureFn: (_, __) => 0));
_closestDatumOtherSeriesPair =
new SeriesDatum<String>(_otherSeries, _closestDatumOtherSeries);
_otherDatumOtherSeriesPair =
new SeriesDatum<String>(_otherSeries, _otherDatumOtherSeries);
});
group('SelectionModel persists values', () {
test('selection model is empty by default', () {
expect(_selectionModel.hasDatumSelection, isFalse);
expect(_selectionModel.hasSeriesSelection, isFalse);
});
test('all datum are selected but only the first Series is', () {
// Select the 'closest' datum for each Series.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
new SeriesDatum(_otherSeries, _closestDatumOtherSeries),
], [
_closestSeries
]);
expect(_selectionModel.hasDatumSelection, isTrue);
expect(_selectionModel.selectedDatum, hasLength(2));
expect(_selectionModel.selectedDatum,
contains(_closestDatumClosestSeriesPair));
expect(_selectionModel.selectedDatum,
contains(_closestDatumOtherSeriesPair));
expect(
_selectionModel.selectedDatum.contains(_otherDatumClosestSeriesPair),
isFalse);
expect(_selectionModel.selectedDatum.contains(_otherDatumOtherSeriesPair),
isFalse);
expect(_selectionModel.hasSeriesSelection, isTrue);
expect(_selectionModel.selectedSeries, hasLength(1));
expect(_selectionModel.selectedSeries, contains(_closestSeries));
expect(_selectionModel.selectedSeries.contains(_otherSeries), isFalse);
});
test('selection can change', () {
// Select the 'closest' datum for each Series.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
new SeriesDatum(_otherSeries, _closestDatumOtherSeries),
], [
_closestSeries
]);
// Change selection to just the other datum on the other series.
_selectionModel.updateSelection([
new SeriesDatum(_otherSeries, _otherDatumOtherSeries),
], [
_otherSeries
]);
expect(_selectionModel.selectedDatum, hasLength(1));
expect(
_selectionModel.selectedDatum, contains(_otherDatumOtherSeriesPair));
expect(_selectionModel.selectedSeries, hasLength(1));
expect(_selectionModel.selectedSeries, contains(_otherSeries));
});
test('selection can be series only', () {
// Select the 'closest' Series without datum to simulate legend hovering.
_selectionModel.updateSelection([], [_closestSeries]);
expect(_selectionModel.hasDatumSelection, isFalse);
expect(_selectionModel.selectedDatum, hasLength(0));
expect(_selectionModel.hasSeriesSelection, isTrue);
expect(_selectionModel.selectedSeries, hasLength(1));
expect(_selectionModel.selectedSeries, contains(_closestSeries));
});
test('selection lock prevents change', () {
// Prevent selection changes.
_selectionModel.locked = true;
// Try to the 'closest' datum for each Series.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
new SeriesDatum(_otherSeries, _closestDatumOtherSeries),
], [
_closestSeries
]);
expect(_selectionModel.hasDatumSelection, isFalse);
expect(_selectionModel.hasSeriesSelection, isFalse);
// Allow selection changes.
_selectionModel.locked = false;
// Try to the 'closest' datum for each Series.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
new SeriesDatum(_otherSeries, _closestDatumOtherSeries),
], [
_closestSeries
]);
expect(_selectionModel.hasDatumSelection, isTrue);
expect(_selectionModel.hasSeriesSelection, isTrue);
// Prevent selection changes.
_selectionModel.locked = true;
// Attempt to change selection
_selectionModel.updateSelection([
new SeriesDatum(_otherSeries, _otherDatumOtherSeries),
], [
_otherSeries
]);
// Previous selection should still be set.
expect(_selectionModel.selectedDatum, hasLength(2));
expect(_selectionModel.selectedDatum,
contains(_closestDatumClosestSeriesPair));
expect(_selectionModel.selectedDatum,
contains(_closestDatumOtherSeriesPair));
expect(_selectionModel.selectedSeries, hasLength(1));
expect(_selectionModel.selectedSeries, contains(_closestSeries));
});
});
group('SelectionModel changed listeners', () {
test('listener triggered for change', () {
SelectionModel<String> triggeredModel;
// Listen
_selectionModel
.addSelectionChangedListener((SelectionModel<String> model) {
triggeredModel = model;
});
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should have been triggered.
expect(triggeredModel, equals(_selectionModel));
});
test('listener not triggered for no change', () {
SelectionModel<String> triggeredModel;
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Listen
_selectionModel
.addSelectionChangedListener((SelectionModel<String> model) {
triggeredModel = model;
});
// Try to update the model with the same value.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should not have been triggered.
expect(triggeredModel, isNull);
});
test('removed listener not triggered for change', () {
SelectionModel<String> triggeredModel;
Function cb = (SelectionModel<String> model) {
triggeredModel = model;
};
// Listen
_selectionModel.addSelectionChangedListener(cb);
// Unlisten
_selectionModel.removeSelectionChangedListener(cb);
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should not have been triggered.
expect(triggeredModel, isNull);
});
});
group('SelectionModel updated listeners', () {
test('listener triggered for change', () {
SelectionModel<String> triggeredModel;
// Listen
_selectionModel
.addSelectionUpdatedListener((SelectionModel<String> model) {
triggeredModel = model;
});
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should have been triggered.
expect(triggeredModel, equals(_selectionModel));
});
test('listener triggered for no change', () {
SelectionModel<String> triggeredModel;
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Listen
_selectionModel
.addSelectionUpdatedListener((SelectionModel<String> model) {
triggeredModel = model;
});
// Try to update the model with the same value.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should have been triggered.
expect(triggeredModel, equals(_selectionModel));
});
test('removed listener not triggered for change', () {
SelectionModel<String> triggeredModel;
Function cb = (SelectionModel<String> model) {
triggeredModel = model;
};
// Listen
_selectionModel.addSelectionUpdatedListener(cb);
// Unlisten
_selectionModel.removeSelectionUpdatedListener(cb);
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should not have been triggered.
expect(triggeredModel, isNull);
});
});
}
class MyDatum {
final String id;
MyDatum(this.id);
}

View File

@@ -0,0 +1,48 @@
// 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/layout/layout_config.dart';
import 'package:charts_common/src/chart/layout/layout_manager_impl.dart';
import 'package:test/test.dart';
void main() {
test('default layout', () {
var layout = LayoutManagerImpl();
layout.measure(400, 300);
expect(layout.marginTop, equals(0));
expect(layout.marginRight, equals(0));
expect(layout.marginBottom, equals(0));
expect(layout.marginLeft, equals(0));
});
test('all fixed margin', () {
var layout = LayoutManagerImpl(
config: LayoutConfig(
topSpec: MarginSpec.fixedPixel(12),
rightSpec: MarginSpec.fixedPixel(11),
bottomSpec: MarginSpec.fixedPixel(10),
leftSpec: MarginSpec.fixedPixel(9),
),
);
layout.measure(400, 300);
expect(layout.marginTop, equals(12));
expect(layout.marginRight, equals(11));
expect(layout.marginBottom, equals(10));
expect(layout.marginLeft, equals(9));
});
}

View File

@@ -0,0 +1,644 @@
// 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/line/line_renderer.dart';
import 'package:charts_common/src/chart/line/line_renderer_config.dart';
import 'package:charts_common/src/chart/common/processed_series.dart'
show MutableSeries, ImmutableSeries;
import 'package:charts_common/src/common/color.dart';
import 'package:charts_common/src/common/material_palette.dart'
show MaterialPalette;
import 'package:charts_common/src/data/series.dart' show Series;
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
/// Datum/Row for the chart.
class MyRow {
final String campaignString;
final int campaign;
final int clickCount;
final Color color;
final List<int> dashPattern;
final double strokeWidthPx;
MyRow(this.campaignString, this.campaign, this.clickCount, this.color,
this.dashPattern, this.strokeWidthPx);
}
class MockImmutableSeries<D> extends Mock implements ImmutableSeries<D> {
String _id;
MockImmutableSeries(this._id);
@override
String get id => _id;
}
void main() {
LineRenderer renderer;
List<MutableSeries<int>> numericSeriesList;
List<MutableSeries<String>> ordinalSeriesList;
List<MyRow> myFakeDesktopData;
List<MyRow> myFakeTabletData;
List<MyRow> myFakeMobileData;
setUp(() {
myFakeDesktopData = [
new MyRow(
'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, null, 2.0),
new MyRow(
'MyCampaign2', 2, 25, MaterialPalette.green.shadeDefault, null, 2.0),
new MyRow(
'MyCampaign3', 3, 100, MaterialPalette.red.shadeDefault, null, 2.0),
new MyRow('MyOtherCampaign', 4, 75, MaterialPalette.red.shadeDefault,
null, 2.0),
];
myFakeTabletData = [
new MyRow(
'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, [2, 2], 2.0),
new MyRow(
'MyCampaign2', 2, 25, MaterialPalette.blue.shadeDefault, [3, 3], 2.0),
new MyRow('MyCampaign3', 3, 100, MaterialPalette.blue.shadeDefault,
[4, 4], 2.0),
new MyRow('MyOtherCampaign', 4, 75, MaterialPalette.blue.shadeDefault,
[4, 4], 2.0),
];
myFakeMobileData = [
new MyRow(
'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, null, 2.0),
new MyRow(
'MyCampaign2', 2, 25, MaterialPalette.blue.shadeDefault, null, 3.0),
new MyRow(
'MyCampaign3', 3, 100, MaterialPalette.blue.shadeDefault, null, 4.0),
new MyRow('MyOtherCampaign', 4, 75, MaterialPalette.blue.shadeDefault,
null, 4.0),
];
numericSeriesList = [
new MutableSeries<int>(new Series<MyRow, int>(
id: 'Desktop',
colorFn: (_, __) => MaterialPalette.blue.shadeDefault,
domainFn: (dynamic row, _) => row.campaign,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
data: myFakeDesktopData)),
new MutableSeries<int>(new Series<MyRow, int>(
id: 'Tablet',
colorFn: (_, __) => MaterialPalette.red.shadeDefault,
domainFn: (dynamic row, _) => row.campaign,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
strokeWidthPxFn: (_, __) => 1.25,
data: myFakeTabletData)),
new MutableSeries<int>(new Series<MyRow, int>(
id: 'Mobile',
colorFn: (_, __) => MaterialPalette.green.shadeDefault,
domainFn: (dynamic row, _) => row.campaign,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
strokeWidthPxFn: (_, __) => 3.0,
data: myFakeMobileData))
];
ordinalSeriesList = [
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop',
colorFn: (_, __) => MaterialPalette.blue.shadeDefault,
domainFn: (dynamic row, _) => row.campaignString,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
data: myFakeDesktopData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Tablet',
colorFn: (_, __) => MaterialPalette.red.shadeDefault,
domainFn: (dynamic row, _) => row.campaignString,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
strokeWidthPxFn: (_, __) => 1.25,
data: myFakeTabletData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Mobile',
colorFn: (_, __) => MaterialPalette.green.shadeDefault,
domainFn: (dynamic row, _) => row.campaignString,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
strokeWidthPxFn: (_, __) => 3.0,
data: myFakeMobileData))
];
});
group('preprocess', () {
test('with numeric data and simple lines', () {
renderer = new LineRenderer<num>(
config: new LineRendererConfig(strokeWidthPx: 2.0));
renderer.configureSeries(numericSeriesList);
renderer.preprocessSeries(numericSeriesList);
expect(numericSeriesList.length, equals(3));
// Validate Desktop series.
var series = numericSeriesList[0];
var styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(1));
var segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(4));
expect(segment.strokeWidthPx, equals(2.0));
expect(series.measureOffsetFn(0), 0);
expect(series.measureOffsetFn(1), 0);
expect(series.measureOffsetFn(2), 0);
expect(series.measureOffsetFn(3), 0);
// Validate Tablet series.
series = numericSeriesList[1];
styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(1));
segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.red.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(4));
expect(segment.strokeWidthPx, equals(1.25));
expect(series.measureOffsetFn(0), 0);
expect(series.measureOffsetFn(1), 0);
expect(series.measureOffsetFn(2), 0);
expect(series.measureOffsetFn(3), 0);
// Validate Mobile series.
series = numericSeriesList[2];
styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(1));
segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.green.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(4));
expect(segment.strokeWidthPx, equals(3.0));
expect(series.measureOffsetFn(0), 0);
expect(series.measureOffsetFn(1), 0);
expect(series.measureOffsetFn(2), 0);
expect(series.measureOffsetFn(3), 0);
});
test('with numeric data and stacked lines', () {
renderer = new LineRenderer<num>(
config: new LineRendererConfig(stacked: true, strokeWidthPx: 2.0));
renderer.configureSeries(numericSeriesList);
renderer.preprocessSeries(numericSeriesList);
expect(numericSeriesList.length, equals(3));
// Validate Desktop series.
var series = numericSeriesList[0];
var styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(1));
var segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(4));
expect(segment.strokeWidthPx, equals(2.0));
expect(series.measureOffsetFn(0), 0);
expect(series.measureOffsetFn(1), 0);
expect(series.measureOffsetFn(2), 0);
expect(series.measureOffsetFn(3), 0);
// Validate Tablet series.
series = numericSeriesList[1];
styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(1));
segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.red.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(4));
expect(segment.strokeWidthPx, equals(1.25));
expect(series.measureOffsetFn(0), 5);
expect(series.measureOffsetFn(1), 25);
expect(series.measureOffsetFn(2), 100);
expect(series.measureOffsetFn(3), 75);
// Validate Mobile series.
series = numericSeriesList[2];
styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(1));
segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.green.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(4));
expect(segment.strokeWidthPx, equals(3.0));
expect(series.measureOffsetFn(0), 10);
expect(series.measureOffsetFn(1), 50);
expect(series.measureOffsetFn(2), 200);
expect(series.measureOffsetFn(3), 150);
});
test('with numeric data and changes in style', () {
numericSeriesList = [
new MutableSeries<int>(new Series<MyRow, int>(
id: 'Desktop',
colorFn: (MyRow row, _) => row.color,
dashPatternFn: (MyRow row, _) => row.dashPattern,
strokeWidthPxFn: (MyRow row, _) => row.strokeWidthPx,
domainFn: (dynamic row, _) => row.campaign,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
data: myFakeDesktopData)),
new MutableSeries<int>(new Series<MyRow, int>(
id: 'Tablet',
colorFn: (MyRow row, _) => row.color,
dashPatternFn: (MyRow row, _) => row.dashPattern,
strokeWidthPxFn: (MyRow row, _) => row.strokeWidthPx,
domainFn: (dynamic row, _) => row.campaign,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
data: myFakeTabletData)),
new MutableSeries<int>(new Series<MyRow, int>(
id: 'Mobile',
colorFn: (MyRow row, _) => row.color,
dashPatternFn: (MyRow row, _) => row.dashPattern,
strokeWidthPxFn: (MyRow row, _) => row.strokeWidthPx,
domainFn: (dynamic row, _) => row.campaign,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
data: myFakeMobileData))
];
renderer = new LineRenderer<num>(
config: new LineRendererConfig(strokeWidthPx: 2.0));
renderer.configureSeries(numericSeriesList);
renderer.preprocessSeries(numericSeriesList);
expect(numericSeriesList.length, equals(3));
// Validate Desktop series.
var series = numericSeriesList[0];
var styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(3));
var segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(2));
expect(segment.strokeWidthPx, equals(2.0));
segment = styleSegments[1];
expect(segment.color, equals(MaterialPalette.green.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(2));
expect(segment.domainExtent.end, equals(3));
expect(segment.strokeWidthPx, equals(2.0));
segment = styleSegments[2];
expect(segment.color, equals(MaterialPalette.red.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(3));
expect(segment.domainExtent.end, equals(4));
expect(segment.strokeWidthPx, equals(2.0));
expect(series.measureOffsetFn(0), 0);
expect(series.measureOffsetFn(1), 0);
expect(series.measureOffsetFn(2), 0);
expect(series.measureOffsetFn(3), 0);
// Validate Tablet series.
series = numericSeriesList[1];
styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(3));
segment = segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, equals([2, 2]));
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(2));
expect(segment.strokeWidthPx, equals(2.0));
segment = styleSegments[1];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, equals([3, 3]));
expect(segment.domainExtent.start, equals(2));
expect(segment.domainExtent.end, equals(3));
expect(segment.strokeWidthPx, equals(2.0));
segment = styleSegments[2];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, equals([4, 4]));
expect(segment.domainExtent.start, equals(3));
expect(segment.domainExtent.end, equals(4));
expect(segment.strokeWidthPx, equals(2.0));
expect(series.measureOffsetFn(0), 0);
expect(series.measureOffsetFn(1), 0);
expect(series.measureOffsetFn(2), 0);
expect(series.measureOffsetFn(3), 0);
// Validate Mobile series.
series = numericSeriesList[2];
styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(3));
segment = segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(2));
expect(segment.strokeWidthPx, equals(2.0));
segment = styleSegments[1];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(2));
expect(segment.domainExtent.end, equals(3));
expect(segment.strokeWidthPx, equals(3.0));
segment = styleSegments[2];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals(3));
expect(segment.domainExtent.end, equals(4));
expect(segment.strokeWidthPx, equals(4.0));
expect(series.measureOffsetFn(0), 0);
expect(series.measureOffsetFn(1), 0);
expect(series.measureOffsetFn(2), 0);
expect(series.measureOffsetFn(3), 0);
});
test('with numeric data and repeats in style', () {
var myFakeData = [
new MyRow(
'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, null, 2.0),
new MyRow('MyCampaign2', 2, 25, MaterialPalette.green.shadeDefault,
null, 2.0),
new MyRow('MyCampaign3', 3, 100, MaterialPalette.blue.shadeDefault,
null, 2.0),
new MyRow('MyCampaign4', 4, 75, MaterialPalette.green.shadeDefault,
null, 2.0),
new MyRow(
'MyCampaign1', 5, 5, MaterialPalette.blue.shadeDefault, null, 2.0),
new MyRow('MyCampaign2', 6, 25, MaterialPalette.green.shadeDefault,
null, 2.0),
new MyRow('MyCampaign3', 7, 100, MaterialPalette.blue.shadeDefault,
null, 2.0),
new MyRow('MyCampaign4', 8, 75, MaterialPalette.green.shadeDefault,
null, 2.0),
];
numericSeriesList = [
new MutableSeries<int>(new Series<MyRow, int>(
id: 'Desktop',
colorFn: (MyRow row, _) => row.color,
dashPatternFn: (MyRow row, _) => row.dashPattern,
strokeWidthPxFn: (MyRow row, _) => row.strokeWidthPx,
domainFn: (dynamic row, _) => row.campaign,
measureFn: (dynamic row, _) => row.clickCount,
measureOffsetFn: (_, __) => 0,
data: myFakeData)),
];
renderer = new LineRenderer<num>(
config: new LineRendererConfig(strokeWidthPx: 2.0));
renderer.configureSeries(numericSeriesList);
renderer.preprocessSeries(numericSeriesList);
expect(numericSeriesList.length, equals(1));
// Validate Desktop series.
var series = numericSeriesList[0];
var styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(8));
var segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.domainExtent.start, equals(1));
expect(segment.domainExtent.end, equals(2));
segment = styleSegments[1];
expect(segment.color, equals(MaterialPalette.green.shadeDefault));
expect(segment.domainExtent.start, equals(2));
expect(segment.domainExtent.end, equals(3));
segment = styleSegments[2];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.domainExtent.start, equals(3));
expect(segment.domainExtent.end, equals(4));
segment = styleSegments[3];
expect(segment.color, equals(MaterialPalette.green.shadeDefault));
expect(segment.domainExtent.start, equals(4));
expect(segment.domainExtent.end, equals(5));
segment = styleSegments[4];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.domainExtent.start, equals(5));
expect(segment.domainExtent.end, equals(6));
segment = styleSegments[5];
expect(segment.color, equals(MaterialPalette.green.shadeDefault));
expect(segment.domainExtent.start, equals(6));
expect(segment.domainExtent.end, equals(7));
segment = styleSegments[6];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.domainExtent.start, equals(7));
expect(segment.domainExtent.end, equals(8));
segment = styleSegments[7];
expect(segment.color, equals(MaterialPalette.green.shadeDefault));
expect(segment.domainExtent.start, equals(8));
expect(segment.domainExtent.end, equals(8));
});
test('with ordinal data and simple lines', () {
renderer = new LineRenderer<String>(
config: new LineRendererConfig(strokeWidthPx: 2.0));
renderer.configureSeries(ordinalSeriesList);
renderer.preprocessSeries(ordinalSeriesList);
expect(ordinalSeriesList.length, equals(3));
// Validate Desktop series.
var series = ordinalSeriesList[0];
var styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(1));
var segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.blue.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals('MyCampaign1'));
expect(segment.domainExtent.end, equals('MyOtherCampaign'));
expect(segment.strokeWidthPx, equals(2.0));
// Validate Tablet series.
series = ordinalSeriesList[1];
styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(1));
segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.red.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals('MyCampaign1'));
expect(segment.domainExtent.end, equals('MyOtherCampaign'));
expect(segment.strokeWidthPx, equals(1.25));
// Validate Mobile series.
series = ordinalSeriesList[2];
styleSegments = series.getAttr(styleSegmentsKey);
expect(styleSegments.length, equals(1));
segment = styleSegments[0];
expect(segment.color, equals(MaterialPalette.green.shadeDefault));
expect(segment.dashPattern, isNull);
expect(segment.domainExtent.start, equals('MyCampaign1'));
expect(segment.domainExtent.end, equals('MyOtherCampaign'));
expect(segment.strokeWidthPx, equals(3.0));
});
});
group('Line merging', () {
List<ImmutableSeries<num>> series(List<String> keys) {
return keys.map((key) => MockImmutableSeries<num>(key)).toList();
}
test('simple beginning removal', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c']);
tester.merge(series(['b', 'c']));
// The series should still be there so that it can be animated out.
expect(tester.seriesKeys, equals(['a', 'b', 'c']));
});
test('simple middle removal', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c']);
tester.merge(series(['a', 'c']));
// The series should still be there so that it can be animated out.
expect(tester.seriesKeys, equals(['a', 'b', 'c']));
});
test('simple end removal', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c']);
tester.merge(series(['a', 'b']));
// The series should still be there so that it can be animated out.
expect(tester.seriesKeys, equals(['a', 'b', 'c']));
});
test('simple beginning addition', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c']);
tester.merge(series(['d', 'a', 'b', 'c']));
expect(tester.seriesKeys, equals(['d', 'a', 'b', 'c']));
});
test('simple middle addition', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c']);
tester.merge(series(['a', 'd', 'b', 'c']));
expect(tester.seriesKeys, equals(['a', 'd', 'b', 'c']));
});
test('simple end addition', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c']);
tester.merge(series(['a', 'b', 'c', 'd']));
expect(tester.seriesKeys, equals(['a', 'b', 'c', 'd']));
});
test('replacement begining', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c']);
tester.merge(series(['d', 'b', 'c']));
expect(tester.seriesKeys, equals(['a', 'd', 'b', 'c']));
});
test('replacement end', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c']);
tester.merge(series(['a', 'b', 'd']));
expect(tester.seriesKeys, equals(['a', 'b', 'c', 'd']));
});
test('full replacement', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c']);
tester.merge(series(['d', 'e', 'f']));
expect(tester.seriesKeys, equals(['a', 'b', 'c', 'd', 'e', 'f']));
});
test('mixed replacement', () {
final tester = LineRendererTester(LineRenderer<num>());
tester.setSeriesKeys(['a', 'b', 'c', 'd']);
tester.merge(series(['d', 'a', 'f', 'c']));
expect(tester.seriesKeys, equals(['d', 'a', 'b', 'f', 'c']));
});
});
}

View File

@@ -0,0 +1,354 @@
// 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_chart.dart';
import 'package:charts_common/src/chart/common/chart_canvas.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/line/line_renderer.dart';
import 'package:charts_common/src/chart/line/line_renderer_config.dart';
import 'package:charts_common/src/common/color.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
/// Datum/Row for the chart.
class MyRow {
final int timestamp;
int clickCount;
MyRow(this.timestamp, this.clickCount);
}
// TODO: Test in RTL context as well.
class MockChart extends Mock implements CartesianChart {}
class MockDomainAxis extends Mock implements Axis<int> {}
class MockMeasureAxis extends Mock implements Axis<num> {}
class MockCanvas extends Mock implements ChartCanvas {}
void main() {
/////////////////////////////////////////
// Convenience methods for creating mocks.
/////////////////////////////////////////
MutableSeries<int> _makeSeries({String id, int measureOffset = 0}) {
final data = <MyRow>[
new MyRow(1000, measureOffset + 10),
new MyRow(2000, measureOffset + 20),
new MyRow(3000, measureOffset + 30),
];
final series = new MutableSeries<int>(new Series<MyRow, int>(
id: id,
data: data,
domainFn: (MyRow row, _) => row.timestamp,
measureFn: (MyRow row, _) => row.clickCount,
));
series.measureOffsetFn = (_) => 0.0;
series.colorFn = (_) => new Color.fromHex(code: '#000000');
// Mock the Domain axis results.
final domainAxis = new MockDomainAxis();
when(domainAxis.rangeBand).thenReturn(100.0);
when(domainAxis.getLocation(1000)).thenReturn(70.0);
when(domainAxis.getLocation(2000)).thenReturn(70.0 + 100);
when(domainAxis.getLocation(3000)).thenReturn(70.0 + 200.0);
series.setAttr(domainAxisKey, domainAxis);
// Mock the Measure axis results.
final measureAxis = new MockMeasureAxis();
for (var i = 0; i <= 100; i++) {
when(measureAxis.getLocation(i.toDouble()))
.thenReturn(20.0 + 100.0 - i.toDouble());
}
// Special case where measure is above drawArea.
when(measureAxis.getLocation(500)).thenReturn(20.0 + 100.0 - 500);
series.setAttr(measureAxisKey, measureAxis);
return series;
}
LineRenderer<int> renderer;
bool selectNearestByDomain;
setUp(() {
selectNearestByDomain = true;
renderer = new LineRenderer<int>(
config: new LineRendererConfig(strokeWidthPx: 1.0));
final layoutBounds = new Rectangle<int>(70, 20, 200, 100);
renderer.layout(layoutBounds, layoutBounds);
return renderer;
});
/////////////////////////////////////////
// Additional edge test cases
/////////////////////////////////////////
group('edge cases', () {
test('hit target with missing data in series still selects others', () {
// Setup
final seriesList = <MutableSeries<int>>[
_makeSeries(id: 'foo')..data.clear(),
_makeSeries(id: 'bar'),
];
renderer.configureSeries(seriesList);
renderer.preprocessSeries(seriesList);
renderer.update(seriesList, false);
renderer.paint(new MockCanvas(), 1.0);
// Act Point just below barSeries.data[0]
final details = renderer.getNearestDatumDetailPerSeries(
new Point<double>(70.0 + 10.0, 20.0 + 100.0 - 5.0),
selectNearestByDomain,
null);
// Verify
expect(details.length, equals(1));
final closest = details[0];
expect(closest.domain, equals(1000));
expect(closest.series.id, equals('bar'));
expect(closest.datum, equals(seriesList[1].data[0]));
expect(closest.domainDistance, equals(10));
expect(closest.measureDistance, equals(5));
});
test('all series without data is skipped', () {
// Setup
final seriesList = <MutableSeries<int>>[
_makeSeries(id: 'foo')..data.clear(),
_makeSeries(id: 'bar')..data.clear(),
];
renderer.configureSeries(seriesList);
renderer.preprocessSeries(seriesList);
renderer.update(seriesList, false);
renderer.paint(new MockCanvas(), 1.0);
// Act
final details = renderer.getNearestDatumDetailPerSeries(
new Point<double>(70.0 + 10.0, 20.0 + 100.0 - 5.0),
selectNearestByDomain,
null);
// Verify
expect(details.length, equals(0));
});
test('single overlay series is skipped', () {
// Setup
final seriesList = <MutableSeries<int>>[
_makeSeries(id: 'foo')..overlaySeries = true,
_makeSeries(id: 'bar'),
];
renderer.configureSeries(seriesList);
renderer.preprocessSeries(seriesList);
renderer.update(seriesList, false);
renderer.paint(new MockCanvas(), 1.0);
// Act
final details = renderer.getNearestDatumDetailPerSeries(
new Point<double>(70.0 + 10.0, 20.0 + 100.0 - 5.0),
selectNearestByDomain,
null);
// Verify
expect(details.length, equals(1));
final closest = details[0];
expect(closest.domain, equals(1000));
expect(closest.series.id, equals('bar'));
expect(closest.datum, equals(seriesList[1].data[0]));
expect(closest.domainDistance, equals(10));
expect(closest.measureDistance, equals(5));
});
test('all overlay series is skipped', () {
// Setup
final seriesList = <MutableSeries<int>>[
_makeSeries(id: 'foo')..overlaySeries = true,
_makeSeries(id: 'bar')..overlaySeries = true,
];
renderer.configureSeries(seriesList);
renderer.preprocessSeries(seriesList);
renderer.update(seriesList, false);
renderer.paint(new MockCanvas(), 1.0);
// Act
final details = renderer.getNearestDatumDetailPerSeries(
new Point<double>(70.0 + 10.0, 20.0 + 100.0 - 5.0),
selectNearestByDomain,
null);
// Verify
expect(details.length, equals(0));
});
});
/////////////////////////////////////////
// Vertical BarRenderer
/////////////////////////////////////////
group('LineRenderer', () {
test('hit test works', () {
// Setup
final seriesList = <MutableSeries<int>>[_makeSeries(id: 'foo')];
renderer.configureSeries(seriesList);
renderer.preprocessSeries(seriesList);
renderer.update(seriesList, false);
renderer.paint(new MockCanvas(), 1.0);
// Act
final details = renderer.getNearestDatumDetailPerSeries(
new Point<double>(70.0 + 10.0, 20.0 + 100.0 - 5.0),
selectNearestByDomain,
null);
// Verify
expect(details.length, equals(1));
final closest = details[0];
expect(closest.domain, equals(1000));
expect(closest.series, equals(seriesList[0]));
expect(closest.datum, equals(seriesList[0].data[0]));
expect(closest.domainDistance, equals(10));
expect(closest.measureDistance, equals(5));
});
test('hit test expands to multiple series', () {
// Setup bar series is 20 measure higher than foo.
final seriesList = <MutableSeries<int>>[
_makeSeries(id: 'foo'),
_makeSeries(id: 'bar', measureOffset: 20),
];
renderer.configureSeries(seriesList);
renderer.preprocessSeries(seriesList);
renderer.update(seriesList, false);
renderer.paint(new MockCanvas(), 1.0);
// Act
final details = renderer.getNearestDatumDetailPerSeries(
new Point<double>(70.0 + 10.0, 20.0 + 100.0 - 5.0),
selectNearestByDomain,
null);
// Verify
expect(details.length, equals(2));
final closest = details[0];
expect(closest.domain, equals(1000));
expect(closest.series.id, equals('foo'));
expect(closest.datum, equals(seriesList[0].data[0]));
expect(closest.domainDistance, equals(10));
expect(closest.measureDistance, equals(5));
final next = details[1];
expect(next.domain, equals(1000));
expect(next.series.id, equals('bar'));
expect(next.datum, equals(seriesList[1].data[0]));
expect(next.domainDistance, equals(10));
expect(next.measureDistance, equals(25)); // 20offset + 10measure - 5pt
});
test('hit test expands with missing data in series', () {
// Setup bar series is 20 measure higher than foo and is missing the
// middle point.
final seriesList = <MutableSeries<int>>[
_makeSeries(id: 'foo'),
_makeSeries(id: 'bar', measureOffset: 20)..data.removeAt(1),
];
renderer.configureSeries(seriesList);
renderer.preprocessSeries(seriesList);
renderer.update(seriesList, false);
renderer.paint(new MockCanvas(), 1.0);
// Act
final details = renderer.getNearestDatumDetailPerSeries(
new Point<double>(70.0 + 100.0 + 10.0, 20.0 + 100.0 - 5.0),
selectNearestByDomain,
null);
// Verify
expect(details.length, equals(2));
final closest = details[0];
expect(closest.domain, equals(2000));
expect(closest.series.id, equals('foo'));
expect(closest.datum, equals(seriesList[0].data[1]));
expect(closest.domainDistance, equals(10));
expect(closest.measureDistance, equals(15));
// bar series jumps to last point since it is missing middle.
final next = details[1];
expect(next.domain, equals(3000));
expect(next.series.id, equals('bar'));
expect(next.datum, equals(seriesList[1].data[1]));
expect(next.domainDistance, equals(90));
expect(next.measureDistance, equals(45.0));
});
test('hit test works for points above drawArea', () {
// Setup
final seriesList = <MutableSeries<int>>[
_makeSeries(id: 'foo')..data[1].clickCount = 500
];
renderer.configureSeries(seriesList);
renderer.preprocessSeries(seriesList);
renderer.update(seriesList, false);
renderer.paint(new MockCanvas(), 1.0);
// Act
final details = renderer.getNearestDatumDetailPerSeries(
new Point<double>(70.0 + 100.0 + 10.0, 20.0 + 10.0),
selectNearestByDomain,
null);
// Verify
expect(details.length, equals(1));
final closest = details[0];
expect(closest.domain, equals(2000));
expect(closest.series, equals(seriesList[0]));
expect(closest.datum, equals(seriesList[0].data[1]));
expect(closest.domainDistance, equals(10));
expect(closest.measureDistance, equals(410)); // 500 - 100 + 10
});
test('no selection for points outside of viewport', () {
// Setup
final seriesList = <MutableSeries<int>>[
_makeSeries(id: 'foo')..data.add(new MyRow(-1000, 20))
];
renderer.configureSeries(seriesList);
renderer.preprocessSeries(seriesList);
renderer.update(seriesList, false);
renderer.paint(new MockCanvas(), 1.0);
// Act
// Note: point is in the axis, over a bar outside of the viewport.
final details = renderer.getNearestDatumDetailPerSeries(
new Point<double>(-0.0, 20.0 + 100.0 - 5.0),
selectNearestByDomain,
null);
// Verify
expect(details.length, equals(0));
});
});
}

View File

@@ -0,0 +1,323 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show pi, Point, Rectangle;
import 'package:charts_common/src/chart/common/processed_series.dart'
show ImmutableSeries;
import 'package:charts_common/src/common/color.dart' show Color;
import 'package:charts_common/src/common/graphics_factory.dart'
show GraphicsFactory;
import 'package:charts_common/src/common/line_style.dart' show LineStyle;
import 'package:charts_common/src/common/text_element.dart'
show TextDirection, TextElement, MaxWidthStrategy;
import 'package:charts_common/src/common/text_measurement.dart'
show TextMeasurement;
import 'package:charts_common/src/common/text_style.dart' show TextStyle;
import 'package:charts_common/src/chart/cartesian/axis/spec/axis_spec.dart'
show TextStyleSpec;
import 'package:charts_common/src/chart/common/chart_canvas.dart'
show ChartCanvas;
import 'package:charts_common/src/chart/pie/arc_label_decorator.dart'
show ArcLabelDecorator, ArcLabelPosition;
import 'package:charts_common/src/chart/pie/arc_renderer.dart'
show ArcRendererElement, ArcRendererElementList;
import 'package:charts_common/src/data/series.dart' show AccessorFn;
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockCanvas extends Mock implements ChartCanvas {}
/// A fake [GraphicsFactory] that returns [FakeTextStyle] and [FakeTextElement].
class FakeGraphicsFactory extends GraphicsFactory {
@override
TextStyle createTextPaint() => new FakeTextStyle();
@override
TextElement createTextElement(String text) => new FakeTextElement(text);
@override
LineStyle createLinePaint() => new MockLinePaint();
}
/// Stores [TextStyle] properties for test to verify.
class FakeTextStyle implements TextStyle {
Color color;
int fontSize;
String fontFamily;
}
/// Fake [TextElement] which returns text length as [horizontalSliceWidth].
///
/// Font size is returned for [verticalSliceWidth] and [baseline].
class FakeTextElement implements TextElement {
final String text;
TextStyle textStyle;
int maxWidth;
MaxWidthStrategy maxWidthStrategy;
TextDirection textDirection;
double opacity;
FakeTextElement(this.text);
TextMeasurement get measurement => new TextMeasurement(
horizontalSliceWidth: text.length.toDouble(),
verticalSliceWidth: textStyle.fontSize.toDouble(),
baseline: textStyle.fontSize.toDouble());
}
class MockLinePaint extends Mock implements LineStyle {}
class FakeArcRendererElement extends ArcRendererElement<String> {
final _series = new MockImmutableSeries<String>();
final AccessorFn<String> labelAccessor;
final List<String> data;
FakeArcRendererElement(this.labelAccessor, this.data) {
when(_series.labelAccessorFn).thenReturn(labelAccessor);
when(_series.data).thenReturn(data);
}
ImmutableSeries<String> get series => _series;
}
class MockImmutableSeries<D> extends Mock implements ImmutableSeries<D> {}
void main() {
ChartCanvas canvas;
GraphicsFactory graphicsFactory;
Rectangle<int> drawBounds;
setUpAll(() {
canvas = new MockCanvas();
graphicsFactory = new FakeGraphicsFactory();
drawBounds = new Rectangle(0, 0, 200, 200);
});
group('pie chart', () {
test('Paint labels with default settings', () {
final data = ['A', 'B'];
final arcElements = new ArcRendererElementList()
..arcs = [
// 'A' is small enough to fit inside the arc.
// 'LongLabelB' should not fit inside the arc because it has length
// greater than 10.
new FakeArcRendererElement((_) => 'A', data)
..startAngle = -pi / 2
..endAngle = pi / 2,
new FakeArcRendererElement((_) => 'LongLabelB', data)
..startAngle = pi / 2
..endAngle = 3 * pi / 2,
]
..center = new Point(100.0, 100.0)
..innerRadius = 30.0
..radius = 40.0
..startAngle = -pi / 2;
final decorator = new ArcLabelDecorator();
decorator.decorate(arcElements, canvas, graphicsFactory,
drawBounds: drawBounds, animationPercent: 1.0);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
// Draw text is called twice (once for each arc) and all 3 parameters were
// captured. Total parameters captured expected to be 6.
expect(captured, hasLength(6));
// For arc 'A'.
expect(captured[0].maxWidth, equals(10 - decorator.labelPadding));
expect(captured[0].textDirection, equals(TextDirection.center));
expect(captured[1], equals(135));
expect(captured[2],
equals(100 - decorator.insideLabelStyleSpec.fontSize ~/ 2));
// For arc 'B'.
expect(captured[3].maxWidth, equals(80));
expect(captured[3].textDirection, equals(TextDirection.rtl));
expect(
captured[4],
equals(60 -
decorator.leaderLineStyleSpec.length -
decorator.labelPadding * 3));
expect(captured[5],
equals(100 - decorator.outsideLabelStyleSpec.fontSize ~/ 2));
});
test('LabelPosition.inside always paints inside the arc', () {
final arcElements = new ArcRendererElementList()
..arcs = [
// 'LongLabelABC' would not fit inside the arc because it has length
// greater than 10. [ArcLabelPosition.inside] should override this.
new FakeArcRendererElement((_) => 'LongLabelABC', ['A'])
..startAngle = -pi / 2
..endAngle = pi / 2,
]
..center = new Point(100.0, 100.0)
..innerRadius = 30.0
..radius = 40.0
..startAngle = -pi / 2;
final decorator = new ArcLabelDecorator(
labelPosition: ArcLabelPosition.inside,
insideLabelStyleSpec: new TextStyleSpec(fontSize: 10));
decorator.decorate(arcElements, canvas, graphicsFactory,
drawBounds: drawBounds, animationPercent: 1.0);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
expect(captured, hasLength(3));
expect(captured[0].maxWidth, equals(10 - decorator.labelPadding));
expect(captured[0].textDirection, equals(TextDirection.center));
expect(captured[1], equals(135));
expect(captured[2],
equals(100 - decorator.insideLabelStyleSpec.fontSize ~/ 2));
});
test('LabelPosition.outside always paints outside the arc', () {
final arcElements = new ArcRendererElementList()
..arcs = [
// 'A' will fit inside the arc because it has length less than 10.
// [ArcLabelPosition.outside] should override this.
new FakeArcRendererElement((_) => 'A', ['A'])
..startAngle = -pi / 2
..endAngle = pi / 2,
]
..center = new Point(100.0, 100.0)
..innerRadius = 30.0
..radius = 40.0
..startAngle = -pi / 2;
final decorator = new ArcLabelDecorator(
labelPosition: ArcLabelPosition.outside,
outsideLabelStyleSpec: new TextStyleSpec(fontSize: 10));
decorator.decorate(arcElements, canvas, graphicsFactory,
drawBounds: drawBounds, animationPercent: 1.0);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
expect(captured, hasLength(3));
expect(captured[0].maxWidth, equals(40));
expect(captured[0].textDirection, equals(TextDirection.ltr));
expect(
captured[1],
equals(140 +
decorator.leaderLineStyleSpec.length +
decorator.labelPadding * 3));
expect(captured[2],
equals(100 - decorator.outsideLabelStyleSpec.fontSize ~/ 2));
});
test('Inside and outside label styles are applied', () {
final data = ['A', 'B'];
final arcElements = new ArcRendererElementList()
..arcs = [
// 'A' is small enough to fit inside the arc.
// 'LongLabelB' should not fit inside the arc because it has length
// greater than 10.
new FakeArcRendererElement((_) => 'A', data)
..startAngle = -pi / 2
..endAngle = pi / 2,
new FakeArcRendererElement((_) => 'LongLabelB', data)
..startAngle = pi / 2
..endAngle = 3 * pi / 2,
]
..center = new Point(100.0, 100.0)
..innerRadius = 30.0
..radius = 40.0
..startAngle = -pi / 2;
final insideColor = new Color(r: 0, g: 0, b: 0);
final outsideColor = new Color(r: 255, g: 255, b: 255);
final decorator = new ArcLabelDecorator(
labelPadding: 0,
insideLabelStyleSpec: new TextStyleSpec(
fontSize: 10, fontFamily: 'insideFont', color: insideColor),
outsideLabelStyleSpec: new TextStyleSpec(
fontSize: 8, fontFamily: 'outsideFont', color: outsideColor));
decorator.decorate(arcElements, canvas, graphicsFactory,
drawBounds: drawBounds, animationPercent: 1.0);
final captured =
verify(canvas.drawText(captureAny, captureAny, captureAny)).captured;
// Draw text is called twice (once for each arc) and all 3 parameters were
// captured. Total parameters captured expected to be 6.
expect(captured, hasLength(6));
// For arc 'A'.
expect(captured[0].maxWidth, equals(10 - decorator.labelPadding));
expect(captured[0].textDirection, equals(TextDirection.center));
expect(captured[0].textStyle.fontFamily, equals('insideFont'));
expect(captured[0].textStyle.color, equals(insideColor));
expect(captured[1], equals(135));
expect(captured[2],
equals(100 - decorator.insideLabelStyleSpec.fontSize ~/ 2));
// For arc 'B'.
expect(captured[3].maxWidth, equals(90));
expect(captured[3].textDirection, equals(TextDirection.rtl));
expect(captured[3].textStyle.fontFamily, equals('outsideFont'));
expect(captured[3].textStyle.color, equals(outsideColor));
expect(
captured[4],
equals(50 -
decorator.leaderLineStyleSpec.length -
decorator.labelPadding * 3));
expect(captured[5],
equals(100 - decorator.outsideLabelStyleSpec.fontSize ~/ 2));
});
});
group('Null and empty label scenarios', () {
test('Skip label if label accessor does not exist', () {
final arcElements = new ArcRendererElementList()
..arcs = [
new FakeArcRendererElement(null, ['A'])
..startAngle = -pi / 2
..endAngle = pi / 2,
]
..center = new Point(100.0, 100.0)
..innerRadius = 30.0
..radius = 40.0
..startAngle = -pi / 2;
new ArcLabelDecorator().decorate(arcElements, canvas, graphicsFactory,
drawBounds: drawBounds, animationPercent: 1.0);
verifyNever(canvas.drawText(any, any, any));
});
test('Skip label if label is null or empty', () {
final data = ['A', 'B'];
final arcElements = new ArcRendererElementList()
..arcs = [
new FakeArcRendererElement(null, data)
..startAngle = -pi / 2
..endAngle = pi / 2,
new FakeArcRendererElement((_) => '', data)
..startAngle = pi / 2
..endAngle = 3 * pi / 2,
]
..center = new Point(100.0, 100.0)
..innerRadius = 30.0
..radius = 40.0
..startAngle = -pi / 2;
new ArcLabelDecorator().decorate(arcElements, canvas, graphicsFactory,
drawBounds: drawBounds, animationPercent: 1.0);
verifyNever(canvas.drawText(any, any, any));
});
});
}

View File

@@ -0,0 +1,220 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point, Rectangle;
import 'package:charts_common/src/chart/scatter_plot/comparison_points_decorator.dart';
import 'package:charts_common/src/chart/scatter_plot/point_renderer.dart';
import 'package:test/test.dart';
/// Datum/Row for the chart.
class MyRow {
final int campaign;
final int clickCount;
MyRow(this.campaign, this.clickCount);
}
class TestComparisonPointsDecorator<D> extends ComparisonPointsDecorator<D> {
List<Point<double>> testComputeBoundedPointsForElement(
PointRendererElement<D> pointElement, Rectangle drawBounds) {
return computeBoundedPointsForElement(pointElement, drawBounds);
}
}
void main() {
TestComparisonPointsDecorator decorator;
Rectangle bounds;
setUp(() {
decorator = new TestComparisonPointsDecorator<num>();
bounds = new Rectangle<int>(0, 0, 100, 100);
});
group('compute bounded points', () {
test('with line inside bounds', () {
final element = new PointRendererElement<num>()
..point = new DatumPoint<num>(
x: 10.0,
xLower: 5.0,
xUpper: 50.0,
y: 20.0,
yLower: 20.0,
yUpper: 20.0);
final points =
decorator.testComputeBoundedPointsForElement(element, bounds);
expect(points.length, equals(2));
expect(points[0].x, equals(5.0));
expect(points[0].y, equals(20.0));
expect(points[1].x, equals(50.0));
expect(points[1].y, equals(20.0));
});
test('with line entirely above bounds', () {
final element = new PointRendererElement<num>()
..point = new DatumPoint<num>(
x: 10.0,
xLower: 5.0,
xUpper: 50.0,
y: -20.0,
yLower: -20.0,
yUpper: -20.0);
final points =
decorator.testComputeBoundedPointsForElement(element, bounds);
expect(points, isNull);
});
test('with line entirely below bounds', () {
final element = new PointRendererElement<num>()
..point = new DatumPoint<num>(
x: 10.0,
xLower: 5.0,
xUpper: 50.0,
y: 120.0,
yLower: 120.0,
yUpper: 120.0);
final points =
decorator.testComputeBoundedPointsForElement(element, bounds);
expect(points, isNull);
});
test('with line entirely left of bounds', () {
final element = new PointRendererElement<num>()
..point = new DatumPoint<num>(
x: -10.0,
xLower: -5.0,
xUpper: -50.0,
y: 20.0,
yLower: 20.0,
yUpper: 50.0);
final points =
decorator.testComputeBoundedPointsForElement(element, bounds);
expect(points, isNull);
});
test('with line entirely right of bounds', () {
final element = new PointRendererElement<num>()
..point = new DatumPoint<num>(
x: 110.0,
xLower: 105.0,
xUpper: 150.0,
y: 20.0,
yLower: 20.0,
yUpper: 50.0);
final points =
decorator.testComputeBoundedPointsForElement(element, bounds);
expect(points, isNull);
});
test('with horizontal line extending beyond bounds', () {
final element = new PointRendererElement<num>()
..point = new DatumPoint<num>(
x: 10.0,
xLower: -10.0,
xUpper: 110.0,
y: 20.0,
yLower: 20.0,
yUpper: 20.0);
final points =
decorator.testComputeBoundedPointsForElement(element, bounds);
expect(points.length, equals(2));
expect(points[0].x, equals(0.0));
expect(points[0].y, equals(20.0));
expect(points[1].x, equals(100.0));
expect(points[1].y, equals(20.0));
});
test('with vertical line extending beyond bounds', () {
final element = new PointRendererElement<num>()
..point = new DatumPoint<num>(
x: 20.0,
xLower: 20.0,
xUpper: 20.0,
y: 10.0,
yLower: -10.0,
yUpper: 110.0);
final points =
decorator.testComputeBoundedPointsForElement(element, bounds);
expect(points.length, equals(2));
expect(points[0].x, equals(20.0));
expect(points[0].y, equals(0.0));
expect(points[1].x, equals(20.0));
expect(points[1].y, equals(100.0));
});
test('with diagonal from top left to bottom right', () {
final element = new PointRendererElement<num>()
..point = new DatumPoint<num>(
x: 50.0,
xLower: -50.0,
xUpper: 150.0,
y: 50.0,
yLower: -50.0,
yUpper: 150.0);
final points =
decorator.testComputeBoundedPointsForElement(element, bounds);
expect(points.length, equals(2));
expect(points[0].x, equals(0.0));
expect(points[0].y, equals(0.0));
expect(points[1].x, equals(100.0));
expect(points[1].y, equals(100.0));
});
test('with diagonal from bottom left to top right', () {
final element = new PointRendererElement<num>()
..point = new DatumPoint<num>(
x: 50.0,
xLower: -50.0,
xUpper: 150.0,
y: 50.0,
yLower: 150.0,
yUpper: -50.0);
final points =
decorator.testComputeBoundedPointsForElement(element, bounds);
expect(points.length, equals(2));
expect(points[0].x, equals(0.0));
expect(points[0].y, equals(100.0));
expect(points[1].x, equals(100.0));
expect(points[1].y, equals(0.0));
});
});
}

View File

@@ -0,0 +1,192 @@
// 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/common/processed_series.dart'
show MutableSeries;
import 'package:charts_common/src/chart/scatter_plot/point_renderer.dart';
import 'package:charts_common/src/chart/scatter_plot/point_renderer_config.dart';
import 'package:charts_common/src/common/material_palette.dart'
show MaterialPalette;
import 'package:charts_common/src/data/series.dart' show Series;
import 'package:test/test.dart';
/// Datum/Row for the chart.
class MyRow {
final String campaignString;
final int campaign;
final int clickCount;
final double radius;
final double boundsRadius;
final String shape;
MyRow(this.campaignString, this.campaign, this.clickCount, this.radius,
this.boundsRadius, this.shape);
}
void main() {
PointRenderer renderer;
List<MutableSeries<int>> numericSeriesList;
setUp(() {
var myFakeDesktopData = [
// This datum should get a default bounds line radius value.
new MyRow('MyCampaign1', 0, 5, 3.0, null, null),
new MyRow('MyCampaign2', 10, 25, 5.0, 4.0, 'shape 1'),
new MyRow('MyCampaign3', 12, 75, 4.0, 4.0, 'shape 2'),
// This datum should always get default radius values.
new MyRow('MyCampaign4', 13, 225, null, null, null),
];
final maxMeasure = 300;
numericSeriesList = [
new MutableSeries<int>(new Series<MyRow, int>(
id: 'Desktop',
colorFn: (MyRow row, _) {
// Color bucket the measure column value into 3 distinct colors.
final bucket = row.clickCount / maxMeasure;
if (bucket < 1 / 3) {
return MaterialPalette.blue.shadeDefault;
} else if (bucket < 2 / 3) {
return MaterialPalette.red.shadeDefault;
} else {
return MaterialPalette.green.shadeDefault;
}
},
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
radiusPxFn: (MyRow row, _) => row.radius,
data: myFakeDesktopData)
// Define a bounds line radius function.
..setAttribute(boundsLineRadiusPxFnKey,
(int index) => myFakeDesktopData[index].boundsRadius))
];
});
group('preprocess', () {
test('with numeric data and simple points', () {
renderer = new PointRenderer<int>(config: new PointRendererConfig());
renderer.preprocessSeries(numericSeriesList);
expect(numericSeriesList.length, equals(1));
// Validate Desktop series.
var series = numericSeriesList[0];
var keyFn = series.keyFn;
var elementsList = series.getAttr(pointElementsKey);
expect(elementsList.length, equals(4));
expect(elementsList[0].radiusPx, equals(3.0));
expect(elementsList[1].radiusPx, equals(5.0));
expect(elementsList[2].radiusPx, equals(4.0));
expect(elementsList[3].radiusPx, equals(3.5));
expect(elementsList[0].boundsLineRadiusPx, equals(3.0));
expect(elementsList[1].boundsLineRadiusPx, equals(4.0));
expect(elementsList[2].boundsLineRadiusPx, equals(4.0));
expect(elementsList[3].boundsLineRadiusPx, equals(3.5));
expect(elementsList[0].symbolRendererId, equals(defaultSymbolRendererId));
expect(elementsList[1].symbolRendererId, equals(defaultSymbolRendererId));
expect(elementsList[2].symbolRendererId, equals(defaultSymbolRendererId));
expect(elementsList[3].symbolRendererId, equals(defaultSymbolRendererId));
expect(keyFn(0), equals('Desktop__0__5'));
expect(keyFn(1), equals('Desktop__10__25'));
expect(keyFn(2), equals('Desktop__12__75'));
expect(keyFn(3), equals('Desktop__13__225'));
});
test('with numeric data and missing radiusPxFn', () {
renderer = new PointRenderer<int>(
config:
new PointRendererConfig(radiusPx: 2.0, boundsLineRadiusPx: 1.5));
// Remove the radius functions to test configured defaults.
numericSeriesList[0].radiusPxFn = null;
numericSeriesList[0].setAttr(boundsLineRadiusPxFnKey, null);
renderer.preprocessSeries(numericSeriesList);
expect(numericSeriesList.length, equals(1));
// Validate Desktop series.
var series = numericSeriesList[0];
var elementsList = series.getAttr(pointElementsKey);
expect(elementsList.length, equals(4));
expect(elementsList[0].radiusPx, equals(2.0));
expect(elementsList[1].radiusPx, equals(2.0));
expect(elementsList[2].radiusPx, equals(2.0));
expect(elementsList[3].radiusPx, equals(2.0));
expect(elementsList[0].boundsLineRadiusPx, equals(1.5));
expect(elementsList[1].boundsLineRadiusPx, equals(1.5));
expect(elementsList[2].boundsLineRadiusPx, equals(1.5));
expect(elementsList[3].boundsLineRadiusPx, equals(1.5));
});
test('with custom symbol renderer ID in data', () {
renderer = new PointRenderer<int>(config: new PointRendererConfig());
numericSeriesList[0].setAttr(pointSymbolRendererFnKey,
(int index) => numericSeriesList[0].data[index].shape as String);
renderer.preprocessSeries(numericSeriesList);
expect(numericSeriesList.length, equals(1));
// Validate Desktop series.
var series = numericSeriesList[0];
var elementsList = series.getAttr(pointElementsKey);
expect(elementsList.length, equals(4));
expect(elementsList[0].symbolRendererId, equals(defaultSymbolRendererId));
expect(elementsList[1].symbolRendererId, equals('shape 1'));
expect(elementsList[2].symbolRendererId, equals('shape 2'));
expect(elementsList[3].symbolRendererId, equals(defaultSymbolRendererId));
});
test('with custom symbol renderer ID in series and data', () {
renderer = new PointRenderer<int>(config: new PointRendererConfig());
numericSeriesList[0].setAttr(pointSymbolRendererFnKey,
(int index) => numericSeriesList[0].data[index].shape as String);
numericSeriesList[0].setAttr(pointSymbolRendererIdKey, 'shape 0');
renderer.preprocessSeries(numericSeriesList);
expect(numericSeriesList.length, equals(1));
// Validate Desktop series.
var series = numericSeriesList[0];
var elementsList = series.getAttr(pointElementsKey);
expect(elementsList.length, equals(4));
expect(elementsList[0].symbolRendererId, equals('shape 0'));
expect(elementsList[1].symbolRendererId, equals('shape 1'));
expect(elementsList[2].symbolRendererId, equals('shape 2'));
expect(elementsList[3].symbolRendererId, equals('shape 0'));
});
});
}

View File

@@ -0,0 +1,109 @@
// 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/common/processed_series.dart'
show MutableSeries;
import 'package:charts_common/src/chart/scatter_plot/point_renderer.dart';
import 'package:charts_common/src/chart/scatter_plot/symbol_annotation_renderer.dart';
import 'package:charts_common/src/chart/scatter_plot/symbol_annotation_renderer_config.dart';
import 'package:charts_common/src/common/material_palette.dart'
show MaterialPalette;
import 'package:charts_common/src/data/series.dart' show Series;
import 'package:test/test.dart';
/// Datum/Row for the chart.
class MyRow {
final String campaignString;
final int campaign;
final int campaignLower;
final int campaignUpper;
final double radius;
final double boundsRadius;
final String shape;
MyRow(this.campaignString, this.campaign, this.campaignLower,
this.campaignUpper, this.radius, this.boundsRadius, this.shape);
}
void main() {
SymbolAnnotationRenderer renderer;
List<MutableSeries<int>> numericSeriesList;
setUp(() {
var myFakeDesktopData = [
// This datum should get a default bounds line radius value.
new MyRow('MyCampaign1', 0, 0, 0, 3.0, null, null),
new MyRow('MyCampaign2', 10, 10, 12, 5.0, 4.0, 'shape 1'),
new MyRow('MyCampaign3', 10, 10, 14, 4.0, 4.0, 'shape 2'),
// This datum should always get default radius values.
new MyRow('MyCampaign4', 13, 12, 15, null, null, null),
];
numericSeriesList = [
new MutableSeries<int>(new Series<MyRow, int>(
id: 'Desktop',
colorFn: (MyRow row, _) => MaterialPalette.blue.shadeDefault,
domainFn: (MyRow row, _) => row.campaign,
domainLowerBoundFn: (MyRow row, _) => row.campaignLower,
domainUpperBoundFn: (MyRow row, _) => row.campaignUpper,
measureFn: (MyRow row, _) => 0,
measureOffsetFn: (MyRow row, _) => 0,
radiusPxFn: (MyRow row, _) => row.radius,
data: myFakeDesktopData)
// Define a bounds line radius function.
..setAttribute(boundsLineRadiusPxFnKey,
(int index) => myFakeDesktopData[index].boundsRadius))
];
});
group('preprocess', () {
test('with numeric data and simple points', () {
renderer = new SymbolAnnotationRenderer<int>(
config: new SymbolAnnotationRendererConfig());
renderer.preprocessSeries(numericSeriesList);
expect(numericSeriesList.length, equals(1));
// Validate Desktop series.
var series = numericSeriesList[0];
var keyFn = series.keyFn;
var elementsList = series.getAttr(pointElementsKey);
expect(elementsList.length, equals(4));
expect(elementsList[0].radiusPx, equals(3.0));
expect(elementsList[1].radiusPx, equals(5.0));
expect(elementsList[2].radiusPx, equals(4.0));
expect(elementsList[3].radiusPx, equals(5.0));
expect(elementsList[0].boundsLineRadiusPx, equals(3.0));
expect(elementsList[1].boundsLineRadiusPx, equals(4.0));
expect(elementsList[2].boundsLineRadiusPx, equals(4.0));
expect(elementsList[3].boundsLineRadiusPx, equals(5.0));
expect(elementsList[0].symbolRendererId, equals(defaultSymbolRendererId));
expect(elementsList[1].symbolRendererId, equals(defaultSymbolRendererId));
expect(elementsList[2].symbolRendererId, equals(defaultSymbolRendererId));
expect(elementsList[3].symbolRendererId, equals(defaultSymbolRendererId));
expect(keyFn(0), equals('Desktop__0__0__0'));
expect(keyFn(1), equals('Desktop__10__10__12'));
expect(keyFn(2), equals('Desktop__10__10__14'));
expect(keyFn(3), equals('Desktop__13__12__15'));
});
});
}