// Copyright 2018 the Charts project authors. Please see the AUTHORS file // for details. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import 'package:charts_common/src/chart/line/line_renderer.dart'; import 'package:charts_common/src/chart/line/line_renderer_config.dart'; import 'package:charts_common/src/chart/common/processed_series.dart' show MutableSeries, ImmutableSeries; import 'package:charts_common/src/common/color.dart'; import 'package:charts_common/src/common/material_palette.dart' show MaterialPalette; import 'package:charts_common/src/data/series.dart' show Series; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; /// Datum/Row for the chart. class MyRow { final String campaignString; final int campaign; final int clickCount; final Color color; final List dashPattern; final double strokeWidthPx; MyRow(this.campaignString, this.campaign, this.clickCount, this.color, this.dashPattern, this.strokeWidthPx); } class MockImmutableSeries extends Mock implements ImmutableSeries { String _id; MockImmutableSeries(this._id); @override String get id => _id; } void main() { LineRenderer renderer; List> numericSeriesList; List> ordinalSeriesList; List myFakeDesktopData; List myFakeTabletData; List myFakeMobileData; setUp(() { myFakeDesktopData = [ MyRow('MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, null, 2.0), MyRow( 'MyCampaign2', 2, 25, MaterialPalette.green.shadeDefault, null, 2.0), MyRow('MyCampaign3', 3, 100, MaterialPalette.red.shadeDefault, null, 2.0), MyRow('MyOtherCampaign', 4, 75, MaterialPalette.red.shadeDefault, null, 2.0), ]; myFakeTabletData = [ MyRow( 'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, [2, 2], 2.0), MyRow( 'MyCampaign2', 2, 25, MaterialPalette.blue.shadeDefault, [3, 3], 2.0), MyRow('MyCampaign3', 3, 100, MaterialPalette.blue.shadeDefault, [4, 4], 2.0), MyRow('MyOtherCampaign', 4, 75, MaterialPalette.blue.shadeDefault, [4, 4], 2.0), ]; myFakeMobileData = [ MyRow('MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, null, 2.0), MyRow('MyCampaign2', 2, 25, MaterialPalette.blue.shadeDefault, null, 3.0), MyRow( 'MyCampaign3', 3, 100, MaterialPalette.blue.shadeDefault, null, 4.0), MyRow('MyOtherCampaign', 4, 75, MaterialPalette.blue.shadeDefault, null, 4.0), ]; numericSeriesList = [ MutableSeries(Series( id: 'Desktop', colorFn: (_, __) => MaterialPalette.blue.shadeDefault, domainFn: (dynamic row, _) => row.campaign, measureFn: (dynamic row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, data: myFakeDesktopData)), MutableSeries(Series( id: 'Tablet', colorFn: (_, __) => MaterialPalette.red.shadeDefault, domainFn: (dynamic row, _) => row.campaign, measureFn: (dynamic row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, strokeWidthPxFn: (_, __) => 1.25, data: myFakeTabletData)), MutableSeries(Series( id: 'Mobile', colorFn: (_, __) => MaterialPalette.green.shadeDefault, domainFn: (dynamic row, _) => row.campaign, measureFn: (dynamic row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, strokeWidthPxFn: (_, __) => 3.0, data: myFakeMobileData)) ]; ordinalSeriesList = [ MutableSeries(Series( id: 'Desktop', colorFn: (_, __) => MaterialPalette.blue.shadeDefault, domainFn: (dynamic row, _) => row.campaignString, measureFn: (dynamic row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, data: myFakeDesktopData)), MutableSeries(Series( id: 'Tablet', colorFn: (_, __) => MaterialPalette.red.shadeDefault, domainFn: (dynamic row, _) => row.campaignString, measureFn: (dynamic row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, strokeWidthPxFn: (_, __) => 1.25, data: myFakeTabletData)), MutableSeries(Series( id: 'Mobile', colorFn: (_, __) => MaterialPalette.green.shadeDefault, domainFn: (dynamic row, _) => row.campaignString, measureFn: (dynamic row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, strokeWidthPxFn: (_, __) => 3.0, data: myFakeMobileData)) ]; }); group('preprocess', () { test('with numeric data and simple lines', () { renderer = LineRenderer(config: LineRendererConfig(strokeWidthPx: 2.0)); renderer.configureSeries(numericSeriesList); renderer.preprocessSeries(numericSeriesList); expect(numericSeriesList.length, equals(3)); // Validate Desktop series. var series = numericSeriesList[0]; var styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(1)); var segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(4)); expect(segment.strokeWidthPx, equals(2.0)); expect(series.measureOffsetFn(0), 0); expect(series.measureOffsetFn(1), 0); expect(series.measureOffsetFn(2), 0); expect(series.measureOffsetFn(3), 0); // Validate Tablet series. series = numericSeriesList[1]; styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(1)); segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.red.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(4)); expect(segment.strokeWidthPx, equals(1.25)); expect(series.measureOffsetFn(0), 0); expect(series.measureOffsetFn(1), 0); expect(series.measureOffsetFn(2), 0); expect(series.measureOffsetFn(3), 0); // Validate Mobile series. series = numericSeriesList[2]; styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(1)); segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.green.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(4)); expect(segment.strokeWidthPx, equals(3.0)); expect(series.measureOffsetFn(0), 0); expect(series.measureOffsetFn(1), 0); expect(series.measureOffsetFn(2), 0); expect(series.measureOffsetFn(3), 0); }); test('with numeric data and stacked lines', () { renderer = LineRenderer( config: LineRendererConfig(stacked: true, strokeWidthPx: 2.0)); renderer.configureSeries(numericSeriesList); renderer.preprocessSeries(numericSeriesList); expect(numericSeriesList.length, equals(3)); // Validate Desktop series. var series = numericSeriesList[0]; var styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(1)); var segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(4)); expect(segment.strokeWidthPx, equals(2.0)); expect(series.measureOffsetFn(0), 0); expect(series.measureOffsetFn(1), 0); expect(series.measureOffsetFn(2), 0); expect(series.measureOffsetFn(3), 0); // Validate Tablet series. series = numericSeriesList[1]; styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(1)); segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.red.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(4)); expect(segment.strokeWidthPx, equals(1.25)); expect(series.measureOffsetFn(0), 5); expect(series.measureOffsetFn(1), 25); expect(series.measureOffsetFn(2), 100); expect(series.measureOffsetFn(3), 75); // Validate Mobile series. series = numericSeriesList[2]; styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(1)); segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.green.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(4)); expect(segment.strokeWidthPx, equals(3.0)); expect(series.measureOffsetFn(0), 10); expect(series.measureOffsetFn(1), 50); expect(series.measureOffsetFn(2), 200); expect(series.measureOffsetFn(3), 150); }); test('with numeric data and changes in style', () { numericSeriesList = [ MutableSeries(Series( id: 'Desktop', colorFn: (row, _) => row.color, dashPatternFn: (row, _) => row.dashPattern, strokeWidthPxFn: (row, _) => row.strokeWidthPx, domainFn: (dynamic row, _) => row.campaign, measureFn: (dynamic row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, data: myFakeDesktopData)), MutableSeries(Series( id: 'Tablet', colorFn: (row, _) => row.color, dashPatternFn: (row, _) => row.dashPattern, strokeWidthPxFn: (row, _) => row.strokeWidthPx, domainFn: (dynamic row, _) => row.campaign, measureFn: (dynamic row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, data: myFakeTabletData)), MutableSeries(Series( id: 'Mobile', colorFn: (row, _) => row.color, dashPatternFn: (row, _) => row.dashPattern, strokeWidthPxFn: (row, _) => row.strokeWidthPx, domainFn: (dynamic row, _) => row.campaign, measureFn: (dynamic row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, data: myFakeMobileData)) ]; renderer = LineRenderer(config: LineRendererConfig(strokeWidthPx: 2.0)); renderer.configureSeries(numericSeriesList); renderer.preprocessSeries(numericSeriesList); expect(numericSeriesList.length, equals(3)); // Validate Desktop series. var series = numericSeriesList[0]; var styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(3)); var segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(2)); expect(segment.strokeWidthPx, equals(2.0)); segment = styleSegments[1]; expect(segment.color, equals(MaterialPalette.green.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(2)); expect(segment.domainExtent.end, equals(3)); expect(segment.strokeWidthPx, equals(2.0)); segment = styleSegments[2]; expect(segment.color, equals(MaterialPalette.red.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(3)); expect(segment.domainExtent.end, equals(4)); expect(segment.strokeWidthPx, equals(2.0)); expect(series.measureOffsetFn(0), 0); expect(series.measureOffsetFn(1), 0); expect(series.measureOffsetFn(2), 0); expect(series.measureOffsetFn(3), 0); // Validate Tablet series. series = numericSeriesList[1]; styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(3)); segment = segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, equals([2, 2])); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(2)); expect(segment.strokeWidthPx, equals(2.0)); segment = styleSegments[1]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, equals([3, 3])); expect(segment.domainExtent.start, equals(2)); expect(segment.domainExtent.end, equals(3)); expect(segment.strokeWidthPx, equals(2.0)); segment = styleSegments[2]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, equals([4, 4])); expect(segment.domainExtent.start, equals(3)); expect(segment.domainExtent.end, equals(4)); expect(segment.strokeWidthPx, equals(2.0)); expect(series.measureOffsetFn(0), 0); expect(series.measureOffsetFn(1), 0); expect(series.measureOffsetFn(2), 0); expect(series.measureOffsetFn(3), 0); // Validate Mobile series. series = numericSeriesList[2]; styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(3)); segment = segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(2)); expect(segment.strokeWidthPx, equals(2.0)); segment = styleSegments[1]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(2)); expect(segment.domainExtent.end, equals(3)); expect(segment.strokeWidthPx, equals(3.0)); segment = styleSegments[2]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals(3)); expect(segment.domainExtent.end, equals(4)); expect(segment.strokeWidthPx, equals(4.0)); expect(series.measureOffsetFn(0), 0); expect(series.measureOffsetFn(1), 0); expect(series.measureOffsetFn(2), 0); expect(series.measureOffsetFn(3), 0); }); test('with numeric data and repeats in style', () { var myFakeData = [ MyRow( 'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, null, 2.0), MyRow('MyCampaign2', 2, 25, MaterialPalette.green.shadeDefault, null, 2.0), MyRow('MyCampaign3', 3, 100, MaterialPalette.blue.shadeDefault, null, 2.0), MyRow('MyCampaign4', 4, 75, MaterialPalette.green.shadeDefault, null, 2.0), MyRow( 'MyCampaign1', 5, 5, MaterialPalette.blue.shadeDefault, null, 2.0), MyRow('MyCampaign2', 6, 25, MaterialPalette.green.shadeDefault, null, 2.0), MyRow('MyCampaign3', 7, 100, MaterialPalette.blue.shadeDefault, null, 2.0), MyRow('MyCampaign4', 8, 75, MaterialPalette.green.shadeDefault, null, 2.0), ]; numericSeriesList = [ MutableSeries(Series( id: 'Desktop', colorFn: (row, _) => row.color, dashPatternFn: (row, _) => row.dashPattern, strokeWidthPxFn: (row, _) => row.strokeWidthPx, domainFn: (row, _) => row.campaign, measureFn: (row, _) => row.clickCount, measureOffsetFn: (_, __) => 0, data: myFakeData)), ]; renderer = LineRenderer(config: LineRendererConfig(strokeWidthPx: 2.0)); renderer.configureSeries(numericSeriesList); renderer.preprocessSeries(numericSeriesList); expect(numericSeriesList.length, equals(1)); // Validate Desktop series. var series = numericSeriesList[0]; var styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(8)); var segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.domainExtent.start, equals(1)); expect(segment.domainExtent.end, equals(2)); segment = styleSegments[1]; expect(segment.color, equals(MaterialPalette.green.shadeDefault)); expect(segment.domainExtent.start, equals(2)); expect(segment.domainExtent.end, equals(3)); segment = styleSegments[2]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.domainExtent.start, equals(3)); expect(segment.domainExtent.end, equals(4)); segment = styleSegments[3]; expect(segment.color, equals(MaterialPalette.green.shadeDefault)); expect(segment.domainExtent.start, equals(4)); expect(segment.domainExtent.end, equals(5)); segment = styleSegments[4]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.domainExtent.start, equals(5)); expect(segment.domainExtent.end, equals(6)); segment = styleSegments[5]; expect(segment.color, equals(MaterialPalette.green.shadeDefault)); expect(segment.domainExtent.start, equals(6)); expect(segment.domainExtent.end, equals(7)); segment = styleSegments[6]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.domainExtent.start, equals(7)); expect(segment.domainExtent.end, equals(8)); segment = styleSegments[7]; expect(segment.color, equals(MaterialPalette.green.shadeDefault)); expect(segment.domainExtent.start, equals(8)); expect(segment.domainExtent.end, equals(8)); }); test('with ordinal data and simple lines', () { renderer = LineRenderer(config: LineRendererConfig(strokeWidthPx: 2.0)); renderer.configureSeries(ordinalSeriesList); renderer.preprocessSeries(ordinalSeriesList); expect(ordinalSeriesList.length, equals(3)); // Validate Desktop series. var series = ordinalSeriesList[0]; var styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(1)); var segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals('MyCampaign1')); expect(segment.domainExtent.end, equals('MyOtherCampaign')); expect(segment.strokeWidthPx, equals(2.0)); // Validate Tablet series. series = ordinalSeriesList[1]; styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(1)); segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.red.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals('MyCampaign1')); expect(segment.domainExtent.end, equals('MyOtherCampaign')); expect(segment.strokeWidthPx, equals(1.25)); // Validate Mobile series. series = ordinalSeriesList[2]; styleSegments = series.getAttr(styleSegmentsKey); expect(styleSegments.length, equals(1)); segment = styleSegments[0]; expect(segment.color, equals(MaterialPalette.green.shadeDefault)); expect(segment.dashPattern, isNull); expect(segment.domainExtent.start, equals('MyCampaign1')); expect(segment.domainExtent.end, equals('MyOtherCampaign')); expect(segment.strokeWidthPx, equals(3.0)); }); }); group('Line merging', () { List> series(List keys) { return keys.map((key) => MockImmutableSeries(key)).toList(); } test('simple beginning removal', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c']); tester.merge(series(['b', 'c'])); // The series should still be there so that it can be animated out. expect(tester.seriesKeys, equals(['a', 'b', 'c'])); }); test('simple middle removal', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c']); tester.merge(series(['a', 'c'])); // The series should still be there so that it can be animated out. expect(tester.seriesKeys, equals(['a', 'b', 'c'])); }); test('simple end removal', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c']); tester.merge(series(['a', 'b'])); // The series should still be there so that it can be animated out. expect(tester.seriesKeys, equals(['a', 'b', 'c'])); }); test('simple beginning addition', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c']); tester.merge(series(['d', 'a', 'b', 'c'])); expect(tester.seriesKeys, equals(['d', 'a', 'b', 'c'])); }); test('simple middle addition', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c']); tester.merge(series(['a', 'd', 'b', 'c'])); expect(tester.seriesKeys, equals(['a', 'd', 'b', 'c'])); }); test('simple end addition', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c']); tester.merge(series(['a', 'b', 'c', 'd'])); expect(tester.seriesKeys, equals(['a', 'b', 'c', 'd'])); }); test('replacement begining', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c']); tester.merge(series(['d', 'b', 'c'])); expect(tester.seriesKeys, equals(['a', 'd', 'b', 'c'])); }); test('replacement end', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c']); tester.merge(series(['a', 'b', 'd'])); expect(tester.seriesKeys, equals(['a', 'b', 'c', 'd'])); }); test('full replacement', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c']); tester.merge(series(['d', 'e', 'f'])); expect(tester.seriesKeys, equals(['a', 'b', 'c', 'd', 'e', 'f'])); }); test('mixed replacement', () { final tester = LineRendererTester(LineRenderer()); tester.setSeriesKeys(['a', 'b', 'c', 'd']); tester.merge(series(['d', 'a', 'f', 'c'])); expect(tester.seriesKeys, equals(['d', 'a', 'b', 'f', 'c'])); }); }); }