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:
committed by
Andrew Brogdon
parent
42f2dce01b
commit
3fe927cb29
404
web/charts/common/test/chart/bar/bar_label_decorator_test.dart
Normal file
404
web/charts/common/test/chart/bar/bar_label_decorator_test.dart
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
882
web/charts/common/test/chart/bar/bar_renderer_test.dart
Normal file
882
web/charts/common/test/chart/bar/bar_renderer_test.dart
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
1428
web/charts/common/test/chart/bar/renderer_nearest_detail_test.dart
Normal file
1428
web/charts/common/test/chart/bar/renderer_nearest_detail_test.dart
Normal file
File diff suppressed because it is too large
Load Diff
60
web/charts/common/test/chart/cartesian/axis/axis_test.dart
Normal file
60
web/charts/common/test/chart/cartesian/axis/axis_test.dart
Normal 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]));
|
||||
});
|
||||
}
|
||||
186
web/charts/common/test/chart/cartesian/axis/axis_tick_test.dart
Normal file
186
web/charts/common/test/chart/cartesian/axis/axis_tick_test.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
]));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
105
web/charts/common/test/chart/cartesian/cartesian_chart_test.dart
Normal file
105
web/charts/common/test/chart/cartesian/cartesian_chart_test.dart
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
249
web/charts/common/test/chart/common/gesture_listener_test.dart
Normal file
249
web/charts/common/test/chart/common/gesture_listener_test.dart
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
644
web/charts/common/test/chart/line/line_renderer_test.dart
Normal file
644
web/charts/common/test/chart/line/line_renderer_test.dart
Normal 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']));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
323
web/charts/common/test/chart/pie/arc_label_decorator_test.dart
Normal file
323
web/charts/common/test/chart/pie/arc_label_decorator_test.dart
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user