1
0
mirror of https://github.com/flutter/samples.git synced 2026-03-30 16:23:23 +00:00

Add flutter_web samples (#75)

This commit is contained in:
Kevin Moore
2019-05-07 13:32:08 -07:00
committed by Andrew Brogdon
parent 42f2dce01b
commit 3fe927cb29
697 changed files with 241026 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:charts_common/src/chart/common/chart_context.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/common/behavior/a11y/domain_a11y_explore_behavior.dart';
import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockContext extends Mock implements ChartContext {}
class MockAxis extends Mock implements Axis<String> {}
class FakeCartesianChart extends CartesianChart<String> {
@override
Rectangle<int> drawAreaBounds;
void callFireOnPostprocess(List<MutableSeries<String>> seriesList) {
fireOnPostprocess(seriesList);
}
@override
initDomainAxis() {}
}
void main() {
FakeCartesianChart chart;
DomainA11yExploreBehavior<String> behavior;
MockAxis domainAxis;
MutableSeries<String> _series1;
final _s1D1 = new MyRow('s1d1', 11, 'a11yd1');
final _s1D2 = new MyRow('s1d2', 12, 'a11yd2');
final _s1D3 = new MyRow('s1d3', 13, 'a11yd3');
setUp(() {
chart = new FakeCartesianChart()
..drawAreaBounds = new Rectangle(50, 20, 150, 80);
behavior = new DomainA11yExploreBehavior<String>(
vocalizationCallback: domainVocalization);
behavior.attachTo(chart);
domainAxis = new MockAxis();
_series1 = new MutableSeries(new Series<MyRow, String>(
id: 's1',
data: [_s1D1, _s1D2, _s1D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
))
..setAttr(domainAxisKey, domainAxis);
});
test('creates nodes for vertically drawn charts', () {
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
chart.context = context;
// Drawn vertically
chart.vertical = true;
// Set step size of 50, which should be the width of the bounding box
when(domainAxis.stepSize).thenReturn(50.0);
when(domainAxis.getLocation('s1d1')).thenReturn(75.0);
when(domainAxis.getLocation('s1d2')).thenReturn(125.0);
when(domainAxis.getLocation('s1d3')).thenReturn(175.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 50, 80)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(150, 20, 50, 80)));
});
test('creates nodes for vertically drawn RTL charts', () {
// A RTL chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(true);
when(context.isRtl).thenReturn(true);
chart.context = context;
// Drawn vertically
chart.vertical = true;
// Set step size of 50, which should be the width of the bounding box
when(domainAxis.stepSize).thenReturn(50.0);
when(domainAxis.getLocation('s1d1')).thenReturn(175.0);
when(domainAxis.getLocation('s1d2')).thenReturn(125.0);
when(domainAxis.getLocation('s1d3')).thenReturn(75.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(150, 20, 50, 80)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(50, 20, 50, 80)));
});
test('creates nodes for horizontally drawn charts', () {
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
chart.context = context;
// Drawn horizontally
chart.vertical = false;
// Set step size of 20, which should be the height of the bounding box
when(domainAxis.stepSize).thenReturn(20.0);
when(domainAxis.getLocation('s1d1')).thenReturn(30.0);
when(domainAxis.getLocation('s1d2')).thenReturn(50.0);
when(domainAxis.getLocation('s1d3')).thenReturn(70.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 150, 20)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(50, 40, 150, 20)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(50, 60, 150, 20)));
});
test('creates nodes for horizontally drawn RTL charts', () {
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(true);
when(context.isRtl).thenReturn(true);
chart.context = context;
// Drawn horizontally
chart.vertical = false;
// Set step size of 20, which should be the height of the bounding box
when(domainAxis.stepSize).thenReturn(20.0);
when(domainAxis.getLocation('s1d1')).thenReturn(30.0);
when(domainAxis.getLocation('s1d2')).thenReturn(50.0);
when(domainAxis.getLocation('s1d3')).thenReturn(70.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 150, 20)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(50, 40, 150, 20)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(50, 60, 150, 20)));
});
test('nodes ordered correctly with a series missing a domain', () {
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
chart.context = context;
// Drawn vertically
chart.vertical = true;
// Set step size of 50, which should be the width of the bounding box
when(domainAxis.stepSize).thenReturn(50.0);
when(domainAxis.getLocation('s1d1')).thenReturn(75.0);
when(domainAxis.getLocation('s1d2')).thenReturn(125.0);
when(domainAxis.getLocation('s1d3')).thenReturn(175.0);
// Create a series with a missing domain
final seriesWithMissingDomain = new MutableSeries(new Series<MyRow, String>(
id: 'm1',
data: [_s1D1, _s1D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
))
..setAttr(domainAxisKey, domainAxis);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([seriesWithMissingDomain, _series1]);
final nodes = behavior.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 50, 80)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(150, 20, 50, 80)));
});
test('creates nodes with minimum width', () {
// A behavior with minimum width of 50
final behaviorWithMinWidth =
new DomainA11yExploreBehavior<String>(minimumWidth: 50.0);
behaviorWithMinWidth.attachTo(chart);
// A LTR chart
final context = new MockContext();
when(context.chartContainerIsRtl).thenReturn(false);
when(context.isRtl).thenReturn(false);
chart.context = context;
// Drawn vertically
chart.vertical = true;
// Return a step size of 20, which is less than the minimum width.
// Expect the results to use the minimum width of 50 instead.
when(domainAxis.stepSize).thenReturn(20.0);
when(domainAxis.getLocation('s1d1')).thenReturn(75.0);
when(domainAxis.getLocation('s1d2')).thenReturn(125.0);
when(domainAxis.getLocation('s1d3')).thenReturn(175.0);
// Call fire on post process for the behavior to get the series list.
chart.callFireOnPostprocess([_series1]);
final nodes = behaviorWithMinWidth.createA11yNodes();
expect(nodes, hasLength(3));
expect(nodes[0].label, equals('s1d1'));
expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 50, 80)));
expect(nodes[1].label, equals('s1d2'));
expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80)));
expect(nodes[2].label, equals('s1d3'));
expect(nodes[2].boundingBox, equals(new Rectangle(150, 20, 50, 80)));
});
}
class MyRow {
final String campaign;
final int count;
final String a11yDescription;
MyRow(this.campaign, this.count, this.a11yDescription);
}

View File

@@ -0,0 +1,593 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/processed_series.dart'
show MutableSeries;
import 'package:charts_common/src/chart/common/behavior/calculation/percent_injector.dart';
import 'package:charts_common/src/data/series.dart' show Series;
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
/// Datum/Row for the chart.
class MyRow {
final String campaign;
final int clickCount;
final int clickCountLower;
final int clickCountUpper;
MyRow(this.campaign, this.clickCount, this.clickCountLower,
this.clickCountUpper);
}
class MockChart extends Mock implements CartesianChart {
LifecycleListener lastLifecycleListener;
bool vertical = true;
@override
addLifecycleListener(LifecycleListener listener) =>
lastLifecycleListener = listener;
@override
removeLifecycleListener(LifecycleListener listener) {
expect(listener, equals(lastLifecycleListener));
lastLifecycleListener = null;
return true;
}
}
void main() {
MockChart _chart;
List<MutableSeries<String>> seriesList;
PercentInjector _makeBehavior(
{PercentInjectorTotalType totalType = PercentInjectorTotalType.domain}) {
final behavior = new PercentInjector(totalType: totalType);
behavior.attachTo(_chart);
return behavior;
}
setUp(() {
_chart = new MockChart();
final myFakeDesktopAData = [
new MyRow('MyCampaign1', 1, 1, 1),
new MyRow('MyCampaign2', 2, 2, 2),
new MyRow('MyCampaign3', 3, 3, 3),
];
final myFakeTabletAData = [
new MyRow('MyCampaign1', 2, 2, 2),
new MyRow('MyCampaign2', 3, 3, 3),
new MyRow('MyCampaign3', 4, 4, 4),
];
final myFakeMobileAData = [
new MyRow('MyCampaign1', 3, 3, 3),
new MyRow('MyCampaign2', 4, 4, 4),
new MyRow('MyCampaign3', 5, 5, 5),
];
final myFakeDesktopBData = [
new MyRow('MyCampaign1', 10, 8, 12),
new MyRow('MyCampaign2', 20, 18, 22),
new MyRow('MyCampaign3', 30, 28, 32),
];
final myFakeTabletBData = [
new MyRow('MyCampaign1', 20, 18, 22),
new MyRow('MyCampaign2', 30, 28, 32),
new MyRow('MyCampaign3', 40, 38, 42),
];
final myFakeMobileBData = [
new MyRow('MyCampaign1', 30, 28, 32),
new MyRow('MyCampaign2', 40, 38, 42),
new MyRow('MyCampaign3', 50, 48, 52),
];
seriesList = [
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop A',
seriesCategory: 'A',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeDesktopAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Tablet A',
seriesCategory: 'A',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeTabletAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Mobile A',
seriesCategory: 'A',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeMobileAData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Desktop B',
seriesCategory: 'B',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureLowerBoundFn: (MyRow row, _) => row.clickCountLower,
measureUpperBoundFn: (MyRow row, _) => row.clickCountUpper,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeDesktopBData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Tablet B',
seriesCategory: 'B',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureLowerBoundFn: (MyRow row, _) => row.clickCountLower,
measureUpperBoundFn: (MyRow row, _) => row.clickCountUpper,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeTabletBData)),
new MutableSeries<String>(new Series<MyRow, String>(
id: 'Mobile B',
seriesCategory: 'B',
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.clickCount,
measureLowerBoundFn: (MyRow row, _) => row.clickCountLower,
measureUpperBoundFn: (MyRow row, _) => row.clickCountUpper,
measureOffsetFn: (MyRow row, _) => 0,
data: myFakeMobileBData))
];
});
group('Inject', () {
test('percent of domain', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.domain);
// Act
_chart.lastLifecycleListener.onData(seriesList);
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify first series.
var series = seriesList[0];
expect(series.measureFn(0), equals(1 / 66));
expect(series.measureFn(1), equals(2 / 99));
expect(series.measureFn(2), equals(3 / 132));
expect(series.rawMeasureFn(0), equals(1));
expect(series.rawMeasureFn(1), equals(2));
expect(series.rawMeasureFn(2), equals(3));
// Verify second series.
series = seriesList[1];
expect(series.measureFn(0), equals(2 / 66));
expect(series.measureFn(1), equals(3 / 99));
expect(series.measureFn(2), equals(4 / 132));
expect(series.rawMeasureFn(0), equals(2));
expect(series.rawMeasureFn(1), equals(3));
expect(series.rawMeasureFn(2), equals(4));
// Verify third series.
series = seriesList[2];
expect(series.measureFn(0), equals(3 / 66));
expect(series.measureFn(1), equals(4 / 99));
expect(series.measureFn(2), equals(5 / 132));
expect(series.rawMeasureFn(0), equals(3));
expect(series.rawMeasureFn(1), equals(4));
expect(series.rawMeasureFn(2), equals(5));
// Verify fourth series.
series = seriesList[3];
expect(series.measureFn(0), equals(10 / 66));
expect(series.measureFn(1), equals(20 / 99));
expect(series.measureFn(2), equals(30 / 132));
expect(series.rawMeasureFn(0), equals(10));
expect(series.rawMeasureFn(1), equals(20));
expect(series.rawMeasureFn(2), equals(30));
expect(series.measureLowerBoundFn(0), equals(8 / 66));
expect(series.measureLowerBoundFn(1), equals(18 / 99));
expect(series.measureLowerBoundFn(2), equals(28 / 132));
expect(series.rawMeasureLowerBoundFn(0), equals(8));
expect(series.rawMeasureLowerBoundFn(1), equals(18));
expect(series.rawMeasureLowerBoundFn(2), equals(28));
expect(series.measureUpperBoundFn(0), equals(12 / 66));
expect(series.measureUpperBoundFn(1), equals(22 / 99));
expect(series.measureUpperBoundFn(2), equals(32 / 132));
expect(series.rawMeasureUpperBoundFn(0), equals(12));
expect(series.rawMeasureUpperBoundFn(1), equals(22));
expect(series.rawMeasureUpperBoundFn(2), equals(32));
// Verify fifth series.
series = seriesList[4];
expect(series.measureFn(0), equals(20 / 66));
expect(series.measureFn(1), equals(30 / 99));
expect(series.measureFn(2), equals(40 / 132));
expect(series.rawMeasureFn(0), equals(20));
expect(series.rawMeasureFn(1), equals(30));
expect(series.rawMeasureFn(2), equals(40));
expect(series.measureLowerBoundFn(0), equals(18 / 66));
expect(series.measureLowerBoundFn(1), equals(28 / 99));
expect(series.measureLowerBoundFn(2), equals(38 / 132));
expect(series.rawMeasureLowerBoundFn(0), equals(18));
expect(series.rawMeasureLowerBoundFn(1), equals(28));
expect(series.rawMeasureLowerBoundFn(2), equals(38));
expect(series.measureUpperBoundFn(0), equals(22 / 66));
expect(series.measureUpperBoundFn(1), equals(32 / 99));
expect(series.measureUpperBoundFn(2), equals(42 / 132));
expect(series.rawMeasureUpperBoundFn(0), equals(22));
expect(series.rawMeasureUpperBoundFn(1), equals(32));
expect(series.rawMeasureUpperBoundFn(2), equals(42));
// Verify sixth series.
series = seriesList[5];
expect(series.measureFn(0), equals(30 / 66));
expect(series.measureFn(1), equals(40 / 99));
expect(series.measureFn(2), equals(50 / 132));
expect(series.rawMeasureFn(0), equals(30));
expect(series.rawMeasureFn(1), equals(40));
expect(series.rawMeasureFn(2), equals(50));
expect(series.measureLowerBoundFn(0), equals(28 / 66));
expect(series.measureLowerBoundFn(1), equals(38 / 99));
expect(series.measureLowerBoundFn(2), equals(48 / 132));
expect(series.rawMeasureLowerBoundFn(0), equals(28));
expect(series.rawMeasureLowerBoundFn(1), equals(38));
expect(series.rawMeasureLowerBoundFn(2), equals(48));
expect(series.measureUpperBoundFn(0), equals(32 / 66));
expect(series.measureUpperBoundFn(1), equals(42 / 99));
expect(series.measureUpperBoundFn(2), equals(52 / 132));
expect(series.rawMeasureUpperBoundFn(0), equals(32));
expect(series.rawMeasureUpperBoundFn(1), equals(42));
expect(series.rawMeasureUpperBoundFn(2), equals(52));
});
test('percent of domain, grouped by series category', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.domainBySeriesCategory);
// Act
_chart.lastLifecycleListener.onData(seriesList);
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify first series.
var series = seriesList[0];
expect(series.measureFn(0), equals(1 / 6));
expect(series.measureFn(1), equals(2 / 9));
expect(series.measureFn(2), equals(3 / 12));
expect(series.rawMeasureFn(0), equals(1));
expect(series.rawMeasureFn(1), equals(2));
expect(series.rawMeasureFn(2), equals(3));
// Verify second series.
series = seriesList[1];
expect(series.measureFn(0), equals(2 / 6));
expect(series.measureFn(1), equals(3 / 9));
expect(series.measureFn(2), equals(4 / 12));
expect(series.rawMeasureFn(0), equals(2));
expect(series.rawMeasureFn(1), equals(3));
expect(series.rawMeasureFn(2), equals(4));
// Verify third series.
series = seriesList[2];
expect(series.measureFn(0), equals(3 / 6));
expect(series.measureFn(1), equals(4 / 9));
expect(series.measureFn(2), equals(5 / 12));
expect(series.rawMeasureFn(0), equals(3));
expect(series.rawMeasureFn(1), equals(4));
expect(series.rawMeasureFn(2), equals(5));
// Verify fourth series.
series = seriesList[3];
expect(series.measureFn(0), equals(10 / 60));
expect(series.measureFn(1), equals(20 / 90));
expect(series.measureFn(2), equals(30 / 120));
expect(series.rawMeasureFn(0), equals(10));
expect(series.rawMeasureFn(1), equals(20));
expect(series.rawMeasureFn(2), equals(30));
expect(series.measureLowerBoundFn(0), equals(8 / 60));
expect(series.measureLowerBoundFn(1), equals(18 / 90));
expect(series.measureLowerBoundFn(2), equals(28 / 120));
expect(series.rawMeasureLowerBoundFn(0), equals(8));
expect(series.rawMeasureLowerBoundFn(1), equals(18));
expect(series.rawMeasureLowerBoundFn(2), equals(28));
expect(series.measureUpperBoundFn(0), equals(12 / 60));
expect(series.measureUpperBoundFn(1), equals(22 / 90));
expect(series.measureUpperBoundFn(2), equals(32 / 120));
expect(series.rawMeasureUpperBoundFn(0), equals(12));
expect(series.rawMeasureUpperBoundFn(1), equals(22));
expect(series.rawMeasureUpperBoundFn(2), equals(32));
// Verify fifth series.
series = seriesList[4];
expect(series.measureFn(0), equals(20 / 60));
expect(series.measureFn(1), equals(30 / 90));
expect(series.measureFn(2), equals(40 / 120));
expect(series.rawMeasureFn(0), equals(20));
expect(series.rawMeasureFn(1), equals(30));
expect(series.rawMeasureFn(2), equals(40));
expect(series.measureLowerBoundFn(0), equals(18 / 60));
expect(series.measureLowerBoundFn(1), equals(28 / 90));
expect(series.measureLowerBoundFn(2), equals(38 / 120));
expect(series.rawMeasureLowerBoundFn(0), equals(18));
expect(series.rawMeasureLowerBoundFn(1), equals(28));
expect(series.rawMeasureLowerBoundFn(2), equals(38));
expect(series.measureUpperBoundFn(0), equals(22 / 60));
expect(series.measureUpperBoundFn(1), equals(32 / 90));
expect(series.measureUpperBoundFn(2), equals(42 / 120));
expect(series.rawMeasureUpperBoundFn(0), equals(22));
expect(series.rawMeasureUpperBoundFn(1), equals(32));
expect(series.rawMeasureUpperBoundFn(2), equals(42));
// Verify sixth series.
series = seriesList[5];
expect(series.measureFn(0), equals(30 / 60));
expect(series.measureFn(1), equals(40 / 90));
expect(series.measureFn(2), equals(50 / 120));
expect(series.rawMeasureFn(0), equals(30));
expect(series.rawMeasureFn(1), equals(40));
expect(series.rawMeasureFn(2), equals(50));
expect(series.measureLowerBoundFn(0), equals(28 / 60));
expect(series.measureLowerBoundFn(1), equals(38 / 90));
expect(series.measureLowerBoundFn(2), equals(48 / 120));
expect(series.rawMeasureLowerBoundFn(0), equals(28));
expect(series.rawMeasureLowerBoundFn(1), equals(38));
expect(series.rawMeasureLowerBoundFn(2), equals(48));
expect(series.measureUpperBoundFn(0), equals(32 / 60));
expect(series.measureUpperBoundFn(1), equals(42 / 90));
expect(series.measureUpperBoundFn(2), equals(52 / 120));
expect(series.rawMeasureUpperBoundFn(0), equals(32));
expect(series.rawMeasureUpperBoundFn(1), equals(42));
expect(series.rawMeasureUpperBoundFn(2), equals(52));
});
test('percent of series', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.series);
// Act
_chart.lastLifecycleListener.onData(seriesList);
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify that every series has a total measure value. Technically this is
// handled in MutableSeries, but it is a pre-condition for this behavior
// functioning properly.
expect(seriesList[0].seriesMeasureTotal, equals(6));
expect(seriesList[1].seriesMeasureTotal, equals(9));
expect(seriesList[2].seriesMeasureTotal, equals(12));
expect(seriesList[3].seriesMeasureTotal, equals(60));
expect(seriesList[4].seriesMeasureTotal, equals(90));
expect(seriesList[5].seriesMeasureTotal, equals(120));
// Verify first series.
var series = seriesList[0];
expect(series.measureFn(0), equals(1 / 6));
expect(series.measureFn(1), equals(2 / 6));
expect(series.measureFn(2), equals(3 / 6));
expect(series.rawMeasureFn(0), equals(1));
expect(series.rawMeasureFn(1), equals(2));
expect(series.rawMeasureFn(2), equals(3));
// Verify second series.
series = seriesList[1];
expect(series.measureFn(0), equals(2 / 9));
expect(series.measureFn(1), equals(3 / 9));
expect(series.measureFn(2), equals(4 / 9));
expect(series.rawMeasureFn(0), equals(2));
expect(series.rawMeasureFn(1), equals(3));
expect(series.rawMeasureFn(2), equals(4));
// Verify third series.
series = seriesList[2];
expect(series.measureFn(0), equals(3 / 12));
expect(series.measureFn(1), equals(4 / 12));
expect(series.measureFn(2), equals(5 / 12));
expect(series.rawMeasureFn(0), equals(3));
expect(series.rawMeasureFn(1), equals(4));
expect(series.rawMeasureFn(2), equals(5));
// Verify fourth series.
series = seriesList[3];
expect(series.measureFn(0), equals(10 / 60));
expect(series.measureFn(1), equals(20 / 60));
expect(series.measureFn(2), equals(30 / 60));
expect(series.rawMeasureFn(0), equals(10));
expect(series.rawMeasureFn(1), equals(20));
expect(series.rawMeasureFn(2), equals(30));
expect(series.measureLowerBoundFn(0), equals(8 / 60));
expect(series.measureLowerBoundFn(1), equals(18 / 60));
expect(series.measureLowerBoundFn(2), equals(28 / 60));
expect(series.rawMeasureLowerBoundFn(0), equals(8));
expect(series.rawMeasureLowerBoundFn(1), equals(18));
expect(series.rawMeasureLowerBoundFn(2), equals(28));
expect(series.measureUpperBoundFn(0), equals(12 / 60));
expect(series.measureUpperBoundFn(1), equals(22 / 60));
expect(series.measureUpperBoundFn(2), equals(32 / 60));
expect(series.rawMeasureUpperBoundFn(0), equals(12));
expect(series.rawMeasureUpperBoundFn(1), equals(22));
expect(series.rawMeasureUpperBoundFn(2), equals(32));
// Verify fifth series.
series = seriesList[4];
expect(series.measureFn(0), equals(20 / 90));
expect(series.measureFn(1), equals(30 / 90));
expect(series.measureFn(2), equals(40 / 90));
expect(series.rawMeasureFn(0), equals(20));
expect(series.rawMeasureFn(1), equals(30));
expect(series.rawMeasureFn(2), equals(40));
expect(series.measureLowerBoundFn(0), equals(18 / 90));
expect(series.measureLowerBoundFn(1), equals(28 / 90));
expect(series.measureLowerBoundFn(2), equals(38 / 90));
expect(series.rawMeasureLowerBoundFn(0), equals(18));
expect(series.rawMeasureLowerBoundFn(1), equals(28));
expect(series.rawMeasureLowerBoundFn(2), equals(38));
expect(series.measureUpperBoundFn(0), equals(22 / 90));
expect(series.measureUpperBoundFn(1), equals(32 / 90));
expect(series.measureUpperBoundFn(2), equals(42 / 90));
expect(series.rawMeasureUpperBoundFn(0), equals(22));
expect(series.rawMeasureUpperBoundFn(1), equals(32));
expect(series.rawMeasureUpperBoundFn(2), equals(42));
// Verify sixth series.
series = seriesList[5];
expect(series.measureFn(0), equals(30 / 120));
expect(series.measureFn(1), equals(40 / 120));
expect(series.measureFn(2), equals(50 / 120));
expect(series.rawMeasureFn(0), equals(30));
expect(series.rawMeasureFn(1), equals(40));
expect(series.rawMeasureFn(2), equals(50));
expect(series.measureLowerBoundFn(0), equals(28 / 120));
expect(series.measureLowerBoundFn(1), equals(38 / 120));
expect(series.measureLowerBoundFn(2), equals(48 / 120));
expect(series.rawMeasureLowerBoundFn(0), equals(28));
expect(series.rawMeasureLowerBoundFn(1), equals(38));
expect(series.rawMeasureLowerBoundFn(2), equals(48));
expect(series.measureUpperBoundFn(0), equals(32 / 120));
expect(series.measureUpperBoundFn(1), equals(42 / 120));
expect(series.measureUpperBoundFn(2), equals(52 / 120));
expect(series.rawMeasureUpperBoundFn(0), equals(32));
expect(series.rawMeasureUpperBoundFn(1), equals(42));
expect(series.rawMeasureUpperBoundFn(2), equals(52));
});
});
group('Life cycle', () {
test('sets injected flag for percent of domain', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.domain);
// Act
_chart.lastLifecycleListener.onData(seriesList);
// Verify that each series has an initially false flag.
expect(seriesList[0].getAttr(percentInjectedKey), isFalse);
expect(seriesList[1].getAttr(percentInjectedKey), isFalse);
expect(seriesList[2].getAttr(percentInjectedKey), isFalse);
expect(seriesList[3].getAttr(percentInjectedKey), isFalse);
expect(seriesList[4].getAttr(percentInjectedKey), isFalse);
expect(seriesList[5].getAttr(percentInjectedKey), isFalse);
// Act
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify that each series has a true flag.
expect(seriesList[0].getAttr(percentInjectedKey), isTrue);
expect(seriesList[1].getAttr(percentInjectedKey), isTrue);
expect(seriesList[2].getAttr(percentInjectedKey), isTrue);
expect(seriesList[3].getAttr(percentInjectedKey), isTrue);
expect(seriesList[4].getAttr(percentInjectedKey), isTrue);
expect(seriesList[5].getAttr(percentInjectedKey), isTrue);
});
test('sets injected flag for percent of series', () {
// Setup behavior.
_makeBehavior(totalType: PercentInjectorTotalType.series);
// Act
_chart.lastLifecycleListener.onData(seriesList);
// Verify that each series has an initially false flag.
expect(seriesList[0].getAttr(percentInjectedKey), isFalse);
expect(seriesList[1].getAttr(percentInjectedKey), isFalse);
expect(seriesList[2].getAttr(percentInjectedKey), isFalse);
expect(seriesList[3].getAttr(percentInjectedKey), isFalse);
expect(seriesList[4].getAttr(percentInjectedKey), isFalse);
expect(seriesList[5].getAttr(percentInjectedKey), isFalse);
// Act
_chart.lastLifecycleListener.onPreprocess(seriesList);
// Verify that each series has a true flag.
expect(seriesList[0].getAttr(percentInjectedKey), isTrue);
expect(seriesList[1].getAttr(percentInjectedKey), isTrue);
expect(seriesList[2].getAttr(percentInjectedKey), isTrue);
expect(seriesList[3].getAttr(percentInjectedKey), isTrue);
expect(seriesList[4].getAttr(percentInjectedKey), isTrue);
expect(seriesList[5].getAttr(percentInjectedKey), isTrue);
});
});
}

View File

@@ -0,0 +1,153 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/src/chart/common/series_renderer.dart';
import 'package:mockito/mockito.dart';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/behavior/chart_behavior.dart';
import 'package:charts_common/src/chart/common/datum_details.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:test/test.dart';
class MockBehavior extends Mock implements ChartBehavior<String> {}
class ParentBehavior implements ChartBehavior<String> {
final ChartBehavior<String> child;
ParentBehavior(this.child);
String get role => null;
@override
void attachTo(BaseChart chart) {
chart.addBehavior(child);
}
@override
void removeFrom(BaseChart chart) {
chart.removeBehavior(child);
}
}
class ConcreteChart extends BaseChart<String> {
@override
SeriesRenderer<String> makeDefaultRenderer() => null;
@override
List<DatumDetails<String>> getDatumDetails(SelectionModelType _) => null;
}
void main() {
ConcreteChart chart;
MockBehavior namedBehavior;
MockBehavior unnamedBehavior;
setUp(() {
chart = new ConcreteChart();
namedBehavior = new MockBehavior();
when(namedBehavior.role).thenReturn('foo');
unnamedBehavior = new MockBehavior();
when(unnamedBehavior.role).thenReturn(null);
});
group('Attach & Detach', () {
test('attach is called once', () {
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
verify(namedBehavior.role);
verifyNoMoreInteractions(namedBehavior);
});
test('deteach is called once', () {
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
chart.removeBehavior(namedBehavior);
verify(namedBehavior.removeFrom(chart)).called(1);
verify(namedBehavior.role);
verifyNoMoreInteractions(namedBehavior);
});
test('detach is called when name is reused', () {
final otherBehavior = new MockBehavior();
when(otherBehavior.role).thenReturn('foo');
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
chart.addBehavior(otherBehavior);
verify(namedBehavior.removeFrom(chart)).called(1);
verify(otherBehavior.attachTo(chart)).called(1);
verify(namedBehavior.role);
verify(otherBehavior.role);
verifyNoMoreInteractions(namedBehavior);
verifyNoMoreInteractions(otherBehavior);
});
test('detach is not called when name is null', () {
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
chart.addBehavior(unnamedBehavior);
verify(unnamedBehavior.attachTo(chart)).called(1);
verify(namedBehavior.role);
verify(unnamedBehavior.role);
verifyNoMoreInteractions(namedBehavior);
verifyNoMoreInteractions(unnamedBehavior);
});
test('detach is not called when name is different', () {
final otherBehavior = new MockBehavior();
when(otherBehavior.role).thenReturn('bar');
chart.addBehavior(namedBehavior);
verify(namedBehavior.attachTo(chart)).called(1);
chart.addBehavior(otherBehavior);
verify(otherBehavior.attachTo(chart)).called(1);
verify(namedBehavior.role);
verify(otherBehavior.role);
verifyNoMoreInteractions(namedBehavior);
verifyNoMoreInteractions(otherBehavior);
});
test('behaviors are removed when chart is destroyed', () {
final parentBehavior = new ParentBehavior(unnamedBehavior);
chart.addBehavior(parentBehavior);
// The parent should add the child behavoir.
verify(unnamedBehavior.attachTo(chart)).called(1);
chart.destroy();
// The parent should remove the child behavior.
verify(unnamedBehavior.removeFrom(chart)).called(1);
// Remove should only be called once and shouldn't trigger a concurrent
// modification exception.
verify(unnamedBehavior.role);
verifyNoMoreInteractions(unnamedBehavior);
});
});
}

View File

@@ -0,0 +1,185 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/behavior/domain_highlighter.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/common/material_palette.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChart extends Mock implements BaseChart {
LifecycleListener lastListener;
@override
addLifecycleListener(LifecycleListener listener) => lastListener = listener;
@override
removeLifecycleListener(LifecycleListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
return true;
}
}
class MockSelectionModel extends Mock implements MutableSelectionModel {
SelectionModelListener lastListener;
@override
addSelectionChangedListener(SelectionModelListener listener) =>
lastListener = listener;
@override
removeSelectionChangedListener(SelectionModelListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
}
}
void main() {
MockChart _chart;
MockSelectionModel _selectionModel;
MutableSeries<String> _series1;
final _s1D1 = new MyRow('s1d1', 11);
final _s1D2 = new MyRow('s1d2', 12);
final _s1D3 = new MyRow('s1d3', 13);
MutableSeries<String> _series2;
final _s2D1 = new MyRow('s2d1', 21);
final _s2D2 = new MyRow('s2d2', 22);
final _s2D3 = new MyRow('s2d3', 23);
_setupSelection(List<MyRow> selected) {
for (var i = 0; i < _series1.data.length; i++) {
when(_selectionModel.isDatumSelected(_series1, i))
.thenReturn(selected.contains(_series1.data[i]));
}
for (var i = 0; i < _series2.data.length; i++) {
when(_selectionModel.isDatumSelected(_series2, i))
.thenReturn(selected.contains(_series2.data[i]));
}
}
setUp(() {
_chart = new MockChart();
_selectionModel = new MockSelectionModel();
when(_chart.getSelectionModel(SelectionModelType.info))
.thenReturn(_selectionModel);
_series1 = new MutableSeries(new Series<MyRow, String>(
id: 's1',
data: [_s1D1, _s1D2, _s1D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => MaterialPalette.blue.shadeDefault))
..measureFn = (_) => 0.0;
_series2 = new MutableSeries(new Series<MyRow, String>(
id: 's2',
data: [_s2D1, _s2D2, _s2D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => MaterialPalette.red.shadeDefault))
..measureFn = (_) => 0.0;
});
group('DomainHighligher', () {
test('darkens the selected bars', () {
// Setup
final behavior = new DomainHighlighter(SelectionModelType.info);
behavior.attachTo(_chart);
_setupSelection([_s1D2, _s2D2]);
final seriesList = [_series1, _series2];
// Act
_selectionModel.lastListener(_selectionModel);
verify(_chart.redraw(skipAnimation: true, skipLayout: true));
_chart.lastListener.onPostprocess(seriesList);
// Verify
final s1ColorFn = _series1.colorFn;
expect(s1ColorFn(0), equals(MaterialPalette.blue.shadeDefault));
expect(s1ColorFn(1), equals(MaterialPalette.blue.shadeDefault.darker));
expect(s1ColorFn(2), equals(MaterialPalette.blue.shadeDefault));
final s2ColorFn = _series2.colorFn;
expect(s2ColorFn(0), equals(MaterialPalette.red.shadeDefault));
expect(s2ColorFn(1), equals(MaterialPalette.red.shadeDefault.darker));
expect(s2ColorFn(2), equals(MaterialPalette.red.shadeDefault));
});
test('listens to other selection models', () {
// Setup
final behavior = new DomainHighlighter(SelectionModelType.action);
when(_chart.getSelectionModel(SelectionModelType.action))
.thenReturn(_selectionModel);
// Act
behavior.attachTo(_chart);
// Verify
verify(_chart.getSelectionModel(SelectionModelType.action));
verifyNever(_chart.getSelectionModel(SelectionModelType.info));
});
test('leaves everything alone with no selection', () {
// Setup
final behavior = new DomainHighlighter(SelectionModelType.info);
behavior.attachTo(_chart);
_setupSelection([]);
final seriesList = [_series1, _series2];
// Act
_selectionModel.lastListener(_selectionModel);
verify(_chart.redraw(skipAnimation: true, skipLayout: true));
_chart.lastListener.onPostprocess(seriesList);
// Verify
final s1ColorFn = _series1.colorFn;
expect(s1ColorFn(0), equals(MaterialPalette.blue.shadeDefault));
expect(s1ColorFn(1), equals(MaterialPalette.blue.shadeDefault));
expect(s1ColorFn(2), equals(MaterialPalette.blue.shadeDefault));
final s2ColorFn = _series2.colorFn;
expect(s2ColorFn(0), equals(MaterialPalette.red.shadeDefault));
expect(s2ColorFn(1), equals(MaterialPalette.red.shadeDefault));
expect(s2ColorFn(2), equals(MaterialPalette.red.shadeDefault));
});
test('cleans up', () {
// Setup
final behavior = new DomainHighlighter(SelectionModelType.info);
behavior.attachTo(_chart);
_setupSelection([_s1D2, _s2D2]);
// Act
behavior.removeFrom(_chart);
// Verify
expect(_chart.lastListener, isNull);
expect(_selectionModel.lastListener, isNull);
});
});
}
class MyRow {
final String campaign;
final int count;
MyRow(this.campaign, this.count);
}

View File

@@ -0,0 +1,213 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/chart_canvas.dart';
import 'package:charts_common/src/chart/common/datum_details.dart';
import 'package:charts_common/src/chart/common/behavior/initial_selection.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/common/series_datum.dart';
import 'package:charts_common/src/chart/common/series_renderer.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:test/test.dart';
class FakeRenderer extends BaseSeriesRenderer {
@override
DatumDetails addPositionToDetailsForSeriesDatum(
DatumDetails details, SeriesDatum seriesDatum) {
return null;
}
@override
List<DatumDetails> getNearestDatumDetailPerSeries(
Point<double> chartPoint, bool byDomain, Rectangle<int> boundsOverride) {
return null;
}
@override
void paint(ChartCanvas canvas, double animationPercent) {}
@override
void update(List<ImmutableSeries> seriesList, bool isAnimating) {}
}
class FakeChart extends BaseChart {
@override
List<DatumDetails> getDatumDetails(SelectionModelType type) => [];
@override
SeriesRenderer makeDefaultRenderer() => new FakeRenderer();
void requestOnDraw(List<MutableSeries> seriesList) {
fireOnDraw(seriesList);
}
}
void main() {
FakeChart _chart;
ImmutableSeries _series1;
ImmutableSeries _series2;
ImmutableSeries _series3;
ImmutableSeries _series4;
final infoSelectionType = SelectionModelType.info;
InitialSelection _makeBehavior(SelectionModelType selectionModelType,
{List<String> selectedSeries, List<SeriesDatumConfig> selectedData}) {
InitialSelection behavior = new InitialSelection(
selectionModelType: selectionModelType,
selectedSeriesConfig: selectedSeries,
selectedDataConfig: selectedData);
behavior.attachTo(_chart);
return behavior;
}
setUp(() {
_chart = new FakeChart();
_series1 = new MutableSeries(new Series(
id: 'mySeries1',
data: ['A', 'B', 'C', 'D'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {}));
_series2 = new MutableSeries(new Series(
id: 'mySeries2',
data: ['W', 'X', 'Y', 'Z'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {}));
_series3 = new MutableSeries(new Series(
id: 'mySeries3',
data: ['W', 'X', 'Y', 'Z'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {}));
_series4 = new MutableSeries(new Series(
id: 'mySeries4',
data: ['W', 'X', 'Y', 'Z'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {}));
});
test('selects initial datum', () {
_makeBehavior(infoSelectionType,
selectedData: [new SeriesDatumConfig('mySeries1', 'C')]);
_chart.requestOnDraw([_series1, _series2]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(1));
expect(model.selectedSeries[0], equals(_series1));
expect(model.selectedDatum, hasLength(1));
expect(model.selectedDatum[0].series, equals(_series1));
expect(model.selectedDatum[0].datum, equals('C'));
});
test('selects multiple initial data', () {
_makeBehavior(infoSelectionType, selectedData: [
new SeriesDatumConfig('mySeries1', 'C'),
new SeriesDatumConfig('mySeries1', 'D')
]);
_chart.requestOnDraw([_series1, _series2]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(1));
expect(model.selectedSeries[0], equals(_series1));
expect(model.selectedDatum, hasLength(2));
expect(model.selectedDatum[0].series, equals(_series1));
expect(model.selectedDatum[0].datum, equals('C'));
expect(model.selectedDatum[1].series, equals(_series1));
expect(model.selectedDatum[1].datum, equals('D'));
});
test('selects initial series', () {
_makeBehavior(infoSelectionType, selectedSeries: ['mySeries2']);
_chart.requestOnDraw([_series1, _series2, _series3, _series4]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(1));
expect(model.selectedSeries[0], equals(_series2));
expect(model.selectedDatum, isEmpty);
});
test('selects multiple series', () {
_makeBehavior(infoSelectionType,
selectedSeries: ['mySeries2', 'mySeries4']);
_chart.requestOnDraw([_series1, _series2, _series3, _series4]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(2));
expect(model.selectedSeries[0], equals(_series2));
expect(model.selectedSeries[1], equals(_series4));
expect(model.selectedDatum, isEmpty);
});
test('selects series and datum', () {
_makeBehavior(infoSelectionType,
selectedData: [new SeriesDatumConfig('mySeries1', 'C')],
selectedSeries: ['mySeries4']);
_chart.requestOnDraw([_series1, _series2, _series3, _series4]);
final model = _chart.getSelectionModel(infoSelectionType);
expect(model.selectedSeries, hasLength(2));
expect(model.selectedSeries[0], equals(_series1));
expect(model.selectedSeries[1], equals(_series4));
expect(model.selectedDatum[0].series, equals(_series1));
expect(model.selectedDatum[0].datum, equals('C'));
});
test('selection model is reset when a new series is drawn', () {
_makeBehavior(infoSelectionType, selectedSeries: ['mySeries2']);
_chart.requestOnDraw([_series1, _series2, _series3, _series4]);
final model = _chart.getSelectionModel(infoSelectionType);
// Verify initial selection is selected on first draw
expect(model.selectedSeries, hasLength(1));
expect(model.selectedSeries[0], equals(_series2));
expect(model.selectedDatum, isEmpty);
// Request a draw with a new series list.
_chart.draw(
[
new Series(
id: 'mySeries2',
data: ['W', 'X', 'Y', 'Z'],
domainFn: (dynamic datum, __) => datum,
measureFn: (_, __) {})
],
);
// Verify selection is cleared.
expect(model.selectedSeries, isEmpty);
expect(model.selectedDatum, isEmpty);
});
}

View File

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

View File

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

View File

@@ -0,0 +1,160 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/behavior/selection/lock_selection.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/common/gesture_listener.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChart extends Mock implements BaseChart {
GestureListener lastListener;
@override
GestureListener addGestureListener(GestureListener listener) {
lastListener = listener;
return listener;
}
@override
void removeGestureListener(GestureListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
}
}
class MockSelectionModel extends Mock implements MutableSelectionModel {
bool locked = false;
}
void main() {
MockChart _chart;
MockSelectionModel _hoverSelectionModel;
MockSelectionModel _clickSelectionModel;
LockSelection _makeLockSelectionBehavior(
SelectionModelType selectionModelType) {
LockSelection behavior =
new LockSelection(selectionModelType: selectionModelType);
behavior.attachTo(_chart);
return behavior;
}
_setupChart({Point<double> forPoint, bool isWithinRenderer}) {
if (isWithinRenderer != null) {
when(_chart.pointWithinRenderer(forPoint)).thenReturn(isWithinRenderer);
}
}
setUp(() {
_hoverSelectionModel = new MockSelectionModel();
_clickSelectionModel = new MockSelectionModel();
_chart = new MockChart();
when(_chart.getSelectionModel(SelectionModelType.info))
.thenReturn(_hoverSelectionModel);
when(_chart.getSelectionModel(SelectionModelType.action))
.thenReturn(_clickSelectionModel);
});
group('LockSelection trigger handling', () {
test('can lock model with a selection', () {
// Setup chart matches point with single domain single series.
_makeLockSelectionBehavior(SelectionModelType.info);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true);
when(_hoverSelectionModel.hasAnySelection).thenReturn(true);
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_hoverSelectionModel.hasAnySelection);
expect(_hoverSelectionModel.locked, equals(true));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('can lock and unlock model', () {
// Setup chart matches point with single domain single series.
_makeLockSelectionBehavior(SelectionModelType.info);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true);
when(_hoverSelectionModel.hasAnySelection).thenReturn(true);
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_hoverSelectionModel.hasAnySelection);
expect(_hoverSelectionModel.locked, equals(true));
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_hoverSelectionModel.clearSelection());
expect(_hoverSelectionModel.locked, equals(false));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('does not lock model with empty selection', () {
// Setup chart matches point with single domain single series.
_makeLockSelectionBehavior(SelectionModelType.info);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true);
when(_hoverSelectionModel.hasAnySelection).thenReturn(false);
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_hoverSelectionModel.hasAnySelection);
expect(_hoverSelectionModel.locked, equals(false));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
});
group('Cleanup', () {
test('detach removes listener', () {
// Setup
final behavior = _makeLockSelectionBehavior(SelectionModelType.info);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true);
expect(_chart.lastListener, isNotNull);
// Act
behavior.removeFrom(_chart);
// Validate
expect(_chart.lastListener, isNull);
});
});
}

View File

@@ -0,0 +1,491 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/behavior/selection/select_nearest.dart';
import 'package:charts_common/src/chart/common/behavior/selection/selection_trigger.dart';
import 'package:charts_common/src/chart/common/datum_details.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/common/series_datum.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/common/gesture_listener.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChart extends Mock implements BaseChart<String> {
GestureListener lastListener;
@override
GestureListener addGestureListener(GestureListener listener) {
lastListener = listener;
return listener;
}
@override
void removeGestureListener(GestureListener listener) {
expect(listener, equals(lastListener));
lastListener = null;
}
}
class MockSelectionModel extends Mock implements MutableSelectionModel<String> {
}
void main() {
MockChart _chart;
MockSelectionModel _hoverSelectionModel;
MockSelectionModel _clickSelectionModel;
List<String> _series1Data;
List<String> _series2Data;
MutableSeries<String> _series1;
MutableSeries<String> _series2;
DatumDetails<String> _details1;
DatumDetails<String> _details1Series2;
DatumDetails<String> _details2;
DatumDetails<String> _details3;
SelectNearest<String> _makeBehavior(
SelectionModelType selectionModelType, SelectionTrigger eventTrigger,
{bool expandToDomain,
bool selectClosestSeries,
int maximumDomainDistancePx}) {
SelectNearest<String> behavior = new SelectNearest<String>(
selectionModelType: selectionModelType,
expandToDomain: expandToDomain,
selectClosestSeries: selectClosestSeries,
eventTrigger: eventTrigger,
maximumDomainDistancePx: maximumDomainDistancePx);
behavior.attachTo(_chart);
return behavior;
}
_setupChart(
{Point<double> forPoint,
bool isWithinRenderer,
List<DatumDetails<String>> respondWithDetails,
List<MutableSeries<String>> seriesList}) {
if (isWithinRenderer != null) {
when(_chart.pointWithinRenderer(forPoint)).thenReturn(isWithinRenderer);
}
if (respondWithDetails != null) {
when(_chart.getNearestDatumDetailPerSeries(forPoint, true))
.thenReturn(respondWithDetails);
}
if (seriesList != null) {
when(_chart.currentSeriesList).thenReturn(seriesList);
}
}
setUp(() {
_hoverSelectionModel = new MockSelectionModel();
_clickSelectionModel = new MockSelectionModel();
_chart = new MockChart();
when(_chart.getSelectionModel(SelectionModelType.info))
.thenReturn(_hoverSelectionModel);
when(_chart.getSelectionModel(SelectionModelType.action))
.thenReturn(_clickSelectionModel);
_series1Data = ['myDomain1', 'myDomain2', 'myDomain3'];
_series1 = new MutableSeries<String>(new Series(
id: 'mySeries1',
data: ['myDatum1', 'myDatum2', 'myDatum3'],
domainFn: (_, int i) => _series1Data[i],
measureFn: (_, __) {}));
_details1 = new DatumDetails(
datum: 'myDatum1',
domain: 'myDomain1',
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_details2 = new DatumDetails(
datum: 'myDatum2',
domain: 'myDomain2',
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_details3 = new DatumDetails(
datum: 'myDatum3',
domain: 'myDomain3',
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_series2Data = ['myDomain1'];
_series2 = new MutableSeries<String>(new Series(
id: 'mySeries2',
data: ['myDatum1s2'],
domainFn: (_, int i) => _series2Data[i],
measureFn: (_, __) {}));
_details1Series2 = new DatumDetails(
datum: 'myDatum1s2',
domain: 'myDomain1',
series: _series2,
domainDistance: 10.0,
measureDistance: 20.0);
});
tearDown(resetMockitoState);
group('SelectNearest trigger handling', () {
test('single series selects detail', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(
forPoint: point,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
// Shouldn't be listening to anything else.
expect(_chart.lastListener.onTap, isNull);
expect(_chart.lastListener.onDragStart, isNull);
});
test('can listen to tap', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.action, SelectionTrigger.tap,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(
forPoint: point,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
// Act
_chart.lastListener.onTapTest(point);
_chart.lastListener.onTap(point);
// Validate
verify(_clickSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('can listen to drag', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.pressHold,
expandToDomain: true, selectClosestSeries: true);
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
Point<double> updatePoint1 = new Point(200.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
Point<double> updatePoint2 = new Point(300.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details2],
seriesList: [_series1]);
Point<double> endPoint = new Point(400.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3],
seriesList: [_series1]);
// Act
_chart.lastListener.onTapTest(startPoint);
_chart.lastListener.onDragStart(startPoint);
_chart.lastListener.onDragUpdate(updatePoint1, 1.0);
_chart.lastListener.onDragUpdate(updatePoint2, 1.0);
_chart.lastListener.onDragEnd(endPoint, 1.0, 0.0);
// Validate
// details1 was tripped 2 times (startPoint & updatePoint1)
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1])).called(2);
// details2 was tripped for updatePoint2
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details2.datum)], [_series1]));
// dragEnd deselects even though we are over details3.
verify(_hoverSelectionModel.updateSelection([], []));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('can listen to drag after long press', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.longPressHold,
expandToDomain: true, selectClosestSeries: true);
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
Point<double> updatePoint1 = new Point(200.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2],
seriesList: [_series1]);
Point<double> endPoint = new Point(400.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3],
seriesList: [_series1]);
// Act 1
_chart.lastListener.onTapTest(startPoint);
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
// Act 2
// verify no interaction yet.
_chart.lastListener.onLongPress(startPoint);
_chart.lastListener.onDragStart(startPoint);
_chart.lastListener.onDragUpdate(updatePoint1, 1.0);
_chart.lastListener.onDragEnd(endPoint, 1.0, 0.0);
// Validate
// details1 was tripped 2 times (longPress & dragStart)
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1])).called(2);
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details2.datum)], [_series1]));
// dragEnd deselects even though we are over details3.
verify(_hoverSelectionModel.updateSelection([], []));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('no trigger before long press', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.longPressHold,
expandToDomain: true, selectClosestSeries: true);
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
Point<double> updatePoint1 = new Point(200.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2],
seriesList: [_series1]);
Point<double> endPoint = new Point(400.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3],
seriesList: [_series1]);
// Act
_chart.lastListener.onTapTest(startPoint);
_chart.lastListener.onDragStart(startPoint);
_chart.lastListener.onDragUpdate(updatePoint1, 1.0);
_chart.lastListener.onDragEnd(endPoint, 1.0, 0.0);
// Validate
// No interaction, didn't long press first.
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
});
group('Details', () {
test('expands to domain and includes closest series', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection([
new SeriesDatum(_series1, _details1.datum),
new SeriesDatum(_series2, _details1Series2.datum)
], [
_series1
]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('does not expand to domain', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: false, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection(
[new SeriesDatum(_series1, _details1.datum)], [_series1]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('does not include closest series', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: false);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection([
new SeriesDatum(_series1, _details1.datum),
new SeriesDatum(_series2, _details1Series2.datum)
], []));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('does not include overlay series', () {
// Setup chart with an overlay series.
_series2.overlaySeries = true;
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection([
new SeriesDatum(_series1, _details1.datum),
], [
_series1
]));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
test('selection does not exceed maximumDomainDistancePx', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true,
selectClosestSeries: true,
maximumDomainDistancePx: 1);
Point<double> point = new Point(100.0, 100.0);
_setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [
_details1,
_details1Series2,
], seriesList: [
_series1,
_series2
]);
// Act
_chart.lastListener.onHover(point);
// Validate
verify(_hoverSelectionModel.updateSelection([], []));
verifyNoMoreInteractions(_hoverSelectionModel);
verifyNoMoreInteractions(_clickSelectionModel);
});
});
group('Cleanup', () {
test('detach removes listener', () {
// Setup
SelectNearest behavior = _makeBehavior(
SelectionModelType.info, SelectionTrigger.hover,
expandToDomain: true, selectClosestSeries: true);
Point<double> point = new Point(100.0, 100.0);
_setupChart(
forPoint: point,
isWithinRenderer: true,
respondWithDetails: [_details1],
seriesList: [_series1]);
expect(_chart.lastListener, isNotNull);
// Act
behavior.removeFrom(_chart);
// Validate
expect(_chart.lastListener, isNull);
});
});
}

View File

@@ -0,0 +1,474 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/common/series_datum.dart';
import 'package:charts_common/src/chart/common/series_renderer.dart';
import 'package:charts_common/src/chart/common/behavior/legend/legend_entry_generator.dart';
import 'package:charts_common/src/chart/common/behavior/legend/series_legend.dart';
import 'package:charts_common/src/chart/common/datum_details.dart';
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/common/color.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:test/test.dart';
class ConcreteChart extends BaseChart<String> {
List<MutableSeries<String>> _seriesList;
ConcreteChart(this._seriesList);
@override
SeriesRenderer<String> makeDefaultRenderer() => null;
@override
List<MutableSeries<String>> get currentSeriesList => _seriesList;
@override
List<DatumDetails<String>> getDatumDetails(SelectionModelType _) => null;
set seriesList(List<MutableSeries<String>> seriesList) {
_seriesList = seriesList;
}
void callOnDraw() {
fireOnDraw(_seriesList);
}
void callOnPreProcess() {
fireOnPreprocess(_seriesList);
}
void callOnPostProcess() {
fireOnPostprocess(_seriesList);
}
}
class ConcreteSeriesLegend<D> extends SeriesLegend<D> {
ConcreteSeriesLegend(
{SelectionModelType selectionModelType,
LegendEntryGenerator<D> legendEntryGenerator})
: super(
selectionModelType: selectionModelType,
legendEntryGenerator: legendEntryGenerator);
@override
bool isSeriesRenderer = false;
@override
void hideSeries(String seriesId) {
super.hideSeries(seriesId);
}
@override
void showSeries(String seriesId) {
super.showSeries(seriesId);
}
@override
bool isSeriesHidden(String seriesId) {
return super.isSeriesHidden(seriesId);
}
}
void main() {
MutableSeries<String> series1;
final s1D1 = new MyRow('s1d1', 11);
final s1D2 = new MyRow('s1d2', 12);
final s1D3 = new MyRow('s1d3', 13);
MutableSeries<String> series2;
final s2D1 = new MyRow('s2d1', 21);
final s2D2 = new MyRow('s2d2', 22);
final s2D3 = new MyRow('s2d3', 23);
final blue = new Color(r: 0x21, g: 0x96, b: 0xF3);
final red = new Color(r: 0xF4, g: 0x43, b: 0x36);
ConcreteChart chart;
setUp(() {
series1 = new MutableSeries(new Series<MyRow, String>(
id: 's1',
data: [s1D1, s1D2, s1D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => blue));
series2 = new MutableSeries(new Series<MyRow, String>(
id: 's2',
data: [s2D1, s2D2, s2D3],
domainFn: (MyRow row, _) => row.campaign,
measureFn: (MyRow row, _) => row.count,
colorFn: (_, __) => red));
});
test('Legend entries created on chart post process', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend = new SeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
});
test('default hidden series are removed from list during pre process', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend =
new ConcreteSeriesLegend<String>(selectionModelType: selectionType);
legend.defaultHiddenSeries = ['s2'];
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isFalse);
expect(legend.isSeriesHidden('s2'), isTrue);
expect(seriesList, hasLength(1));
expect(seriesList[0].id, equals('s1'));
});
test('hidden series are removed from list after chart pre process', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend =
new ConcreteSeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
legend.hideSeries('s1');
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isTrue);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList, hasLength(1));
expect(seriesList[0].id, equals('s2'));
});
test('hidden and re-shown series is in the list after chart pre process', () {
final seriesList = [series1, series2];
final seriesList2 = [series1, series2];
final selectionType = SelectionModelType.info;
final legend =
new ConcreteSeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
// First hide the series.
legend.hideSeries('s1');
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isTrue);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList, hasLength(1));
expect(seriesList[0].id, equals('s2'));
// Then un-hide the series. This second list imitates the behavior of the
// chart, which creates a fresh copy of the original data from the user
// during each draw cycle.
legend.showSeries('s1');
chart.seriesList = seriesList2;
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isFalse);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList2, hasLength(2));
expect(seriesList2[0].id, equals('s1'));
expect(seriesList2[1].id, equals('s2'));
});
test('selected series legend entry is updated', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend = new SeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
chart.getSelectionModel(selectionType).updateSelection([], [series1]);
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isTrue);
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
});
test('hidden series removed from chart and later readded is visible', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final legend =
new ConcreteSeriesLegend<String>(selectionModelType: selectionType);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
// First hide the series.
legend.hideSeries('s1');
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isTrue);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList, hasLength(1));
expect(seriesList[0].id, equals('s2'));
// Validate that drawing the same set of series again maintains the hidden
// states.
final seriesList2 = [series1, series2];
chart.seriesList = seriesList2;
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isTrue);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList2, hasLength(1));
expect(seriesList2[0].id, equals('s2'));
// Next, redraw the chart with only the visible series2.
final seriesList3 = [series2];
chart.seriesList = seriesList3;
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList3, hasLength(1));
expect(seriesList3[0].id, equals('s2'));
// Finally, add series1 back to the chart, and validate that it is not
// hidden.
final seriesList4 = [series1, series2];
chart.seriesList = seriesList4;
chart.callOnDraw();
chart.callOnPreProcess();
expect(legend.isSeriesHidden('s1'), isFalse);
expect(legend.isSeriesHidden('s2'), isFalse);
expect(seriesList4, hasLength(2));
expect(seriesList4[0].id, equals('s1'));
expect(seriesList4[1].id, equals('s2'));
});
test('generated legend entries use provided formatters', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter =
(num value) => 'measure ${value?.toStringAsFixed(0)}';
final secondaryMeasureFormatter =
(num value) => 'secondary ${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter);
series2.setAttr(measureAxisIdKey, 'secondaryMeasureAxisId');
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
chart.getSelectionModel(selectionType).updateSelection(
[new SeriesDatum(series1, s1D1), new SeriesDatum(series2, s2D1)],
[series1, series2]);
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].isSelected, isTrue);
expect(legendEntries[0].value, equals(11.0));
expect(legendEntries[0].formattedValue, equals('measure 11'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].isSelected, isTrue);
expect(legendEntries[1].value, equals(21.0));
expect(legendEntries[1].formattedValue, equals('secondary 21'));
});
test('series legend - show measure sum when there is no selection', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
legendDefaultMeasure: LegendDefaultMeasure.sum,
measureFormatter: measureFormatter);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[0].value, equals(36.0));
expect(legendEntries[0].formattedValue, equals('36'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
expect(legendEntries[1].value, equals(66.0));
expect(legendEntries[1].formattedValue, equals('66'));
});
test('series legend - show measure average when there is no selection', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
legendDefaultMeasure: LegendDefaultMeasure.average,
measureFormatter: measureFormatter);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[0].value, equals(12.0));
expect(legendEntries[0].formattedValue, equals('12'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
expect(legendEntries[1].value, equals(22.0));
expect(legendEntries[1].formattedValue, equals('22'));
});
test('series legend - show first measure when there is no selection', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
legendDefaultMeasure: LegendDefaultMeasure.firstValue,
measureFormatter: measureFormatter);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[0].value, equals(11.0));
expect(legendEntries[0].formattedValue, equals('11'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
expect(legendEntries[1].value, equals(21.0));
expect(legendEntries[1].formattedValue, equals('21'));
});
test('series legend - show last measure when there is no selection', () {
final seriesList = [series1, series2];
final selectionType = SelectionModelType.info;
final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}';
final legend = new SeriesLegend<String>(
selectionModelType: selectionType,
legendDefaultMeasure: LegendDefaultMeasure.lastValue,
measureFormatter: measureFormatter);
chart = new ConcreteChart(seriesList);
legend.attachTo(chart);
chart.callOnDraw();
chart.callOnPreProcess();
chart.callOnPostProcess();
final legendEntries = legend.legendState.legendEntries;
expect(legendEntries, hasLength(2));
expect(legendEntries[0].series, equals(series1));
expect(legendEntries[0].label, equals('s1'));
expect(legendEntries[0].color, equals(blue));
expect(legendEntries[0].isSelected, isFalse);
expect(legendEntries[0].value, equals(13.0));
expect(legendEntries[0].formattedValue, equals('13'));
expect(legendEntries[1].series, equals(series2));
expect(legendEntries[1].label, equals('s2'));
expect(legendEntries[1].color, equals(red));
expect(legendEntries[1].isSelected, isFalse);
expect(legendEntries[1].value, equals(23.0));
expect(legendEntries[1].formattedValue, equals('23'));
});
}
class MyRow {
final String campaign;
final int count;
MyRow(this.campaign, this.count);
}

View File

@@ -0,0 +1,611 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart';
import 'package:charts_common/src/chart/cartesian/axis/axis.dart';
import 'package:charts_common/src/chart/common/base_chart.dart';
import 'package:charts_common/src/chart/common/datum_details.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/common/behavior/slider/slider.dart';
import 'package:charts_common/src/chart/common/behavior/selection/selection_trigger.dart';
import 'package:charts_common/src/common/gesture_listener.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockChart extends Mock implements CartesianChart {
GestureListener lastGestureListener;
LifecycleListener lastLifecycleListener;
bool vertical = true;
@override
GestureListener addGestureListener(GestureListener listener) {
lastGestureListener = listener;
return listener;
}
@override
void removeGestureListener(GestureListener listener) {
expect(listener, equals(lastGestureListener));
lastGestureListener = null;
}
@override
addLifecycleListener(LifecycleListener listener) =>
lastLifecycleListener = listener;
@override
removeLifecycleListener(LifecycleListener listener) {
expect(listener, equals(lastLifecycleListener));
lastLifecycleListener = null;
return true;
}
}
class MockDomainAxis extends Mock implements NumericAxis {
@override
double getDomain(num location) {
return (location / 20.0).toDouble();
}
@override
double getLocation(num domain) {
return (domain * 20.0).toDouble();
}
}
void main() {
MockChart _chart;
MockDomainAxis _domainAxis;
ImmutableSeries _series1;
DatumDetails _details1;
DatumDetails _details2;
DatumDetails _details3;
SliderTester tester;
Slider _makeBehavior(SelectionTrigger eventTrigger,
{Point<double> handleOffset,
Rectangle<int> handleSize,
double initialDomainValue,
SliderListenerCallback onChangeCallback,
bool snapToDatum = false,
SliderHandlePosition handlePosition = SliderHandlePosition.middle}) {
Slider behavior = new Slider(
eventTrigger: eventTrigger,
initialDomainValue: initialDomainValue,
onChangeCallback: onChangeCallback,
snapToDatum: snapToDatum,
style: new SliderStyle(
handleOffset: handleOffset, handlePosition: handlePosition));
behavior.attachTo(_chart);
tester = new SliderTester(behavior);
// Mock out chart layout by assigning bounds to the layout view.
tester.layout(
new Rectangle<int>(0, 0, 200, 200), new Rectangle<int>(0, 0, 200, 200));
return behavior;
}
_setupChart(
{Point<double> forPoint,
bool isWithinRenderer,
List<DatumDetails> respondWithDetails}) {
when(_chart.domainAxis).thenReturn(_domainAxis);
if (isWithinRenderer != null) {
when(_chart.pointWithinRenderer(forPoint)).thenReturn(isWithinRenderer);
}
if (respondWithDetails != null) {
when(_chart.getNearestDatumDetailPerSeries(forPoint, true))
.thenReturn(respondWithDetails);
}
}
setUp(() {
_chart = new MockChart();
_domainAxis = new MockDomainAxis();
_series1 = new MutableSeries(new Series(
id: 'mySeries1',
data: [],
domainFn: (_, __) {},
measureFn: (_, __) {}));
_details1 = new DatumDetails(
chartPosition: new Point(20.0, 80.0),
datum: 'myDatum1',
domain: 1.0,
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_details2 = new DatumDetails(
chartPosition: new Point(40.0, 80.0),
datum: 'myDatum2',
domain: 2.0,
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
_details3 = new DatumDetails(
chartPosition: new Point(90.0, 80.0),
datum: 'myDatum3',
domain: 4.5,
series: _series1,
domainDistance: 10.0,
measureDistance: 20.0);
});
group('Slider trigger handling', () {
test('can listen to tap and drag', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.tapAndDrag,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20));
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onTap(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint1));
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint2));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
});
test('slider handle can render at top', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.tapAndDrag,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20),
handlePosition: SliderHandlePosition.top);
Point<double> startPoint = new Point(100.0, 0.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 0.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 0.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 0.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onTap(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, -10, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, -10, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, -10, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, -10, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, -10, 10, 20)));
});
test('can listen to press hold', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.pressHold,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20));
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onLongPress(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint1));
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint2));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
});
test('can listen to long press hold', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.longPressHold,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20));
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onLongPress(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint1));
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint2));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
});
test('no position update before long press', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.longPressHold,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20));
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Simulate onDragEnd.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
});
test('can snap to datum', () {
// Setup chart matches point with single domain single series.
_makeBehavior(SelectionTrigger.tapAndDrag,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20),
snapToDatum: true);
Point<double> startPoint = new Point(100.0, 100.0);
_setupChart(
forPoint: startPoint,
isWithinRenderer: true,
respondWithDetails: [_details1]);
Point<double> updatePoint1 = new Point(50.0, 100.0);
_setupChart(
forPoint: updatePoint1,
isWithinRenderer: true,
respondWithDetails: [_details2]);
Point<double> updatePoint2 = new Point(100.0, 100.0);
_setupChart(
forPoint: updatePoint2,
isWithinRenderer: true,
respondWithDetails: [_details3]);
Point<double> endPoint = new Point(120.0, 100.0);
_setupChart(
forPoint: endPoint,
isWithinRenderer: true,
respondWithDetails: [_details3]);
// Act
_chart.lastLifecycleListener.onAxisConfigured();
_chart.lastGestureListener.onTapTest(startPoint);
_chart.lastGestureListener.onTap(startPoint);
// Start the drag.
_chart.lastGestureListener.onDragStart(startPoint);
expect(tester.domainCenterPoint, equals(startPoint));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag to first update point. The slider should follow the mouse during
// each drag update.
_chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint1));
expect(tester.domainValue, equals(2.5));
expect(tester.handleBounds, equals(new Rectangle<int>(45, 90, 10, 20)));
// Drag to first update point.
_chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0);
expect(tester.domainCenterPoint, equals(updatePoint2));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Drag the point to the end point.
_chart.lastGestureListener.onDragUpdate(endPoint, 1.0);
expect(tester.domainCenterPoint, equals(endPoint));
expect(tester.domainValue, equals(6.0));
expect(tester.handleBounds, equals(new Rectangle<int>(115, 90, 10, 20)));
// Simulate onDragEnd. This is where we expect the snap to occur.
_chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0);
expect(tester.domainCenterPoint, equals(new Point<int>(90, 100)));
expect(tester.domainValue, equals(4.5));
expect(tester.handleBounds, equals(new Rectangle<int>(85, 90, 10, 20)));
});
});
group('Slider manual control', () {
test('can set domain position', () {
// Setup chart matches point with single domain single series.
final slider = _makeBehavior(SelectionTrigger.tapAndDrag,
handleOffset: new Point<double>(0.0, 0.0),
handleSize: new Rectangle<int>(0, 0, 10, 20),
initialDomainValue: 1.0);
_setupChart();
// Act
_chart.lastLifecycleListener.onAxisConfigured();
// Verify initial position.
expect(tester.domainCenterPoint, equals(new Point(20.0, 100.0)));
expect(tester.domainValue, equals(1.0));
expect(tester.handleBounds, equals(new Rectangle<int>(15, 90, 10, 20)));
// Move to first domain value.
slider.moveSliderToDomain(2);
expect(tester.domainCenterPoint, equals(new Point(40.0, 100.0)));
expect(tester.domainValue, equals(2.0));
expect(tester.handleBounds, equals(new Rectangle<int>(35, 90, 10, 20)));
// Move to second domain value.
slider.moveSliderToDomain(5);
expect(tester.domainCenterPoint, equals(new Point(100.0, 100.0)));
expect(tester.domainValue, equals(5.0));
expect(tester.handleBounds, equals(new Rectangle<int>(95, 90, 10, 20)));
// Move to second domain value.
slider.moveSliderToDomain(7.5);
expect(tester.domainCenterPoint, equals(new Point(150.0, 100.0)));
expect(tester.domainValue, equals(7.5));
expect(tester.handleBounds, equals(new Rectangle<int>(145, 90, 10, 20)));
});
});
group('Cleanup', () {
test('detach removes listener', () {
// Setup
Slider behavior = _makeBehavior(SelectionTrigger.tapAndDrag);
Point<double> point = new Point(100.0, 100.0);
_setupChart(
forPoint: point,
isWithinRenderer: true,
respondWithDetails: [_details1]);
expect(_chart.lastGestureListener, isNotNull);
// Act
behavior.removeFrom(_chart);
// Validate
expect(_chart.lastGestureListener, isNull);
});
});
}

View File

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

View File

@@ -0,0 +1,331 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/src/chart/common/selection_model/selection_model.dart';
import 'package:charts_common/src/chart/common/processed_series.dart';
import 'package:charts_common/src/chart/common/series_datum.dart';
import 'package:charts_common/src/data/series.dart';
import 'package:test/test.dart';
void main() {
MutableSelectionModel<String> _selectionModel;
ImmutableSeries<String> _closestSeries;
MyDatum _closestDatumClosestSeries;
SeriesDatum<String> _closestDatumClosestSeriesPair;
MyDatum _otherDatumClosestSeries;
SeriesDatum<String> _otherDatumClosestSeriesPair;
ImmutableSeries<String> _otherSeries;
MyDatum _closestDatumOtherSeries;
SeriesDatum<String> _closestDatumOtherSeriesPair;
MyDatum _otherDatumOtherSeries;
SeriesDatum<String> _otherDatumOtherSeriesPair;
setUp(() {
_selectionModel = new MutableSelectionModel<String>();
_closestDatumClosestSeries = new MyDatum('cDcS');
_otherDatumClosestSeries = new MyDatum('oDcS');
_closestSeries = new MutableSeries<String>(new Series<MyDatum, String>(
id: 'closest',
data: [_closestDatumClosestSeries, _otherDatumClosestSeries],
domainFn: (dynamic d, _) => d.id,
measureFn: (_, __) => 0));
_closestDatumClosestSeriesPair =
new SeriesDatum<String>(_closestSeries, _closestDatumClosestSeries);
_otherDatumClosestSeriesPair =
new SeriesDatum<String>(_closestSeries, _otherDatumClosestSeries);
_closestDatumOtherSeries = new MyDatum('cDoS');
_otherDatumOtherSeries = new MyDatum('oDoS');
_otherSeries = new MutableSeries<String>(new Series<MyDatum, String>(
id: 'other',
data: [_closestDatumOtherSeries, _otherDatumOtherSeries],
domainFn: (dynamic d, _) => d.id,
measureFn: (_, __) => 0));
_closestDatumOtherSeriesPair =
new SeriesDatum<String>(_otherSeries, _closestDatumOtherSeries);
_otherDatumOtherSeriesPair =
new SeriesDatum<String>(_otherSeries, _otherDatumOtherSeries);
});
group('SelectionModel persists values', () {
test('selection model is empty by default', () {
expect(_selectionModel.hasDatumSelection, isFalse);
expect(_selectionModel.hasSeriesSelection, isFalse);
});
test('all datum are selected but only the first Series is', () {
// Select the 'closest' datum for each Series.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
new SeriesDatum(_otherSeries, _closestDatumOtherSeries),
], [
_closestSeries
]);
expect(_selectionModel.hasDatumSelection, isTrue);
expect(_selectionModel.selectedDatum, hasLength(2));
expect(_selectionModel.selectedDatum,
contains(_closestDatumClosestSeriesPair));
expect(_selectionModel.selectedDatum,
contains(_closestDatumOtherSeriesPair));
expect(
_selectionModel.selectedDatum.contains(_otherDatumClosestSeriesPair),
isFalse);
expect(_selectionModel.selectedDatum.contains(_otherDatumOtherSeriesPair),
isFalse);
expect(_selectionModel.hasSeriesSelection, isTrue);
expect(_selectionModel.selectedSeries, hasLength(1));
expect(_selectionModel.selectedSeries, contains(_closestSeries));
expect(_selectionModel.selectedSeries.contains(_otherSeries), isFalse);
});
test('selection can change', () {
// Select the 'closest' datum for each Series.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
new SeriesDatum(_otherSeries, _closestDatumOtherSeries),
], [
_closestSeries
]);
// Change selection to just the other datum on the other series.
_selectionModel.updateSelection([
new SeriesDatum(_otherSeries, _otherDatumOtherSeries),
], [
_otherSeries
]);
expect(_selectionModel.selectedDatum, hasLength(1));
expect(
_selectionModel.selectedDatum, contains(_otherDatumOtherSeriesPair));
expect(_selectionModel.selectedSeries, hasLength(1));
expect(_selectionModel.selectedSeries, contains(_otherSeries));
});
test('selection can be series only', () {
// Select the 'closest' Series without datum to simulate legend hovering.
_selectionModel.updateSelection([], [_closestSeries]);
expect(_selectionModel.hasDatumSelection, isFalse);
expect(_selectionModel.selectedDatum, hasLength(0));
expect(_selectionModel.hasSeriesSelection, isTrue);
expect(_selectionModel.selectedSeries, hasLength(1));
expect(_selectionModel.selectedSeries, contains(_closestSeries));
});
test('selection lock prevents change', () {
// Prevent selection changes.
_selectionModel.locked = true;
// Try to the 'closest' datum for each Series.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
new SeriesDatum(_otherSeries, _closestDatumOtherSeries),
], [
_closestSeries
]);
expect(_selectionModel.hasDatumSelection, isFalse);
expect(_selectionModel.hasSeriesSelection, isFalse);
// Allow selection changes.
_selectionModel.locked = false;
// Try to the 'closest' datum for each Series.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
new SeriesDatum(_otherSeries, _closestDatumOtherSeries),
], [
_closestSeries
]);
expect(_selectionModel.hasDatumSelection, isTrue);
expect(_selectionModel.hasSeriesSelection, isTrue);
// Prevent selection changes.
_selectionModel.locked = true;
// Attempt to change selection
_selectionModel.updateSelection([
new SeriesDatum(_otherSeries, _otherDatumOtherSeries),
], [
_otherSeries
]);
// Previous selection should still be set.
expect(_selectionModel.selectedDatum, hasLength(2));
expect(_selectionModel.selectedDatum,
contains(_closestDatumClosestSeriesPair));
expect(_selectionModel.selectedDatum,
contains(_closestDatumOtherSeriesPair));
expect(_selectionModel.selectedSeries, hasLength(1));
expect(_selectionModel.selectedSeries, contains(_closestSeries));
});
});
group('SelectionModel changed listeners', () {
test('listener triggered for change', () {
SelectionModel<String> triggeredModel;
// Listen
_selectionModel
.addSelectionChangedListener((SelectionModel<String> model) {
triggeredModel = model;
});
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should have been triggered.
expect(triggeredModel, equals(_selectionModel));
});
test('listener not triggered for no change', () {
SelectionModel<String> triggeredModel;
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Listen
_selectionModel
.addSelectionChangedListener((SelectionModel<String> model) {
triggeredModel = model;
});
// Try to update the model with the same value.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should not have been triggered.
expect(triggeredModel, isNull);
});
test('removed listener not triggered for change', () {
SelectionModel<String> triggeredModel;
Function cb = (SelectionModel<String> model) {
triggeredModel = model;
};
// Listen
_selectionModel.addSelectionChangedListener(cb);
// Unlisten
_selectionModel.removeSelectionChangedListener(cb);
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should not have been triggered.
expect(triggeredModel, isNull);
});
});
group('SelectionModel updated listeners', () {
test('listener triggered for change', () {
SelectionModel<String> triggeredModel;
// Listen
_selectionModel
.addSelectionUpdatedListener((SelectionModel<String> model) {
triggeredModel = model;
});
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should have been triggered.
expect(triggeredModel, equals(_selectionModel));
});
test('listener triggered for no change', () {
SelectionModel<String> triggeredModel;
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Listen
_selectionModel
.addSelectionUpdatedListener((SelectionModel<String> model) {
triggeredModel = model;
});
// Try to update the model with the same value.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should have been triggered.
expect(triggeredModel, equals(_selectionModel));
});
test('removed listener not triggered for change', () {
SelectionModel<String> triggeredModel;
Function cb = (SelectionModel<String> model) {
triggeredModel = model;
};
// Listen
_selectionModel.addSelectionUpdatedListener(cb);
// Unlisten
_selectionModel.removeSelectionUpdatedListener(cb);
// Set the selection to closest datum.
_selectionModel.updateSelection([
new SeriesDatum(_closestSeries, _closestDatumClosestSeries),
], [
_closestSeries
]);
// Callback should not have been triggered.
expect(triggeredModel, isNull);
});
});
}
class MyDatum {
final String id;
MyDatum(this.id);
}