1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-08 22:09:06 +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

1
web/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
!**/pubspec.lock

View File

@@ -0,0 +1,202 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Called by https://pub.dartlang.org/packages/peanut to generate example pages
// for hosting.
//
// Requires at least v3.2.0 of `package:peanut`
import 'dart:convert';
import 'dart:io';
import 'package:markdown/markdown.dart';
import 'package:path/path.dart' as p;
void main(List<String> args) {
final buildDir = args[0];
final fileMap =
(jsonDecode(args[1]) as Map<String, dynamic>).cast<String, String>();
if (fileMap.length < 2) {
throw StateError('We are assuming there is more than one sample!');
}
// This is USUALLY the case where we have more than one demo
for (var exampleDir in fileMap.values) {
for (var htmlFile in Directory(p.join(buildDir, exampleDir))
.listSync()
.whereType<File>()
.where((f) => p.extension(f.path) == '.html')) {
_writeAnalytics(htmlFile, buildDir);
}
}
final tocFile = File(p.join(buildDir, 'index.html'));
if (!tocFile.existsSync()) {
throw StateError('$tocFile should exist!');
}
tocFile.writeAsStringSync(
_tocTemplate(
fileMap.entries.map(
(entry) => _Demo(
_prettyName(entry.value),
entry.key,
entry.value,
),
),
),
flush: true);
}
void _writeAnalytics(File htmlFile, String buildDir) {
final content = htmlFile.readAsStringSync();
final newContent = content.replaceFirst('<head>', '<head>\n$_analytics');
final filePath = p.relative(htmlFile.path, from: buildDir);
if (newContent == content) {
print('!!! Did not replace contents in $filePath');
} else {
print('Replaced contents in $filePath');
htmlFile.writeAsStringSync(newContent, flush: true);
}
}
class _Demo {
final String name, pkgDir, buildDir;
_Demo(this.name, this.pkgDir, this.buildDir);
String get content {
final path = p.normalize(p.join(pkgDir, '..', 'README.md'));
final readmeFile = File(path);
if (!readmeFile.existsSync()) {
print(' $path No readme!');
return '';
}
var readmeContent = readmeFile.readAsStringSync();
final tripleLineIndex = readmeContent.indexOf('\n\n\n');
var secondDoubleIndex = readmeContent.indexOf('\n\n');
if (secondDoubleIndex >= 0) {
secondDoubleIndex = readmeContent.indexOf('\n\n', secondDoubleIndex + 1);
}
final endIndices =
([tripleLineIndex, secondDoubleIndex].where((i) => i >= 0).toList()
..sort());
final endIndex =
endIndices.isEmpty ? readmeContent.length : endIndices.first;
return markdownToHtml(readmeContent.substring(0, endIndex - 1));
}
String get html => '''
<div>
<a href='$buildDir'>
<img src='${p.url.join(buildDir, 'preview.png')}' width="300" alt="$name">
</a>
<a class='demo-title' href='$buildDir'>$name</a>
<div>
${_indent(content, 2)}
</div>
</div>
''';
}
final _underscoreOrSlash = RegExp('_|/');
String _prettyName(String input) =>
input.split(_underscoreOrSlash).where((e) => e.isNotEmpty).map((e) {
return e.substring(0, 1).toUpperCase() + e.substring(1);
}).join(' ');
// flutter.github.io
const _analyticsId = 'UA-67589403-8';
const _analytics = '''
<script async src="https://www.googletagmanager.com/gtag/js?id=$_analyticsId"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '$_analyticsId');
</script>''';
String _indent(String content, int spaces) =>
LineSplitter.split(content).join('\n' + ' ' * spaces);
const _itemsReplace = r'<!-- ITEMS -->';
String _tocTemplate(Iterable<_Demo> items) => '''
<!DOCTYPE html>
<html lang="en">
<head>
${_indent(_analytics, 2)}
<title>Examples</title>
<meta name="generator" content="https://pub.dartlang.org/packages/peanut">
<style>
body {
font-family: "Google Sans", "Roboto", sans-serif;
text-align: center;
}
a {
text-decoration: none;
color: #1389FD;
}
a:hover {
text-decoration: underline;
}
#toc {
text-align: left;
max-width: 1050px;
display: flex;
flex-wrap: wrap;
align-self: center;
margin: 0 auto;
align-content: space-between;
justify-content: center;
}
#toc > div {
width: 300px;
padding: 1rem;
margin: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 4px;
}
#toc > div img {
display: block;
margin: 0 auto 1rem;
}
.demo-title {
font-size: 1.25rem;
}
#toc > div p {
margin-top: 0.5rem;
margin-bottom: 0;
}
</style>
</head>
<body>
<h2><a href='https://github.com/flutter/flutter_web'>Flutter for web</a> samples</h2>
<a href='https://github.com/flutter/samples/tree/master/web'>Sample source code</a>
<div id="toc">
$_itemsReplace
</div>
</body>
</html>
'''
.replaceAll(_itemsReplace, _indent(items.map((d) => d.html).join('\n'), 4));

33
web/_tool/pubspec.lock Normal file
View File

@@ -0,0 +1,33 @@
# Generated by pub
# See https://www.dartlang.org/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.1"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
markdown:
dependency: "direct main"
description:
name: markdown
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
path:
dependency: "direct main"
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.2"
sdks:
dart: ">=2.0.0 <3.0.0"

6
web/_tool/pubspec.yaml Normal file
View File

@@ -0,0 +1,6 @@
name: tool
publish_to: none
dependencies:
markdown: ^2.0.3
path: ^1.6.2

View File

@@ -0,0 +1,87 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'package:path/path.dart' as p;
const _ansiGreen = 32;
const _ansiRed = 31;
const _ansiMagenta = 35;
void main() async {
final packageDirs = _listPackageDirs(Directory.current)
.map((path) => p.relative(path, from: Directory.current.path))
.toList();
print('Package dirs:\n${packageDirs.map((path) => ' $path').join('\n')}');
final results = <bool>[];
for (var i = 0; i < packageDirs.length; i++) {
final dir = packageDirs[i];
_logWrapped(_ansiMagenta, '\n$dir (${i + 1} of ${packageDirs.length})');
results.add(await _run(dir, 'pub', ['upgrade', '--no-precompile']));
results.add(await _run(
dir,
'dartanalyzer',
['--fatal-infos', '--fatal-warnings', '.'],
));
_printStatus(results);
}
if (results.any((v) => !v)) {
exitCode = 1;
}
}
void _printStatus(List<bool> results) {
var successCount = results.where((t) => t).length;
var success = (successCount == results.length);
var pct = 100 * successCount / results.length;
_logWrapped(success ? _ansiGreen : _ansiRed,
'$successCount of ${results.length} (${pct.toStringAsFixed(2)}%)');
}
void _logWrapped(int code, String message) {
print('\x1B[${code}m$message\x1B[0m');
}
Future<bool> _run(
String workingDir, String commandName, List<String> args) async {
var commandDescription = '`${([commandName]..addAll(args)).join(' ')}`';
_logWrapped(_ansiMagenta, ' Running $commandDescription');
var proc = await Process.start(
commandName,
args,
workingDirectory: Directory.current.path + '/' + workingDir,
mode: ProcessStartMode.inheritStdio,
);
var exitCode = await proc.exitCode;
if (exitCode != 0) {
_logWrapped(
_ansiRed, ' Failed! ($exitCode) $workingDir $commandDescription');
return false;
} else {
_logWrapped(_ansiGreen, ' Success! $workingDir $commandDescription');
return true;
}
}
Iterable<String> _listPackageDirs(Directory dir) sync* {
if (File('${dir.path}/pubspec.yaml').existsSync()) {
yield dir.path;
} else {
for (var subDir in dir
.listSync(followLinks: false)
.whereType<Directory>()
.where((d) => !Uri.file(d.path).pathSegments.last.startsWith('.'))) {
yield* _listPackageDirs(subDir);
}
}
}

View File

@@ -0,0 +1,48 @@
# 0.6.0
* Bars can now be rendered on line charts.
* Negative measure values will now be rendered on bar charts as a separate stack from the positive
values.
* Added a Datum Legend, which displays one entry per value in the first series on the chart. This is
useful for pie and scatter plot charts.
* The AxisPosition enum in RTLSpec was refactored to AxisDirection to better reflect its effect on
swapping the positions of all start and end components, and not just positioning the measure axes.
* Added custom colors for line renderer area skirts and confidence intervals. A new "areaColorFn"
has been added to Series, and corresponding data to the datum. We could not use the fillColorFn for
these elements, because that color is already applied to the internal section of points on line
charts (including highlighter behaviors).
# 0.5.0
* SelectionModelConfig's listener parameter has been renamed to "changeListener". This is a breaking
change. Please rename any existing uses of the "listener" parameter to "changeListener". This was
named in order to add an additional listener "updateListener" that listens to any update requests,
regardless if the selection model has changed.
* CartesianChart's method getMeasureAxis(String axisId) has been changed to
getMeasureAxis({String axisId) so that getting the primary measure axis will not need passing any id
that does not match the secondary measure axis id. This affects users implementing custom behaviors
using the existing method.
# 0.4.0
* Declare compatibility with Dart 2.
* BasicNumericTickFormatterSpec now takes in a callback instead of NumberFormat as the default constructor. Use named constructor withNumberFormat instead. This is a breaking change.
* BarRendererConfig is no longer default of type String, please change current usage to BarRendererConfig<String>. This is a breaking change.
* BarTargetLineRendererConfig is no longer default of type String, please change current usage to BarTargetLineRendererConfig<String>. This is a breaking change.
# 0.3.0
* Simplified API by removing the requirement for specifying the datum type when creating a chart.
For example, previously to construct a bar chart the syntax was 'new BarChart<MyDatumType>()'.
The syntax is now cleaned up to be 'new BarChart()'. Please refer to the
[online gallery](https://google.github.io/charts/flutter/gallery.html) for the correct syntax.
* Added scatter plot charts
* Added tap to hide for legends
* Added support for rendering area skirts to line charts
* Added support for configurable fill colors to bar charts
# 0.2.0
* Update color palette. Please use MaterialPalette instead of QuantumPalette.
* Dart2 fixes
# 0.1.0
Initial release.

202
web/charts/common/LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@@ -0,0 +1,5 @@
# Common Charting library
[![pub package](https://img.shields.io/pub/v/charts_common.svg)](https://pub.dartlang.org/packages/charts_common)
Common componnets for charting libraries.

View File

@@ -0,0 +1,240 @@
// 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.
export 'src/chart/bar/bar_chart.dart' show BarChart;
export 'src/chart/bar/bar_label_decorator.dart'
show BarLabelAnchor, BarLabelDecorator, BarLabelPosition;
export 'src/chart/bar/bar_lane_renderer_config.dart' show BarLaneRendererConfig;
export 'src/chart/bar/bar_renderer.dart'
show BarRenderer, ImmutableBarRendererElement;
export 'src/chart/bar/bar_renderer_config.dart'
show
BarRendererConfig,
CornerStrategy,
ConstCornerStrategy,
NoCornerStrategy;
export 'src/chart/bar/bar_renderer_decorator.dart' show BarRendererDecorator;
export 'src/chart/bar/bar_target_line_renderer.dart' show BarTargetLineRenderer;
export 'src/chart/bar/bar_target_line_renderer_config.dart'
show BarTargetLineRendererConfig;
export 'src/chart/bar/base_bar_renderer_config.dart'
show BarGroupingType, BaseBarRendererConfig;
export 'src/chart/cartesian/axis/axis.dart'
show
domainAxisKey,
measureAxisIdKey,
measureAxisKey,
Axis,
NumericAxis,
OrdinalAxis,
OrdinalViewport;
export 'src/chart/cartesian/axis/numeric_extents.dart' show NumericExtents;
export 'src/chart/cartesian/axis/draw_strategy/gridline_draw_strategy.dart'
show GridlineRendererSpec;
export 'src/chart/cartesian/axis/draw_strategy/none_draw_strategy.dart'
show NoneRenderSpec;
export 'src/chart/cartesian/axis/draw_strategy/small_tick_draw_strategy.dart'
show SmallTickRendererSpec;
export 'src/chart/cartesian/axis/tick_formatter.dart'
show SimpleTickFormatterBase, TickFormatter;
export 'src/chart/cartesian/axis/spec/axis_spec.dart'
show
AxisSpec,
LineStyleSpec,
RenderSpec,
TextStyleSpec,
TickLabelAnchor,
TickLabelJustification,
TickFormatterSpec;
export 'src/chart/cartesian/axis/spec/bucketing_axis_spec.dart'
show BucketingAxisSpec, BucketingNumericTickProviderSpec;
export 'src/chart/cartesian/axis/spec/date_time_axis_spec.dart'
show
DateTimeAxisSpec,
DayTickProviderSpec,
AutoDateTimeTickFormatterSpec,
AutoDateTimeTickProviderSpec,
DateTimeEndPointsTickProviderSpec,
DateTimeTickFormatterSpec,
DateTimeTickProviderSpec,
TimeFormatterSpec,
StaticDateTimeTickProviderSpec;
export 'src/chart/cartesian/axis/spec/end_points_time_axis_spec.dart'
show EndPointsTimeAxisSpec;
export 'src/chart/cartesian/axis/spec/numeric_axis_spec.dart'
show
NumericAxisSpec,
NumericEndPointsTickProviderSpec,
NumericTickProviderSpec,
NumericTickFormatterSpec,
BasicNumericTickFormatterSpec,
BasicNumericTickProviderSpec,
StaticNumericTickProviderSpec;
export 'src/chart/cartesian/axis/spec/ordinal_axis_spec.dart'
show
BasicOrdinalTickProviderSpec,
BasicOrdinalTickFormatterSpec,
OrdinalAxisSpec,
OrdinalTickFormatterSpec,
OrdinalTickProviderSpec,
StaticOrdinalTickProviderSpec;
export 'src/chart/cartesian/axis/spec/percent_axis_spec.dart'
show PercentAxisSpec;
export 'src/chart/cartesian/axis/time/date_time_extents.dart'
show DateTimeExtents;
export 'src/chart/cartesian/axis/time/date_time_tick_formatter.dart'
show DateTimeTickFormatter;
export 'src/chart/cartesian/axis/spec/tick_spec.dart' show TickSpec;
export 'src/chart/cartesian/cartesian_chart.dart'
show CartesianChart, NumericCartesianChart, OrdinalCartesianChart;
export 'src/chart/cartesian/cartesian_renderer.dart' show BaseCartesianRenderer;
export 'src/chart/common/base_chart.dart' show BaseChart, LifecycleListener;
export 'src/chart/common/behavior/a11y/a11y_explore_behavior.dart'
show ExploreModeTrigger;
export 'src/chart/common/behavior/a11y/a11y_node.dart' show A11yNode;
export 'src/chart/common/behavior/a11y/domain_a11y_explore_behavior.dart'
show DomainA11yExploreBehavior, VocalizationCallback;
export 'src/chart/common/behavior/chart_behavior.dart'
show
BehaviorPosition,
ChartBehavior,
InsideJustification,
OutsideJustification;
export 'src/chart/common/behavior/calculation/percent_injector.dart'
show PercentInjector, PercentInjectorTotalType;
export 'src/chart/common/behavior/domain_highlighter.dart'
show DomainHighlighter;
export 'src/chart/common/behavior/initial_selection.dart' show InitialSelection;
export 'src/chart/common/behavior/legend/legend.dart'
show Legend, LegendCellPadding, LegendState, LegendTapHandling;
export 'src/chart/common/behavior/legend/legend_entry.dart' show LegendEntry;
export 'src/chart/common/behavior/legend/legend_entry_generator.dart'
show LegendEntryGenerator, LegendDefaultMeasure;
export 'src/chart/common/behavior/legend/datum_legend.dart' show DatumLegend;
export 'src/chart/common/behavior/legend/series_legend.dart' show SeriesLegend;
export 'src/chart/common/behavior/line_point_highlighter.dart'
show LinePointHighlighter, LinePointHighlighterFollowLineType;
export 'src/chart/common/behavior/range_annotation.dart'
show
AnnotationLabelAnchor,
AnnotationLabelDirection,
AnnotationLabelPosition,
AnnotationSegment,
LineAnnotationSegment,
RangeAnnotation,
RangeAnnotationAxisType,
RangeAnnotationSegment;
export 'src/chart/common/behavior/sliding_viewport.dart' show SlidingViewport;
export 'src/chart/common/behavior/chart_title/chart_title.dart'
show ChartTitle, ChartTitleDirection;
export 'src/chart/common/behavior/selection/lock_selection.dart'
show LockSelection;
export 'src/chart/common/behavior/selection/select_nearest.dart'
show SelectNearest;
export 'src/chart/common/behavior/selection/selection_trigger.dart'
show SelectionTrigger;
export 'src/chart/common/behavior/slider/slider.dart'
show
Slider,
SliderHandlePosition,
SliderListenerCallback,
SliderListenerDragState,
SliderStyle;
export 'src/chart/common/behavior/zoom/initial_hint_behavior.dart'
show InitialHintBehavior;
export 'src/chart/common/behavior/zoom/pan_and_zoom_behavior.dart'
show PanAndZoomBehavior;
export 'src/chart/common/behavior/zoom/pan_behavior.dart'
show PanBehavior, PanningCompletedCallback;
export 'src/chart/common/behavior/zoom/panning_tick_provider.dart'
show PanningTickProviderMode;
export 'src/chart/common/canvas_shapes.dart'
show CanvasBarStack, CanvasPie, CanvasPieSlice, CanvasRect;
export 'src/chart/common/chart_canvas.dart' show ChartCanvas, FillPatternType;
export 'src/chart/common/chart_context.dart' show ChartContext;
export 'src/chart/common/datum_details.dart'
show DatumDetails, DomainFormatter, MeasureFormatter;
export 'src/chart/common/processed_series.dart'
show ImmutableSeries, MutableSeries;
export 'src/chart/common/series_datum.dart' show SeriesDatum, SeriesDatumConfig;
export 'src/chart/common/selection_model/selection_model.dart'
show SelectionModel, SelectionModelType, SelectionModelListener;
export 'src/chart/common/series_renderer.dart'
show rendererIdKey, rendererKey, SeriesRenderer;
export 'src/chart/common/series_renderer_config.dart'
show RendererAttributeKey, SeriesRendererConfig;
export 'src/chart/layout/layout_config.dart' show LayoutConfig, MarginSpec;
export 'src/chart/layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
ViewMargin,
ViewMeasuredSizes;
export 'src/chart/line/line_chart.dart' show LineChart;
export 'src/chart/line/line_renderer.dart' show LineRenderer;
export 'src/chart/line/line_renderer_config.dart' show LineRendererConfig;
export 'src/chart/pie/arc_label_decorator.dart'
show ArcLabelDecorator, ArcLabelLeaderLineStyleSpec, ArcLabelPosition;
export 'src/chart/pie/arc_renderer.dart' show ArcRenderer;
export 'src/chart/pie/arc_renderer_config.dart' show ArcRendererConfig;
export 'src/chart/pie/pie_chart.dart' show PieChart;
export 'src/chart/scatter_plot/comparison_points_decorator.dart'
show ComparisonPointsDecorator;
export 'src/chart/scatter_plot/point_renderer.dart'
show
boundsLineRadiusPxKey,
boundsLineRadiusPxFnKey,
pointSymbolRendererFnKey,
pointSymbolRendererIdKey,
PointRenderer;
export 'src/chart/scatter_plot/point_renderer_config.dart'
show PointRendererConfig;
export 'src/chart/scatter_plot/point_renderer_decorator.dart'
show PointRendererDecorator;
export 'src/chart/scatter_plot/scatter_plot_chart.dart' show ScatterPlotChart;
export 'src/chart/scatter_plot/symbol_annotation_renderer.dart'
show SymbolAnnotationRenderer;
export 'src/chart/scatter_plot/symbol_annotation_renderer_config.dart'
show SymbolAnnotationRendererConfig;
export 'src/chart/time_series/time_series_chart.dart' show TimeSeriesChart;
export 'src/common/color.dart' show Color;
export 'src/common/date_time_factory.dart'
show DateTimeFactory, LocalDateTimeFactory, UTCDateTimeFactory;
export 'src/common/gesture_listener.dart' show GestureListener;
export 'src/common/graphics_factory.dart' show GraphicsFactory;
export 'src/common/line_style.dart' show LineStyle;
export 'src/common/material_palette.dart' show MaterialPalette;
export 'src/common/performance.dart' show Performance;
export 'src/common/proxy_gesture_listener.dart' show ProxyGestureListener;
export 'src/common/rtl_spec.dart' show AxisDirection, RTLSpec;
export 'src/common/style/material_style.dart' show MaterialStyle;
export 'src/common/style/style_factory.dart' show StyleFactory;
export 'src/common/symbol_renderer.dart'
show
CircleSymbolRenderer,
CylinderSymbolRenderer,
LineSymbolRenderer,
PointSymbolRenderer,
RectSymbolRenderer,
RoundedRectSymbolRenderer,
SymbolRenderer;
export 'src/common/text_element.dart'
show TextElement, TextDirection, MaxWidthStrategy;
export 'src/common/text_measurement.dart' show TextMeasurement;
export 'src/common/text_style.dart' show TextStyle;
export 'src/data/series.dart' show Series, TypedAccessorFn;

View File

@@ -0,0 +1,43 @@
// 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:collection' show LinkedHashMap;
import '../bar/bar_renderer.dart' show BarRenderer;
import '../cartesian/axis/axis.dart' show NumericAxis;
import '../cartesian/cartesian_chart.dart' show OrdinalCartesianChart;
import '../common/series_renderer.dart' show SeriesRenderer;
import '../layout/layout_config.dart' show LayoutConfig;
class BarChart extends OrdinalCartesianChart {
BarChart(
{bool vertical,
LayoutConfig layoutConfig,
NumericAxis primaryMeasureAxis,
NumericAxis secondaryMeasureAxis,
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
: super(
vertical: vertical,
layoutConfig: layoutConfig,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes);
@override
SeriesRenderer<String> makeDefaultRenderer() {
return new BarRenderer<String>()
..rendererId = SeriesRenderer.defaultRendererId;
}
}

View File

@@ -0,0 +1,234 @@
// 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:meta/meta.dart' show required;
import '../../common/color.dart' show Color;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/text_element.dart' show TextDirection;
import '../../common/text_style.dart' show TextStyle;
import '../../data/series.dart' show AccessorFn;
import '../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../common/chart_canvas.dart' show ChartCanvas;
import 'bar_renderer.dart' show ImmutableBarRendererElement;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
class BarLabelDecorator<D> extends BarRendererDecorator<D> {
// Default configuration
static const _defaultLabelPosition = BarLabelPosition.auto;
static const _defaultLabelPadding = 5;
static const _defaultLabelAnchor = BarLabelAnchor.start;
static final _defaultInsideLabelStyle =
new TextStyleSpec(fontSize: 12, color: Color.white);
static final _defaultOutsideLabelStyle =
new TextStyleSpec(fontSize: 12, color: Color.black);
/// Configures [TextStyleSpec] for labels placed inside the bars.
final TextStyleSpec insideLabelStyleSpec;
/// Configures [TextStyleSpec] for labels placed outside the bars.
final TextStyleSpec outsideLabelStyleSpec;
/// Configures where to place the label relative to the bars.
final BarLabelPosition labelPosition;
/// For labels drawn inside the bar, configures label anchor position.
final BarLabelAnchor labelAnchor;
/// Space before and after the label text.
final int labelPadding;
BarLabelDecorator(
{TextStyleSpec insideLabelStyleSpec,
TextStyleSpec outsideLabelStyleSpec,
this.labelPosition = _defaultLabelPosition,
this.labelPadding = _defaultLabelPadding,
this.labelAnchor = _defaultLabelAnchor})
: insideLabelStyleSpec = insideLabelStyleSpec ?? _defaultInsideLabelStyle,
outsideLabelStyleSpec =
outsideLabelStyleSpec ?? _defaultOutsideLabelStyle;
@override
void decorate(Iterable<ImmutableBarRendererElement<D>> barElements,
ChartCanvas canvas, GraphicsFactory graphicsFactory,
{@required Rectangle drawBounds,
@required double animationPercent,
@required bool renderingVertically,
bool rtl = false}) {
// TODO: Decorator not yet available for vertical charts.
assert(renderingVertically == false);
// Only decorate the bars when animation is at 100%.
if (animationPercent != 1.0) {
return;
}
// Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
// The [GraphicsFactory] is needed so it can't be created earlier.
final insideLabelStyle =
_getTextStyle(graphicsFactory, insideLabelStyleSpec);
final outsideLabelStyle =
_getTextStyle(graphicsFactory, outsideLabelStyleSpec);
for (var element in barElements) {
final labelFn = element.series.labelAccessorFn;
final datumIndex = element.index;
final label = (labelFn != null) ? labelFn(datumIndex) : null;
// If there are custom styles, use that instead of the default or the
// style defined for the entire decorator.
final datumInsideLabelStyle = _getDatumStyle(
element.series.insideLabelStyleAccessorFn,
datumIndex,
graphicsFactory,
defaultStyle: insideLabelStyle);
final datumOutsideLabelStyle = _getDatumStyle(
element.series.outsideLabelStyleAccessorFn,
datumIndex,
graphicsFactory,
defaultStyle: outsideLabelStyle);
// Skip calculation and drawing for this element if no label.
if (label == null || label.isEmpty) {
continue;
}
final bounds = element.bounds;
// Get space available inside and outside the bar.
final totalPadding = labelPadding * 2;
final insideBarWidth = bounds.width - totalPadding;
final outsideBarWidth = drawBounds.width - bounds.width - totalPadding;
final labelElement = graphicsFactory.createTextElement(label);
var calculatedLabelPosition = labelPosition;
if (calculatedLabelPosition == BarLabelPosition.auto) {
// For auto, first try to fit the text inside the bar.
labelElement.textStyle = datumInsideLabelStyle;
// A label fits if the space inside the bar is >= outside bar or if the
// length of the text fits and the space. This is because if the bar has
// more space than the outside, it makes more sense to place the label
// inside the bar, even if the entire label does not fit.
calculatedLabelPosition = (insideBarWidth >= outsideBarWidth ||
labelElement.measurement.horizontalSliceWidth < insideBarWidth)
? BarLabelPosition.inside
: BarLabelPosition.outside;
}
// Set the max width and text style.
if (calculatedLabelPosition == BarLabelPosition.inside) {
labelElement.textStyle = datumInsideLabelStyle;
labelElement.maxWidth = insideBarWidth;
} else {
// calculatedLabelPosition == LabelPosition.outside
labelElement.textStyle = datumOutsideLabelStyle;
labelElement.maxWidth = outsideBarWidth;
}
// Only calculate and draw label if there's actually space for the label.
if (labelElement.maxWidth > 0) {
// Calculate the start position of label based on [labelAnchor].
int labelX;
if (calculatedLabelPosition == BarLabelPosition.inside) {
switch (labelAnchor) {
case BarLabelAnchor.middle:
labelX = (bounds.left +
bounds.width / 2 -
labelElement.measurement.horizontalSliceWidth / 2)
.round();
labelElement.textDirection =
rtl ? TextDirection.rtl : TextDirection.ltr;
break;
case BarLabelAnchor.end:
case BarLabelAnchor.start:
final alignLeft = rtl
? (labelAnchor == BarLabelAnchor.end)
: (labelAnchor == BarLabelAnchor.start);
if (alignLeft) {
labelX = bounds.left + labelPadding;
labelElement.textDirection = TextDirection.ltr;
} else {
labelX = bounds.right - labelPadding;
labelElement.textDirection = TextDirection.rtl;
}
break;
}
} else {
// calculatedLabelPosition == LabelPosition.outside
labelX = bounds.right + labelPadding;
labelElement.textDirection = TextDirection.ltr;
}
// Center the label inside the bar.
final labelY = (bounds.top +
(bounds.bottom - bounds.top) / 2 -
labelElement.measurement.verticalSliceWidth / 2)
.round();
canvas.drawText(labelElement, labelX, labelY);
}
}
}
// Helper function that converts [TextStyleSpec] to [TextStyle].
TextStyle _getTextStyle(
GraphicsFactory graphicsFactory, TextStyleSpec labelSpec) {
return graphicsFactory.createTextPaint()
..color = labelSpec?.color ?? Color.black
..fontFamily = labelSpec?.fontFamily
..fontSize = labelSpec?.fontSize ?? 12;
}
/// Helper function to get datum specific style
TextStyle _getDatumStyle(AccessorFn<TextStyleSpec> labelFn, int datumIndex,
GraphicsFactory graphicsFactory,
{TextStyle defaultStyle}) {
final styleSpec = (labelFn != null) ? labelFn(datumIndex) : null;
return (styleSpec != null)
? _getTextStyle(graphicsFactory, styleSpec)
: defaultStyle;
}
}
/// Configures where to place the label relative to the bars.
enum BarLabelPosition {
/// Automatically try to place the label inside the bar first and place it on
/// the outside of the space available outside the bar is greater than space
/// available inside the bar.
auto,
/// Always place label on the outside.
outside,
/// Always place label on the inside.
inside,
}
/// Configures where to anchor the label for labels drawn inside the bars.
enum BarLabelAnchor {
/// Anchor to the measure start.
start,
/// Anchor to the middle of the measure range.
middle,
/// Anchor to the measure end.
end,
}

View File

@@ -0,0 +1,369 @@
// 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:collection' show LinkedHashMap;
import '../../data/series.dart' show AttributeKey;
import '../cartesian/axis/axis.dart'
show ImmutableAxis, domainAxisKey, measureAxisKey;
import '../cartesian/cartesian_chart.dart' show CartesianChart;
import '../common/chart_canvas.dart' show ChartCanvas;
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
import 'bar_lane_renderer_config.dart' show BarLaneRendererConfig;
import 'bar_renderer.dart' show AnimatedBar, BarRenderer, BarRendererElement;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
import 'base_bar_renderer.dart'
show
barGroupCountKey,
barGroupIndexKey,
barGroupWeightKey,
previousBarGroupWeightKey,
stackKeyKey;
import 'base_bar_renderer_element.dart' show BaseBarRendererElement;
/// Key for storing a list of all domain values that exist in the series data.
///
/// In grouped stacked mode, this list will contain a combination of domain
/// value and series category.
const domainValuesKey = const AttributeKey<Set>('BarLaneRenderer.domainValues');
/// Renders series data as a series of bars with lanes.
///
/// Every stack of bars will have a swim lane rendered underneath the series
/// data, in a gray color by default. The swim lane occupies the same width as
/// the bar elements, and will be completely covered up if the bar stack happens
/// to take up the entire measure domain range.
///
/// If every bar that shares a domain value has a null measure value, then the
/// swim lanes may optionally be merged together into one wide lane that covers
/// the full domain range band width.
class BarLaneRenderer<D> extends BarRenderer<D> {
final BarRendererDecorator barRendererDecorator;
/// Store a map of domain+barGroupIndex+category index to bar lanes in a
/// stack.
///
/// This map is used to render all the bars in a stack together, to account
/// for rendering effects that need to take the full stack into account (e.g.
/// corner rounding).
///
/// [LinkedHashMap] is used to render the bars on the canvas in the same order
/// as the data was given to the chart. For the case where both grouping and
/// stacking are disabled, this means that bars for data later in the series
/// will be drawn "on top of" bars earlier in the series.
final _barLaneStackMap = new LinkedHashMap<String, List<AnimatedBar<D>>>();
/// Store a map of flags to track whether all measure values for a given
/// domain value are null, for every series on the chart.
final _allMeasuresForDomainNullMap = new LinkedHashMap<D, bool>();
factory BarLaneRenderer({BarLaneRendererConfig config, String rendererId}) {
rendererId ??= 'bar';
config ??= new BarLaneRendererConfig();
return new BarLaneRenderer._internal(
config: config, rendererId: rendererId);
}
BarLaneRenderer._internal({BarLaneRendererConfig config, String rendererId})
: barRendererDecorator = config.barRendererDecorator,
super.internal(config: config, rendererId: rendererId);
@override
void preprocessSeries(List<MutableSeries<D>> seriesList) {
super.preprocessSeries(seriesList);
_allMeasuresForDomainNullMap.clear();
seriesList.forEach((MutableSeries<D> series) {
final domainFn = series.domainFn;
final measureFn = series.rawMeasureFn;
final domainValues = new Set<D>();
for (var barIndex = 0; barIndex < series.data.length; barIndex++) {
final domain = domainFn(barIndex);
final measure = measureFn(barIndex);
domainValues.add(domain);
// Update the "all measure null" tracking for bars that have the
// current domain value.
if ((config as BarLaneRendererConfig).mergeEmptyLanes) {
final allNull = _allMeasuresForDomainNullMap[domain];
final isNull = measure == null;
_allMeasuresForDomainNullMap[domain] =
allNull != null ? allNull && isNull : isNull;
}
}
series.setAttr(domainValuesKey, domainValues);
});
}
@override
void update(List<ImmutableSeries<D>> seriesList, bool isAnimatingThisDraw) {
super.update(seriesList, isAnimatingThisDraw);
// Add gray bars to render under every bar stack.
seriesList.forEach((ImmutableSeries<D> series) {
Set<D> domainValues = series.getAttr(domainValuesKey) as Set<D>;
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final seriesStackKey = series.getAttr(stackKeyKey);
final barGroupCount = series.getAttr(barGroupCountKey);
final barGroupIndex = series.getAttr(barGroupIndexKey);
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
final barGroupWeight = series.getAttr(barGroupWeightKey);
final measureAxisPosition = measureAxis.getLocation(0.0);
final maxMeasureValue = _getMaxMeasureValue(measureAxis);
// Create a fake series for [BarLabelDecorator] to use when looking up the
// index of each datum.
final laneSeries = new MutableSeries<D>.clone(seriesList[0]);
laneSeries.data = [];
// Don't render any labels on the swim lanes.
laneSeries.labelAccessorFn = (int index) => '';
var laneSeriesIndex = 0;
domainValues.forEach((D domainValue) {
// Skip adding any background bars if they will be covered up by the
// domain-spanning null bar.
if (_allMeasuresForDomainNullMap[domainValue] == true) {
return;
}
// Add a fake datum to the series for [BarLabelDecorator].
final datum = {'index': laneSeriesIndex};
laneSeries.data.add(datum);
// Each bar should be stored in barStackMap in a structure that mirrors
// the visual rendering of the bars. Thus, they should be grouped by
// domain value, series category (by way of the stack keys that were
// generated for each series in the preprocess step), and bar group
// index to account for all combinations of grouping and stacking.
final barStackMapKey = domainValue.toString() +
'__' +
seriesStackKey +
'__' +
barGroupIndex.toString();
final barKey = barStackMapKey + '0';
final barStackList = _barLaneStackMap.putIfAbsent(
barStackMapKey, () => <AnimatedBar<D>>[]);
// If we already have an AnimatingBar for that index, use it.
var animatingBar = barStackList.firstWhere(
(AnimatedBar bar) => bar.key == barKey,
orElse: () => null);
// If we don't have any existing bar element, create a new bar and have
// it animate in from the domain axis.
if (animatingBar == null) {
animatingBar = makeAnimatedBar(
key: barKey,
series: laneSeries,
datum: datum,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: (config as BarLaneRendererConfig).backgroundBarColor,
details: new BarRendererElement<D>(),
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
measureValue: maxMeasureValue,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: config.strokeWidthPx,
measureIsNull: false,
measureIsNegative: false);
barStackList.add(animatingBar);
} else {
animatingBar
..datum = datum
..series = laneSeries
..domainValue = domainValue;
}
// Get the barElement we are going to setup.
// Optimization to prevent allocation in non-animating case.
BaseBarRendererElement barElement = makeBarRendererElement(
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: (config as BarLaneRendererConfig).backgroundBarColor,
details: new BarRendererElement<D>(),
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
measureValue: maxMeasureValue,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: config.strokeWidthPx,
measureIsNull: false,
measureIsNegative: false);
animatingBar.setNewTarget(barElement);
laneSeriesIndex++;
});
});
// Add domain-spanning bars to render when every measure value for every
// datum of a given domain is null.
if ((config as BarLaneRendererConfig).mergeEmptyLanes) {
// Use the axes from the first series.
final domainAxis =
seriesList[0].getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis =
seriesList[0].getAttr(measureAxisKey) as ImmutableAxis<num>;
final measureAxisPosition = measureAxis.getLocation(0.0);
final maxMeasureValue = _getMaxMeasureValue(measureAxis);
final barGroupIndex = 0;
final previousBarGroupWeight = 0.0;
final barGroupWeight = 1.0;
final barGroupCount = 1;
// Create a fake series for [BarLabelDecorator] to use when looking up the
// index of each datum. We don't care about any other series values for
// the merged lanes, so just clone the first series.
final mergedSeries = new MutableSeries<D>.clone(seriesList[0]);
mergedSeries.data = [];
// Add a label accessor that returns the empty lane label.
mergedSeries.labelAccessorFn =
(int index) => (config as BarLaneRendererConfig).emptyLaneLabel;
var mergedSeriesIndex = 0;
_allMeasuresForDomainNullMap.forEach((D domainValue, bool allNull) {
if (allNull) {
// Add a fake datum to the series for [BarLabelDecorator].
final datum = {'index': mergedSeriesIndex};
mergedSeries.data.add(datum);
final barStackMapKey = domainValue.toString() + '__allNull__';
final barKey = barStackMapKey + '0';
final barStackList = _barLaneStackMap.putIfAbsent(
barStackMapKey, () => <AnimatedBar<D>>[]);
// If we already have an AnimatingBar for that index, use it.
var animatingBar = barStackList.firstWhere(
(AnimatedBar bar) => bar.key == barKey,
orElse: () => null);
// If we don't have any existing bar element, create a new bar and have
// it animate in from the domain axis.
if (animatingBar == null) {
animatingBar = makeAnimatedBar(
key: barKey,
series: mergedSeries,
datum: datum,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: (config as BarLaneRendererConfig).backgroundBarColor,
details: new BarRendererElement<D>(),
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
measureValue: maxMeasureValue,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: config.strokeWidthPx,
measureIsNull: false,
measureIsNegative: false);
barStackList.add(animatingBar);
} else {
animatingBar
..datum = datum
..series = mergedSeries
..domainValue = domainValue;
}
// Get the barElement we are going to setup.
// Optimization to prevent allocation in non-animating case.
BaseBarRendererElement barElement = makeBarRendererElement(
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: (config as BarLaneRendererConfig).backgroundBarColor,
details: new BarRendererElement<D>(),
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
measureValue: maxMeasureValue,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: config.strokeWidthPx,
measureIsNull: false,
measureIsNegative: false);
animatingBar.setNewTarget(barElement);
mergedSeriesIndex++;
}
});
}
}
/// Gets the maximum measure value that will fit in the draw area.
num _getMaxMeasureValue(ImmutableAxis<num> measureAxis) {
final pos = (chart as CartesianChart).vertical
? chart.drawAreaBounds.top
: isRtl ? chart.drawAreaBounds.left : chart.drawAreaBounds.right;
return measureAxis.getDomain(pos.toDouble());
}
/// Paints the current bar data on the canvas.
@override
void paint(ChartCanvas canvas, double animationPercent) {
_barLaneStackMap.forEach((String stackKey, List<AnimatedBar<D>> barStack) {
// Turn this into a list so that the getCurrentBar isn't called more than
// once for each animationPercent if the barElements are iterated more
// than once.
List<BarRendererElement<D>> barElements = barStack
.map((AnimatedBar<D> animatingBar) =>
animatingBar.getCurrentBar(animationPercent))
.toList();
paintBar(canvas, animationPercent, barElements);
});
super.paint(canvas, animationPercent);
}
}

View File

@@ -0,0 +1,104 @@
// 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 '../../common/color.dart' show Color;
import '../../common/style/style_factory.dart' show StyleFactory;
import '../../common/symbol_renderer.dart';
import '../common/chart_canvas.dart' show FillPatternType;
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
import 'bar_label_decorator.dart' show BarLabelDecorator;
import 'bar_lane_renderer.dart' show BarLaneRenderer;
import 'bar_renderer_config.dart' show BarRendererConfig, CornerStrategy;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
import 'base_bar_renderer_config.dart' show BarGroupingType;
/// Configuration for a bar lane renderer.
class BarLaneRendererConfig extends BarRendererConfig<String> {
/// The color of background bars.
final Color backgroundBarColor;
/// Label text to draw on a merged empty lane.
///
/// This will only be drawn if all of the measures for a domain are null, and
/// [mergeEmptyLanes] is enabled.
///
/// The renderer must be configured with a [BarLabelDecorator] for this label
/// to be drawn.
final String emptyLaneLabel;
/// Whether or not all lanes for a given domain value should be merged into
/// one wide lane if all measure values for said domain are null.
final bool mergeEmptyLanes;
BarLaneRendererConfig({
String customRendererId,
CornerStrategy cornerStrategy,
this.emptyLaneLabel = 'No data',
FillPatternType fillPattern,
BarGroupingType groupingType,
int layoutPaintOrder = LayoutViewPaintOrder.bar,
this.mergeEmptyLanes = false,
int minBarLengthPx = 0,
double stackHorizontalSeparator,
double strokeWidthPx = 0.0,
BarRendererDecorator barRendererDecorator,
SymbolRenderer symbolRenderer,
Color backgroundBarColor,
List<int> weightPattern,
}) : backgroundBarColor =
backgroundBarColor ?? StyleFactory.style.noDataColor,
super(
barRendererDecorator: barRendererDecorator,
cornerStrategy: cornerStrategy,
customRendererId: customRendererId,
groupingType: groupingType ?? BarGroupingType.grouped,
layoutPaintOrder: layoutPaintOrder,
minBarLengthPx: minBarLengthPx,
fillPattern: fillPattern,
stackHorizontalSeparator: stackHorizontalSeparator,
strokeWidthPx: strokeWidthPx,
symbolRenderer: symbolRenderer,
weightPattern: weightPattern,
);
@override
BarLaneRenderer<String> build() {
return new BarLaneRenderer<String>(
config: this, rendererId: customRendererId);
}
@override
bool operator ==(other) {
if (identical(this, other)) {
return true;
}
if (!(other is BarLaneRendererConfig)) {
return false;
}
return other.backgroundBarColor == backgroundBarColor &&
other.emptyLaneLabel == emptyLaneLabel &&
other.mergeEmptyLanes == mergeEmptyLanes &&
super == (other);
}
@override
int get hashCode {
var hash = super.hashCode;
hash = hash * 31 + (backgroundBarColor?.hashCode ?? 0);
hash = hash * 31 + (emptyLaneLabel?.hashCode ?? 0);
hash = hash * 31 + (mergeEmptyLanes?.hashCode ?? 0);
return hash;
}
}

View File

@@ -0,0 +1,556 @@
// 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 max, min, Point, Rectangle;
import 'package:meta/meta.dart' show protected, required;
import '../../common/color.dart' show Color;
import '../cartesian/axis/axis.dart'
show ImmutableAxis, domainAxisKey, measureAxisKey;
import '../common/canvas_shapes.dart' show CanvasBarStack, CanvasRect;
import '../common/chart_canvas.dart' show ChartCanvas, FillPatternType;
import '../common/datum_details.dart' show DatumDetails;
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
import '../common/series_datum.dart' show SeriesDatum;
import 'bar_renderer_config.dart' show BarRendererConfig, CornerStrategy;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
import 'base_bar_renderer.dart'
show
BaseBarRenderer,
barGroupCountKey,
barGroupIndexKey,
previousBarGroupWeightKey,
barGroupWeightKey;
import 'base_bar_renderer_element.dart'
show BaseAnimatedBar, BaseBarRendererElement;
/// Renders series data as a series of bars.
class BarRenderer<D>
extends BaseBarRenderer<D, BarRendererElement<D>, AnimatedBar<D>> {
/// If we are grouped, use this spacing between the bars in a group.
final _barGroupInnerPadding = 2;
/// The padding between bar stacks.
///
/// The padding comes out of the bottom of the bar.
final _stackedBarPadding = 1;
final BarRendererDecorator barRendererDecorator;
factory BarRenderer({BarRendererConfig config, String rendererId}) {
rendererId ??= 'bar';
config ??= new BarRendererConfig();
return new BarRenderer.internal(config: config, rendererId: rendererId);
}
/// This constructor is protected because it is used by child classes, which
/// cannot call the factory in their own constructors.
@protected
BarRenderer.internal({BarRendererConfig config, String rendererId})
: barRendererDecorator = config.barRendererDecorator,
super(
config: config,
rendererId: rendererId,
layoutPaintOrder: config.layoutPaintOrder);
@override
void configureSeries(List<MutableSeries<D>> seriesList) {
assignMissingColors(getOrderedSeriesList(seriesList),
emptyCategoryUsesSinglePalette: true);
}
DatumDetails<D> addPositionToDetailsForSeriesDatum(
DatumDetails<D> details, SeriesDatum<D> seriesDatum) {
final series = details.series;
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final barGroupIndex = series.getAttr(barGroupIndexKey);
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
final barGroupWeight = series.getAttr(barGroupWeightKey);
final numBarGroups = series.getAttr(barGroupCountKey);
final bounds = _getBarBounds(
details.domain,
domainAxis,
domainAxis.rangeBand.round(),
details.measure,
details.measureOffset,
measureAxis,
barGroupIndex,
previousBarGroupWeight,
barGroupWeight,
numBarGroups);
Point<double> chartPosition;
if (renderingVertically) {
chartPosition = new Point<double>(
(bounds.left + (bounds.width / 2)).toDouble(), bounds.top.toDouble());
} else {
chartPosition = new Point<double>(
isRtl ? bounds.left.toDouble() : bounds.right.toDouble(),
(bounds.top + (bounds.height / 2)).toDouble());
}
return new DatumDetails.from(details, chartPosition: chartPosition);
}
@override
BarRendererElement<D> getBaseDetails(dynamic datum, int index) {
return new BarRendererElement<D>();
}
CornerStrategy get cornerStrategy {
return (config as BarRendererConfig).cornerStrategy;
}
/// Generates an [AnimatedBar] to represent the previous and current state
/// of one bar on the chart.
@override
AnimatedBar<D> makeAnimatedBar(
{String key,
ImmutableSeries<D> series,
List<int> dashPattern,
dynamic datum,
Color color,
BarRendererElement<D> details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups,
bool measureIsNull,
bool measureIsNegative}) {
return new AnimatedBar<D>(
key: key, datum: datum, series: series, domainValue: domainValue)
..setNewTarget(makeBarRendererElement(
color: color,
dashPattern: dashPattern,
details: details,
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainWidth,
measureValue: measureValue,
measureOffsetValue: measureOffsetValue,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
fillColor: fillColor,
fillPattern: fillPattern,
strokeWidthPx: strokeWidthPx,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
numBarGroups: numBarGroups,
measureIsNull: measureIsNull,
measureIsNegative: measureIsNegative));
}
/// Generates a [BarRendererElement] to represent the rendering data for one
/// bar on the chart.
@override
BarRendererElement<D> makeBarRendererElement(
{Color color,
List<int> dashPattern,
BarRendererElement<D> details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups,
bool measureIsNull,
bool measureIsNegative}) {
return new BarRendererElement<D>()
..color = color
..dashPattern = dashPattern
..fillColor = fillColor
..fillPattern = fillPattern
..measureAxisPosition = measureAxisPosition
..roundPx = details.roundPx
..strokeWidthPx = strokeWidthPx
..measureIsNull = measureIsNull
..measureIsNegative = measureIsNegative
..bounds = _getBarBounds(
domainValue,
domainAxis,
domainWidth,
measureValue,
measureOffsetValue,
measureAxis,
barGroupIndex,
previousBarGroupWeight,
barGroupWeight,
numBarGroups);
}
@override
void paintBar(ChartCanvas canvas, double animationPercent,
Iterable<BarRendererElement<D>> barElements) {
final bars = <CanvasRect>[];
// When adjusting bars for stacked bar padding, do not modify the first bar
// if rendering vertically and do not modify the last bar if rendering
// horizontally.
final unmodifiedBar =
renderingVertically ? barElements.first : barElements.last;
// Find the max bar width from each segment to calculate corner radius.
int maxBarWidth = 0;
var measureIsNegative = false;
for (var bar in barElements) {
var bounds = bar.bounds;
measureIsNegative = measureIsNegative || bar.measureIsNegative;
if (bar != unmodifiedBar) {
bounds = renderingVertically
? new Rectangle<int>(
bar.bounds.left,
max(
0,
bar.bounds.top +
(measureIsNegative ? _stackedBarPadding : 0)),
bar.bounds.width,
max(0, bar.bounds.height - _stackedBarPadding),
)
: new Rectangle<int>(
max(
0,
bar.bounds.left +
(measureIsNegative ? _stackedBarPadding : 0)),
bar.bounds.top,
max(0, bar.bounds.width - _stackedBarPadding),
bar.bounds.height,
);
}
bars.add(new CanvasRect(bounds,
dashPattern: bar.dashPattern,
fill: bar.fillColor,
pattern: bar.fillPattern,
stroke: bar.color,
strokeWidthPx: bar.strokeWidthPx));
maxBarWidth = max(
maxBarWidth, (renderingVertically ? bounds.width : bounds.height));
}
bool roundTopLeft;
bool roundTopRight;
bool roundBottomLeft;
bool roundBottomRight;
if (measureIsNegative) {
// Negative bars should be rounded towards the negative axis direction.
// In vertical mode, this is the bottom. In horizontal mode, this is the
// left side of the chart for LTR, or the right side for RTL.
roundTopLeft = !renderingVertically && !isRtl ? true : false;
roundTopRight = !renderingVertically && isRtl ? true : false;
roundBottomLeft = renderingVertically || !isRtl ? true : false;
roundBottomRight = renderingVertically || isRtl ? true : false;
} else {
// Positive bars should be rounded towards the positive axis direction.
// In vertical mode, this is the top. In horizontal mode, this is the
// right side of the chart for LTR, or the left side for RTL.
roundTopLeft = renderingVertically || isRtl ? true : false;
roundTopRight = isRtl ? false : true;
roundBottomLeft = isRtl ? true : false;
roundBottomRight = renderingVertically || isRtl ? false : true;
}
final barStack = new CanvasBarStack(
bars,
radius: cornerStrategy.getRadius(maxBarWidth),
stackedBarPadding: _stackedBarPadding,
roundTopLeft: roundTopLeft,
roundTopRight: roundTopRight,
roundBottomLeft: roundBottomLeft,
roundBottomRight: roundBottomRight,
);
// If bar stack's range width is:
// * Within the component bounds, then draw the bar stack.
// * Partially out of component bounds, then clip the stack where it is out
// of bounds.
// * Fully out of component bounds, do not draw.
final barOutsideBounds = renderingVertically
? barStack.fullStackRect.left < componentBounds.left ||
barStack.fullStackRect.right > componentBounds.right
: barStack.fullStackRect.top < componentBounds.top ||
barStack.fullStackRect.bottom > componentBounds.bottom;
// TODO: When we have initial viewport, add image test for
// clipping.
if (barOutsideBounds) {
final clipBounds = _getBarStackBounds(barStack.fullStackRect);
// Do not draw the bar stack if it is completely outside of the component
// bounds.
if (clipBounds.width <= 0 || clipBounds.height <= 0) {
return;
}
canvas.setClipBounds(clipBounds);
}
canvas.drawBarStack(barStack, drawAreaBounds: componentBounds);
if (barOutsideBounds) {
canvas.resetClipBounds();
}
// Decorate the bar segments if there is a decorator.
barRendererDecorator?.decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: animationPercent,
renderingVertically: renderingVertically,
rtl: isRtl);
}
/// Calculate the clipping region for a rectangle that represents the full bar
/// stack.
Rectangle<int> _getBarStackBounds(Rectangle<int> barStackRect) {
int left;
int right;
int top;
int bottom;
if (renderingVertically) {
// Only clip at the start and end so that the bar's width stays within
// the viewport, but any bar decorations above the bar can still show.
left = max(componentBounds.left, barStackRect.left);
right = min(componentBounds.right, barStackRect.right);
top = barStackRect.top;
bottom = barStackRect.bottom;
} else {
// Only clip at the top and bottom so that the bar's height stays within
// the viewport, but any bar decorations to the right of the bar can still
// show.
left = barStackRect.left;
right = barStackRect.right;
top = max(componentBounds.top, barStackRect.top);
bottom = min(componentBounds.bottom, barStackRect.bottom);
}
final width = right - left;
final height = bottom - top;
return new Rectangle(left, top, width, height);
}
/// Generates a set of bounds that describe a bar.
Rectangle<int> _getBarBounds(
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups) {
// If no weights were passed in, default to equal weight per bar.
if (barGroupWeight == null) {
barGroupWeight = 1 / numBarGroups;
previousBarGroupWeight = barGroupIndex * barGroupWeight;
}
// Calculate how wide each bar should be within the group of bars. If we
// only have one series, or are stacked, then barWidth should equal
// domainWidth.
int spacingLoss = (_barGroupInnerPadding * (numBarGroups - 1));
int barWidth = ((domainWidth - spacingLoss) * barGroupWeight).round();
// Make sure that bars are at least one pixel wide, so that they will always
// be visible on the chart. Ideally we should do something clever with the
// size of the chart, and the density and periodicity of the data, but this
// at least ensures that dense charts still have visible data.
barWidth = max(1, barWidth);
// Flip bar group index for calculating location on the domain axis if RTL.
final adjustedBarGroupIndex =
isRtl ? numBarGroups - barGroupIndex - 1 : barGroupIndex;
// Calculate the start and end of the bar, taking into account accumulated
// padding for grouped bars.
int previousAverageWidth = adjustedBarGroupIndex > 0
? ((domainWidth - spacingLoss) *
(previousBarGroupWeight / adjustedBarGroupIndex))
.round()
: 0;
int domainStart = (domainAxis.getLocation(domainValue) -
(domainWidth / 2) +
(previousAverageWidth + _barGroupInnerPadding) *
adjustedBarGroupIndex)
.round();
int domainEnd = domainStart + barWidth;
measureValue = measureValue != null ? measureValue : 0;
// Calculate measure locations. Stacked bars should have their
// offset calculated previously.
int measureStart;
int measureEnd;
if (measureValue < 0) {
measureEnd = measureAxis.getLocation(measureOffsetValue).round();
measureStart =
measureAxis.getLocation(measureValue + measureOffsetValue).round();
} else {
measureStart = measureAxis.getLocation(measureOffsetValue).round();
measureEnd =
measureAxis.getLocation(measureValue + measureOffsetValue).round();
}
Rectangle<int> bounds;
if (this.renderingVertically) {
// Rectangle clamps to zero width/height
bounds = new Rectangle<int>(domainStart, measureEnd,
domainEnd - domainStart, measureStart - measureEnd);
} else {
// Rectangle clamps to zero width/height
bounds = new Rectangle<int>(min(measureStart, measureEnd), domainStart,
(measureEnd - measureStart).abs(), domainEnd - domainStart);
}
return bounds;
}
@override
Rectangle<int> getBoundsForBar(BarRendererElement bar) => bar.bounds;
}
abstract class ImmutableBarRendererElement<D> {
ImmutableSeries<D> get series;
dynamic get datum;
int get index;
Rectangle<int> get bounds;
}
class BarRendererElement<D> extends BaseBarRendererElement
implements ImmutableBarRendererElement<D> {
ImmutableSeries<D> series;
Rectangle<int> bounds;
int roundPx;
int index;
dynamic _datum;
dynamic get datum => _datum;
set datum(dynamic datum) {
_datum = datum;
index = series?.data?.indexOf(datum);
}
BarRendererElement();
BarRendererElement.clone(BarRendererElement other) : super.clone(other) {
series = other.series;
bounds = other.bounds;
roundPx = other.roundPx;
index = other.index;
_datum = other._datum;
}
@override
void updateAnimationPercent(BaseBarRendererElement previous,
BaseBarRendererElement target, double animationPercent) {
final BarRendererElement localPrevious = previous;
final BarRendererElement localTarget = target;
final previousBounds = localPrevious.bounds;
final targetBounds = localTarget.bounds;
var top = ((targetBounds.top - previousBounds.top) * animationPercent) +
previousBounds.top;
var right =
((targetBounds.right - previousBounds.right) * animationPercent) +
previousBounds.right;
var bottom =
((targetBounds.bottom - previousBounds.bottom) * animationPercent) +
previousBounds.bottom;
var left = ((targetBounds.left - previousBounds.left) * animationPercent) +
previousBounds.left;
bounds = new Rectangle<int>(left.round(), top.round(),
(right - left).round(), (bottom - top).round());
roundPx = localTarget.roundPx;
super.updateAnimationPercent(previous, target, animationPercent);
}
}
class AnimatedBar<D> extends BaseAnimatedBar<D, BarRendererElement<D>> {
AnimatedBar(
{@required String key,
@required dynamic datum,
@required ImmutableSeries<D> series,
@required D domainValue})
: super(key: key, datum: datum, series: series, domainValue: domainValue);
@override
animateElementToMeasureAxisPosition(BaseBarRendererElement target) {
final BarRendererElement localTarget = target;
// TODO: Animate out bars in the middle of a stack.
localTarget.bounds = new Rectangle<int>(
localTarget.bounds.left + (localTarget.bounds.width / 2).round(),
localTarget.measureAxisPosition.round(),
0,
0);
}
BarRendererElement<D> getCurrentBar(double animationPercent) {
final BarRendererElement<D> bar = super.getCurrentBar(animationPercent);
// Update with series and datum information to pass to bar decorator.
bar.series = series;
bar.datum = datum;
return bar;
}
@override
BarRendererElement<D> clone(BarRendererElement bar) =>
new BarRendererElement<D>.clone(bar);
}

View File

@@ -0,0 +1,99 @@
// 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 '../../common/symbol_renderer.dart';
import '../common/chart_canvas.dart' show FillPatternType;
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
import 'bar_renderer.dart' show BarRenderer;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
import 'base_bar_renderer_config.dart'
show BarGroupingType, BaseBarRendererConfig;
/// Configuration for a bar renderer.
class BarRendererConfig<D> extends BaseBarRendererConfig<D> {
/// Strategy for determining the corner radius of a bar.
final CornerStrategy cornerStrategy;
/// Decorator for optionally decorating painted bars.
final BarRendererDecorator barRendererDecorator;
BarRendererConfig({
String customRendererId,
CornerStrategy cornerStrategy,
FillPatternType fillPattern,
BarGroupingType groupingType,
int layoutPaintOrder = LayoutViewPaintOrder.bar,
int minBarLengthPx = 0,
double stackHorizontalSeparator,
double strokeWidthPx = 0.0,
this.barRendererDecorator,
SymbolRenderer symbolRenderer,
List<int> weightPattern,
}) : cornerStrategy = cornerStrategy ?? const ConstCornerStrategy(2),
super(
customRendererId: customRendererId,
groupingType: groupingType ?? BarGroupingType.grouped,
layoutPaintOrder: layoutPaintOrder,
minBarLengthPx: minBarLengthPx,
fillPattern: fillPattern,
stackHorizontalSeparator: stackHorizontalSeparator,
strokeWidthPx: strokeWidthPx,
symbolRenderer: symbolRenderer,
weightPattern: weightPattern,
);
@override
BarRenderer<D> build() {
return new BarRenderer<D>(config: this, rendererId: customRendererId);
}
@override
bool operator ==(other) {
if (identical(this, other)) {
return true;
}
if (!(other is BarRendererConfig)) {
return false;
}
return other.cornerStrategy == cornerStrategy && super == (other);
}
@override
int get hashCode {
var hash = super.hashCode;
hash = hash * 31 + (cornerStrategy?.hashCode ?? 0);
return hash;
}
}
abstract class CornerStrategy {
/// Returns the radius of the rounded corners in pixels.
int getRadius(int barWidth);
}
/// Strategy for constant corner radius.
class ConstCornerStrategy implements CornerStrategy {
final int radius;
const ConstCornerStrategy(this.radius);
@override
int getRadius(_) => radius;
}
/// Strategy for no corner radius.
class NoCornerStrategy extends ConstCornerStrategy {
const NoCornerStrategy() : super(0);
}

View File

@@ -0,0 +1,34 @@
// 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:meta/meta.dart' show required;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../common/chart_canvas.dart' show ChartCanvas;
import 'bar_renderer.dart' show ImmutableBarRendererElement;
/// Decorates bars after the bars have already been painted.
abstract class BarRendererDecorator<D> {
const BarRendererDecorator();
void decorate(Iterable<ImmutableBarRendererElement<D>> barElements,
ChartCanvas canvas, GraphicsFactory graphicsFactory,
{@required Rectangle drawBounds,
@required double animationPercent,
@required bool renderingVertically,
bool rtl = false});
}

View File

@@ -0,0 +1,422 @@
// 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, max, min;
import 'package:meta/meta.dart' show required;
import '../../common/color.dart' show Color;
import '../cartesian/axis/axis.dart'
show ImmutableAxis, domainAxisKey, measureAxisKey;
import '../common/chart_canvas.dart' show ChartCanvas, FillPatternType;
import '../common/datum_details.dart' show DatumDetails;
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
import '../common/series_datum.dart' show SeriesDatum;
import 'bar_target_line_renderer_config.dart' show BarTargetLineRendererConfig;
import 'base_bar_renderer.dart'
show
BaseBarRenderer,
barGroupCountKey,
barGroupIndexKey,
previousBarGroupWeightKey,
barGroupWeightKey;
import 'base_bar_renderer_element.dart'
show BaseAnimatedBar, BaseBarRendererElement;
/// Renders series data as a series of bar target lines.
///
/// Usually paired with a BarRenderer to display target metrics alongside actual
/// metrics.
class BarTargetLineRenderer<D> extends BaseBarRenderer<D,
_BarTargetLineRendererElement, _AnimatedBarTargetLine<D>> {
/// If we are grouped, use this spacing between the bars in a group.
final _barGroupInnerPadding = 2;
/// Standard color for all bar target lines.
final _color = new Color(r: 0, g: 0, b: 0, a: 153);
factory BarTargetLineRenderer(
{BarTargetLineRendererConfig<D> config,
String rendererId = 'barTargetLine'}) {
config ??= new BarTargetLineRendererConfig<D>();
return new BarTargetLineRenderer._internal(
config: config, rendererId: rendererId);
}
BarTargetLineRenderer._internal(
{BarTargetLineRendererConfig<D> config, String rendererId})
: super(
config: config,
rendererId: rendererId,
layoutPaintOrder: config.layoutPaintOrder);
@override
void configureSeries(List<MutableSeries<D>> seriesList) {
seriesList.forEach((MutableSeries<D> series) {
series.colorFn ??= (_) => _color;
series.fillColorFn ??= (_) => _color;
});
}
DatumDetails<D> addPositionToDetailsForSeriesDatum(
DatumDetails<D> details, SeriesDatum<D> seriesDatum) {
final series = details.series;
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final barGroupIndex = series.getAttr(barGroupIndexKey);
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
final barGroupWeight = series.getAttr(barGroupWeightKey);
final numBarGroups = series.getAttr(barGroupCountKey);
final points = _getTargetLinePoints(
details.domain,
domainAxis,
domainAxis.rangeBand.round(),
details.measure,
details.measureOffset,
measureAxis,
barGroupIndex,
previousBarGroupWeight,
barGroupWeight,
numBarGroups);
Point<double> chartPosition;
if (renderingVertically) {
chartPosition = new Point<double>(
(points[0].x + (points[1].x - points[0].x) / 2).toDouble(),
points[0].y.toDouble());
} else {
chartPosition = new Point<double>(points[0].x.toDouble(),
(points[0].y + (points[1].y - points[0].y) / 2).toDouble());
}
return new DatumDetails.from(details, chartPosition: chartPosition);
}
@override
_BarTargetLineRendererElement getBaseDetails(dynamic datum, int index) {
final BarTargetLineRendererConfig<D> localConfig = config;
return new _BarTargetLineRendererElement()
..roundEndCaps = localConfig.roundEndCaps;
}
/// Generates an [_AnimatedBarTargetLine] to represent the previous and
/// current state of one bar target line on the chart.
@override
_AnimatedBarTargetLine<D> makeAnimatedBar(
{String key,
ImmutableSeries<D> series,
dynamic datum,
Color color,
List<int> dashPattern,
_BarTargetLineRendererElement details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
Color fillColor,
FillPatternType fillPattern,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups,
double strokeWidthPx,
bool measureIsNull,
bool measureIsNegative}) {
return new _AnimatedBarTargetLine(
key: key, datum: datum, series: series, domainValue: domainValue)
..setNewTarget(makeBarRendererElement(
color: color,
details: details,
dashPattern: dashPattern,
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainWidth,
measureValue: measureValue,
measureOffsetValue: measureOffsetValue,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
fillColor: fillColor,
fillPattern: fillPattern,
strokeWidthPx: strokeWidthPx,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
numBarGroups: numBarGroups,
measureIsNull: measureIsNull,
measureIsNegative: measureIsNegative));
}
/// Generates a [_BarTargetLineRendererElement] to represent the rendering
/// data for one bar target line on the chart.
@override
_BarTargetLineRendererElement makeBarRendererElement(
{Color color,
List<int> dashPattern,
_BarTargetLineRendererElement details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups,
bool measureIsNull,
bool measureIsNegative}) {
return new _BarTargetLineRendererElement()
..color = color
..dashPattern = dashPattern
..fillColor = fillColor
..fillPattern = fillPattern
..measureAxisPosition = measureAxisPosition
..roundEndCaps = details.roundEndCaps
..strokeWidthPx = strokeWidthPx
..measureIsNull = measureIsNull
..measureIsNegative = measureIsNegative
..points = _getTargetLinePoints(
domainValue,
domainAxis,
domainWidth,
measureValue,
measureOffsetValue,
measureAxis,
barGroupIndex,
previousBarGroupWeight,
barGroupWeight,
numBarGroups);
}
@override
void paintBar(
ChartCanvas canvas,
double animationPercent,
Iterable<_BarTargetLineRendererElement> barElements,
) {
barElements.forEach((_BarTargetLineRendererElement bar) {
// TODO: Combine common line attributes into
// GraphicsFactory.lineStyle or similar.
canvas.drawLine(
clipBounds: drawBounds,
points: bar.points,
stroke: bar.color,
roundEndCaps: bar.roundEndCaps,
strokeWidthPx: bar.strokeWidthPx);
});
}
/// Generates a set of points that describe a bar target line.
List<Point<int>> _getTargetLinePoints(
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups) {
// If no weights were passed in, default to equal weight per bar.
if (barGroupWeight == null) {
barGroupWeight = 1 / numBarGroups;
previousBarGroupWeight = barGroupIndex * barGroupWeight;
}
final BarTargetLineRendererConfig<D> localConfig = config;
// Calculate how wide each bar target line should be within the group of
// bar target lines. If we only have one series, or are stacked, then
// barWidth should equal domainWidth.
int spacingLoss = (_barGroupInnerPadding * (numBarGroups - 1));
int barWidth = ((domainWidth - spacingLoss) * barGroupWeight).round();
// Get the overdraw boundaries.
var overDrawOuterPx = localConfig.overDrawOuterPx;
var overDrawPx = localConfig.overDrawPx;
int overDrawStartPx = (barGroupIndex == 0) && overDrawOuterPx != null
? overDrawOuterPx
: overDrawPx;
int overDrawEndPx =
(barGroupIndex == numBarGroups - 1) && overDrawOuterPx != null
? overDrawOuterPx
: overDrawPx;
// Flip bar group index for calculating location on the domain axis if RTL.
final adjustedBarGroupIndex =
isRtl ? numBarGroups - barGroupIndex - 1 : barGroupIndex;
// Calculate the start and end of the bar target line, taking into account
// accumulated padding for grouped bars.
num previousAverageWidth = adjustedBarGroupIndex > 0
? ((domainWidth - spacingLoss) *
(previousBarGroupWeight / adjustedBarGroupIndex))
.round()
: 0;
int domainStart = (domainAxis.getLocation(domainValue) -
(domainWidth / 2) +
(previousAverageWidth + _barGroupInnerPadding) *
adjustedBarGroupIndex -
overDrawStartPx)
.round();
int domainEnd = domainStart + barWidth + overDrawStartPx + overDrawEndPx;
measureValue = measureValue != null ? measureValue : 0;
// Calculate measure locations. Stacked bars should have their
// offset calculated previously.
int measureStart =
measureAxis.getLocation(measureValue + measureOffsetValue).round();
List<Point<int>> points;
if (renderingVertically) {
points = [
new Point<int>(domainStart, measureStart),
new Point<int>(domainEnd, measureStart)
];
} else {
points = [
new Point<int>(measureStart, domainStart),
new Point<int>(measureStart, domainEnd)
];
}
return points;
}
@override
Rectangle<int> getBoundsForBar(_BarTargetLineRendererElement bar) {
final points = bar.points;
int top;
int bottom;
int left;
int right;
points.forEach((Point<int> p) {
top = top != null ? min(top, p.y) : p.y;
left = left != null ? min(left, p.x) : p.x;
bottom = bottom != null ? max(bottom, p.y) : p.y;
right = right != null ? max(right, p.x) : p.x;
});
return new Rectangle<int>(left, top, right - left, bottom - top);
}
}
class _BarTargetLineRendererElement extends BaseBarRendererElement {
List<Point<int>> points;
bool roundEndCaps;
_BarTargetLineRendererElement();
_BarTargetLineRendererElement.clone(_BarTargetLineRendererElement other)
: super.clone(other) {
points = new List<Point<int>>.from(other.points);
roundEndCaps = other.roundEndCaps;
}
@override
void updateAnimationPercent(BaseBarRendererElement previous,
BaseBarRendererElement target, double animationPercent) {
final _BarTargetLineRendererElement localPrevious = previous;
final _BarTargetLineRendererElement localTarget = target;
final previousPoints = localPrevious.points;
final targetPoints = localTarget.points;
Point<int> lastPoint;
int pointIndex;
for (pointIndex = 0; pointIndex < targetPoints.length; pointIndex++) {
var targetPoint = targetPoints[pointIndex];
// If we have more points than the previous line, animate in the new point
// by starting its measure position at the last known official point.
Point<int> previousPoint;
if (previousPoints.length - 1 >= pointIndex) {
previousPoint = previousPoints[pointIndex];
lastPoint = previousPoint;
} else {
previousPoint = new Point<int>(targetPoint.x, lastPoint.y);
}
var x = ((targetPoint.x - previousPoint.x) * animationPercent) +
previousPoint.x;
var y = ((targetPoint.y - previousPoint.y) * animationPercent) +
previousPoint.y;
if (points.length - 1 >= pointIndex) {
points[pointIndex] = new Point<int>(x.round(), y.round());
} else {
points.add(new Point<int>(x.round(), y.round()));
}
}
// Removing extra points that don't exist anymore.
if (pointIndex < points.length) {
points.removeRange(pointIndex, points.length);
}
strokeWidthPx = ((localTarget.strokeWidthPx - localPrevious.strokeWidthPx) *
animationPercent) +
localPrevious.strokeWidthPx;
roundEndCaps = localTarget.roundEndCaps;
super.updateAnimationPercent(previous, target, animationPercent);
}
}
class _AnimatedBarTargetLine<D>
extends BaseAnimatedBar<D, _BarTargetLineRendererElement> {
_AnimatedBarTargetLine(
{@required String key,
@required dynamic datum,
@required ImmutableSeries<D> series,
@required D domainValue})
: super(key: key, datum: datum, series: series, domainValue: domainValue);
@override
animateElementToMeasureAxisPosition(BaseBarRendererElement target) {
final _BarTargetLineRendererElement localTarget = target;
final newPoints = <Point<int>>[];
for (var index = 0; index < localTarget.points.length; index++) {
final targetPoint = localTarget.points[index];
newPoints.add(new Point<int>(
targetPoint.x, localTarget.measureAxisPosition.round()));
}
localTarget.points = newPoints;
}
@override
_BarTargetLineRendererElement clone(_BarTargetLineRendererElement bar) =>
new _BarTargetLineRendererElement.clone(bar);
}

View File

@@ -0,0 +1,92 @@
// 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 '../../common/symbol_renderer.dart'
show SymbolRenderer, LineSymbolRenderer;
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
import 'bar_target_line_renderer.dart' show BarTargetLineRenderer;
import 'base_bar_renderer_config.dart'
show BarGroupingType, BaseBarRendererConfig;
/// Configuration for a bar target line renderer.
class BarTargetLineRendererConfig<D> extends BaseBarRendererConfig<D> {
/// The number of pixels that the line will extend beyond the bandwidth at the
/// edges of the bar group.
///
/// If set, this overrides overDrawPx for the beginning side of the first bar
/// target line in the group, and the ending side of the last bar target line.
/// overDrawPx will be used for overdrawing the target lines for interior
/// sides of the bars.
final int overDrawOuterPx;
/// The number of pixels that the line will extend beyond the bandwidth for
/// every bar in a group.
final int overDrawPx;
/// Whether target lines should have round end caps, or square if false.
final bool roundEndCaps;
BarTargetLineRendererConfig(
{String customRendererId,
List<int> dashPattern,
groupingType = BarGroupingType.grouped,
int layoutPaintOrder = LayoutViewPaintOrder.barTargetLine,
int minBarLengthPx = 0,
this.overDrawOuterPx,
this.overDrawPx = 0,
this.roundEndCaps = true,
double strokeWidthPx = 3.0,
SymbolRenderer symbolRenderer,
List<int> weightPattern})
: super(
customRendererId: customRendererId,
dashPattern: dashPattern,
groupingType: groupingType,
layoutPaintOrder: layoutPaintOrder,
minBarLengthPx: minBarLengthPx,
strokeWidthPx: strokeWidthPx,
symbolRenderer: symbolRenderer ?? new LineSymbolRenderer(),
weightPattern: weightPattern,
);
@override
BarTargetLineRenderer<D> build() {
return new BarTargetLineRenderer<D>(
config: this, rendererId: customRendererId);
}
@override
bool operator ==(other) {
if (identical(this, other)) {
return true;
}
if (!(other is BarTargetLineRendererConfig)) {
return false;
}
return other.overDrawOuterPx == overDrawOuterPx &&
other.overDrawPx == overDrawPx &&
other.roundEndCaps == roundEndCaps &&
super == (other);
}
@override
int get hashCode {
var hash = 1;
hash = hash * 31 + (overDrawOuterPx?.hashCode ?? 0);
hash = hash * 31 + (overDrawPx?.hashCode ?? 0);
hash = hash * 31 + (roundEndCaps?.hashCode ?? 0);
return hash;
}
}

View File

@@ -0,0 +1,803 @@
// 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:collection' show LinkedHashMap, HashSet;
import 'dart:math' show Point, Rectangle, max;
import 'package:meta/meta.dart' show protected, required;
import '../../common/color.dart' show Color;
import '../../common/math.dart' show clamp;
import '../../common/symbol_renderer.dart' show RoundedRectSymbolRenderer;
import '../../data/series.dart' show AttributeKey;
import '../cartesian/axis/axis.dart'
show ImmutableAxis, OrdinalAxis, domainAxisKey, measureAxisKey;
import '../cartesian/axis/scale.dart' show RangeBandConfig;
import '../cartesian/cartesian_renderer.dart' show BaseCartesianRenderer;
import '../common/base_chart.dart' show BaseChart;
import '../common/chart_canvas.dart' show ChartCanvas, FillPatternType;
import '../common/datum_details.dart' show DatumDetails;
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
import 'base_bar_renderer_config.dart' show BaseBarRendererConfig;
import 'base_bar_renderer_element.dart'
show BaseAnimatedBar, BaseBarRendererElement;
const barGroupIndexKey = const AttributeKey<int>('BarRenderer.barGroupIndex');
const barGroupCountKey = const AttributeKey<int>('BarRenderer.barGroupCount');
const barGroupWeightKey =
const AttributeKey<double>('BarRenderer.barGroupWeight');
const previousBarGroupWeightKey =
const AttributeKey<double>('BarRenderer.previousBarGroupWeight');
const stackKeyKey = const AttributeKey<String>('BarRenderer.stackKey');
const barElementsKey =
const AttributeKey<List<BaseBarRendererElement>>('BarRenderer.elements');
/// Base class for bar renderers that implements common stacking and grouping
/// logic.
///
/// Bar renderers support 4 different modes of rendering multiple series on the
/// chart, configured by the grouped and stacked flags.
/// * grouped - Render bars for each series that shares a domain value
/// side-by-side.
/// * stacked - Render bars for each series that shares a domain value in a
/// stack, ordered in the same order as the series list.
/// * grouped-stacked: Render bars for each series that shares a domain value in
/// a group of bar stacks. Each stack will contain all the series that share a
/// series category.
/// * floating style - When grouped and stacked are both false, all bars that
/// share a domain value will be rendered in the same domain space. Each datum
/// should be configured with a measure offset to position its bar along the
/// measure axis. Bars will freely overlap if their measure values and measure
/// offsets overlap. Note that bars for each series will be rendered in order,
/// such that bars from the last series will be "on top" of bars from previous
/// series.
abstract class BaseBarRenderer<D, R extends BaseBarRendererElement,
B extends BaseAnimatedBar<D, R>> extends BaseCartesianRenderer<D> {
final BaseBarRendererConfig config;
@protected
BaseChart<D> chart;
/// Store a map of domain+barGroupIndex+category index to bars in a stack.
///
/// This map is used to render all the bars in a stack together, to account
/// for rendering effects that need to take the full stack into account (e.g.
/// corner rounding).
///
/// [LinkedHashMap] is used to render the bars on the canvas in the same order
/// as the data was given to the chart. For the case where both grouping and
/// stacking are disabled, this means that bars for data later in the series
/// will be drawn "on top of" bars earlier in the series.
final _barStackMap = new LinkedHashMap<String, List<B>>();
// Store a list of bar stacks that exist in the series data.
//
// This list will be used to remove any AnimatingBars that were rendered in
// previous draw cycles, but no longer have a corresponding datum in the new
// data.
final _currentKeys = <String>[];
/// Stores a list of stack keys for each group key.
final _currentGroupsStackKeys = new LinkedHashMap<D, Set<String>>();
/// Optimization for getNearest to avoid scanning all data if possible.
ImmutableAxis<D> _prevDomainAxis;
BaseBarRenderer(
{@required this.config, String rendererId, int layoutPaintOrder})
: super(
rendererId: rendererId,
layoutPaintOrder: layoutPaintOrder,
symbolRenderer:
config?.symbolRenderer ?? new RoundedRectSymbolRenderer(),
);
@override
void preprocessSeries(List<MutableSeries<D>> seriesList) {
var barGroupIndex = 0;
// Maps used to store the final measure offset of the previous series, for
// each domain value.
final posDomainToStackKeyToDetailsMap = {};
final negDomainToStackKeyToDetailsMap = {};
final categoryToIndexMap = {};
// Keep track of the largest bar stack size. This should be 1 for grouped
// bars, and it should be the size of the tallest stack for stacked or
// grouped stacked bars.
var maxBarStackSize = 0;
final orderedSeriesList = getOrderedSeriesList(seriesList);
orderedSeriesList.forEach((MutableSeries<D> series) {
var elements = <BaseBarRendererElement>[];
var domainFn = series.domainFn;
var measureFn = series.measureFn;
var measureOffsetFn = series.measureOffsetFn;
var fillPatternFn = series.fillPatternFn;
var strokeWidthPxFn = series.strokeWidthPxFn;
series.dashPatternFn ??= (_) => config.dashPattern;
// Identifies which stack the series will go in, by default a single
// stack.
var stackKey = '__defaultKey__';
// Override the stackKey with seriesCategory if we are GROUPED_STACKED
// so we have a way to choose which series go into which stacks.
if (config.grouped && config.stacked) {
if (series.seriesCategory != null) {
stackKey = series.seriesCategory;
}
barGroupIndex = categoryToIndexMap[stackKey];
if (barGroupIndex == null) {
barGroupIndex = categoryToIndexMap.length;
categoryToIndexMap[stackKey] = barGroupIndex;
}
}
var needsMeasureOffset = false;
for (var barIndex = 0; barIndex < series.data.length; barIndex++) {
dynamic datum = series.data[barIndex];
final details = getBaseDetails(datum, barIndex);
details.barStackIndex = 0;
details.measureOffset = 0;
if (fillPatternFn != null) {
details.fillPattern = fillPatternFn(barIndex);
} else {
details.fillPattern = config.fillPattern;
}
if (strokeWidthPxFn != null) {
details.strokeWidthPx = strokeWidthPxFn(barIndex).toDouble();
} else {
details.strokeWidthPx = config.strokeWidthPx;
}
// When stacking is enabled, adjust the measure offset for each domain
// value in each series by adding up the measures and offsets of lower
// series.
if (config.stacked) {
needsMeasureOffset = true;
var domain = domainFn(barIndex);
var measure = measureFn(barIndex);
// We will render positive bars in one stack, and negative bars in a
// separate stack. Keep track of the measure offsets for these stacks
// independently.
var domainToCategoryToDetailsMap = measure == null || measure >= 0
? posDomainToStackKeyToDetailsMap
: negDomainToStackKeyToDetailsMap;
var categoryToDetailsMap =
domainToCategoryToDetailsMap.putIfAbsent(domain, () => {});
var prevDetail = categoryToDetailsMap[stackKey];
if (prevDetail != null) {
details.barStackIndex = prevDetail.barStackIndex + 1;
}
details.cumulativeTotal = measure != null ? measure : 0;
// Get the previous series' measure offset.
var measureOffset = measureOffsetFn(barIndex);
if (prevDetail != null) {
measureOffset += prevDetail.measureOffsetPlusMeasure;
details.cumulativeTotal += prevDetail.cumulativeTotal;
}
// And overwrite the details measure offset.
details.measureOffset = measureOffset;
var measureValue = (measure != null ? measure : 0);
details.measureOffsetPlusMeasure = measureOffset + measureValue;
categoryToDetailsMap[stackKey] = details;
}
maxBarStackSize = max(maxBarStackSize, details.barStackIndex + 1);
elements.add(details);
}
if (needsMeasureOffset) {
// Override the measure offset function to return the measure offset we
// calculated for each datum. This already includes any measure offset
// that was configured in the series data.
series.measureOffsetFn = (index) => elements[index].measureOffset;
}
series.setAttr(barGroupIndexKey, barGroupIndex);
series.setAttr(stackKeyKey, stackKey);
series.setAttr(barElementsKey, elements);
if (config.grouped) {
barGroupIndex++;
}
});
// Compute number of bar groups. This must be done after we have processed
// all of the series once, so that we know how many categories we have.
var numBarGroups = 0;
if (config.grouped && config.stacked) {
// For grouped stacked bars, categoryToIndexMap effectively one list per
// group of stacked bars.
numBarGroups = categoryToIndexMap.length;
} else if (config.stacked) {
numBarGroups = 1;
} else {
numBarGroups = seriesList.length;
}
// Compute bar group weights.
final barWeights = _calculateBarWeights(numBarGroups);
seriesList.forEach((MutableSeries<D> series) {
series.setAttr(barGroupCountKey, numBarGroups);
if (barWeights.isNotEmpty) {
final barGroupIndex = series.getAttr(barGroupIndexKey);
final barWeight = barWeights[barGroupIndex];
// In RTL mode, we need to grab the weights for the bars that follow
// this datum in the series (instead of precede it). The first datum is
// physically positioned on the canvas to the right of all the rest of
// the bar group data that follows it.
final previousBarWeights = isRtl
? barWeights.getRange(barGroupIndex + 1, numBarGroups)
: barWeights.getRange(0, barGroupIndex);
final previousBarWeight = previousBarWeights.isNotEmpty
? previousBarWeights.reduce((a, b) => a + b)
: 0.0;
series.setAttr(barGroupWeightKey, barWeight);
series.setAttr(previousBarGroupWeightKey, previousBarWeight);
}
});
}
/// Calculates bar weights for a list of series from [config.weightPattern].
///
/// If [config.weightPattern] is not set, then this will assign a weight
/// proportional to the number of bar groups for every series.
List<double> _calculateBarWeights(int numBarGroups) {
// Set up bar weights for each series as a ratio of the total weight.
final weights = <double>[];
if (config.weightPattern != null) {
if (numBarGroups > config.weightPattern.length) {
throw new ArgumentError('Number of series exceeds length of weight '
'pattern ${config.weightPattern}');
}
var totalBarWeight = 0;
for (var i = 0; i < numBarGroups; i++) {
totalBarWeight += config.weightPattern[i];
}
for (var i = 0; i < numBarGroups; i++) {
weights.add(config.weightPattern[i] / totalBarWeight);
}
} else {
for (var i = 0; i < numBarGroups; i++) {
weights.add(1 / numBarGroups);
}
}
return weights;
}
/// Construct a base details element for a given datum.
///
/// This is intended to be overridden by child classes that need to add
/// customized rendering properties.
R getBaseDetails(dynamic datum, int index);
@override
void configureDomainAxes(List<MutableSeries<D>> seriesList) {
super.configureDomainAxes(seriesList);
// Configure the domain axis to use a range band configuration.
if (seriesList.isNotEmpty) {
// Given that charts can only have one domain axis, just grab it from the
// first series.
final domainAxis = seriesList.first.getAttr(domainAxisKey);
domainAxis.setRangeBandConfig(new RangeBandConfig.styleAssignedPercent());
}
}
void update(List<ImmutableSeries<D>> seriesList, bool isAnimatingThisDraw) {
_currentKeys.clear();
_currentGroupsStackKeys.clear();
final orderedSeriesList = getOrderedSeriesList(seriesList);
orderedSeriesList.forEach((final ImmutableSeries<D> series) {
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final domainFn = series.domainFn;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final measureFn = series.measureFn;
final colorFn = series.colorFn;
final dashPatternFn = series.dashPatternFn;
final fillColorFn = series.fillColorFn;
final seriesStackKey = series.getAttr(stackKeyKey);
final barGroupCount = series.getAttr(barGroupCountKey);
final barGroupIndex = series.getAttr(barGroupIndexKey);
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
final barGroupWeight = series.getAttr(barGroupWeightKey);
final measureAxisPosition = measureAxis.getLocation(0.0);
var elementsList = series.getAttr(barElementsKey);
// Save off domainAxis for getNearest.
_prevDomainAxis = domainAxis;
for (var barIndex = 0; barIndex < series.data.length; barIndex++) {
final datum = series.data[barIndex];
BaseBarRendererElement details = elementsList[barIndex];
D domainValue = domainFn(barIndex);
final measureValue = measureFn(barIndex);
final measureIsNull = measureValue == null;
final measureIsNegative = !measureIsNull && measureValue < 0;
// Each bar should be stored in barStackMap in a structure that mirrors
// the visual rendering of the bars. Thus, they should be grouped by
// domain value, series category (by way of the stack keys that were
// generated for each series in the preprocess step), and bar group
// index to account for all combinations of grouping and stacking.
var barStackMapKey = domainValue.toString() +
'__' +
seriesStackKey +
'__' +
(measureIsNegative ? 'pos' : 'neg') +
'__' +
barGroupIndex.toString();
var barKey = barStackMapKey + details.barStackIndex.toString();
var barStackList = _barStackMap.putIfAbsent(barStackMapKey, () => []);
// If we already have an AnimatingBarfor that index, use it.
var animatingBar = barStackList.firstWhere((B bar) => bar.key == barKey,
orElse: () => null);
// If we don't have any existing bar element, create a new bar and have
// it animate in from the domain axis.
// TODO: Animate bars in the middle of a stack from their
// nearest neighbors, instead of the measure axis.
if (animatingBar == null) {
// If the measure is null and there was no existing animating bar, it
// means we don't need to draw this bar at all.
if (!measureIsNull) {
animatingBar = makeAnimatedBar(
key: barKey,
series: series,
datum: datum,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: colorFn(barIndex),
dashPattern: dashPatternFn(barIndex),
details: details,
domainValue: domainFn(barIndex),
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: fillColorFn(barIndex),
fillPattern: details.fillPattern,
measureValue: 0.0,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: details.strokeWidthPx,
measureIsNull: measureIsNull,
measureIsNegative: measureIsNegative);
barStackList.add(animatingBar);
}
} else {
animatingBar
..datum = datum
..series = series
..domainValue = domainValue;
}
if (animatingBar == null) {
continue;
}
// Update the set of bars that still exist in the series data.
_currentKeys.add(barKey);
// Store off stack keys for each bar group to help getNearest identify
// groups of stacks.
_currentGroupsStackKeys
.putIfAbsent(domainValue, () => new Set<String>())
.add(barStackMapKey);
// Get the barElement we are going to setup.
// Optimization to prevent allocation in non-animating case.
BaseBarRendererElement barElement = makeBarRendererElement(
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: colorFn(barIndex),
dashPattern: dashPatternFn(barIndex),
details: details,
domainValue: domainFn(barIndex),
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: fillColorFn(barIndex),
fillPattern: details.fillPattern,
measureValue: measureValue,
measureOffsetValue: details.measureOffset,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: details.strokeWidthPx,
measureIsNull: measureIsNull,
measureIsNegative: measureIsNegative);
animatingBar.setNewTarget(barElement);
}
});
// Animate out bars that don't exist anymore.
_barStackMap.forEach((String key, List<B> barStackList) {
for (var barIndex = 0; barIndex < barStackList.length; barIndex++) {
final bar = barStackList[barIndex];
if (_currentKeys.contains(bar.key) != true) {
bar.animateOut();
}
}
});
}
/// Generates a [BaseAnimatedBar] to represent the previous and current state
/// of one bar on the chart.
B makeAnimatedBar(
{String key,
ImmutableSeries<D> series,
dynamic datum,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
Color color,
List<int> dashPattern,
R details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
int numBarGroups,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
bool measureIsNull,
bool measureIsNegative});
/// Generates a [BaseBarRendererElement] to represent the rendering data for
/// one bar on the chart.
R makeBarRendererElement(
{int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
Color color,
List<int> dashPattern,
R details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
int numBarGroups,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
bool measureIsNull,
bool measureIsNegative});
@override
void onAttach(BaseChart<D> chart) {
super.onAttach(chart);
// We only need the chart.context.isRtl setting, but context is not yet
// available when the default renderer is attached to the chart on chart
// creation time, since chart onInit is called after the chart is created.
this.chart = chart;
}
/// Paints the current bar data on the canvas.
void paint(ChartCanvas canvas, double animationPercent) {
// Clean up the bars that no longer exist.
if (animationPercent == 1.0) {
final keysToRemove = new HashSet<String>();
_barStackMap.forEach((String key, List<B> barStackList) {
barStackList.retainWhere(
(B bar) => !bar.animatingOut && !bar.targetBar.measureIsNull);
if (barStackList.isEmpty) {
keysToRemove.add(key);
}
});
// When cleaning up the animation, also clean up the keys used to lookup
// if a bar is selected.
for (String key in keysToRemove) {
_barStackMap.remove(key);
_currentKeys.remove(key);
}
_currentGroupsStackKeys.forEach((domain, keys) {
keys.removeWhere(keysToRemove.contains);
});
}
_barStackMap.forEach((String stackKey, List<B> barStack) {
// Turn this into a list so that the getCurrentBar isn't called more than
// once for each animationPercent if the barElements are iterated more
// than once.
final barElements = barStack
.map((B animatingBar) => animatingBar.getCurrentBar(animationPercent))
.toList();
if (barElements.isNotEmpty) {
paintBar(canvas, animationPercent, barElements);
}
});
}
/// Paints a stack of bar elements on the canvas.
void paintBar(
ChartCanvas canvas, double animationPercent, Iterable<R> barElements);
@override
List<DatumDetails<D>> getNearestDatumDetailPerSeries(
Point<double> chartPoint, bool byDomain, Rectangle<int> boundsOverride) {
var nearest = <DatumDetails<D>>[];
// Was it even in the component bounds?
if (!isPointWithinBounds(chartPoint, boundsOverride)) {
return nearest;
}
if (_prevDomainAxis is OrdinalAxis) {
final domainValue = _prevDomainAxis
.getDomain(renderingVertically ? chartPoint.x : chartPoint.y);
// If we have a domainValue for the event point, then find all segments
// that match it.
if (domainValue != null) {
if (renderingVertically) {
nearest = _getVerticalDetailsForDomainValue(domainValue, chartPoint);
} else {
nearest =
_getHorizontalDetailsForDomainValue(domainValue, chartPoint);
}
}
} else {
if (renderingVertically) {
nearest = _getVerticalDetailsForDomainValue(null, chartPoint);
} else {
nearest = _getHorizontalDetailsForDomainValue(null, chartPoint);
}
// Find the closest domain and only keep values that match the domain.
var minRelativeDistance = double.maxFinite;
var minDomainDistance = double.maxFinite;
var minMeasureDistance = double.maxFinite;
D nearestDomain;
// TODO: Optimize this with a binary search based on chartX.
for (DatumDetails<D> detail in nearest) {
if (byDomain) {
if (detail.domainDistance < minDomainDistance ||
(detail.domainDistance == minDomainDistance &&
detail.measureDistance < minMeasureDistance)) {
minDomainDistance = detail.domainDistance;
minMeasureDistance = detail.measureDistance;
nearestDomain = detail.domain;
}
} else {
if (detail.relativeDistance < minRelativeDistance) {
minRelativeDistance = detail.relativeDistance;
nearestDomain = detail.domain;
}
}
}
nearest.retainWhere((d) => d.domain == nearestDomain);
}
// If we didn't find anything, then keep an empty list.
nearest ??= <DatumDetails<D>>[];
// Note: the details are already sorted by domain & measure distance in
// base chart.
return nearest;
}
Rectangle<int> getBoundsForBar(R bar);
@protected
List<BaseAnimatedBar<D, R>> _getSegmentsForDomainValue(D domainValue,
{bool where(BaseAnimatedBar<D, R> bar)}) {
final matchingSegments = <BaseAnimatedBar<D, R>>[];
// [domainValue] is null only when the bar renderer is being used with in
// a non ordinal axis (ex. date time axis).
//
// In the case of null [domainValue] return all values to be compared, since
// we can't use the optimized comparison for [OrdinalAxis].
final stackKeys = (domainValue != null)
? _currentGroupsStackKeys[domainValue]
: _currentGroupsStackKeys.values
.reduce((allKeys, keys) => allKeys..addAll(keys));
stackKeys?.forEach((String stackKey) {
if (where != null) {
matchingSegments.addAll(_barStackMap[stackKey].where(where));
} else {
matchingSegments.addAll(_barStackMap[stackKey]);
}
});
return matchingSegments;
}
// In the case of null [domainValue] return all values to be compared, since
// we can't use the optimized comparison for [OrdinalAxis].
List<DatumDetails<D>> _getVerticalDetailsForDomainValue(
D domainValue, Point<double> chartPoint) {
return new List<DatumDetails<D>>.from(_getSegmentsForDomainValue(
domainValue,
where: (BaseAnimatedBar<D, R> bar) => !bar.series.overlaySeries)
.map<DatumDetails<D>>((BaseAnimatedBar<D, R> bar) {
final barBounds = getBoundsForBar(bar.currentBar);
final segmentDomainDistance =
_getDistance(chartPoint.x.round(), barBounds.left, barBounds.right);
final segmentMeasureDistance =
_getDistance(chartPoint.y.round(), barBounds.top, barBounds.bottom);
final nearestPoint = new Point<double>(
clamp(chartPoint.x, barBounds.left, barBounds.right).toDouble(),
clamp(chartPoint.y, barBounds.top, barBounds.bottom).toDouble());
final relativeDistance = chartPoint.distanceTo(nearestPoint);
return new DatumDetails<D>(
series: bar.series,
datum: bar.datum,
domain: bar.domainValue,
domainDistance: segmentDomainDistance,
measureDistance: segmentMeasureDistance,
relativeDistance: relativeDistance,
);
}));
}
List<DatumDetails<D>> _getHorizontalDetailsForDomainValue(
D domainValue, Point<double> chartPoint) {
return new List<DatumDetails<D>>.from(_getSegmentsForDomainValue(
domainValue,
where: (BaseAnimatedBar<D, R> bar) => !bar.series.overlaySeries)
.map((BaseAnimatedBar<D, R> bar) {
final barBounds = getBoundsForBar(bar.currentBar);
final segmentDomainDistance =
_getDistance(chartPoint.y.round(), barBounds.top, barBounds.bottom);
final segmentMeasureDistance =
_getDistance(chartPoint.x.round(), barBounds.left, barBounds.right);
return new DatumDetails<D>(
series: bar.series,
datum: bar.datum,
domain: bar.domainValue,
domainDistance: segmentDomainDistance,
measureDistance: segmentMeasureDistance,
);
}));
}
double _getDistance(int point, int min, int max) {
if (max >= point && min <= point) {
return 0.0;
}
return (point > max ? (point - max) : (min - point)).toDouble();
}
/// Gets the iterator for the series based grouped/stacked and orientation.
///
/// For vertical stacked bars:
/// * If grouped, return the iterator that keeps the category order but
/// reverse the order of the series so the first series is on the top of the
/// stack.
/// * Otherwise, return iterator of the reversed list
///
/// All other types, use the in order iterator.
@protected
Iterable<S> getOrderedSeriesList<S extends ImmutableSeries>(
List<S> seriesList) {
return (renderingVertically && config.stacked)
? config.grouped
? new _ReversedSeriesIterable(seriesList)
: seriesList.reversed
: seriesList;
}
bool get isRtl => chart.context.isRtl;
}
/// Iterable wrapping the seriesList that returns the ReversedSeriesItertor.
class _ReversedSeriesIterable<S extends ImmutableSeries> extends Iterable<S> {
final List<S> seriesList;
_ReversedSeriesIterable(this.seriesList);
@override
Iterator<S> get iterator => new _ReversedSeriesIterator(seriesList);
}
/// Iterator that keeps reverse series order but keeps category order.
///
/// This is needed because for grouped stacked bars, the category stays in the
/// order it was passed in for the grouping, but the series is flipped so that
/// the first series of that category is on the top of the stack.
class _ReversedSeriesIterator<S extends ImmutableSeries> extends Iterator<S> {
final List<S> _list;
final _visitIndex = <int>[];
int _current;
_ReversedSeriesIterator(List<S> list) : _list = list {
// In the order of the list, save the category and the indices of the series
// with the same category.
final categoryAndSeriesIndexMap = <String, List<int>>{};
for (var i = 0; i < list.length; i++) {
categoryAndSeriesIndexMap
.putIfAbsent(list[i].seriesCategory, () => <int>[])
.add(i);
}
// Creates a visit that is categories in order, but the series is reversed.
categoryAndSeriesIndexMap
.forEach((_, indices) => _visitIndex.addAll(indices.reversed));
}
@override
bool moveNext() {
_current = (_current == null) ? 0 : _current + 1;
return _current < _list.length;
}
@override
S get current => _list[_visitIndex[_current]];
}

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:collection/collection.dart' show ListEquality;
import '../../common/symbol_renderer.dart'
show SymbolRenderer, RoundedRectSymbolRenderer;
import '../common/chart_canvas.dart' show FillPatternType;
import '../common/series_renderer_config.dart'
show RendererAttributes, SeriesRendererConfig;
import '../layout/layout_view.dart' show LayoutViewConfig;
/// Shared configuration for bar chart renderers.
///
/// Bar renderers support 4 different modes of rendering multiple series on the
/// chart, configured by the grouped and stacked flags.
/// * grouped - Render bars for each series that shares a domain value
/// side-by-side.
/// * stacked - Render bars for each series that shares a domain value in a
/// stack, ordered in the same order as the series list.
/// * grouped-stacked: Render bars for each series that shares a domain value in
/// a group of bar stacks. Each stack will contain all the series that share a
/// series category.
/// * floating style - When grouped and stacked are both false, all bars that
/// share a domain value will be rendered in the same domain space. Each datum
/// should be configured with a measure offset to position its bar along the
/// measure axis. Bars will freely overlap if their measure values and measure
/// offsets overlap. Note that bars for each series will be rendered in order,
/// such that bars from the last series will be "on top" of bars from previous
/// series.
abstract class BaseBarRendererConfig<D> extends LayoutViewConfig
implements SeriesRendererConfig<D> {
final String customRendererId;
final SymbolRenderer symbolRenderer;
/// Dash pattern for the stroke line around the edges of the bar.
final List<int> dashPattern;
/// Defines the way multiple series of bars are rendered per domain.
final BarGroupingType groupingType;
/// The order to paint this renderer on the canvas.
final int layoutPaintOrder;
final int minBarLengthPx;
final FillPatternType fillPattern;
final double stackHorizontalSeparator;
/// Stroke width of the target line.
final double strokeWidthPx;
/// Sets the series weight pattern. This is a pattern of weights used to
/// calculate the width of bars within a bar group. If not specified, each bar
/// in the group will have an equal width.
///
/// The pattern will not repeat. If more series are assigned to the renderer
/// than there are segments in the weight pattern, an error will be thrown.
///
/// e.g. For the pattern [2, 1], the first bar in a group should be rendered
/// twice as wide as the second bar.
///
/// If the expected bar width of the chart is 12px, then the first bar will
/// render at 16px and the second will render at 8px. The default weight
/// pattern of null means that all bars should be the same width, or 12px in
/// this case.
///
/// Not used for stacked bars.
final List<int> weightPattern;
final rendererAttributes = new RendererAttributes();
BaseBarRendererConfig(
{this.customRendererId,
this.dashPattern,
this.groupingType = BarGroupingType.grouped,
this.layoutPaintOrder,
this.minBarLengthPx = 0,
this.fillPattern,
this.stackHorizontalSeparator,
this.strokeWidthPx = 0.0,
SymbolRenderer symbolRenderer,
this.weightPattern})
: this.symbolRenderer = symbolRenderer ?? new RoundedRectSymbolRenderer();
/// Whether or not the bars should be organized into groups.
bool get grouped =>
groupingType == BarGroupingType.grouped ||
groupingType == BarGroupingType.groupedStacked;
/// Whether or not the bars should be organized into stacks.
bool get stacked =>
groupingType == BarGroupingType.stacked ||
groupingType == BarGroupingType.groupedStacked;
@override
bool operator ==(other) {
if (identical(this, other)) {
return true;
}
if (!(other is BaseBarRendererConfig)) {
return false;
}
return other.customRendererId == customRendererId &&
other.dashPattern == dashPattern &&
other.fillPattern == fillPattern &&
other.groupingType == groupingType &&
other.minBarLengthPx == minBarLengthPx &&
other.stackHorizontalSeparator == stackHorizontalSeparator &&
other.strokeWidthPx == strokeWidthPx &&
other.symbolRenderer == symbolRenderer &&
new ListEquality().equals(other.weightPattern, weightPattern);
}
int get hashcode {
var hash = 1;
hash = hash * 31 + (customRendererId?.hashCode ?? 0);
hash = hash * 31 + (dashPattern?.hashCode ?? 0);
hash = hash * 31 + (fillPattern?.hashCode ?? 0);
hash = hash * 31 + (groupingType?.hashCode ?? 0);
hash = hash * 31 + (minBarLengthPx?.hashCode ?? 0);
hash = hash * 31 + (stackHorizontalSeparator?.hashCode ?? 0);
hash = hash * 31 + (strokeWidthPx?.hashCode ?? 0);
hash = hash * 31 + (symbolRenderer?.hashCode ?? 0);
hash = hash * 31 + (weightPattern?.hashCode ?? 0);
return hash;
}
}
/// Defines the way multiple series of bars are renderered per domain.
///
/// * [grouped] - Render bars for each series that shares a domain value
/// side-by-side.
/// * [stacked] - Render bars for each series that shares a domain value in a
/// stack, ordered in the same order as the series list.
/// * [groupedStacked]: Render bars for each series that shares a domain value
/// in a group of bar stacks. Each stack will contain all the series that
/// share a series category.
enum BarGroupingType { grouped, groupedStacked, stacked }

View File

@@ -0,0 +1,129 @@
// 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 '../../common/color.dart' show Color;
import '../common/chart_canvas.dart' show getAnimatedColor, FillPatternType;
import '../common/processed_series.dart' show ImmutableSeries;
abstract class BaseBarRendererElement {
int barStackIndex;
Color color;
num cumulativeTotal;
List<int> dashPattern;
Color fillColor;
FillPatternType fillPattern;
double measureAxisPosition;
num measureOffset;
num measureOffsetPlusMeasure;
double strokeWidthPx;
bool measureIsNull;
bool measureIsNegative;
BaseBarRendererElement();
BaseBarRendererElement.clone(BaseBarRendererElement other) {
barStackIndex = other.barStackIndex;
color =
other.color != null ? new Color.fromOther(color: other.color) : null;
cumulativeTotal = other.cumulativeTotal;
dashPattern = other.dashPattern;
fillColor = other.fillColor != null
? new Color.fromOther(color: other.fillColor)
: null;
fillPattern = other.fillPattern;
measureAxisPosition = other.measureAxisPosition;
measureOffset = other.measureOffset;
measureOffsetPlusMeasure = other.measureOffsetPlusMeasure;
strokeWidthPx = other.strokeWidthPx;
measureIsNull = other.measureIsNull;
measureIsNegative = other.measureIsNegative;
}
void updateAnimationPercent(BaseBarRendererElement previous,
BaseBarRendererElement target, double animationPercent) {
color = getAnimatedColor(previous.color, target.color, animationPercent);
fillColor = getAnimatedColor(
previous.fillColor, target.fillColor, animationPercent);
measureIsNull = target.measureIsNull;
measureIsNegative = target.measureIsNegative;
}
}
abstract class BaseAnimatedBar<D, R extends BaseBarRendererElement> {
final String key;
dynamic datum;
ImmutableSeries<D> series;
D domainValue;
R _previousBar;
R _targetBar;
R _currentBar;
// Flag indicating whether this bar is being animated out of the chart.
bool animatingOut = false;
BaseAnimatedBar({this.key, this.datum, this.series, this.domainValue});
/// Animates a bar that was removed from the series out of the view.
///
/// This should be called in place of "setNewTarget" for bars that represent
/// data that has been removed from the series.
///
/// Animates the height of the bar down to the measure axis position (position
/// of 0). Animates the width of the bar down to 0, centered in the middle of
/// the original bar width.
void animateOut() {
var newTarget = clone(_currentBar);
animateElementToMeasureAxisPosition(newTarget);
setNewTarget(newTarget);
animatingOut = true;
}
/// Sets the bounds for the target to the measure axis position.
void animateElementToMeasureAxisPosition(R target);
/// Sets a new element to render.
void setNewTarget(R newTarget) {
animatingOut = false;
_currentBar ??= clone(newTarget);
_previousBar = clone(_currentBar);
_targetBar = newTarget;
}
R get currentBar => _currentBar;
R get previousBar => _previousBar;
R get targetBar => _targetBar;
/// Gets the new state of the bar element for painting, updated for a
/// transition between the previous state and the new animationPercent.
R getCurrentBar(double animationPercent) {
if (animationPercent == 1.0 || _previousBar == null) {
_currentBar = _targetBar;
_previousBar = _targetBar;
return _currentBar;
}
_currentBar.updateAnimationPercent(
_previousBar, _targetBar, animationPercent);
return _currentBar;
}
R clone(R bar);
}

View File

@@ -0,0 +1,594 @@
// 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, min, max;
import 'package:meta/meta.dart' show protected, visibleForTesting;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/text_element.dart' show TextElement;
import '../../../data/series.dart' show AttributeKey;
import '../../common/chart_canvas.dart' show ChartCanvas;
import '../../common/chart_context.dart' show ChartContext;
import '../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
ViewMeasuredSizes;
import 'axis_tick.dart' show AxisTicks;
import 'draw_strategy/small_tick_draw_strategy.dart' show SmallTickDrawStrategy;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'linear/linear_scale.dart' show LinearScale;
import 'numeric_extents.dart' show NumericExtents;
import 'numeric_scale.dart' show NumericScale;
import 'numeric_tick_provider.dart' show NumericTickProvider;
import 'ordinal_tick_provider.dart' show OrdinalTickProvider;
import 'scale.dart'
show MutableScale, RangeBandConfig, ScaleOutputExtent, Scale;
import 'simple_ordinal_scale.dart' show SimpleOrdinalScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart'
show TickFormatter, OrdinalTickFormatter, NumericTickFormatter;
import 'tick_provider.dart' show TickProvider;
const measureAxisIdKey = const AttributeKey<String>('Axis.measureAxisId');
const measureAxisKey = const AttributeKey<Axis>('Axis.measureAxis');
const domainAxisKey = const AttributeKey<Axis>('Axis.domainAxis');
/// Orientation of an Axis.
enum AxisOrientation { top, right, bottom, left }
abstract class ImmutableAxis<D> {
/// Compare domain to the viewport.
///
/// 0 if the domain is in the viewport.
/// 1 if the domain is to the right of the viewport.
/// -1 if the domain is to the left of the viewport.
int compareDomainValueToViewport(D domain);
/// Get location for the domain.
double getLocation(D domain);
D getDomain(double location);
/// Rangeband for this axis.
double get rangeBand;
/// Step size for this axis.
double get stepSize;
/// Output range for this axis.
ScaleOutputExtent get range;
}
abstract class Axis<D> extends ImmutableAxis<D> implements LayoutView {
static const primaryMeasureAxisId = 'primaryMeasureAxisId';
static const secondaryMeasureAxisId = 'secondaryMeasureAxisId';
final MutableScale<D> _scale;
/// [Scale] of this axis.
@protected
MutableScale<D> get scale => _scale;
/// Previous [Scale] of this axis, used to calculate tick animation.
MutableScale<D> _previousScale;
TickProvider<D> _tickProvider;
/// [TickProvider] for this axis.
TickProvider<D> get tickProvider => _tickProvider;
set tickProvider(TickProvider<D> tickProvider) {
_tickProvider = tickProvider;
}
/// [TickFormatter] for this axis.
TickFormatter<D> _tickFormatter;
set tickFormatter(TickFormatter<D> formatter) {
if (_tickFormatter != formatter) {
_tickFormatter = formatter;
_formatterValueCache.clear();
}
}
TickFormatter<D> get tickFormatter => _tickFormatter;
final _formatterValueCache = <D, String>{};
/// [TickDrawStrategy] for this axis.
TickDrawStrategy<D> tickDrawStrategy;
/// [AxisOrientation] for this axis.
AxisOrientation axisOrientation;
ChartContext context;
/// If the output range should be reversed.
bool reverseOutputRange = false;
/// Whether or not the axis will configure the viewport to have "niced" ticks
/// around the domain values.
bool _autoViewport = true;
/// If the axis line should always be drawn.
bool forceDrawAxisLine;
/// If true, do not allow axis to be modified.
///
/// Ticks (including their location) are not updated.
/// Viewport changes not allowed.
bool lockAxis = false;
/// Ticks provided by the tick provider.
List<Tick> _providedTicks;
/// Ticks used by the axis for drawing.
final _axisTicks = <AxisTicks<D>>[];
Rectangle<int> _componentBounds;
Rectangle<int> _drawAreaBounds;
GraphicsFactory _graphicsFactory;
/// Order for chart layout painting.
///
/// In general, domain axes should be drawn on top of measure axes to ensure
/// that the domain axis line appears on top of any measure axis grid lines.
int layoutPaintOrder = LayoutViewPaintOrder.measureAxis;
Axis(
{TickProvider<D> tickProvider,
TickFormatter<D> tickFormatter,
MutableScale<D> scale})
: this._scale = scale,
this._tickProvider = tickProvider,
this._tickFormatter = tickFormatter;
@protected
MutableScale<D> get mutableScale => _scale;
/// Rangeband for this axis.
@override
double get rangeBand => _scale.rangeBand;
@override
double get stepSize => _scale.stepSize;
@override
ScaleOutputExtent get range => _scale.range;
/// Configures whether the viewport should be reset back to default values
/// when the domain is reset.
///
/// This should generally be disabled when the viewport will be managed
/// externally, e.g. from pan and zoom behaviors.
set autoViewport(bool autoViewport) {
_autoViewport = autoViewport;
}
bool get autoViewport => _autoViewport;
void setRangeBandConfig(RangeBandConfig rangeBandConfig) {
mutableScale.rangeBandConfig = rangeBandConfig;
}
void addDomainValue(D domain) {
if (lockAxis) {
return;
}
_scale.addDomain(domain);
}
void resetDomains() {
if (lockAxis) {
return;
}
// If the series list changes, clear the cache.
//
// There are cases where tick formatter has not "changed", but if measure
// formatter provided to the tick formatter uses a closure value, the
// formatter cache needs to be cleared.
//
// This type of use case for the measure formatter surfaced where the series
// list also changes. So this is a round about way to also clear the
// tick formatter cache.
//
// TODO: Measure formatter should be changed from a typedef to
// a concrete class to force users to create a new tick formatter when
// formatting is different, so we can recognize when the tick formatter is
// changed and then clear cache accordingly.
//
// Remove this when bug above is fixed, and verify it did not cause
// regression for b/110371453.
_formatterValueCache.clear();
_scale.resetDomain();
reverseOutputRange = false;
if (_autoViewport) {
_scale.resetViewportSettings();
}
// TODO: Reset rangeband and step size when we port over config
//scale.rangeBandConfig = get range band config
//scale.stepSizeConfig = get step size config
}
@override
double getLocation(D domain) => domain != null ? _scale[domain] : null;
@override
D getDomain(double location) => _scale.reverse(location);
@override
int compareDomainValueToViewport(D domain) {
return _scale.compareDomainValueToViewport(domain);
}
void setOutputRange(int start, int end) {
_scale.range = new ScaleOutputExtent(start, end);
}
/// Request update ticks from tick provider and update the painted ticks.
void updateTicks() {
_updateProvidedTicks();
_updateAxisTicks();
}
/// Request ticks from tick provider.
void _updateProvidedTicks() {
if (lockAxis) {
return;
}
// TODO: Ensure that tick providers take manually configured
// viewport settings into account, so that we still get the right number.
_providedTicks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: _scale,
formatter: tickFormatter,
formatterValueCache: _formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
orientation: axisOrientation,
viewportExtensionEnabled: _autoViewport);
}
/// Updates the ticks that are actually used for drawing.
void _updateAxisTicks() {
if (lockAxis) {
return;
}
final providedTicks = new List.from(_providedTicks ?? []);
for (AxisTicks<D> animatedTick in _axisTicks) {
final tick = providedTicks?.firstWhere(
(t) => t.value == animatedTick.value,
orElse: () => null);
if (tick != null) {
// Swap out the text element only if the settings are different.
// This prevents a costly new TextPainter in Flutter.
if (!TextElement.elementSettingsSame(
animatedTick.textElement, tick.textElement)) {
animatedTick.textElement = tick.textElement;
}
// Update target for all existing ticks
animatedTick.setNewTarget(_scale[tick.value]);
providedTicks.remove(tick);
} else {
// Animate out ticks that do not exist any more.
animatedTick.animateOut(_scale[animatedTick.value].toDouble());
}
}
// Add new ticks
providedTicks?.forEach((tick) {
final animatedTick = new AxisTicks<D>(tick);
if (_previousScale != null) {
animatedTick.animateInFrom(_previousScale[tick.value].toDouble());
}
_axisTicks.add(animatedTick);
});
_axisTicks.sort();
// Save a copy of the current scale to be used as the previous scale when
// ticks are updated.
_previousScale = _scale.copy();
}
/// Configures the zoom and translate.
///
/// [viewportScale] is the zoom factor to use, likely >= 1.0 where 1.0 maps
/// the complete data extents to the output range, and 2.0 only maps half the
/// data to the output range.
///
/// [viewportTranslatePx] is the translate/pan to use in pixel units,
/// likely <= 0 which shifts the start of the data before the edge of the
/// chart giving us a pan.
///
/// [drawAreaWidth] is the width of the draw area for the series data in pixel
/// units, at minimum viewport scale level (1.0). When provided,
/// [viewportTranslatePx] will be clamped such that the axis cannot be panned
/// beyond the bounds of the data.
void setViewportSettings(double viewportScale, double viewportTranslatePx,
{int drawAreaWidth}) {
// Don't let the viewport be panned beyond the bounds of the data.
viewportTranslatePx = _clampTranslatePx(viewportScale, viewportTranslatePx,
drawAreaWidth: drawAreaWidth);
_scale.setViewportSettings(viewportScale, viewportTranslatePx);
}
/// Returns the current viewport scale.
///
/// A scale of 1.0 would map the data directly to the output range, while a
/// value of 2.0 would map the data to an output of double the range so you
/// only see half the data in the viewport. This is the equivalent to
/// zooming. Its value is likely >= 1.0.
double get viewportScalingFactor => _scale.viewportScalingFactor;
/// Returns the current pixel viewport offset
///
/// The translate is used by the scale function when it applies the scale.
/// This is the equivalent to panning. Its value is likely <= 0 to pan the
/// data to the left.
double get viewportTranslatePx => _scale?.viewportTranslatePx;
/// Clamps a possible change in domain translation to fit within the range of
/// the data.
double _clampTranslatePx(
double viewportScalingFactor, double viewportTranslatePx,
{int drawAreaWidth}) {
if (drawAreaWidth == null) {
return viewportTranslatePx;
}
// Bound the viewport translate to the range of the data.
final maxNegativeTranslate =
-1.0 * ((drawAreaWidth * viewportScalingFactor) - drawAreaWidth);
viewportTranslatePx =
min(max(viewportTranslatePx, maxNegativeTranslate), 0.0);
return viewportTranslatePx;
}
//
// LayoutView methods.
//
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
@override
LayoutViewConfig get layoutConfig => new LayoutViewConfig(
paintOrder: layoutPaintOrder,
position: _layoutPosition,
positionOrder: LayoutViewPositionOrder.axis);
/// Get layout position from axis orientation.
LayoutPosition get _layoutPosition {
LayoutPosition position;
switch (axisOrientation) {
case AxisOrientation.top:
position = LayoutPosition.Top;
break;
case AxisOrientation.right:
position = LayoutPosition.Right;
break;
case AxisOrientation.bottom:
position = LayoutPosition.Bottom;
break;
case AxisOrientation.left:
position = LayoutPosition.Left;
break;
}
return position;
}
/// The axis is rendered vertically.
bool get isVertical =>
axisOrientation == AxisOrientation.left ||
axisOrientation == AxisOrientation.right;
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
return isVertical
? _measureVerticalAxis(maxWidth, maxHeight)
: _measureHorizontalAxis(maxWidth, maxHeight);
}
ViewMeasuredSizes _measureVerticalAxis(int maxWidth, int maxHeight) {
setOutputRange(maxHeight, 0);
_updateProvidedTicks();
return tickDrawStrategy.measureVerticallyDrawnTicks(
_providedTicks, maxWidth, maxHeight);
}
ViewMeasuredSizes _measureHorizontalAxis(int maxWidth, int maxHeight) {
setOutputRange(0, maxWidth);
_updateProvidedTicks();
return tickDrawStrategy.measureHorizontallyDrawnTicks(
_providedTicks, maxWidth, maxHeight);
}
/// Layout this component.
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
_componentBounds = componentBounds;
_drawAreaBounds = drawAreaBounds;
// Update the output range if it is different than the current one.
// This is necessary because during the measure cycle, the output range is
// set between zero and the max range available. On layout, the output range
// needs to be updated to account of the offset of the axis view.
final outputStart =
isVertical ? _componentBounds.bottom : _componentBounds.left;
final outputEnd =
isVertical ? _componentBounds.top : _componentBounds.right;
final outputRange = reverseOutputRange
? new ScaleOutputExtent(outputEnd, outputStart)
: new ScaleOutputExtent(outputStart, outputEnd);
if (_scale.range != outputRange) {
_scale.range = outputRange;
}
_updateProvidedTicks();
// Update animated ticks in layout, because updateTicks are called during
// measure and we don't want to update the animation at that time.
_updateAxisTicks();
}
@override
bool get isSeriesRenderer => false;
@override
Rectangle<int> get componentBounds => this._componentBounds;
bool get drawAxisLine {
if (forceDrawAxisLine != null) {
return forceDrawAxisLine;
}
return tickDrawStrategy is SmallTickDrawStrategy;
}
@override
void paint(ChartCanvas canvas, double animationPercent) {
if (animationPercent == 1.0) {
_axisTicks.removeWhere((t) => t.markedForRemoval);
}
for (var i = 0; i < _axisTicks.length; i++) {
final animatedTick = _axisTicks[i];
tickDrawStrategy.draw(
canvas, animatedTick..setCurrentTick(animationPercent),
orientation: axisOrientation,
axisBounds: _componentBounds,
drawAreaBounds: _drawAreaBounds,
isFirst: i == 0,
isLast: i == _axisTicks.length - 1);
}
if (drawAxisLine) {
tickDrawStrategy.drawAxisLine(canvas, axisOrientation, _componentBounds);
}
}
}
class NumericAxis extends Axis<num> {
NumericAxis({TickProvider<num> tickProvider})
: super(
tickProvider: tickProvider ?? new NumericTickProvider(),
tickFormatter: new NumericTickFormatter(),
scale: new LinearScale(),
);
void setScaleViewport(NumericExtents viewport) {
autoViewport = false;
(_scale as NumericScale).viewportDomain = viewport;
}
}
class OrdinalAxis extends Axis<String> {
OrdinalAxis({
TickDrawStrategy tickDrawStrategy,
TickProvider tickProvider,
TickFormatter tickFormatter,
}) : super(
tickProvider: tickProvider ?? const OrdinalTickProvider(),
tickFormatter: tickFormatter ?? const OrdinalTickFormatter(),
scale: new SimpleOrdinalScale(),
);
void setScaleViewport(OrdinalViewport viewport) {
autoViewport = false;
(_scale as SimpleOrdinalScale)
.setViewport(viewport.dataSize, viewport.startingDomain);
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
super.layout(componentBounds, drawAreaBounds);
// We are purposely clearing the viewport starting domain and data size
// post layout.
//
// Originally we set a flag in [setScaleViewport] to recalculate viewport
// settings on next scale update and then reset the flag. This doesn't work
// because chart's measure cycle provides different ranges to the scale,
// causing the scale to update multiple times before it is finalized after
// layout.
//
// By resetting the viewport after layout, we guarantee the correct range
// was used to apply the viewport and behaviors that update the viewport
// based on translate and scale changes will not be affected (pan/zoom).
(_scale as SimpleOrdinalScale).setViewport(null, null);
}
}
/// Viewport to cover [dataSize] data points starting at [startingDomain] value.
class OrdinalViewport {
final String startingDomain;
final int dataSize;
OrdinalViewport(this.startingDomain, this.dataSize);
@override
bool operator ==(Object other) {
return other is OrdinalViewport &&
startingDomain == other.startingDomain &&
dataSize == other.dataSize;
}
@override
int get hashCode {
int hashcode = startingDomain.hashCode;
hashcode = (hashcode * 37) + dataSize;
return hashcode;
}
}
@visibleForTesting
class AxisTester<D> {
final Axis<D> _axis;
AxisTester(this._axis);
List<AxisTicks<D>> get axisTicks => _axis._axisTicks;
MutableScale<D> get scale => _axis._scale;
List<D> get axisValues => axisTicks.map((t) => t.value).toList();
}

View File

@@ -0,0 +1,112 @@
// 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 'tick.dart' show Tick;
class AxisTicks<D> extends Tick<D> implements Comparable<AxisTicks<D>> {
/// This tick is being animated out.
bool _markedForRemoval;
/// This tick's current location.
double _currentLocation;
/// This tick's previous target location.
double _previousLocation;
/// This tick's current target location.
double _targetLocation;
/// This tick's current opacity.
double _currentOpacity;
/// This tick's previous opacity.
double _previousOpacity;
/// This tick's target opacity.
double _targetOpacity;
AxisTicks(Tick<D> tick)
: super(
value: tick.value,
textElement: tick.textElement,
locationPx: tick.locationPx,
labelOffsetPx: tick.labelOffsetPx) {
/// Set the initial target for a new animated tick.
_markedForRemoval = false;
_targetLocation = tick.locationPx;
}
bool get markedForRemoval => _markedForRemoval;
/// Animate the tick in from [previousLocation].
void animateInFrom(double previousLocation) {
_markedForRemoval = false;
_previousLocation = previousLocation;
_previousOpacity = 0.0;
_targetOpacity = 1.0;
}
/// Animate out this tick to [newLocation].
void animateOut(double newLocation) {
_markedForRemoval = true;
_previousLocation = _currentLocation;
_targetLocation = newLocation;
_previousOpacity = _currentOpacity;
_targetOpacity = 0.0;
}
/// Set new target for this tick to be [newLocation].
void setNewTarget(double newLocation) {
_markedForRemoval = false;
_previousLocation = _currentLocation;
_targetLocation = newLocation;
_previousOpacity = _currentOpacity;
_targetOpacity = 1.0;
}
/// Update tick's location and opacity based on animation percent.
void setCurrentTick(double animationPercent) {
if (animationPercent == 1.0) {
_currentLocation = _targetLocation;
_previousLocation = _targetLocation;
_currentOpacity = markedForRemoval ? 0.0 : 1.0;
} else if (_previousLocation == null) {
_currentLocation = _targetLocation;
_currentOpacity = 1.0;
} else {
_currentLocation =
_lerpDouble(_previousLocation, _targetLocation, animationPercent);
_currentOpacity =
_lerpDouble(_previousOpacity, _targetOpacity, animationPercent);
}
locationPx = _currentLocation;
textElement.opacity = _currentOpacity;
}
/// Linearly interpolate between two numbers.
///
/// From lerpDouble in dart:ui which is Flutter only.
double _lerpDouble(double a, double b, double t) {
if (a == null && b == null) return null;
a ??= 0.0;
b ??= 0.0;
return a + (b - a) * t;
}
int compareTo(AxisTicks<D> other) {
return _targetLocation.compareTo(other._targetLocation);
}
}

View File

@@ -0,0 +1,38 @@
// 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:meta/meta.dart' show required;
import 'tick.dart' show Tick;
/// A report that contains a list of ticks and if they collide.
class CollisionReport {
/// If [ticks] collide.
final bool ticksCollide;
final List<Tick> ticks;
final bool alternateTicksUsed;
CollisionReport(
{@required this.ticksCollide,
@required this.ticks,
bool alternateTicksUsed})
: alternateTicksUsed = alternateTicksUsed ?? false;
CollisionReport.empty()
: ticksCollide = false,
ticks = [],
alternateTicksUsed = false;
}

View File

@@ -0,0 +1,436 @@
// 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:meta/meta.dart' show immutable, protected, required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/line_style.dart' show LineStyle;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../../common/text_element.dart' show TextDirection;
import '../../../../common/text_style.dart' show TextStyle;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../common/chart_context.dart' show ChartContext;
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
import '../axis.dart' show AxisOrientation;
import '../collision_report.dart' show CollisionReport;
import '../spec/axis_spec.dart'
show
TextStyleSpec,
TickLabelAnchor,
TickLabelJustification,
LineStyleSpec,
RenderSpec;
import '../tick.dart' show Tick;
import 'tick_draw_strategy.dart' show TickDrawStrategy;
@immutable
abstract class BaseRenderSpec<D> implements RenderSpec<D> {
final TextStyleSpec labelStyle;
final TickLabelAnchor labelAnchor;
final TickLabelJustification labelJustification;
final int labelOffsetFromAxisPx;
/// Absolute distance from the tick to the text if using start/end
final int labelOffsetFromTickPx;
final int minimumPaddingBetweenLabelsPx;
final LineStyleSpec axisLineStyle;
const BaseRenderSpec({
this.labelStyle,
this.labelAnchor,
this.labelJustification,
this.labelOffsetFromAxisPx,
this.labelOffsetFromTickPx,
this.minimumPaddingBetweenLabelsPx,
this.axisLineStyle,
});
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is BaseRenderSpec &&
labelStyle == other.labelStyle &&
labelAnchor == other.labelAnchor &&
labelJustification == other.labelJustification &&
labelOffsetFromTickPx == other.labelOffsetFromTickPx &&
labelOffsetFromAxisPx == other.labelOffsetFromAxisPx &&
minimumPaddingBetweenLabelsPx ==
other.minimumPaddingBetweenLabelsPx &&
axisLineStyle == other.axisLineStyle);
}
@override
int get hashCode {
int hashcode = labelStyle?.hashCode ?? 0;
hashcode = (hashcode * 37) + labelAnchor?.hashCode ?? 0;
hashcode = (hashcode * 37) + labelJustification?.hashCode ?? 0;
hashcode = (hashcode * 37) + labelOffsetFromTickPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + labelOffsetFromAxisPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + minimumPaddingBetweenLabelsPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + axisLineStyle?.hashCode ?? 0;
return hashcode;
}
}
/// Base strategy that draws tick labels and checks for label collisions.
abstract class BaseTickDrawStrategy<D> implements TickDrawStrategy<D> {
final ChartContext chartContext;
LineStyle axisLineStyle;
TextStyle labelStyle;
TickLabelAnchor tickLabelAnchor;
TickLabelJustification tickLabelJustification;
int labelOffsetFromAxisPx;
int labelOffsetFromTickPx;
int minimumPaddingBetweenLabelsPx;
BaseTickDrawStrategy(this.chartContext, GraphicsFactory graphicsFactory,
{TextStyleSpec labelStyleSpec,
LineStyleSpec axisLineStyleSpec,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
int minimumPaddingBetweenLabelsPx}) {
labelStyle = (graphicsFactory.createTextPaint()
..color = labelStyleSpec?.color ?? StyleFactory.style.tickColor
..fontFamily = labelStyleSpec?.fontFamily
..fontSize = labelStyleSpec?.fontSize ?? 12);
axisLineStyle = graphicsFactory.createLinePaint()
..color = axisLineStyleSpec?.color ?? labelStyle.color
..dashPattern = axisLineStyleSpec?.dashPattern
..strokeWidth = axisLineStyleSpec?.thickness ?? 1;
tickLabelAnchor = labelAnchor ?? TickLabelAnchor.centered;
tickLabelJustification =
labelJustification ?? TickLabelJustification.inside;
this.labelOffsetFromAxisPx = labelOffsetFromAxisPx ?? 5;
this.labelOffsetFromTickPx = labelOffsetFromTickPx ?? 5;
this.minimumPaddingBetweenLabelsPx = minimumPaddingBetweenLabelsPx ?? 50;
}
@override
void decorateTicks(List<Tick<D>> ticks) {
for (Tick<D> tick in ticks) {
// If no style at all, set the default style.
if (tick.textElement.textStyle == null) {
tick.textElement.textStyle = labelStyle;
} else {
//Fill in whatever is missing
tick.textElement.textStyle.color ??= labelStyle.color;
tick.textElement.textStyle.fontFamily ??= labelStyle.fontFamily;
tick.textElement.textStyle.fontSize ??= labelStyle.fontSize;
}
}
}
@override
CollisionReport collides(List<Tick<D>> ticks, AxisOrientation orientation) {
// If there are no ticks, they do not collide.
if (ticks == null) {
return new CollisionReport(
ticksCollide: false, ticks: ticks, alternateTicksUsed: false);
}
final vertical = orientation == AxisOrientation.left ||
orientation == AxisOrientation.right;
// First sort ticks by smallest locationPx first (NOT sorted by value).
// This allows us to only check if a tick collides with the previous tick.
ticks.sort((a, b) {
if (a.locationPx < b.locationPx) {
return -1;
} else if (a.locationPx > b.locationPx) {
return 1;
} else {
return 0;
}
});
double previousEnd = double.negativeInfinity;
bool collides = false;
for (final tick in ticks) {
final tickSize = tick.textElement.measurement;
if (vertical) {
final adjustedHeight =
tickSize.verticalSliceWidth + minimumPaddingBetweenLabelsPx;
if (tickLabelAnchor == TickLabelAnchor.inside) {
if (identical(tick, ticks.first)) {
// Top most tick draws down from the location
collides = false;
previousEnd = tick.locationPx + adjustedHeight;
} else if (identical(tick, ticks.last)) {
// Bottom most tick draws up from the location
collides = previousEnd > tick.locationPx - adjustedHeight;
previousEnd = tick.locationPx;
} else {
// All other ticks is centered.
final halfHeight = adjustedHeight / 2;
collides = previousEnd > tick.locationPx - halfHeight;
previousEnd = tick.locationPx + halfHeight;
}
} else {
collides = previousEnd > tick.locationPx;
previousEnd = tick.locationPx + adjustedHeight;
}
} else {
// Use the text direction the ticks specified, unless the label anchor
// is set to [TickLabelAnchor.inside]. When 'inside' is set, the text
// direction is normalized such that the left most tick is drawn ltr,
// the last tick is drawn rtl, and all other ticks are in the center.
// This is not set until it is painted, so collision check needs to get
// the value also.
final textDirection = _normalizeHorizontalAnchor(
tickLabelAnchor,
chartContext.isRtl,
identical(tick, ticks.first),
identical(tick, ticks.last));
final adjustedWidth =
tickSize.horizontalSliceWidth + minimumPaddingBetweenLabelsPx;
switch (textDirection) {
case TextDirection.ltr:
collides = previousEnd > tick.locationPx;
previousEnd = tick.locationPx + adjustedWidth;
break;
case TextDirection.rtl:
collides = previousEnd > (tick.locationPx - adjustedWidth);
previousEnd = tick.locationPx;
break;
case TextDirection.center:
final halfWidth = adjustedWidth / 2;
collides = previousEnd > tick.locationPx - halfWidth;
previousEnd = tick.locationPx + halfWidth;
break;
}
}
if (collides) {
return new CollisionReport(
ticksCollide: true, ticks: ticks, alternateTicksUsed: false);
}
}
return new CollisionReport(
ticksCollide: false, ticks: ticks, alternateTicksUsed: false);
}
@override
ViewMeasuredSizes measureVerticallyDrawnTicks(
List<Tick<D>> ticks, int maxWidth, int maxHeight) {
// TODO: Add spacing to account for the distance between the
// text and the axis baseline (even if it isn't drawn).
final maxHorizontalSliceWidth = ticks
.fold(
0.0,
(double prevMax, tick) => max(
prevMax,
tick.textElement.measurement.horizontalSliceWidth +
labelOffsetFromAxisPx))
.round();
return new ViewMeasuredSizes(
preferredWidth: maxHorizontalSliceWidth, preferredHeight: maxHeight);
}
@override
ViewMeasuredSizes measureHorizontallyDrawnTicks(
List<Tick<D>> ticks, int maxWidth, int maxHeight) {
final maxVerticalSliceWidth = ticks
.fold(
0.0,
(double prevMax, tick) =>
max(prevMax, tick.textElement.measurement.verticalSliceWidth))
.round();
return new ViewMeasuredSizes(
preferredWidth: maxWidth,
preferredHeight: maxVerticalSliceWidth + labelOffsetFromAxisPx);
}
@override
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
Rectangle<int> axisBounds) {
Point<num> start;
Point<num> end;
switch (orientation) {
case AxisOrientation.top:
start = axisBounds.bottomLeft;
end = axisBounds.bottomRight;
break;
case AxisOrientation.bottom:
start = axisBounds.topLeft;
end = axisBounds.topRight;
break;
case AxisOrientation.right:
start = axisBounds.topLeft;
end = axisBounds.bottomLeft;
break;
case AxisOrientation.left:
start = axisBounds.topRight;
end = axisBounds.bottomRight;
break;
}
canvas.drawLine(
points: [start, end],
fill: axisLineStyle.color,
stroke: axisLineStyle.color,
strokeWidthPx: axisLineStyle.strokeWidth.toDouble(),
dashPattern: axisLineStyle.dashPattern,
);
}
@protected
void drawLabel(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast}) {
final locationPx = tick.locationPx;
final measurement = tick.textElement.measurement;
final isRtl = chartContext.isRtl;
int x = 0;
int y = 0;
final labelOffsetPx = tick.labelOffsetPx ?? 0;
if (orientation == AxisOrientation.bottom ||
orientation == AxisOrientation.top) {
y = orientation == AxisOrientation.bottom
? axisBounds.top + labelOffsetFromAxisPx
: axisBounds.bottom -
measurement.verticalSliceWidth.toInt() -
labelOffsetFromAxisPx;
final direction =
_normalizeHorizontalAnchor(tickLabelAnchor, isRtl, isFirst, isLast);
tick.textElement.textDirection = direction;
switch (direction) {
case TextDirection.rtl:
x = (locationPx + labelOffsetFromTickPx + labelOffsetPx).toInt();
break;
case TextDirection.ltr:
x = (locationPx - labelOffsetFromTickPx - labelOffsetPx).toInt();
break;
case TextDirection.center:
default:
x = (locationPx - labelOffsetPx).toInt();
break;
}
} else {
if (orientation == AxisOrientation.left) {
if (tickLabelJustification == TickLabelJustification.inside) {
x = axisBounds.right - labelOffsetFromAxisPx;
tick.textElement.textDirection = TextDirection.rtl;
} else {
x = axisBounds.left + labelOffsetFromAxisPx;
tick.textElement.textDirection = TextDirection.ltr;
}
} else {
// orientation == right
if (tickLabelJustification == TickLabelJustification.inside) {
x = axisBounds.left + labelOffsetFromAxisPx;
tick.textElement.textDirection = TextDirection.ltr;
} else {
x = axisBounds.right - labelOffsetFromAxisPx;
tick.textElement.textDirection = TextDirection.rtl;
}
}
switch (_normalizeVerticalAnchor(tickLabelAnchor, isFirst, isLast)) {
case _PixelVerticalDirection.over:
y = (locationPx -
measurement.verticalSliceWidth -
labelOffsetFromTickPx -
labelOffsetPx)
.toInt();
break;
case _PixelVerticalDirection.under:
y = (locationPx + labelOffsetFromTickPx + labelOffsetPx).toInt();
break;
case _PixelVerticalDirection.center:
default:
y = (locationPx - measurement.verticalSliceWidth / 2 + labelOffsetPx)
.toInt();
break;
}
}
canvas.drawText(tick.textElement, x, y);
}
TextDirection _normalizeHorizontalAnchor(
TickLabelAnchor anchor, bool isRtl, bool isFirst, bool isLast) {
switch (anchor) {
case TickLabelAnchor.before:
return isRtl ? TextDirection.ltr : TextDirection.rtl;
case TickLabelAnchor.after:
return isRtl ? TextDirection.rtl : TextDirection.ltr;
case TickLabelAnchor.inside:
if (isFirst) {
return TextDirection.ltr;
}
if (isLast) {
return TextDirection.rtl;
}
return TextDirection.center;
case TickLabelAnchor.centered:
default:
return TextDirection.center;
}
}
_PixelVerticalDirection _normalizeVerticalAnchor(
TickLabelAnchor anchor, bool isFirst, bool isLast) {
switch (anchor) {
case TickLabelAnchor.before:
return _PixelVerticalDirection.under;
case TickLabelAnchor.after:
return _PixelVerticalDirection.over;
case TickLabelAnchor.inside:
if (isFirst) {
return _PixelVerticalDirection.over;
}
if (isLast) {
return _PixelVerticalDirection.under;
}
return _PixelVerticalDirection.center;
case TickLabelAnchor.centered:
default:
return _PixelVerticalDirection.center;
}
}
}
enum _PixelVerticalDirection {
over,
center,
under,
}

View File

@@ -0,0 +1,174 @@
// 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:meta/meta.dart' show immutable, required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/line_style.dart' show LineStyle;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../spec/axis_spec.dart'
show TextStyleSpec, LineStyleSpec, TickLabelAnchor, TickLabelJustification;
import '../tick.dart' show Tick;
import 'base_tick_draw_strategy.dart' show BaseTickDrawStrategy;
import 'small_tick_draw_strategy.dart' show SmallTickRendererSpec;
import 'tick_draw_strategy.dart' show TickDrawStrategy;
@immutable
class GridlineRendererSpec<D> extends SmallTickRendererSpec<D> {
const GridlineRendererSpec({
TextStyleSpec labelStyle,
LineStyleSpec lineStyle,
LineStyleSpec axisLineStyle,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int tickLengthPx,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
int minimumPaddingBetweenLabelsPx,
}) : super(
labelStyle: labelStyle,
lineStyle: lineStyle,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx,
tickLengthPx: tickLengthPx,
axisLineStyle: axisLineStyle);
@override
TickDrawStrategy<D> createDrawStrategy(
ChartContext context, GraphicsFactory graphicsFactory) =>
new GridlineTickDrawStrategy<D>(context, graphicsFactory,
tickLengthPx: tickLengthPx,
lineStyleSpec: lineStyle,
labelStyleSpec: labelStyle,
axisLineStyleSpec: axisLineStyle,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx);
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is GridlineRendererSpec && super == (other));
}
@override
int get hashCode {
int hashcode = super.hashCode;
return hashcode;
}
}
/// Draws line across chart draw area for each tick.
///
/// Extends [BaseTickDrawStrategy].
class GridlineTickDrawStrategy<D> extends BaseTickDrawStrategy<D> {
int tickLength;
LineStyle lineStyle;
GridlineTickDrawStrategy(
ChartContext chartContext,
GraphicsFactory graphicsFactory, {
int tickLengthPx,
LineStyleSpec lineStyleSpec,
TextStyleSpec labelStyleSpec,
LineStyleSpec axisLineStyleSpec,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
int minimumPaddingBetweenLabelsPx,
}) : super(chartContext, graphicsFactory,
labelStyleSpec: labelStyleSpec,
axisLineStyleSpec: axisLineStyleSpec ?? lineStyleSpec,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx) {
lineStyle =
StyleFactory.style.createGridlineStyle(graphicsFactory, lineStyleSpec);
this.tickLength = tickLengthPx ?? 0;
}
@override
void draw(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast}) {
Point<num> lineStart;
Point<num> lineEnd;
switch (orientation) {
case AxisOrientation.top:
final x = tick.locationPx;
lineStart = new Point(x, axisBounds.bottom - tickLength);
lineEnd = new Point(x, drawAreaBounds.bottom);
break;
case AxisOrientation.bottom:
final x = tick.locationPx;
lineStart = new Point(x, drawAreaBounds.top + tickLength);
lineEnd = new Point(x, axisBounds.top);
break;
case AxisOrientation.right:
final y = tick.locationPx;
if (tickLabelAnchor == TickLabelAnchor.after ||
tickLabelAnchor == TickLabelAnchor.before) {
lineStart = new Point(axisBounds.right, y);
} else {
lineStart = new Point(axisBounds.left + tickLength, y);
}
lineEnd = new Point(drawAreaBounds.left, y);
break;
case AxisOrientation.left:
final y = tick.locationPx;
if (tickLabelAnchor == TickLabelAnchor.after ||
tickLabelAnchor == TickLabelAnchor.before) {
lineStart = new Point(axisBounds.left, y);
} else {
lineStart = new Point(axisBounds.right - tickLength, y);
}
lineEnd = new Point(drawAreaBounds.right, y);
break;
}
canvas.drawLine(
points: [lineStart, lineEnd],
dashPattern: lineStyle.dashPattern,
fill: lineStyle.color,
stroke: lineStyle.color,
strokeWidthPx: lineStyle.strokeWidth.toDouble(),
);
drawLabel(canvas, tick,
orientation: orientation,
axisBounds: axisBounds,
drawAreaBounds: drawAreaBounds,
isFirst: isFirst,
isLast: isLast);
}
}

View File

@@ -0,0 +1,136 @@
// 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:meta/meta.dart' show immutable, required;
import '../../../../common/color.dart' show Color;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/line_style.dart' show LineStyle;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../../common/text_style.dart' show TextStyle;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../common/chart_context.dart' show ChartContext;
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
import '../axis.dart' show AxisOrientation;
import '../collision_report.dart' show CollisionReport;
import '../spec/axis_spec.dart' show RenderSpec, LineStyleSpec;
import '../tick.dart' show Tick;
import 'tick_draw_strategy.dart';
/// Renders no ticks no labels, and claims no space in layout.
/// However, it does render the axis line if asked to by the axis.
@immutable
class NoneRenderSpec<D> extends RenderSpec<D> {
final LineStyleSpec axisLineStyle;
const NoneRenderSpec({this.axisLineStyle});
@override
TickDrawStrategy<D> createDrawStrategy(
ChartContext context, GraphicsFactory graphicFactory) =>
new NoneDrawStrategy<D>(context, graphicFactory,
axisLineStyleSpec: axisLineStyle);
@override
bool operator ==(Object other) =>
identical(this, other) || other is NoneRenderSpec;
@override
int get hashCode => 0;
}
class NoneDrawStrategy<D> implements TickDrawStrategy<D> {
LineStyle axisLineStyle;
TextStyle noneTextStyle;
NoneDrawStrategy(ChartContext chartContext, GraphicsFactory graphicsFactory,
{LineStyleSpec axisLineStyleSpec}) {
axisLineStyle = StyleFactory.style
.createAxisLineStyle(graphicsFactory, axisLineStyleSpec);
noneTextStyle = graphicsFactory.createTextPaint()
..color = Color.transparent
..fontSize = 0;
}
@override
CollisionReport collides(List<Tick> ticks, AxisOrientation orientation) =>
new CollisionReport(ticksCollide: false, ticks: ticks);
@override
void decorateTicks(List<Tick> ticks) {
// Even though no text is rendered, the text style for each element should
// still be set to handle the case of the draw strategy being switched to
// a different draw strategy. The new draw strategy will try to animate
// the old ticks out and the text style property is used.
ticks.forEach((tick) => tick.textElement.textStyle = noneTextStyle);
}
@override
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
Rectangle<int> axisBounds) {
Point<num> start;
Point<num> end;
switch (orientation) {
case AxisOrientation.top:
start = axisBounds.bottomLeft;
end = axisBounds.bottomRight;
break;
case AxisOrientation.bottom:
start = axisBounds.topLeft;
end = axisBounds.topRight;
break;
case AxisOrientation.right:
start = axisBounds.topLeft;
end = axisBounds.bottomLeft;
break;
case AxisOrientation.left:
start = axisBounds.topRight;
end = axisBounds.bottomRight;
break;
}
canvas.drawLine(
points: [start, end],
dashPattern: axisLineStyle.dashPattern,
fill: axisLineStyle.color,
stroke: axisLineStyle.color,
strokeWidthPx: axisLineStyle.strokeWidth.toDouble(),
);
}
@override
void draw(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast}) {}
@override
ViewMeasuredSizes measureHorizontallyDrawnTicks(
List<Tick> ticks, int maxWidth, int maxHeight) {
return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
}
@override
ViewMeasuredSizes measureVerticallyDrawnTicks(
List<Tick> ticks, int maxWidth, int maxHeight) {
return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
}
}

View File

@@ -0,0 +1,168 @@
// 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:meta/meta.dart' show immutable, required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/line_style.dart' show LineStyle;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../spec/axis_spec.dart'
show TextStyleSpec, LineStyleSpec, TickLabelAnchor, TickLabelJustification;
import '../tick.dart' show Tick;
import 'base_tick_draw_strategy.dart' show BaseRenderSpec, BaseTickDrawStrategy;
import 'tick_draw_strategy.dart' show TickDrawStrategy;
///
@immutable
class SmallTickRendererSpec<D> extends BaseRenderSpec<D> {
final LineStyleSpec lineStyle;
final int tickLengthPx;
const SmallTickRendererSpec({
TextStyleSpec labelStyle,
this.lineStyle,
LineStyleSpec axisLineStyle,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
this.tickLengthPx,
int minimumPaddingBetweenLabelsPx,
}) : super(
labelStyle: labelStyle,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx,
axisLineStyle: axisLineStyle);
@override
TickDrawStrategy<D> createDrawStrategy(
ChartContext context, GraphicsFactory graphicsFactory) =>
new SmallTickDrawStrategy<D>(context, graphicsFactory,
tickLengthPx: tickLengthPx,
lineStyleSpec: lineStyle,
labelStyleSpec: labelStyle,
axisLineStyleSpec: axisLineStyle,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx);
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is SmallTickRendererSpec &&
lineStyle == other.lineStyle &&
tickLengthPx == other.tickLengthPx &&
super == (other));
}
@override
int get hashCode {
int hashcode = lineStyle?.hashCode ?? 0;
hashcode = (hashcode * 37) + tickLengthPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + super.hashCode;
return hashcode;
}
}
/// Draws small tick lines for each tick. Extends [BaseTickDrawStrategy].
class SmallTickDrawStrategy<D> extends BaseTickDrawStrategy<D> {
int tickLength;
LineStyle lineStyle;
SmallTickDrawStrategy(
ChartContext chartContext,
GraphicsFactory graphicsFactory, {
int tickLengthPx,
LineStyleSpec lineStyleSpec,
TextStyleSpec labelStyleSpec,
LineStyleSpec axisLineStyleSpec,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
int minimumPaddingBetweenLabelsPx,
}) : super(chartContext, graphicsFactory,
labelStyleSpec: labelStyleSpec,
axisLineStyleSpec: axisLineStyleSpec ?? lineStyleSpec,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx) {
this.tickLength = tickLengthPx ?? StyleFactory.style.tickLength;
lineStyle =
StyleFactory.style.createTickLineStyle(graphicsFactory, lineStyleSpec);
}
@override
void draw(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast}) {
Point<num> tickStart;
Point<num> tickEnd;
switch (orientation) {
case AxisOrientation.top:
double x = tick.locationPx;
tickStart = new Point(x, axisBounds.bottom - tickLength);
tickEnd = new Point(x, axisBounds.bottom);
break;
case AxisOrientation.bottom:
double x = tick.locationPx;
tickStart = new Point(x, axisBounds.top);
tickEnd = new Point(x, axisBounds.top + tickLength);
break;
case AxisOrientation.right:
double y = tick.locationPx;
tickStart = new Point(axisBounds.left, y);
tickEnd = new Point(axisBounds.left + tickLength, y);
break;
case AxisOrientation.left:
double y = tick.locationPx;
tickStart = new Point(axisBounds.right - tickLength, y);
tickEnd = new Point(axisBounds.right, y);
break;
}
canvas.drawLine(
points: [tickStart, tickEnd],
dashPattern: lineStyle.dashPattern,
fill: lineStyle.color,
stroke: lineStyle.color,
strokeWidthPx: lineStyle.strokeWidth.toDouble(),
);
drawLabel(canvas, tick,
orientation: orientation,
axisBounds: axisBounds,
drawAreaBounds: drawAreaBounds,
isFirst: isFirst,
isLast: isLast);
}
}

View File

@@ -0,0 +1,59 @@
// 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:meta/meta.dart' show required;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
import '../axis.dart' show AxisOrientation;
import '../collision_report.dart' show CollisionReport;
import '../tick.dart' show Tick;
/// Strategy for drawing ticks and checking for collisions.
abstract class TickDrawStrategy<D> {
/// Decorate the existing list of ticks.
///
/// This can be used to further modify ticks after they have been generated
/// with location data and formatted labels.
void decorateTicks(List<Tick<D>> ticks);
/// Returns a [CollisionReport] indicating if there are any collisions.
CollisionReport collides(List<Tick<D>> ticks, AxisOrientation orientation);
/// Returns measurement of ticks drawn vertically.
ViewMeasuredSizes measureVerticallyDrawnTicks(
List<Tick<D>> ticks, int maxWidth, int maxHeight);
/// Returns measurement of ticks drawn horizontally.
ViewMeasuredSizes measureHorizontallyDrawnTicks(
List<Tick<D>> ticks, int maxWidth, int maxHeight);
/// Draws tick onto [ChartCanvas].
///
/// [orientation] the orientation of the axis that this [tick] belongs to.
/// [axisBounds] the bounds of the axis.
/// [drawAreaBounds] the bounds of the chart draw area adjacent to the axis.
void draw(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast});
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
Rectangle<int> axisBounds);
}

View File

@@ -0,0 +1,111 @@
// 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:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'numeric_scale.dart' show NumericScale;
import 'ordinal_scale.dart' show OrdinalScale;
import 'scale.dart' show MutableScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
import 'tick_provider.dart' show BaseTickProvider, TickHint;
import 'time/date_time_scale.dart' show DateTimeScale;
/// Tick provider that provides ticks at the two end points of the axis range.
class EndPointsTickProvider<D> extends BaseTickProvider<D> {
@override
List<Tick<D>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required MutableScale<D> scale,
@required TickFormatter<D> formatter,
@required Map<D, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<D> tickHint,
}) {
final ticks = <Tick<D>>[];
// Check to see if the axis has been configured with some domain values.
//
// An un-configured axis has no domain step size, and its scale defaults to
// infinity.
if (scale.domainStepSize.abs() != double.infinity) {
final start = _getStartValue(tickHint, scale);
final end = _getEndValue(tickHint, scale);
final labels = formatter.format([start, end], formatterValueCache,
stepSize: scale.domainStepSize);
ticks.add(new Tick(
value: start,
textElement: graphicsFactory.createTextElement(labels[0]),
locationPx: scale[start]));
ticks.add(new Tick(
value: end,
textElement: graphicsFactory.createTextElement(labels[1]),
locationPx: scale[end]));
// Allow draw strategy to decorate the ticks.
tickDrawStrategy.decorateTicks(ticks);
}
return ticks;
}
/// Get the start value from the scale.
D _getStartValue(TickHint<D> tickHint, MutableScale<D> scale) {
Object start;
if (tickHint != null) {
start = tickHint.start;
} else {
if (scale is NumericScale) {
start = (scale as NumericScale).viewportDomain.min;
} else if (scale is DateTimeScale) {
start = (scale as DateTimeScale).viewportDomain.start;
} else if (scale is OrdinalScale) {
start = (scale as OrdinalScale).domain.first;
}
}
return start;
}
/// Get the end value from the scale.
D _getEndValue(TickHint<D> tickHint, MutableScale<D> scale) {
Object end;
if (tickHint != null) {
end = tickHint.end;
} else {
if (scale is NumericScale) {
end = (scale as NumericScale).viewportDomain.max;
} else if (scale is DateTimeScale) {
end = (scale as DateTimeScale).viewportDomain.end;
} else if (scale is OrdinalScale) {
end = (scale as OrdinalScale).domain.last;
}
}
return end;
}
}

View File

@@ -0,0 +1,76 @@
// 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 '../axis.dart' show NumericAxis;
import 'bucketing_numeric_tick_provider.dart' show BucketingNumericTickProvider;
/// A numeric [Axis] that positions all values beneath a certain [threshold]
/// into a reserved space on the axis range. The label for the bucket line will
/// be drawn in the middle of the bucket range, rather than aligned with the
/// gridline for that value's position on the scale.
///
/// An example illustration of a bucketing measure axis on a point chart
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
/// of the axis, since they are less than the [threshold] value of 10%.
///
/// 100% ┠─────────────────────────
/// ┃ *
/// ┃ *
/// 50% ┠──────*──────────────────
/// ┃
/// ┠─────────────────────────
/// < 10% ┃ * *
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
/// 0 50 100
///
/// This axis will format numbers as percents by default.
class BucketingNumericAxis extends NumericAxis {
/// All values smaller than the threshold will be bucketed into the same
/// position in the reserved space on the axis.
num _threshold;
/// Whether or not measure values bucketed below the [threshold] should be
/// visible on the chart, or collapsed.
///
/// If this is false, then any data with measure values smaller than
/// [threshold] will be rendered at the baseline of the chart. The
bool _showBucket;
BucketingNumericAxis()
: super(tickProvider: new BucketingNumericTickProvider());
set threshold(num threshold) {
_threshold = threshold;
(tickProvider as BucketingNumericTickProvider).threshold = threshold;
}
set showBucket(bool showBucket) {
_showBucket = showBucket;
(tickProvider as BucketingNumericTickProvider).showBucket = showBucket;
}
/// Gets the location of [domain] on the axis, repositioning any value less
/// than [threshold] to the middle of the reserved bucket.
@override
double getLocation(num domain) {
if (domain == null) {
return null;
} else if (_threshold != null && domain < _threshold) {
return _showBucket ? scale[_threshold / 2] : scale[0.0];
} else {
return scale[domain];
}
}
}

View File

@@ -0,0 +1,151 @@
// 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:meta/meta.dart' show required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import '../numeric_scale.dart' show NumericScale;
import '../numeric_tick_provider.dart' show NumericTickProvider;
import '../tick.dart' show Tick;
import '../tick_formatter.dart' show SimpleTickFormatterBase, TickFormatter;
import '../tick_provider.dart' show TickHint;
/// Tick provider that generates ticks for a [BucketingNumericAxis].
///
/// An example illustration of a bucketing measure axis on a point chart
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
/// of the axis, since they are less than the [threshold] value of 10%.
///
/// 100% ┠─────────────────────────
/// ┃ *
/// ┃ *
/// 50% ┠──────*──────────────────
/// ┃
/// ┠─────────────────────────
/// < 10% ┃ * *
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
/// 0 50 100
///
/// This tick provider will generate ticks using the same strategy as
/// [NumericTickProvider], except that any ticks that are smaller than
/// [threshold] will be hidden with an empty label. A special tick will be added
/// at the [threshold] position, with a label offset that moves its label down
/// to the middle of the bucket.
class BucketingNumericTickProvider extends NumericTickProvider {
/// All values smaller than the threshold will be bucketed into the same
/// position in the reserved space on the axis.
num _threshold;
set threshold(num threshold) {
_threshold = threshold;
}
/// Whether or not measure values bucketed below the [threshold] should be
/// visible on the chart, or collapsed.
bool _showBucket;
set showBucket(bool showBucket) {
_showBucket = showBucket;
}
@override
List<Tick<num>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required NumericScale scale,
@required TickFormatter<num> formatter,
@required Map<num, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<num> tickHint,
}) {
if (_threshold == null) {
throw ('Bucketing threshold must be set before getting ticks.');
}
if (_showBucket == null) {
throw ('The showBucket flag must be set before getting ticks.');
}
final localFormatter = new _BucketingFormatter()
..threshold = _threshold
..originalFormatter = formatter;
final ticks = super.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: localFormatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
orientation: orientation,
viewportExtensionEnabled: viewportExtensionEnabled);
assert(scale != null);
// Create a tick for the threshold.
final thresholdTick = new Tick<num>(
value: _threshold,
textElement: graphicsFactory
.createTextElement(localFormatter.formatValue(_threshold)),
locationPx: _showBucket ? scale[_threshold] : scale[0],
labelOffsetPx:
_showBucket ? -0.5 * (scale[_threshold] - scale[0]) : 0.0);
tickDrawStrategy.decorateTicks(<Tick<num>>[thresholdTick]);
// Filter out ticks that sit below the threshold.
ticks.removeWhere((Tick<num> tick) =>
tick.value <= thresholdTick.value && tick.value != 0.0);
// Finally, add our threshold tick to the list.
ticks.add(thresholdTick);
// Make sure they are sorted by increasing value.
ticks.sort((a, b) {
if (a.value < b.value) {
return -1;
} else if (a.value > b.value) {
return 1;
} else {
return 0;
}
});
return ticks;
}
}
class _BucketingFormatter extends SimpleTickFormatterBase<num> {
/// All values smaller than the threshold will be formatted into an empty
/// string.
num threshold;
SimpleTickFormatterBase<num> originalFormatter;
/// Formats a single tick value.
String formatValue(num value) {
if (value < threshold) {
return '';
} else if (value == threshold) {
return '< ' + originalFormatter.formatValue(value);
} else {
return originalFormatter.formatValue(value);
}
}
}

View File

@@ -0,0 +1,246 @@
// 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 '../numeric_extents.dart' show NumericExtents;
import '../numeric_scale.dart' show NumericScale;
import '../scale.dart' show RangeBandConfig, ScaleOutputExtent, StepSizeConfig;
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
import 'linear_scale_function.dart' show LinearScaleFunction;
import 'linear_scale_viewport.dart' show LinearScaleViewportSettings;
/// [NumericScale] that lays out the domain linearly across the range.
///
/// A [Scale] which converts numeric domain units to a given numeric range units
/// linearly (as opposed to other methods like log scales). This is used to map
/// the domain's values to the available pixel range of the chart using the
/// apply method.
///
/// <p>The domain extent of the scale are determined by adding all domain
/// values to the scale. It can, however, be overwritten by calling
/// [domainOverride] to define the extent of the data.
///
/// <p>The scale can be zoomed & panned by calling either [setViewportSettings]
/// with a zoom and translate, or by setting [viewportExtent] with the domain
/// extent to show in the output range.
///
/// <p>[rangeBandConfig]: By default, this scale will map the domain extent
/// exactly to the output range in a simple ratio mapping. If a
/// [RangeBandConfig] other than NONE is used to define the width of bar groups,
/// then the scale calculation may be altered to that there is a half a stepSize
/// at the start and end of the range to ensure that a bar group can be shown
/// and centered on the scale's result.
///
/// <p>[stepSizeConfig]: By default, this scale will calculate the stepSize as
/// being auto detected using the minimal distance between two consecutive
/// datum. If you don't assign a [RangeBandConfig], then changing the
/// [stepSizeConfig] is a no-op.
class LinearScale implements NumericScale {
final LinearScaleDomainInfo _domainInfo;
final LinearScaleViewportSettings _viewportSettings;
final LinearScaleFunction _scaleFunction = new LinearScaleFunction();
RangeBandConfig rangeBandConfig = const RangeBandConfig.none();
StepSizeConfig stepSizeConfig = const StepSizeConfig.auto();
bool _scaleReady = false;
LinearScale()
: _domainInfo = new LinearScaleDomainInfo(),
_viewportSettings = new LinearScaleViewportSettings();
LinearScale._copy(LinearScale other)
: _domainInfo = new LinearScaleDomainInfo.copy(other._domainInfo),
_viewportSettings =
new LinearScaleViewportSettings.copy(other._viewportSettings),
rangeBandConfig = other.rangeBandConfig,
stepSizeConfig = other.stepSizeConfig;
@override
LinearScale copy() => new LinearScale._copy(this);
//
// Domain methods
//
@override
addDomain(num domainValue) {
_domainInfo.addDomainValue(domainValue);
}
@override
resetDomain() {
_scaleReady = false;
_domainInfo.reset();
}
@override
resetViewportSettings() {
_viewportSettings.reset();
}
@override
NumericExtents get dataExtent => new NumericExtents(
_domainInfo.dataDomainStart, _domainInfo.dataDomainEnd);
@override
num get minimumDomainStep => _domainInfo.minimumDetectedDomainStep;
@override
bool canTranslate(_) => true;
@override
set domainOverride(NumericExtents domainMaxExtent) {
_domainInfo.domainOverride = domainMaxExtent;
}
get domainOverride => _domainInfo.domainOverride;
@override
int compareDomainValueToViewport(num domainValue) {
NumericExtents dataExtent = _viewportSettings.domainExtent != null
? _viewportSettings.domainExtent
: _domainInfo.extent;
return dataExtent.compareValue(domainValue);
}
//
// Viewport methods
//
@override
setViewportSettings(double viewportScale, double viewportTranslatePx) {
_viewportSettings
..scalingFactor = viewportScale
..translatePx = viewportTranslatePx
..domainExtent = null;
_scaleReady = false;
}
@override
double get viewportScalingFactor => _viewportSettings.scalingFactor;
@override
double get viewportTranslatePx => _viewportSettings.translatePx;
@override
set viewportDomain(NumericExtents extent) {
_scaleReady = false;
_viewportSettings.domainExtent = extent;
}
@override
NumericExtents get viewportDomain {
_configureScale();
return _viewportSettings.domainExtent;
}
@override
set keepViewportWithinData(bool autoAdjustViewportToNiceValues) {
_scaleReady = false;
_viewportSettings.keepViewportWithinData = true;
}
@override
bool get keepViewportWithinData => _viewportSettings.keepViewportWithinData;
@override
double computeViewportScaleFactor(double domainWindow) =>
_domainInfo.domainDiff / domainWindow;
@override
set range(ScaleOutputExtent extent) {
_viewportSettings.range = extent;
_scaleReady = false;
}
@override
ScaleOutputExtent get range => _viewportSettings.range;
//
// Scale application methods
//
@override
num operator [](num domainValue) {
_configureScale();
return _scaleFunction[domainValue];
}
@override
num reverse(double viewPixels) {
_configureScale();
final num domain = _scaleFunction.reverse(viewPixels);
return domain;
}
@override
double get rangeBand {
_configureScale();
return _scaleFunction.rangeBandPixels;
}
@override
double get stepSize {
_configureScale();
return _scaleFunction.stepSizePixels;
}
@override
double get domainStepSize => _domainInfo.minimumDetectedDomainStep.toDouble();
@override
int get rangeWidth => (range.end - range.start).abs().toInt();
@override
bool isRangeValueWithinViewport(double rangeValue) =>
range.containsValue(rangeValue);
//
// Private update
//
_configureScale() {
if (_scaleReady) return;
assert(_viewportSettings.range != null);
// If the viewport's domainExtent are set, then we can calculate the
// viewport's scaleFactor now that the domainInfo has been loaded.
// The viewport also has a chance to correct the scaleFactor.
_viewportSettings.updateViewportScaleFactor(_domainInfo);
// Now that the viewport's scalingFactor is setup, set it on the scale
// function.
_scaleFunction.updateScaleFactor(
_viewportSettings, _domainInfo, rangeBandConfig, stepSizeConfig);
// If the viewport's domainExtent are set, then we can calculate the
// viewport's translate now that the scaleFactor has been loaded.
// The viewport also has a chance to correct the translate.
_viewportSettings.updateViewportTranslatePx(
_domainInfo, _scaleFunction.scalingFactor);
// Now that the viewport has a chance to update the translate, set it on the
// scale function.
_scaleFunction.updateTranslateAndRangeBand(
_viewportSettings, _domainInfo, rangeBandConfig);
// Now that the viewport's scaleFactor and translate have been updated
// set the effective domainExtent of the viewport.
_viewportSettings.updateViewportDomainExtent(
_domainInfo, _scaleFunction.scalingFactor);
// Cached computed values are updated.
_scaleReady = true;
}
}

View File

@@ -0,0 +1,118 @@
// 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 '../numeric_extents.dart' show NumericExtents;
/// Encapsulation of all the domain processing logic for the [LinearScale].
class LinearScaleDomainInfo {
/// User (or axis) overridden extent in domain units.
NumericExtents domainOverride;
/// The minimum added domain value.
num _dataDomainStart = double.infinity;
num get dataDomainStart => _dataDomainStart;
/// The maximum added domain value.
num _dataDomainEnd = double.negativeInfinity;
num get dataDomainEnd => _dataDomainEnd;
/// Previous domain added so we can calculate minimumDetectedDomainStep.
num _previouslyAddedDomain;
/// The step size between data points in domain units.
///
/// Measured as the minimum distance between consecutive added points.
num _minimumDetectedDomainStep = double.infinity;
num get minimumDetectedDomainStep => _minimumDetectedDomainStep;
///The diff of the nicedDomain extent.
num get domainDiff => extent.width;
LinearScaleDomainInfo();
LinearScaleDomainInfo.copy(LinearScaleDomainInfo other) {
if (other.domainOverride != null) {
domainOverride = other.domainOverride;
}
_dataDomainStart = other._dataDomainStart;
_dataDomainEnd = other._dataDomainEnd;
_previouslyAddedDomain = other._previouslyAddedDomain;
_minimumDetectedDomainStep = other._minimumDetectedDomainStep;
}
/// Resets everything back to initial state.
void reset() {
_previouslyAddedDomain = null;
_dataDomainStart = double.infinity;
_dataDomainEnd = double.negativeInfinity;
_minimumDetectedDomainStep = double.infinity;
}
/// Updates the domain extent and detected step size given the [domainValue].
void addDomainValue(num domainValue) {
if (domainValue == null || !domainValue.isFinite) {
return;
}
extendDomain(domainValue);
if (_previouslyAddedDomain != null) {
final domainStep = (domainValue - _previouslyAddedDomain).abs();
if (domainStep != 0.0 && domainStep < minimumDetectedDomainStep) {
_minimumDetectedDomainStep = domainStep;
}
}
_previouslyAddedDomain = domainValue;
}
/// Extends the data domain extent without modifying step size detection.
///
/// Returns whether the the domain interval was extended. If the domain value
/// was already contained in the domain interval, the domain interval does not
/// change.
bool extendDomain(num domainValue) {
if (domainValue == null || !domainValue.isFinite) {
return false;
}
bool domainExtended = false;
if (domainValue < _dataDomainStart) {
_dataDomainStart = domainValue;
domainExtended = true;
}
if (domainValue > _dataDomainEnd) {
_dataDomainEnd = domainValue;
domainExtended = true;
}
return domainExtended;
}
/// Returns the extent based on the current domain range and overrides.
NumericExtents get extent {
num tmpDomainStart;
num tmpDomainEnd;
if (domainOverride != null) {
// override was set.
tmpDomainStart = domainOverride.min;
tmpDomainEnd = domainOverride.max;
} else {
// domainEnd is less than domainStart if no domain values have been set.
tmpDomainStart = _dataDomainStart.isFinite ? _dataDomainStart : 0.0;
tmpDomainEnd = _dataDomainEnd.isFinite ? _dataDomainEnd : 1.0;
}
return new NumericExtents(tmpDomainStart, tmpDomainEnd);
}
}

View File

@@ -0,0 +1,201 @@
// 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 '../scale.dart'
show RangeBandConfig, RangeBandType, StepSizeConfig, StepSizeType;
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
import 'linear_scale_viewport.dart' show LinearScaleViewportSettings;
/// Component of the LinearScale which actually handles the apply and reverse
/// function of the scale.
class LinearScaleFunction {
/// Cached rangeBand width in pixels given the RangeBandConfig and the current
/// domain & range.
double rangeBandPixels = 0.0;
/// Cached amount in domain units to shift the input value as a part of
/// translation.
num domainTranslate = 0.0;
/// Cached translation ratio for scale translation.
double scalingFactor = 1.0;
/// Cached amount in pixel units to shift the output value as a part of
/// translation.
double rangeTranslate = 0.0;
/// The calculated step size given the step size config.
double stepSizePixels = 0.0;
/// Translates the given domainValue to the range output.
double operator [](num domainValue) {
return (((domainValue + domainTranslate) * scalingFactor) + rangeTranslate)
.toDouble();
}
/// Translates the given range output back to a domainValue.
double reverse(double viewPixels) {
return ((viewPixels - rangeTranslate) / scalingFactor) - domainTranslate;
}
/// Update the scale function's scaleFactor given the current state of the
/// viewport.
void updateScaleFactor(
LinearScaleViewportSettings viewportSettings,
LinearScaleDomainInfo domainInfo,
RangeBandConfig rangeBandConfig,
StepSizeConfig stepSizeConfig) {
double rangeDiff = viewportSettings.range.diff.toDouble();
// Note: if you provided a nicing function that extends the domain, we won't
// muck with the extended side.
bool hasHalfStepAtStart =
domainInfo.extent.min == domainInfo.dataDomainStart;
bool hasHalfStepAtEnd = domainInfo.extent.max == domainInfo.dataDomainEnd;
// Determine the stepSize and reserved range values.
// The percentage of the step reserved from the scale's range due to the
// possible half step at the start and end.
double reservedRangePercentOfStep =
getStepReservationPercent(hasHalfStepAtStart, hasHalfStepAtEnd);
_updateStepSizeAndScaleFactor(viewportSettings, domainInfo, rangeDiff,
reservedRangePercentOfStep, rangeBandConfig, stepSizeConfig);
}
/// Returns the percentage of the step reserved from the output range due to
/// maybe having to hold half stepSizes on the start and end of the output.
double getStepReservationPercent(
bool hasHalfStepAtStart, bool hasHalfStepAtEnd) {
if (!hasHalfStepAtStart && !hasHalfStepAtEnd) {
return 0.0;
}
if (hasHalfStepAtStart && hasHalfStepAtEnd) {
return 1.0;
}
return 0.5;
}
/// Updates the scale function's translate and rangeBand given the current
/// state of the viewport.
void updateTranslateAndRangeBand(LinearScaleViewportSettings viewportSettings,
LinearScaleDomainInfo domainInfo, RangeBandConfig rangeBandConfig) {
// Assign the rangeTranslate using the current viewportSettings.translatePx
// and diffs.
if (domainInfo.domainDiff == 0) {
// Translate it to the center of the range.
rangeTranslate =
viewportSettings.range.start + (viewportSettings.range.diff / 2);
} else {
bool hasHalfStepAtStart =
domainInfo.extent.min == domainInfo.dataDomainStart;
// The pixel shift of the scale function due to the half a step at the
// beginning.
double reservedRangePixelShift =
hasHalfStepAtStart ? (stepSizePixels / 2.0) : 0.0;
rangeTranslate = (viewportSettings.range.start +
viewportSettings.translatePx +
reservedRangePixelShift);
}
// We need to subtract the start from any incoming domain to apply the
// scale, so flip its sign.
domainTranslate = -1 * domainInfo.extent.min;
// Update the rangeBand size.
rangeBandPixels = _calculateRangeBandSize(rangeBandConfig);
}
/// Calculates and stores the current rangeBand given the config and current
/// step size.
double _calculateRangeBandSize(RangeBandConfig rangeBandConfig) {
switch (rangeBandConfig.type) {
case RangeBandType.fixedDomain:
return rangeBandConfig.size * scalingFactor;
case RangeBandType.fixedPixel:
return rangeBandConfig.size;
case RangeBandType.fixedPixelSpaceFromStep:
return stepSizePixels - rangeBandConfig.size;
case RangeBandType.styleAssignedPercentOfStep:
case RangeBandType.fixedPercentOfStep:
return stepSizePixels * rangeBandConfig.size;
case RangeBandType.none:
return 0.0;
}
return 0.0;
}
/// Calculates and Stores the current step size and scale factor together,
/// given the viewport, domain, and config.
///
/// <p>Scale factor and step size are related closely and should be calculated
/// together so that we do not lose accuracy due to double arithmetic.
void _updateStepSizeAndScaleFactor(
LinearScaleViewportSettings viewportSettings,
LinearScaleDomainInfo domainInfo,
double rangeDiff,
double reservedRangePercentOfStep,
RangeBandConfig rangeBandConfig,
StepSizeConfig stepSizeConfig) {
final domainDiff = domainInfo.domainDiff;
// If we are going to have any rangeBands, then ensure that we account for
// needed space on the beginning and end of the range.
if (rangeBandConfig.type != RangeBandType.none) {
switch (stepSizeConfig.type) {
case StepSizeType.autoDetect:
double minimumDetectedDomainStep =
domainInfo.minimumDetectedDomainStep.toDouble();
if (minimumDetectedDomainStep != null &&
minimumDetectedDomainStep.isFinite) {
scalingFactor = viewportSettings.scalingFactor *
(rangeDiff /
(domainDiff +
(minimumDetectedDomainStep *
reservedRangePercentOfStep)));
stepSizePixels = (minimumDetectedDomainStep * scalingFactor);
} else {
stepSizePixels = rangeDiff.abs();
scalingFactor = 1.0;
}
return;
case StepSizeType.fixedPixels:
stepSizePixels = stepSizeConfig.size;
double reservedRangeForStepPixels =
stepSizePixels * reservedRangePercentOfStep;
scalingFactor = domainDiff == 0
? 1.0
: viewportSettings.scalingFactor *
(rangeDiff - reservedRangeForStepPixels) /
domainDiff;
return;
case StepSizeType.fixedDomain:
double domainStepWidth = stepSizeConfig.size;
double totalDomainDiff =
(domainDiff + (domainStepWidth * reservedRangePercentOfStep));
scalingFactor = totalDomainDiff == 0
? 1.0
: viewportSettings.scalingFactor * (rangeDiff / totalDomainDiff);
stepSizePixels = domainStepWidth * scalingFactor;
return;
}
}
// If no cases matched, use zero step size.
stepSizePixels = 0.0;
scalingFactor = domainDiff == 0
? 1.0
: viewportSettings.scalingFactor * rangeDiff / domainDiff;
}
}

View File

@@ -0,0 +1,141 @@
// 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' as math show max, min;
import '../numeric_extents.dart' show NumericExtents;
import '../scale.dart' show ScaleOutputExtent;
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
/// Component of the LinearScale responsible for the configuration and
/// calculations of the viewport.
class LinearScaleViewportSettings {
/// Output extent for the scale, typically set by the axis as the pixel
/// output.
ScaleOutputExtent range;
/// Determines whether the scale should be extended to the nice values
/// provided by the tick provider. If true, we wont touch the viewport config
/// since the axis will configure it, if false, we will still ensure sane zoom
/// and translates.
bool keepViewportWithinData = true;
/// User configured viewport scale as a zoom multiplier where 1.0 is
/// 100% (default) and 2.0 is 200% zooming in making the data take up twice
/// the space (showing half as much data in the viewport).
double scalingFactor = 1.0;
/// User configured viewport translate in pixel units.
double translatePx = 0.0;
/// The current extent of the viewport in domain units.
NumericExtents _domainExtent;
set domainExtent(NumericExtents extent) {
_domainExtent = extent;
_manualDomainExtent = extent != null;
}
NumericExtents get domainExtent => _domainExtent;
/// Indicates that the viewportExtends are to be read from to determine the
/// internal scaleFactor and rangeTranslate.
bool _manualDomainExtent = false;
LinearScaleViewportSettings();
LinearScaleViewportSettings.copy(LinearScaleViewportSettings other) {
range = other.range;
keepViewportWithinData = other.keepViewportWithinData;
scalingFactor = other.scalingFactor;
translatePx = other.translatePx;
_manualDomainExtent = other._manualDomainExtent;
_domainExtent = other._domainExtent;
}
/// Resets the viewport calculated fields back to their initial settings.
void reset() {
// Likely an auto assigned viewport (niced), so reset it between draws.
scalingFactor = 1.0;
translatePx = 0.0;
domainExtent = null;
}
int get rangeWidth => range.diff.abs().toInt();
bool isRangeValueWithinViewport(double rangeValue) =>
range.containsValue(rangeValue);
/// Updates the viewport's internal scalingFactor given the current
/// domainInfo.
void updateViewportScaleFactor(LinearScaleDomainInfo domainInfo) {
// If we are loading from the viewport, then update the scalingFactor given
// the viewport size compared to the data size.
if (_manualDomainExtent) {
double viewportDomainDiff = _domainExtent?.width?.toDouble();
if (domainInfo.domainDiff != 0.0) {
scalingFactor = domainInfo.domainDiff / viewportDomainDiff;
} else {
scalingFactor = 1.0;
// The domain claims to have no date, extend it to the viewport's
domainInfo.extendDomain(_domainExtent?.min);
domainInfo.extendDomain(_domainExtent?.max);
}
}
// Make sure that the viewportSettings.scalingFactor is sane if desired.
if (!keepViewportWithinData) {
// Make sure we don't zoom out beyond the max domain extent.
scalingFactor = math.max(1.0, scalingFactor);
}
}
/// Updates the viewport's internal translate given the current domainInfo and
/// main scalingFactor from LinearScaleFunction (not internal scalingFactor).
void updateViewportTranslatePx(
LinearScaleDomainInfo domainInfo, double scaleScalingFactor) {
// If we are loading from the viewport, then update the translate now that
// the scaleFactor has been setup.
if (_manualDomainExtent) {
translatePx = (-1.0 *
scaleScalingFactor *
(_domainExtent.min - domainInfo.extent.min));
}
// Make sure that the viewportSettings.translatePx is sane if desired.
if (!keepViewportWithinData) {
int rangeDiff = range.diff.toInt();
// Make sure we don't translate beyond the max domain extent.
translatePx = math.min(0.0, translatePx);
translatePx = math.max(rangeDiff * (1.0 - scalingFactor), translatePx);
}
}
/// Calculates and stores the viewport's domainExtent if we did not load from
/// them in the first place.
void updateViewportDomainExtent(
LinearScaleDomainInfo domainInfo, double scaleScalingFactor) {
// If we didn't load from the viewport extent, then update them given the
// current scale configuration.
if (!_manualDomainExtent) {
double viewportDomainDiff = domainInfo.domainDiff / scalingFactor;
double viewportStart =
(-1.0 * translatePx / scaleScalingFactor) + domainInfo.extent.min;
_domainExtent =
new NumericExtents(viewportStart, viewportStart + viewportDomainDiff);
}
}
}

View File

@@ -0,0 +1,105 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'scale.dart' show Extents;
/// Represents the starting and ending extent of a dataset.
class NumericExtents implements Extents<num> {
final num min;
final num max;
/// Precondition: [min] <= [max].
// TODO: When initializer list asserts are supported everywhere,
// add the precondition as an initializer list assert. This is supported in
// Flutter only.
const NumericExtents(this.min, this.max);
/// Returns [Extents] based on the min and max of the given values.
/// Returns [NumericExtents.empty] if [values] are empty
factory NumericExtents.fromValues(Iterable<num> values) {
if (values.isEmpty) {
return NumericExtents.empty;
}
var min = values.first;
var max = values.first;
for (final value in values) {
if (value < min) {
min = value;
} else if (max < value) {
max = value;
}
}
return new NumericExtents(min, max);
}
/// Returns the union of this and other.
NumericExtents plus(NumericExtents other) {
if (min <= other.min) {
if (max >= other.max) {
return this;
} else {
return new NumericExtents(min, other.max);
}
} else {
if (other.max >= max) {
return other;
} else {
return new NumericExtents(other.min, max);
}
}
}
/// Compares the given [value] against the extents.
///
/// Returns -1 if the value is less than the extents.
/// Returns 0 if the value is within the extents inclusive.
/// Returns 1 if the value is greater than the extents.
int compareValue(num value) {
if (value < min) {
return -1;
}
if (value > max) {
return 1;
}
return 0;
}
bool _containsValue(double value) => compareValue(value) == 0;
// Returns true if these [NumericExtents] collides with [other].
bool overlaps(NumericExtents other) {
return _containsValue(other.min) ||
_containsValue(other.max) ||
other._containsValue(min) ||
other._containsValue(max);
}
@override
bool operator ==(other) {
return other is NumericExtents && min == other.min && max == other.max;
}
@override
int get hashCode => (min.hashCode + (max.hashCode * 31));
num get width => max - min;
@override
String toString() => 'Extent($min, $max)';
static const NumericExtents unbounded =
const NumericExtents(double.negativeInfinity, double.infinity);
static const NumericExtents empty = const NumericExtents(0.0, 0.0);
}

View File

@@ -0,0 +1,57 @@
// 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 'numeric_extents.dart' show NumericExtents;
import 'scale.dart' show MutableScale;
/// Scale used to convert numeric domain input units to output range units.
///
/// The input represents a continuous numeric domain which maps to a given range
/// output. This is used to map the domain's values to the available pixel
/// range of the chart.
abstract class NumericScale extends MutableScale<num> {
/// Keeps the scale and translate sane if true (default).
///
/// Setting this to false disables some pan/zoom protections that prevent you
/// from going beyond the data extent.
bool get keepViewportWithinData;
set keepViewportWithinData(bool keep);
/// Returns the extent of the actual data (not the viewport max).
NumericExtents get dataExtent;
/// Returns the minimum step size of the actual data.
num get minimumDomainStep;
/// Overrides the domain extent if set, null otherwise.
///
/// Overrides the extent of the actual data to lie about the range of the
/// data so that panning has a start and end point to go between beyond the
/// received data. This allows lazy loading of data into the gaps in the
/// expanded lied about areas.
NumericExtents get domainOverride;
set domainOverride(NumericExtents extent);
/// Returns the domain extent visible in the viewport of the drawArea.
NumericExtents get viewportDomain;
/// Sets the domain extent visible in the viewport of the drawArea.
///
/// Invalidates the viewportScale & viewportTranslatePx.
set viewportDomain(NumericExtents extent);
/// Returns the viewportScaleFactor needed to present the given domainWindow.
double computeViewportScaleFactor(double domainWindow);
}

View File

@@ -0,0 +1,585 @@
// 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 log, log10e, max, min, pow;
import 'package:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import '../../common/unitconverter/identity_converter.dart'
show IdentityConverter;
import '../../common/unitconverter/unit_converter.dart' show UnitConverter;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'numeric_extents.dart' show NumericExtents;
import 'numeric_scale.dart' show NumericScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
import 'tick_provider.dart' show BaseTickProvider, TickHint;
/// Tick provider that allows you to specify how many ticks to present while
/// also choosing tick values that appear "nice" or "rounded" to the user. By
/// default it will try to guess an appropriate number of ticks given the size
/// of the range available, but the min and max tick counts can be set by
/// calling setTickCounts().
///
/// You can control whether the axis is bound to zero (default) or follows the
/// data by calling setZeroBound().
///
/// This provider will choose "nice" ticks with the following priority order.
/// * Ticks do not collide with each other.
/// * Alternate rendering is not used to avoid collisions.
/// * Provide the least amount of domain range covering all data points (while
/// still selecting "nice" ticks values.
class NumericTickProvider extends BaseTickProvider<num> {
/// Used to determine the automatic tick count calculation.
static const MIN_DIPS_BETWEEN_TICKS = 25;
/// Potential steps available to the baseTen value of the data.
static const DEFAULT_STEPS = const [
0.01,
0.02,
0.025,
0.03,
0.04,
0.05,
0.06,
0.07,
0.08,
0.09,
0.1,
0.2,
0.25,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
0.9,
1.0,
2.0,
2.50,
3.0,
4.0,
5.0,
6.0,
7.0,
8.0,
9.0
];
// Settings
/// Sets whether the the tick provider should always include a zero tick.
///
/// If set the data range may be extended to include zero.
///
/// Note that the zero value in axis units is chosen, which may be different
/// than zero value in data units if a data to axis unit converter is set.
bool zeroBound = true;
/// If your data can only be in whole numbers, then set this to true.
///
/// It should prevent the scale from choosing fractional ticks. For example,
/// if you had a office head count, don't generate a tick for 1.5, instead
/// jump to 2.
///
/// Note that the provider will choose whole number ticks in the axis units,
/// not data units if a data to axis unit converter is set.
bool dataIsInWholeNumbers = true;
// Desired min and max tick counts are set by [setFixedTickCount] and
// [setTickCount]. These are not guaranteed tick counts.
int _desiredMaxTickCount;
int _desiredMinTickCount;
/// Allowed steps the tick provider can choose from.
var _allowedSteps = DEFAULT_STEPS;
/// Convert input data units to the desired units on the axis.
/// If not set no conversion will take place.
///
/// Combining this with an appropriate [TickFormatter] would result in axis
/// ticks that are in different unit than the actual data units.
UnitConverter<num, num> dataToAxisUnitConverter =
const IdentityConverter<num>();
// Tick calculation state
num _low;
num _high;
int _rangeWidth;
int _minTickCount;
int _maxTickCount;
// The parameters used in previous tick calculation
num _prevLow;
num _prevHigh;
int _prevRangeWidth;
int _prevMinTickCount;
int _prevMaxTickCount;
bool _prevDataIsInWholeNumbers;
/// Sets the desired tick count.
///
/// While the provider will try to satisfy the requirement, it is not
/// guaranteed, such as cases where ticks may overlap or are insufficient.
///
/// [tickCount] the fixed number of major (labeled) ticks to draw for the axis
/// Passing null will result in falling back on the automatic tick count
/// assignment.
void setFixedTickCount(int tickCount) {
// Don't allow a single tick, it doesn't make sense. so tickCount > 1
_desiredMinTickCount =
tickCount != null && tickCount > 1 ? tickCount : null;
_desiredMaxTickCount = _desiredMinTickCount;
}
/// Sets the desired min and max tick count when providing ticks.
///
/// The values are suggested requirements but are not guaranteed to be the
/// actual tick count in cases where it is not possible.
///
/// [maxTickCount] The max tick count must be greater than 1.
/// [minTickCount] The min tick count must be greater than 1.
void setTickCount(int maxTickCount, int minTickCount) {
// Don't allow a single tick, it doesn't make sense. so tickCount > 1
if (maxTickCount != null && maxTickCount > 1) {
_desiredMaxTickCount = maxTickCount;
if (minTickCount != null &&
minTickCount > 1 &&
minTickCount <= _desiredMaxTickCount) {
_desiredMinTickCount = minTickCount;
} else {
_desiredMinTickCount = 2;
}
} else {
_desiredMaxTickCount = null;
_desiredMinTickCount = null;
}
}
/// Sets the allowed step sizes this tick provider can choose from.
///
/// All ticks will be a power of 10 multiple of the given step sizes.
///
/// Note that if only very few step sizes are allowed the tick range maybe
/// much bigger than the data range.
///
/// The step sizes setup here apply in axis units, which is different than
/// input units if a data to axis unit converter is set.
///
/// [steps] allowed step sizes in the [1, 10) range.
set allowedSteps(List<double> steps) {
assert(steps != null && steps.isNotEmpty);
steps.sort();
final stepSet = new Set.from(steps);
_allowedSteps = new List<double>(stepSet.length * 3);
int stepIndex = 0;
for (double step in stepSet) {
assert(1.0 <= step && step < 10.0);
_allowedSteps[stepIndex] = _removeRoundingErrors(step / 100);
_allowedSteps[stepSet.length + stepIndex] =
_removeRoundingErrors(step / 10).toDouble();
_allowedSteps[2 * stepSet.length + stepIndex] =
_removeRoundingErrors(step);
stepIndex++;
}
}
List<Tick<num>> _getTicksFromHint({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required NumericScale scale,
@required TickFormatter<num> formatter,
@required Map<num, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required TickHint<num> tickHint,
}) {
final stepSize = (tickHint.end - tickHint.start) / (tickHint.tickCount - 1);
// Find the first tick that is greater than or equal to the min
// viewportDomain.
final tickZeroShift = tickHint.start -
(stepSize *
(tickHint.start >= 0
? (tickHint.start / stepSize).floor()
: (tickHint.start / stepSize).ceil()));
final tickStart =
(scale.viewportDomain.min / stepSize).ceil() * stepSize + tickZeroShift;
final stepInfo = new _TickStepInfo(stepSize.abs(), tickStart);
final tickValues = _getTickValues(stepInfo, tickHint.tickCount);
// Create ticks from domain values.
return createTicks(tickValues,
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
stepSize: stepInfo.stepSize);
}
@override
List<Tick<num>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required NumericScale scale,
@required TickFormatter<num> formatter,
@required Map<num, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<num> tickHint,
}) {
List<Tick<num>> ticks;
_rangeWidth = scale.rangeWidth;
_updateDomainExtents(scale.viewportDomain);
// Bypass searching for a tick range since we are getting ticks using
// information in [tickHint].
if (tickHint != null) {
return _getTicksFromHint(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
tickHint: tickHint,
);
}
if (_hasTickParametersChanged() || ticks == null) {
var selectedTicksRange = double.maxFinite;
var foundPreferredTicks = false;
var viewportDomain = scale.viewportDomain;
final axisUnitsHigh = dataToAxisUnitConverter.convert(_high);
final axisUnitsLow = dataToAxisUnitConverter.convert(_low);
_updateTickCounts(axisUnitsHigh, axisUnitsLow);
// Only create a copy of the scale if [viewportExtensionEnabled].
NumericScale mutableScale =
viewportExtensionEnabled ? scale.copy() : null;
// Walk to available tick count from max to min looking for the first one
// that gives you the least amount of range used. If a non colliding tick
// count is not found use the min tick count to generate ticks.
for (int tickCount = _maxTickCount;
tickCount >= _minTickCount;
tickCount--) {
final stepInfo =
_getStepsForTickCount(tickCount, axisUnitsHigh, axisUnitsLow);
if (stepInfo == null) {
continue;
}
final firstTick = dataToAxisUnitConverter.invert(stepInfo.tickStart);
final lastTick = dataToAxisUnitConverter
.invert(stepInfo.tickStart + stepInfo.stepSize * (tickCount - 1));
final range = lastTick - firstTick;
// Calculate ticks if it is a better range or if preferred ticks have
// not been found yet.
if (range < selectedTicksRange || !foundPreferredTicks) {
final tickValues = _getTickValues(stepInfo, tickCount);
if (viewportExtensionEnabled) {
mutableScale.viewportDomain =
new NumericExtents(firstTick, lastTick);
}
// Create ticks from domain values.
final preferredTicks = createTicks(tickValues,
context: context,
graphicsFactory: graphicsFactory,
scale: viewportExtensionEnabled ? mutableScale : scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
stepSize: stepInfo.stepSize);
// Request collision check from draw strategy.
final collisionReport =
tickDrawStrategy.collides(preferredTicks, orientation);
// Don't choose colliding ticks unless it was our last resort
if (collisionReport.ticksCollide && tickCount > _minTickCount) {
continue;
}
// Only choose alternate ticks if preferred ticks is not found.
if (foundPreferredTicks && collisionReport.alternateTicksUsed) {
continue;
}
ticks = collisionReport.alternateTicksUsed
? collisionReport.ticks
: preferredTicks;
foundPreferredTicks = !collisionReport.alternateTicksUsed;
selectedTicksRange = range;
// If viewport extended, save the viewport used.
viewportDomain = mutableScale?.viewportDomain ?? scale.viewportDomain;
}
}
_setPreviousTickCalculationParameters();
// If [viewportExtensionEnabled] and has changed, then set the scale's
// viewport to what was used to generate ticks. By only setting viewport
// when it has changed, we do not trigger the flag to recalculate scale.
if (viewportExtensionEnabled && scale.viewportDomain != viewportDomain) {
scale.viewportDomain = viewportDomain;
}
}
return ticks;
}
/// Checks whether the parameters that are used in determining the right set
/// of ticks changed from the last time we calculated ticks. If not we should
/// be able to use the cached ticks.
bool _hasTickParametersChanged() {
return _low != _prevLow ||
_high != _prevHigh ||
_rangeWidth != _prevRangeWidth ||
_minTickCount != _prevMinTickCount ||
_maxTickCount != _prevMaxTickCount ||
dataIsInWholeNumbers != _prevDataIsInWholeNumbers;
}
/// Save the last set of parameters used while determining ticks.
void _setPreviousTickCalculationParameters() {
_prevLow = _low;
_prevHigh = _high;
_prevRangeWidth = _rangeWidth;
_prevMinTickCount = _minTickCount;
_prevMaxTickCount = _maxTickCount;
_prevDataIsInWholeNumbers = dataIsInWholeNumbers;
}
/// Calculates the domain extents that this provider will cover based on the
/// axis extents passed in and the settings in the numeric tick provider.
/// Stores the domain extents in [_low] and [_high].
void _updateDomainExtents(NumericExtents axisExtents) {
_low = axisExtents.min;
_high = axisExtents.max;
// Correct the extents for zero bound
if (zeroBound) {
_low = _low > 0.0 ? 0.0 : _low;
_high = _high < 0.0 ? 0.0 : _high;
}
// Correct cases where high and low equal to give the tick provider an
// actual range to go off of when picking ticks.
if (_high == _low) {
if (_high == 0.0) {
// Corner case: the only values we've seen are zero, so lets just say
// the high is 1 and leave the low at zero.
_high = 1.0;
} else {
// The values are all the same, so assume a range of -5% to +5% from the
// single value.
if (_high > 0.0) {
_high = _high * 1.05;
_low = _low * 0.95;
} else {
// (high == low) < 0
_high = _high * 0.95;
_low = _low * 1.05;
}
}
}
}
/// Given [tickCount] and the domain range, finds the smallest tick increment,
/// chosen from power of 10 multiples of allowed steps, that covers the whole
/// data range.
_TickStepInfo _getStepsForTickCount(int tickCount, num high, num low) {
// A region is the space between ticks.
final regionCount = tickCount - 1;
// If the range contains zero, ensure that zero is a tick.
if (high >= 0 && low <= 0) {
// determine the ratio of regions that are above the zero axis.
final posRegionRatio = (high > 0 ? min(1.0, high / (high - low)) : 0.0);
var positiveRegionCount = (regionCount * posRegionRatio).ceil();
var negativeRegionCount = regionCount - positiveRegionCount;
// Ensure that negative regions are not excluded, unless there are no
// regions to spare.
if (negativeRegionCount == 0 && low < 0 && regionCount > 1) {
positiveRegionCount--;
negativeRegionCount++;
}
// If we have positive and negative values, ensure that we have ticks in
// both regions.
//
// This should not happen unless the axis is manually configured with a
// tick count. [_updateTickCounts] should ensure that we have do not try
// to generate fewer than three.
assert(
!(low < 0 &&
high > 0 &&
(negativeRegionCount == 0 || positiveRegionCount == 0)),
'Numeric tick provider cannot generate ${tickCount} '
'ticks when the axis range contains both positive and negative '
'values. A minimum of three ticks are required to include zero.');
// Determine the "favored" axis direction (the one which will control the
// ticks based on having a greater value / regions).
//
// Example: 13 / 3 (4.33 per tick) vs -5 / 1 (5 per tick)
// making -5 the favored number. A step size that includes this number
// ensures the other is also includes in the opposite direction.
final favorPositive = (high > 0 ? high / positiveRegionCount : 0).abs() >
(low < 0 ? low / negativeRegionCount : 0).abs();
final favoredNum = (favorPositive ? high : low).abs();
final favoredRegionCount =
favorPositive ? positiveRegionCount : negativeRegionCount;
final favoredTensBase = (_getEnclosingPowerOfTen(favoredNum)).abs();
// Check each step size and see if it would contain the "favored" value
for (double step in _allowedSteps) {
final tmpStepSize = _removeRoundingErrors(step * favoredTensBase);
// If prefer whole number, then don't allow a step that isn't one.
if (dataIsInWholeNumbers && (tmpStepSize).round() != tmpStepSize) {
continue;
}
// TODO: Skip steps that format to the same string.
// But wait until the last step to prevent the cost of the formatter.
// Potentially store the formatted strings in TickStepInfo?
if (tmpStepSize * favoredRegionCount >= favoredNum) {
double stepStart = negativeRegionCount > 0
? (-1 * tmpStepSize * negativeRegionCount)
: 0.0;
return new _TickStepInfo(tmpStepSize, stepStart);
}
}
} else {
// Find the range base to calculate step sizes.
final diffTensBase = _getEnclosingPowerOfTen(high - low);
// Walk the step sizes calculating a starting point and seeing if the high
// end is included in the range given that step size.
for (double step in _allowedSteps) {
final tmpStepSize = _removeRoundingErrors(step * diffTensBase);
// If prefer whole number, then don't allow a step that isn't one.
if (dataIsInWholeNumbers && (tmpStepSize).round() != tmpStepSize) {
continue;
}
// TODO: Skip steps that format to the same string.
// But wait until the last step to prevent the cost of the formatter.
double tmpStepStart = _getStepLessThan(low, tmpStepSize);
if (tmpStepStart + (tmpStepSize * regionCount) >= high) {
return new _TickStepInfo(tmpStepSize, tmpStepStart);
}
}
}
return new _TickStepInfo(1.0, low.floorToDouble());
}
List<double> _getTickValues(_TickStepInfo steps, int tickCount) {
final tickValues = new List<double>(tickCount);
// We have our size and start, assign all the tick values to the given array.
for (int i = 0; i < tickCount; i++) {
tickValues[i] = dataToAxisUnitConverter.invert(
_removeRoundingErrors(steps.tickStart + (i * steps.stepSize)));
}
return tickValues;
}
/// Given the axisDimensions update the tick counts given they are not fixed.
void _updateTickCounts(num high, num low) {
int tmpMaxNumMajorTicks;
int tmpMinNumMajorTicks;
// If the domain range contains both positive and negative values, then we
// need a minimum of three ticks to include zero as a tick. Otherwise, we
// only need an upper and lower tick.
final absoluteMinTicks = (low < 0 && 0 < high) ? 3 : 2;
// If there is a desired tick range use it, if not calculate one.
if (_desiredMaxTickCount != null) {
tmpMinNumMajorTicks = max(_desiredMinTickCount, absoluteMinTicks);
tmpMaxNumMajorTicks = max(_desiredMaxTickCount, tmpMinNumMajorTicks);
} else {
double minPixelsPerTick = MIN_DIPS_BETWEEN_TICKS.toDouble();
tmpMinNumMajorTicks = absoluteMinTicks;
tmpMaxNumMajorTicks =
max(absoluteMinTicks, (_rangeWidth / minPixelsPerTick).floor());
}
// Don't blow away the previous array if it hasn't changed.
if (tmpMaxNumMajorTicks != _maxTickCount ||
tmpMinNumMajorTicks != _minTickCount) {
_maxTickCount = tmpMaxNumMajorTicks;
_minTickCount = tmpMinNumMajorTicks;
}
}
/// Returns the power of 10 which contains the [number].
///
/// If [number] is 0 returns 1.
/// Examples:
/// [number] of 63 returns 100
/// [number] of -63 returns -100
/// [number] of 0.63 returns 1
static double _getEnclosingPowerOfTen(num number) {
if (number == 0) {
return 1.0;
}
return pow(10, (log10e * log(number.abs())).ceil()) *
(number < 0.0 ? -1.0 : 1.0);
}
/// Returns the step numerically less than the number by step increments.
static double _getStepLessThan(double number, double stepSize) {
if (number == 0.0 || stepSize == 0.0) {
return 0.0;
}
return (stepSize > 0.0
? (number / stepSize).floor()
: (number / stepSize).ceil()) *
stepSize;
}
/// Attempts to slice off very small floating point rounding effects for the
/// given number.
///
/// @param number the number to round.
/// @return the rounded number.
static double _removeRoundingErrors(double number) {
// sufficiently large multiplier to handle generating ticks on the order
// of 10^-9.
const multiplier = 1.0e9;
return number > 100.0
? number.roundToDouble()
: (number * multiplier).roundToDouble() / multiplier;
}
}
class _TickStepInfo {
double stepSize;
double tickStart;
_TickStepInfo(this.stepSize, this.tickStart);
}

View File

@@ -0,0 +1,44 @@
// 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:collection' show HashSet;
import 'scale.dart' show Extents;
/// A range of ordinals.
class OrdinalExtents extends Extents<String> {
final List<String> _range;
/// The extents representing the ordinal values in [range].
///
/// The elements of [range] must all be unique.
///
/// [D] is the domain class type for the elements in the extents.
OrdinalExtents(List<String> range) : _range = range {
// This asserts that all elements in [range] are unique.
final uniqueValueCount = new HashSet.from(_range).length;
assert(uniqueValueCount == range.length);
}
factory OrdinalExtents.all(List<String> range) => new OrdinalExtents(range);
bool get isEmpty => _range.isEmpty;
/// The number of values inside this extent.
int get length => _range.length;
String operator [](int index) => _range[index];
int indexOf(String value) => _range.indexOf(value);
}

View File

@@ -0,0 +1,40 @@
// 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 'ordinal_scale_domain_info.dart' show OrdinalScaleDomainInfo;
import 'scale.dart' show MutableScale;
abstract class OrdinalScale extends MutableScale<String> {
/// The current domain collection with all added unique values.
OrdinalScaleDomainInfo get domain;
/// Sets the viewport of the scale based on the number of data points to show
/// and the starting domain value.
///
/// [viewportDataSize] How many ordinal domain values to show in the viewport.
/// [startingDomain] The starting domain value of the viewport. Note that if
/// the starting domain is in terms of position less than [domainValuesToShow]
/// from the last domain value the viewport will be fixed to the last value
/// and not guaranteed that this domain value is the first in the viewport.
void setViewport(int viewportDataSize, String startingDomain);
/// The number of full ordinal steps that fit in the viewport.
int get viewportDataSize;
/// The first fully visible ordinal step within the viewport.
///
/// Null if no domains exist.
String get viewportStartingDomain;
}

View File

@@ -0,0 +1,77 @@
// 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:collection' show HashMap;
import 'ordinal_extents.dart' show OrdinalExtents;
/// A domain processor for [OrdinalScale].
///
/// [D] domain class type of the values being tracked.
///
/// Unique domain values are kept, so duplicates will not increase the extent.
class OrdinalScaleDomainInfo {
int _index = 0;
/// A map of domain value and the order it was added.
final _domainsToOrder = new HashMap<String, int>();
/// A list of domain values kept to support [getDomainAtIndex].
final _domainList = <String>[];
OrdinalScaleDomainInfo();
OrdinalScaleDomainInfo copy() {
return new OrdinalScaleDomainInfo()
.._domainsToOrder.addAll(_domainsToOrder)
.._index = _index
.._domainList.addAll(_domainList);
}
void add(String domain) {
if (!_domainsToOrder.containsKey(domain)) {
_domainsToOrder[domain] = _index;
_index += 1;
_domainList.add(domain);
}
}
int indexOf(String domain) => _domainsToOrder[domain];
String getDomainAtIndex(int index) {
assert(index >= 0);
assert(index < _index);
return _domainList[index];
}
List<String> get domains => _domainList;
String get first => _domainList.isEmpty ? null : _domainList.first;
String get last => _domainList.isEmpty ? null : _domainList.last;
bool get isEmpty => (_index == 0);
bool get isNotEmpty => !isEmpty;
OrdinalExtents get extent => new OrdinalExtents.all(_domainList);
int get size => _index;
/// Clears all domain values.
void clear() {
_domainsToOrder.clear();
_domainList.clear();
_index = 0;
}
}

View File

@@ -0,0 +1,58 @@
// 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:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'ordinal_scale.dart' show OrdinalScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
import 'tick_provider.dart' show BaseTickProvider, TickHint;
/// A strategy for selecting ticks to draw given ordinal domain values.
class OrdinalTickProvider extends BaseTickProvider<String> {
const OrdinalTickProvider();
@override
List<Tick<String>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required List<String> domainValues,
@required OrdinalScale scale,
@required TickFormatter formatter,
@required Map<String, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<String> tickHint,
}) {
return createTicks(scale.domain.domains,
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy);
}
@override
bool operator ==(other) => other is OrdinalTickProvider;
@override
int get hashCode => 31;
}

View File

@@ -0,0 +1,313 @@
// 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' as math show max, min;
/// Scale used to convert data input domain units to output range units.
///
/// This is the immutable portion of the Scale definition. Used for converting
/// data from the dataset in domain units to an output in range units (likely
/// pixel range of the area to draw on).
///
/// <p>The Scale/MutableScale split is to show the intention of what you can or
/// should be doing with the scale during different stages of chart draw
/// process.
///
/// [D] is the domain class type for the values passed in.
abstract class Scale<D> {
/// Applies the scale function to the [domainValue].
///
/// Returns the pixel location for the given [domainValue] or null if the
/// domainValue could not be found/translated by this scale.
/// Non-numeric scales should be the only ones that can return null.
num operator [](D domainValue);
/// Reverse application of the scale.
D reverse(double pixelLocation);
/// Tests a [domainValue] to see if the scale can translate it.
///
/// Returns true if the scale can translate the given domainValue.
/// (Ex: linear scales can translate any number, but ordinal scales can only
/// translate values previously passed in.)
bool canTranslate(D domainValue);
/// Returns the previously set output range for the scale function.
ScaleOutputExtent get range;
/// Returns the absolute width between the max and min range values.
int get rangeWidth;
/// Returns the configuration used to determine the rangeBand.
///
/// This is most often used to define the bar group width.
RangeBandConfig get rangeBandConfig;
/// Returns the rangeBand width in pixels.
///
/// The rangeBand is determined using the RangeBandConfig potentially with the
/// measured step size. This value is used as the bar group width. If
/// StepSizeConfig is set to auto detect, then you must wait until after
/// the chart's onPostLayout phase before you'll get a valid number.
double get rangeBand;
/// Returns the stepSize width in pixels.
///
/// The step size is determined using the [StepSizeConfig].
double get stepSize;
/// Returns the stepSize domain value.
double get domainStepSize;
/// Tests whether the given [domainValue] is within the axis' range.
///
/// Returns < 0 if the [domainValue] would plot before the viewport, 0 if it
/// would plot within the viewport and > 0 if it would plot beyond the
/// viewport of the axis.
int compareDomainValueToViewport(D domainValue);
/// Returns true if the given [rangeValue] point is within the output range.
///
/// Not to be confused with the start and end of the domain.
bool isRangeValueWithinViewport(double rangeValue);
/// Returns the current viewport scale.
///
/// A scale of 1.0 would map the data directly to the output range, while a
/// value of 2.0 would map the data to an output of double the range so you
/// only see half the data in the viewport. This is the equivalent to
/// zooming. Its value is likely >= 1.0.
double get viewportScalingFactor;
/// Returns the current pixel viewport offset
///
/// The translate is used by the scale function when it applies the scale.
/// This is the equivalent to panning. Its value is likely <= 0 to pan the
/// data to the left.
double get viewportTranslatePx;
/// Returns a mutable copy of the scale.
///
/// Mutating the returned scale will not effect the original one.
MutableScale<D> copy();
}
/// Mutable extension of the [Scale] definition.
///
/// Used for converting data from the dataset to some range (likely pixel range)
/// of the area to draw on.
///
/// [D] the domain class type for the values passed in.
abstract class MutableScale<D> extends Scale<D> {
/// Reset the domain for this [Scale].
void resetDomain();
/// Reset the viewport settings for this [Scale].
void resetViewportSettings();
/// Add [domainValue] to this [Scale]'s domain.
///
/// Domains should be added in order to allow proper stepSize detection.
/// [domainValue] is the data value to add to the scale used to update the
/// domain extent.
void addDomain(D domainValue);
/// Sets the output range to use for the scale's conversion.
///
/// The range start is mapped to the domain's min and the range end is
/// mapped to the domain's max for the conversion using the domain nicing
/// function.
///
/// [extent] is the extent of the range which will likely be the pixel
/// range of the drawing area to convert to.
set range(ScaleOutputExtent extent);
/// Configures the zoom and translate.
///
/// [viewportScale] is the zoom factor to use, likely >= 1.0 where 1.0 maps
/// the complete data extents to the output range, and 2.0 only maps half the
/// data to the output range.
///
/// [viewportTranslatePx] is the translate/pan to use in pixel units,
/// likely <= 0 which shifts the start of the data before the edge of the
/// chart giving us a pan.
void setViewportSettings(double viewportScale, double viewportTranslatePx);
/// Sets the configuration used to determine the rangeBand (bar group width).
set rangeBandConfig(RangeBandConfig barGroupWidthConfig);
/// Sets the method for determining the step size.
///
/// This is the domain space between data points.
StepSizeConfig get stepSizeConfig;
set stepSizeConfig(StepSizeConfig config);
}
/// Tuple of the output for a scale in pixels from [start] to [end] inclusive.
///
/// It is different from [Extent] because it focuses on start and end and not
/// min and max, meaning that start could be greater or less than end.
class ScaleOutputExtent {
final int start;
final int end;
const ScaleOutputExtent(this.start, this.end);
int get min => math.min(start, end);
int get max => math.max(start, end);
bool containsValue(double value) => value >= min && value <= max;
/// Returns the difference between the extents.
///
/// If the [end] is less than the [start] (think vertical measure axis), then
/// this will correctly return a negative value.
int get diff => end - start;
/// Returns the width of the extent.
int get width => diff.abs();
@override
bool operator ==(other) =>
other is ScaleOutputExtent && start == other.start && end == other.end;
@override
int get hashCode => start.hashCode + (end.hashCode * 31);
@override
String toString() => "ScaleOutputRange($start, $end)";
}
/// Type of RangeBand used to determine the rangeBand size units.
enum RangeBandType {
/// No rangeBand (not suitable for bars or step line charts).
none,
/// Size is specified in pixel units.
fixedPixel,
/// Size is specified domain scale units.
fixedDomain,
/// Size is a percentage of the minimum step size between points.
fixedPercentOfStep,
/// Size is a style pack assigned percentage of the minimum step size between
/// points.
styleAssignedPercentOfStep,
/// Size is subtracted from the minimum step size between points in pixel
/// units.
fixedPixelSpaceFromStep,
}
/// Defines the method for calculating the rangeBand of the Scale.
///
/// The rangeBand is used to determine the width of a group of bars. The term
/// rangeBand comes from the d3 JavaScript library which the JS library uses
/// internally.
///
/// <p>RangeBandConfig is immutable, See factory methods for creating one.
class RangeBandConfig {
final RangeBandType type;
/// The width of the band in units specified by the bandType.
final double size;
/// Creates a rangeBand definition of zero, no rangeBand.
const RangeBandConfig.none()
: type = RangeBandType.none,
size = 0.0;
/// Creates a fixed rangeBand definition in pixel width.
///
/// Used to determine a bar width or a step width in the line renderer.
const RangeBandConfig.fixedPixel(double pixels)
: type = RangeBandType.fixedPixel,
size = pixels;
/// Creates a fixed rangeBand definition in domain unit width.
///
/// Used to determine a bar width or a step width in the line renderer.
const RangeBandConfig.fixedDomain(double domainSize)
: type = RangeBandType.fixedDomain,
size = domainSize;
/// Creates a config that defines the rangeBand as equal to the stepSize.
const RangeBandConfig.stepChartBand()
: type = RangeBandType.fixedPercentOfStep,
size = 1.0;
/// Creates a config that defines the rangeBand as percentage of the stepSize.
///
/// [percentOfStepWidth] is the percentage of the step from 0.0 - 1.0.
RangeBandConfig.percentOfStep(double percentOfStepWidth)
: type = RangeBandType.fixedPercentOfStep,
size = percentOfStepWidth {
assert(percentOfStepWidth >= 0 && percentOfStepWidth <= 1.0);
}
/// Creates a config that assigns the rangeBand according to the stylepack.
///
/// <p>Note: renderers can detect this setting and update the percent based on
/// the number of series in their preprocess.
RangeBandConfig.styleAssignedPercent([int seriesCount = 1])
: type = RangeBandType.styleAssignedPercentOfStep,
// TODO: retrieve value from the stylepack once available.
size = 0.65;
/// Creates a config that defines the rangeBand as the stepSize - pixels.
///
/// Where fixedPixels() gave you a constant rangBand in pixels, this will give
/// you a constant space between rangeBands in pixels.
const RangeBandConfig.fixedPixelSpaceBetweenStep(double pixels)
: type = RangeBandType.fixedPixelSpaceFromStep,
size = pixels;
}
/// Type of step size calculation to use.
enum StepSizeType { autoDetect, fixedDomain, fixedPixels }
/// Defines the method for calculating the stepSize between points.
///
/// Typically auto will work fine in most cases, but if your data is
/// irregular or you only have one data point, then you may want to override the
/// stepSize detection specifying the exact expected stepSize.
class StepSizeConfig {
final StepSizeType type;
final double size;
/// Creates a StepSizeConfig that calculates step size based on incoming data.
///
/// The stepSize is determined is calculated by detecting the smallest
/// distance between two adjacent data points. This may not be suitable if
/// you have irregular data or just a single data point.
const StepSizeConfig.auto()
: type = StepSizeType.autoDetect,
size = 0.0;
/// Creates a StepSizeConfig specifying the exact step size in pixel units.
const StepSizeConfig.fixedPixels(double pixels)
: type = StepSizeType.fixedPixels,
size = pixels;
/// Creates a StepSizeConfig specifying the exact step size in domain units.
const StepSizeConfig.fixedDomain(double domainSize)
: type = StepSizeType.fixedDomain,
size = domainSize;
}
// TODO: make other extent subclasses plural.
abstract class Extents<D> {}

View File

@@ -0,0 +1,344 @@
// 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 min, max;
import 'ordinal_scale.dart' show OrdinalScale;
import 'ordinal_scale_domain_info.dart' show OrdinalScaleDomainInfo;
import 'scale.dart'
show
RangeBandConfig,
RangeBandType,
StepSizeConfig,
StepSizeType,
ScaleOutputExtent;
/// Scale that converts ordinal values of type [D] to a given range output.
///
/// A `SimpleOrdinalScale` is used to map values from its domain to the
/// available pixel range of the chart. Typically used for bar charts where the
/// width of the bar is [rangeBand] and the position of the bar is retrieved
/// by [[]].
class SimpleOrdinalScale implements OrdinalScale {
final _stepSizeConfig = new StepSizeConfig.auto();
OrdinalScaleDomainInfo _domain;
ScaleOutputExtent _range = new ScaleOutputExtent(0, 1);
double _viewportScale = 1.0;
double _viewportTranslatePx = 0.0;
RangeBandConfig _rangeBandConfig = new RangeBandConfig.styleAssignedPercent();
bool _scaleChanged = true;
double _cachedStepSizePixels;
double _cachedRangeBandShift;
double _cachedRangeBandSize;
int _viewportDataSize;
String _viewportStartingDomain;
SimpleOrdinalScale() : _domain = new OrdinalScaleDomainInfo();
SimpleOrdinalScale._copy(SimpleOrdinalScale other)
: _domain = other._domain.copy(),
_range = new ScaleOutputExtent(other._range.start, other._range.end),
_viewportScale = other._viewportScale,
_viewportTranslatePx = other._viewportTranslatePx,
_rangeBandConfig = other._rangeBandConfig;
@override
double get rangeBand {
if (_scaleChanged) {
_updateScale();
}
return _cachedRangeBandSize;
}
@override
double get stepSize {
if (_scaleChanged) {
_updateScale();
}
return _cachedStepSizePixels;
}
@override
double get domainStepSize => 1.0;
@override
set rangeBandConfig(RangeBandConfig barGroupWidthConfig) {
if (barGroupWidthConfig == null) {
throw new ArgumentError.notNull('RangeBandConfig must not be null.');
}
if (barGroupWidthConfig.type == RangeBandType.fixedDomain ||
barGroupWidthConfig.type == RangeBandType.none) {
throw new ArgumentError(
'barGroupWidthConfig must not be NONE or FIXED_DOMAIN');
}
_rangeBandConfig = barGroupWidthConfig;
_scaleChanged = true;
}
@override
RangeBandConfig get rangeBandConfig => _rangeBandConfig;
@override
set stepSizeConfig(StepSizeConfig config) {
if (config != null && config.type != StepSizeType.autoDetect) {
throw new ArgumentError(
'Ordinal scales only support StepSizeConfig of type Auto');
}
// Nothing is set because only auto is supported.
}
@override
StepSizeConfig get stepSizeConfig => _stepSizeConfig;
/// Converts [domainValue] to the position to place the band/bar.
///
/// Returns 0 if not found.
@override
num operator [](String domainValue) {
if (_scaleChanged) {
_updateScale();
}
final i = _domain.indexOf(domainValue);
if (i != null) {
return viewportTranslatePx +
_range.start +
_cachedRangeBandShift +
(_cachedStepSizePixels * i);
}
// If it wasn't found
return 0.0;
}
@override
String reverse(double pixelLocation) {
final index = ((pixelLocation -
viewportTranslatePx -
_range.start -
_cachedRangeBandShift) /
_cachedStepSizePixels);
// The last pixel belongs in the last step even if it tries to round up.
//
// Index may be less than 0 when [pixelLocation] is less than the width of
// the range band shift. This may happen on the far left side of the chart,
// where we want the first datum anyways. Wrapping the result in "max(0, x)"
// cuts off these negative values.
return _domain
.getDomainAtIndex(max(0, min(index.round(), domain.size - 1)));
}
@override
bool canTranslate(String domainValue) =>
(_domain.indexOf(domainValue) != null);
@override
OrdinalScaleDomainInfo get domain => _domain;
/// Update the scale to include [domainValue].
@override
void addDomain(String domainValue) {
_domain.add(domainValue);
_scaleChanged = true;
}
@override
set range(ScaleOutputExtent extent) {
_range = extent;
_scaleChanged = true;
}
@override
ScaleOutputExtent get range => _range;
@override
resetDomain() {
_domain.clear();
_scaleChanged = true;
}
@override
resetViewportSettings() {
_viewportScale = 1.0;
_viewportTranslatePx = 0.0;
_scaleChanged = true;
}
@override
int get rangeWidth => (range.start - range.end).abs().toInt();
@override
double get viewportScalingFactor => _viewportScale;
@override
double get viewportTranslatePx => _viewportTranslatePx;
@override
void setViewportSettings(double viewportScale, double viewportTranslatePx) {
_viewportScale = viewportScale;
_viewportTranslatePx =
min(0.0, max(rangeWidth * (1.0 - viewportScale), viewportTranslatePx));
_scaleChanged = true;
}
@override
void setViewport(int viewportDataSize, String startingDomain) {
if (startingDomain != null &&
viewportDataSize != null &&
viewportDataSize <= 0) {
throw new ArgumentError('viewportDataSize can' 't be less than 1.');
}
_scaleChanged = true;
_viewportDataSize = viewportDataSize;
_viewportStartingDomain = startingDomain;
}
/// Update this scale's viewport using settings [_viewportDataSize] and
/// [_viewportStartingDomain].
void _updateViewport() {
setViewportSettings(1.0, 0.0);
_recalculateScale();
if (_domain.isEmpty) {
return;
}
// Update the scale with zoom level to help find the correct translate.
setViewportSettings(
_domain.size / min(_viewportDataSize, _domain.size), 0.0);
_recalculateScale();
final domainIndex = _domain.indexOf(_viewportStartingDomain);
if (domainIndex != null) {
// Update the translate so that the scale starts half a step before the
// chosen domain.
final viewportTranslatePx = -(_cachedStepSizePixels * domainIndex);
setViewportSettings(_viewportScale, viewportTranslatePx);
}
}
@override
int get viewportDataSize {
if (_scaleChanged) {
_updateScale();
}
return _domain.isEmpty ? 0 : (rangeWidth ~/ _cachedStepSizePixels);
}
@override
String get viewportStartingDomain {
if (_scaleChanged) {
_updateScale();
}
if (_domain.isEmpty) {
return null;
}
return _domain.getDomainAtIndex(
(-_viewportTranslatePx / _cachedStepSizePixels).ceil().toInt());
}
@override
bool isRangeValueWithinViewport(double rangeValue) {
return range != null && rangeValue >= range.min && rangeValue <= range.max;
}
@override
int compareDomainValueToViewport(String domainValue) {
// TODO: This currently works because range defaults to 0-1
// This needs to be looked into further.
var i = _domain.indexOf(domainValue);
if (i != null && range != null) {
var domainPx = this[domainValue];
if (domainPx < range.min) {
return -1;
}
if (domainPx > range.max) {
return 1;
}
return 0;
}
return -1;
}
@override
SimpleOrdinalScale copy() => new SimpleOrdinalScale._copy(this);
void _updateCachedFields(
double stepSizePixels, double rangeBandPixels, double rangeBandShift) {
_cachedStepSizePixels = stepSizePixels;
_cachedRangeBandSize = rangeBandPixels;
_cachedRangeBandShift = rangeBandShift;
// TODO: When there are horizontal bars increasing from where
// the domain and measure axis intersects but the desired behavior is
// flipped. The plan is to fix this by fixing code to flip the range in the
// code.
// If range start is less than range end, then the domain is calculated by
// adding the band width. If range start is greater than range end, then the
// domain is calculated by subtracting from the band width (ex. horizontal
// bar charts where first series is at the bottom of the chart).
if (range.start > range.end) {
_cachedStepSizePixels *= -1;
_cachedRangeBandShift *= -1;
}
_scaleChanged = false;
}
void _updateScale() {
if (_viewportStartingDomain != null && _viewportDataSize != null) {
// Update viewport recalculates the scale.
_updateViewport();
}
_recalculateScale();
}
void _recalculateScale() {
final stepSizePixels = _domain.isEmpty
? 0.0
: _viewportScale * (rangeWidth.toDouble() / _domain.size.toDouble());
double rangeBandPixels;
switch (rangeBandConfig.type) {
case RangeBandType.fixedPixel:
rangeBandPixels = rangeBandConfig.size.toDouble();
break;
case RangeBandType.fixedPixelSpaceFromStep:
var spaceInPixels = rangeBandConfig.size.toDouble();
rangeBandPixels = max(0.0, stepSizePixels - spaceInPixels);
break;
case RangeBandType.styleAssignedPercentOfStep:
case RangeBandType.fixedPercentOfStep:
var percent = rangeBandConfig.size.toDouble();
rangeBandPixels = stepSizePixels * percent;
break;
case RangeBandType.fixedDomain:
case RangeBandType.none:
default:
throw new StateError('RangeBandType must not be NONE or FIXED_DOMAIN');
break;
}
_updateCachedFields(stepSizePixels, rangeBandPixels, stepSizePixels / 2.0);
}
}

View File

@@ -0,0 +1,181 @@
// 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:meta/meta.dart' show immutable;
import '../../../../common/color.dart' show Color;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show Axis;
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import '../tick_formatter.dart' show TickFormatter;
import '../tick_provider.dart' show TickProvider;
@immutable
class AxisSpec<D> {
final bool showAxisLine;
final RenderSpec<D> renderSpec;
final TickProviderSpec<D> tickProviderSpec;
final TickFormatterSpec<D> tickFormatterSpec;
const AxisSpec({
this.renderSpec,
this.tickProviderSpec,
this.tickFormatterSpec,
this.showAxisLine,
});
factory AxisSpec.from(
AxisSpec other, {
RenderSpec<D> renderSpec,
TickProviderSpec<D> tickProviderSpec,
TickFormatterSpec<D> tickFormatterSpec,
bool showAxisLine,
}) {
return new AxisSpec(
renderSpec: renderSpec ?? other.renderSpec,
tickProviderSpec: tickProviderSpec ?? other.tickProviderSpec,
tickFormatterSpec: tickFormatterSpec ?? other.tickFormatterSpec,
showAxisLine: showAxisLine ?? other.showAxisLine,
);
}
configure(
Axis<D> axis, ChartContext context, GraphicsFactory graphicsFactory) {
if (showAxisLine != null) {
axis.forceDrawAxisLine = showAxisLine;
}
if (renderSpec != null) {
axis.tickDrawStrategy =
renderSpec.createDrawStrategy(context, graphicsFactory);
}
if (tickProviderSpec != null) {
axis.tickProvider = tickProviderSpec.createTickProvider(context);
}
if (tickFormatterSpec != null) {
axis.tickFormatter = tickFormatterSpec.createTickFormatter(context);
}
}
/// Creates an appropriately typed [Axis].
Axis<D> createAxis() => null;
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AxisSpec &&
renderSpec == other.renderSpec &&
tickProviderSpec == other.tickProviderSpec &&
tickFormatterSpec == other.tickFormatterSpec &&
showAxisLine == other.showAxisLine);
@override
int get hashCode {
int hashcode = renderSpec?.hashCode ?? 0;
hashcode = (hashcode * 37) + tickProviderSpec.hashCode;
hashcode = (hashcode * 37) + tickFormatterSpec.hashCode;
hashcode = (hashcode * 37) + showAxisLine.hashCode;
return hashcode;
}
}
@immutable
abstract class TickProviderSpec<D> {
TickProvider<D> createTickProvider(ChartContext context);
}
@immutable
abstract class TickFormatterSpec<D> {
TickFormatter<D> createTickFormatter(ChartContext context);
}
@immutable
abstract class RenderSpec<D> {
const RenderSpec();
TickDrawStrategy<D> createDrawStrategy(
ChartContext context, GraphicsFactory graphicFactory);
}
@immutable
class TextStyleSpec {
final String fontFamily;
final int fontSize;
final Color color;
const TextStyleSpec({this.fontFamily, this.fontSize, this.color});
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is TextStyleSpec &&
fontFamily == other.fontFamily &&
fontSize == other.fontSize &&
color == other.color);
}
@override
int get hashCode {
int hashcode = fontFamily?.hashCode ?? 0;
hashcode = (hashcode * 37) + fontSize?.hashCode ?? 0;
hashcode = (hashcode * 37) + color?.hashCode ?? 0;
return hashcode;
}
}
@immutable
class LineStyleSpec {
final Color color;
final List<int> dashPattern;
final int thickness;
const LineStyleSpec({this.color, this.dashPattern, this.thickness});
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is LineStyleSpec &&
color == other.color &&
dashPattern == other.dashPattern &&
thickness == other.thickness);
}
@override
int get hashCode {
int hashcode = color?.hashCode ?? 0;
hashcode = (hashcode * 37) + dashPattern?.hashCode ?? 0;
hashcode = (hashcode * 37) + thickness?.hashCode ?? 0;
return hashcode;
}
}
enum TickLabelAnchor {
before,
centered,
after,
/// The top most tick draws all text under the location.
/// The bottom most tick draws all text above the location.
/// The rest of the ticks are centered.
inside,
}
enum TickLabelJustification {
inside,
outside,
}

View File

@@ -0,0 +1,170 @@
// 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:intl/intl.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show Axis, NumericAxis;
import '../linear/bucketing_numeric_axis.dart' show BucketingNumericAxis;
import '../linear/bucketing_numeric_tick_provider.dart'
show BucketingNumericTickProvider;
import '../numeric_extents.dart' show NumericExtents;
import 'axis_spec.dart' show AxisSpec, RenderSpec;
import 'numeric_axis_spec.dart'
show
BasicNumericTickFormatterSpec,
BasicNumericTickProviderSpec,
NumericAxisSpec,
NumericTickProviderSpec,
NumericTickFormatterSpec;
/// A numeric [AxisSpec] that positions all values beneath a certain [threshold]
/// into a reserved space on the axis range. The label for the bucket line will
/// be drawn in the middle of the bucket range, rather than aligned with the
/// gridline for that value's position on the scale.
///
/// An example illustration of a bucketing measure axis on a point chart
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
/// of the axis, since they are less than the [threshold] value of 10%.
///
/// 100% ┠─────────────────────────
/// ┃ *
/// ┃ *
/// 50% ┠──────*──────────────────
/// ┃
/// ┠─────────────────────────
/// < 10% ┃ * *
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
/// 0 50 100
///
/// This axis will format numbers as percents by default.
@immutable
class BucketingAxisSpec extends NumericAxisSpec {
/// All values smaller than the threshold will be bucketed into the same
/// position in the reserved space on the axis.
final num threshold;
/// Whether or not measure values bucketed below the [threshold] should be
/// visible on the chart, or collapsed.
///
/// If this is false, then any data with measure values smaller than
/// [threshold] will not be rendered on the chart.
final bool showBucket;
/// Creates a [NumericAxisSpec] that is specialized for percentage data.
BucketingAxisSpec({
RenderSpec<num> renderSpec,
NumericTickProviderSpec tickProviderSpec,
NumericTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
bool showBucket,
this.threshold,
NumericExtents viewport,
}) : this.showBucket = showBucket ?? true,
super(
renderSpec: renderSpec,
tickProviderSpec:
tickProviderSpec ?? const BucketingNumericTickProviderSpec(),
tickFormatterSpec: tickFormatterSpec ??
new BasicNumericTickFormatterSpec.fromNumberFormat(
new NumberFormat.percentPattern()),
showAxisLine: showAxisLine,
viewport: viewport ?? const NumericExtents(0.0, 1.0));
@override
configure(
Axis<num> axis, ChartContext context, GraphicsFactory graphicsFactory) {
super.configure(axis, context, graphicsFactory);
if (axis is NumericAxis && viewport != null) {
axis.setScaleViewport(viewport);
}
if (axis is BucketingNumericAxis && threshold != null) {
axis.threshold = threshold;
}
if (axis is BucketingNumericAxis && showBucket != null) {
axis.showBucket = showBucket;
}
}
@override
BucketingNumericAxis createAxis() => new BucketingNumericAxis();
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is BucketingAxisSpec &&
showBucket == other.showBucket &&
threshold == other.threshold &&
super == (other));
@override
int get hashCode {
int hashcode = super.hashCode;
hashcode = (hashcode * 37) + showBucket.hashCode;
hashcode = (hashcode * 37) + threshold.hashCode;
return hashcode;
}
}
@immutable
class BucketingNumericTickProviderSpec extends BasicNumericTickProviderSpec {
/// Creates a [TickProviderSpec] that generates ticks for a bucketing axis.
///
/// [zeroBound] automatically include zero in the data range.
/// [dataIsInWholeNumbers] skip over ticks that would produce
/// fractional ticks that don't make sense for the domain (ie: headcount).
/// [desiredTickCount] the fixed number of ticks to try to make. Convenience
/// that sets [desiredMinTickCount] and [desiredMaxTickCount] the same.
/// Both min and max win out if they are set along with
/// [desiredTickCount].
/// [desiredMinTickCount] automatically choose the best tick
/// count to produce the 'nicest' ticks but make sure we have this many.
/// [desiredMaxTickCount] automatically choose the best tick
/// count to produce the 'nicest' ticks but make sure we don't have more
/// than this many.
const BucketingNumericTickProviderSpec(
{bool zeroBound,
bool dataIsInWholeNumbers,
int desiredTickCount,
int desiredMinTickCount,
int desiredMaxTickCount})
: super(
zeroBound: zeroBound ?? true,
dataIsInWholeNumbers: dataIsInWholeNumbers ?? false,
desiredTickCount: desiredTickCount,
desiredMinTickCount: desiredMinTickCount,
desiredMaxTickCount: desiredMaxTickCount,
);
@override
BucketingNumericTickProvider createTickProvider(ChartContext context) {
final provider = new BucketingNumericTickProvider()
..zeroBound = zeroBound
..dataIsInWholeNumbers = dataIsInWholeNumbers;
if (desiredMinTickCount != null ||
desiredMaxTickCount != null ||
desiredTickCount != null) {
provider.setTickCount(desiredMaxTickCount ?? desiredTickCount ?? 10,
desiredMinTickCount ?? desiredTickCount ?? 2);
}
return provider;
}
}

View File

@@ -0,0 +1,327 @@
// 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:meta/meta.dart' show immutable;
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show Axis;
import '../end_points_tick_provider.dart' show EndPointsTickProvider;
import '../static_tick_provider.dart' show StaticTickProvider;
import '../time/auto_adjusting_date_time_tick_provider.dart'
show AutoAdjustingDateTimeTickProvider;
import '../time/date_time_axis.dart' show DateTimeAxis;
import '../time/date_time_extents.dart' show DateTimeExtents;
import '../time/date_time_tick_formatter.dart' show DateTimeTickFormatter;
import '../time/day_time_stepper.dart' show DayTimeStepper;
import '../time/hour_tick_formatter.dart' show HourTickFormatter;
import '../time/time_range_tick_provider_impl.dart'
show TimeRangeTickProviderImpl;
import '../time/time_tick_formatter.dart' show TimeTickFormatter;
import '../time/time_tick_formatter_impl.dart'
show CalendarField, TimeTickFormatterImpl;
import 'axis_spec.dart'
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
import 'tick_spec.dart' show TickSpec;
/// Generic [AxisSpec] specialized for Timeseries charts.
@immutable
class DateTimeAxisSpec extends AxisSpec<DateTime> {
/// Sets viewport for this Axis.
///
/// If pan / zoom behaviors are set, this is the initial viewport.
final DateTimeExtents viewport;
/// Creates a [AxisSpec] that specialized for timeseries charts.
///
/// [renderSpec] spec used to configure how the ticks and labels
/// actually render. Possible values are [GridlineRendererSpec],
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
/// given to the RenderSpec is of type [DateTime] for Timeseries.
/// [tickProviderSpec] spec used to configure what ticks are generated.
/// [tickFormatterSpec] spec used to configure how the tick labels
/// are formatted.
/// [showAxisLine] override to force the axis to draw the axis
/// line.
const DateTimeAxisSpec({
RenderSpec<DateTime> renderSpec,
DateTimeTickProviderSpec tickProviderSpec,
DateTimeTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
this.viewport,
}) : super(
renderSpec: renderSpec,
tickProviderSpec: tickProviderSpec,
tickFormatterSpec: tickFormatterSpec,
showAxisLine: showAxisLine);
@override
configure(Axis<DateTime> axis, ChartContext context,
GraphicsFactory graphicsFactory) {
super.configure(axis, context, graphicsFactory);
if (axis is DateTimeAxis && viewport != null) {
axis.setScaleViewport(viewport);
}
}
Axis<DateTime> createAxis() {
assert(false, 'Call createDateTimeAxis() to create a DateTimeAxis.');
return null;
}
/// Creates a [DateTimeAxis]. This should be called in place of createAxis.
DateTimeAxis createDateTimeAxis(DateTimeFactory dateTimeFactory) =>
new DateTimeAxis(dateTimeFactory);
@override
bool operator ==(Object other) =>
other is DateTimeAxisSpec &&
viewport == other.viewport &&
super == (other);
@override
int get hashCode {
int hashcode = super.hashCode;
hashcode = (hashcode * 37) + viewport.hashCode;
return hashcode;
}
}
abstract class DateTimeTickProviderSpec extends TickProviderSpec<DateTime> {}
abstract class DateTimeTickFormatterSpec extends TickFormatterSpec<DateTime> {}
/// [TickProviderSpec] that sets up the automatically assigned time ticks based
/// on the extents of your data.
@immutable
class AutoDateTimeTickProviderSpec implements DateTimeTickProviderSpec {
final bool includeTime;
/// Creates a [TickProviderSpec] that dynamically chooses ticks based on the
/// extents of the data.
///
/// [includeTime] - flag that indicates whether the time should be
/// included when choosing appropriate tick intervals.
const AutoDateTimeTickProviderSpec({this.includeTime = true});
@override
AutoAdjustingDateTimeTickProvider createTickProvider(ChartContext context) {
if (includeTime) {
return new AutoAdjustingDateTimeTickProvider.createDefault(
context.dateTimeFactory);
} else {
return new AutoAdjustingDateTimeTickProvider.createWithoutTime(
context.dateTimeFactory);
}
}
@override
bool operator ==(Object other) =>
other is AutoDateTimeTickProviderSpec && includeTime == other.includeTime;
@override
int get hashCode => includeTime?.hashCode ?? 0;
}
/// [TickProviderSpec] that sets up time ticks with days increments only.
@immutable
class DayTickProviderSpec implements DateTimeTickProviderSpec {
final List<int> increments;
const DayTickProviderSpec({this.increments});
/// Creates a [TickProviderSpec] that dynamically chooses ticks based on the
/// extents of the data, limited to day increments.
///
/// [increments] specify the number of day increments that can be chosen from
/// when searching for the appropriate tick intervals.
@override
AutoAdjustingDateTimeTickProvider createTickProvider(ChartContext context) {
return new AutoAdjustingDateTimeTickProvider.createWith([
new TimeRangeTickProviderImpl(new DayTimeStepper(context.dateTimeFactory,
allowedTickIncrements: increments))
]);
}
@override
bool operator ==(Object other) =>
other is DayTickProviderSpec && increments == other.increments;
@override
int get hashCode => increments?.hashCode ?? 0;
}
/// [TickProviderSpec] that sets up time ticks at the two end points of the axis
/// range.
@immutable
class DateTimeEndPointsTickProviderSpec implements DateTimeTickProviderSpec {
const DateTimeEndPointsTickProviderSpec();
/// Creates a [TickProviderSpec] that dynamically chooses time ticks at the
/// two end points of the axis range
@override
EndPointsTickProvider<DateTime> createTickProvider(ChartContext context) {
return new EndPointsTickProvider<DateTime>();
}
@override
bool operator ==(Object other) => other is DateTimeEndPointsTickProviderSpec;
}
/// [TickProviderSpec] that allows you to specific the ticks to be used.
@immutable
class StaticDateTimeTickProviderSpec implements DateTimeTickProviderSpec {
final List<TickSpec<DateTime>> tickSpecs;
const StaticDateTimeTickProviderSpec(this.tickSpecs);
@override
StaticTickProvider<DateTime> createTickProvider(ChartContext context) =>
new StaticTickProvider<DateTime>(tickSpecs);
@override
bool operator ==(Object other) =>
other is StaticDateTimeTickProviderSpec && tickSpecs == other.tickSpecs;
@override
int get hashCode => tickSpecs.hashCode;
}
/// Formatters for a single level of the [DateTimeTickFormatterSpec].
@immutable
class TimeFormatterSpec {
final String format;
final String transitionFormat;
final String noonFormat;
/// Creates a formatter for a particular granularity of data.
///
/// [format] [DateFormat] format string used to format non-transition ticks.
/// The string is given to the dateTimeFactory to support i18n formatting.
/// [transitionFormat] [DateFormat] format string used to format transition
/// ticks. Examples of transition ticks:
/// Day ticks would have a transition tick at month boundaries.
/// Hour ticks would have a transition tick at day boundaries.
/// The first tick is typically a transition tick.
/// [noonFormat] [DateFormat] format string used only for formatting hours
/// in the event that you want to format noon differently than other
/// hours (ie: [10, 11, 12p, 1, 2, 3]).
const TimeFormatterSpec(
{this.format, this.transitionFormat, this.noonFormat});
@override
bool operator ==(Object other) =>
other is TimeFormatterSpec &&
format == other.format &&
transitionFormat == other.transitionFormat &&
noonFormat == other.noonFormat;
@override
int get hashCode {
int hashcode = format?.hashCode ?? 0;
hashcode = (hashcode * 37) + transitionFormat?.hashCode ?? 0;
hashcode = (hashcode * 37) + noonFormat?.hashCode ?? 0;
return hashcode;
}
}
/// [TickFormatterSpec] that automatically chooses the appropriate level of
/// formatting based on the tick stepSize. Each level of date granularity has
/// its own [TimeFormatterSpec] used to specify the formatting strings at that
/// level.
@immutable
class AutoDateTimeTickFormatterSpec implements DateTimeTickFormatterSpec {
final TimeFormatterSpec minute;
final TimeFormatterSpec hour;
final TimeFormatterSpec day;
final TimeFormatterSpec month;
final TimeFormatterSpec year;
/// Creates a [TickFormatterSpec] that automatically chooses the formatting
/// given the individual [TimeFormatterSpec] formatters that are set.
///
/// There is a default formatter for each level that is configurable, but
/// by specifying a level here it replaces the default for that particular
/// granularity. This is useful for swapping out one or all of the formatters.
const AutoDateTimeTickFormatterSpec(
{this.minute, this.hour, this.day, this.month, this.year});
@override
DateTimeTickFormatter createTickFormatter(ChartContext context) {
final Map<int, TimeTickFormatter> map = {};
if (minute != null) {
map[DateTimeTickFormatter.MINUTE] =
_makeFormatter(minute, CalendarField.hourOfDay, context);
}
if (hour != null) {
map[DateTimeTickFormatter.HOUR] =
_makeFormatter(hour, CalendarField.date, context);
}
if (day != null) {
map[23 * DateTimeTickFormatter.HOUR] =
_makeFormatter(day, CalendarField.month, context);
}
if (month != null) {
map[28 * DateTimeTickFormatter.DAY] =
_makeFormatter(month, CalendarField.year, context);
}
if (year != null) {
map[364 * DateTimeTickFormatter.DAY] =
_makeFormatter(year, CalendarField.year, context);
}
return new DateTimeTickFormatter(context.dateTimeFactory, overrides: map);
}
TimeTickFormatterImpl _makeFormatter(TimeFormatterSpec spec,
CalendarField transitionField, ChartContext context) {
if (spec.noonFormat != null) {
return new HourTickFormatter(
dateTimeFactory: context.dateTimeFactory,
simpleFormat: spec.format,
transitionFormat: spec.transitionFormat,
noonFormat: spec.noonFormat);
} else {
return new TimeTickFormatterImpl(
dateTimeFactory: context.dateTimeFactory,
simpleFormat: spec.format,
transitionFormat: spec.transitionFormat,
transitionField: transitionField);
}
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AutoDateTimeTickFormatterSpec &&
minute == other.minute &&
hour == other.hour &&
day == other.day &&
month == other.month &&
year == other.year);
@override
int get hashCode {
int hashcode = minute?.hashCode ?? 0;
hashcode = (hashcode * 37) + hour?.hashCode ?? 0;
hashcode = (hashcode * 37) + day?.hashCode ?? 0;
hashcode = (hashcode * 37) + month?.hashCode ?? 0;
hashcode = (hashcode * 37) + year?.hashCode ?? 0;
return hashcode;
}
}

View File

@@ -0,0 +1,65 @@
// 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:meta/meta.dart' show immutable;
import '../draw_strategy/small_tick_draw_strategy.dart'
show SmallTickRendererSpec;
import '../time/date_time_extents.dart' show DateTimeExtents;
import 'axis_spec.dart' show AxisSpec, RenderSpec, TickLabelAnchor;
import 'date_time_axis_spec.dart'
show
DateTimeAxisSpec,
DateTimeEndPointsTickProviderSpec,
DateTimeTickFormatterSpec,
DateTimeTickProviderSpec;
/// Default [AxisSpec] used for Timeseries charts.
@immutable
class EndPointsTimeAxisSpec extends DateTimeAxisSpec {
/// Creates a [AxisSpec] that specialized for timeseries charts.
///
/// [renderSpec] spec used to configure how the ticks and labels
/// actually render. Possible values are [GridlineRendererSpec],
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
/// given to the RenderSpec is of type [DateTime] for Timeseries.
/// [tickProviderSpec] spec used to configure what ticks are generated.
/// [tickFormatterSpec] spec used to configure how the tick labels
/// are formatted.
/// [showAxisLine] override to force the axis to draw the axis
/// line.
const EndPointsTimeAxisSpec({
RenderSpec<DateTime> renderSpec,
DateTimeTickProviderSpec tickProviderSpec,
DateTimeTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
DateTimeExtents viewport,
bool usingBarRenderer = false,
}) : super(
renderSpec: renderSpec ??
const SmallTickRendererSpec<DateTime>(
labelAnchor: TickLabelAnchor.inside,
labelOffsetFromTickPx: 0),
tickProviderSpec:
tickProviderSpec ?? const DateTimeEndPointsTickProviderSpec(),
tickFormatterSpec: tickFormatterSpec,
showAxisLine: showAxisLine,
viewport: viewport);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is EndPointsTimeAxisSpec && super == (other));
}

View File

@@ -0,0 +1,253 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart';
import 'package:meta/meta.dart' show immutable;
import 'package:intl/intl.dart';
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../../../common/datum_details.dart' show MeasureFormatter;
import '../axis.dart' show Axis, NumericAxis;
import '../end_points_tick_provider.dart' show EndPointsTickProvider;
import '../numeric_extents.dart' show NumericExtents;
import '../numeric_tick_provider.dart' show NumericTickProvider;
import '../static_tick_provider.dart' show StaticTickProvider;
import '../tick_formatter.dart' show NumericTickFormatter;
import 'axis_spec.dart'
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
import 'tick_spec.dart' show TickSpec;
/// [AxisSpec] specialized for numeric/continuous axes like the measure axis.
@immutable
class NumericAxisSpec extends AxisSpec<num> {
/// Sets viewport for this Axis.
///
/// If pan / zoom behaviors are set, this is the initial viewport.
final NumericExtents viewport;
/// Creates a [AxisSpec] that specialized for numeric data.
///
/// [renderSpec] spec used to configure how the ticks and labels
/// actually render. Possible values are [GridlineRendererSpec],
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
/// given to the RenderSpec is of type [num] when using this spec.
/// [tickProviderSpec] spec used to configure what ticks are generated.
/// [tickFormatterSpec] spec used to configure how the tick labels are
/// formatted.
/// [showAxisLine] override to force the axis to draw the axis line.
const NumericAxisSpec({
RenderSpec<num> renderSpec,
NumericTickProviderSpec tickProviderSpec,
NumericTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
this.viewport,
}) : super(
renderSpec: renderSpec,
tickProviderSpec: tickProviderSpec,
tickFormatterSpec: tickFormatterSpec,
showAxisLine: showAxisLine);
factory NumericAxisSpec.from(
NumericAxisSpec other, {
RenderSpec<num> renderSpec,
TickProviderSpec tickProviderSpec,
TickFormatterSpec tickFormatterSpec,
bool showAxisLine,
NumericExtents viewport,
}) {
return new NumericAxisSpec(
renderSpec: renderSpec ?? other.renderSpec,
tickProviderSpec: tickProviderSpec ?? other.tickProviderSpec,
tickFormatterSpec: tickFormatterSpec ?? other.tickFormatterSpec,
showAxisLine: showAxisLine ?? other.showAxisLine,
viewport: viewport ?? other.viewport,
);
}
@override
configure(
Axis<num> axis, ChartContext context, GraphicsFactory graphicsFactory) {
super.configure(axis, context, graphicsFactory);
if (axis is NumericAxis && viewport != null) {
axis.setScaleViewport(viewport);
}
}
@override
NumericAxis createAxis() => new NumericAxis();
@override
bool operator ==(Object other) =>
other is NumericAxisSpec &&
viewport == other.viewport &&
super == (other);
@override
int get hashCode {
int hashcode = super.hashCode;
hashcode = (hashcode * 37) + viewport.hashCode;
hashcode = (hashcode * 37) + super.hashCode;
return hashcode;
}
}
abstract class NumericTickProviderSpec extends TickProviderSpec<num> {}
abstract class NumericTickFormatterSpec extends TickFormatterSpec<num> {}
@immutable
class BasicNumericTickProviderSpec implements NumericTickProviderSpec {
final bool zeroBound;
final bool dataIsInWholeNumbers;
final int desiredTickCount;
final int desiredMinTickCount;
final int desiredMaxTickCount;
/// Creates a [TickProviderSpec] that dynamically chooses the number of
/// ticks based on the extents of the data.
///
/// [zeroBound] automatically include zero in the data range.
/// [dataIsInWholeNumbers] skip over ticks that would produce
/// fractional ticks that don't make sense for the domain (ie: headcount).
/// [desiredTickCount] the fixed number of ticks to try to make. Convenience
/// that sets [desiredMinTickCount] and [desiredMaxTickCount] the same.
/// Both min and max win out if they are set along with
/// [desiredTickCount].
/// [desiredMinTickCount] automatically choose the best tick
/// count to produce the 'nicest' ticks but make sure we have this many.
/// [desiredMaxTickCount] automatically choose the best tick
/// count to produce the 'nicest' ticks but make sure we don't have more
/// than this many.
const BasicNumericTickProviderSpec(
{this.zeroBound,
this.dataIsInWholeNumbers,
this.desiredTickCount,
this.desiredMinTickCount,
this.desiredMaxTickCount});
@override
NumericTickProvider createTickProvider(ChartContext context) {
final provider = new NumericTickProvider();
if (zeroBound != null) {
provider.zeroBound = zeroBound;
}
if (dataIsInWholeNumbers != null) {
provider.dataIsInWholeNumbers = dataIsInWholeNumbers;
}
if (desiredMinTickCount != null ||
desiredMaxTickCount != null ||
desiredTickCount != null) {
provider.setTickCount(desiredMaxTickCount ?? desiredTickCount ?? 10,
desiredMinTickCount ?? desiredTickCount ?? 2);
}
return provider;
}
@override
bool operator ==(Object other) =>
other is BasicNumericTickProviderSpec &&
zeroBound == other.zeroBound &&
dataIsInWholeNumbers == other.dataIsInWholeNumbers &&
desiredTickCount == other.desiredTickCount &&
desiredMinTickCount == other.desiredMinTickCount &&
desiredMaxTickCount == other.desiredMaxTickCount;
@override
int get hashCode {
int hashcode = zeroBound?.hashCode ?? 0;
hashcode = (hashcode * 37) + dataIsInWholeNumbers?.hashCode ?? 0;
hashcode = (hashcode * 37) + desiredTickCount?.hashCode ?? 0;
hashcode = (hashcode * 37) + desiredMinTickCount?.hashCode ?? 0;
hashcode = (hashcode * 37) + desiredMaxTickCount?.hashCode ?? 0;
return hashcode;
}
}
/// [TickProviderSpec] that sets up numeric ticks at the two end points of the
/// axis range.
@immutable
class NumericEndPointsTickProviderSpec implements NumericTickProviderSpec {
/// Creates a [TickProviderSpec] that dynamically chooses numeric ticks at the
/// two end points of the axis range
const NumericEndPointsTickProviderSpec();
@override
EndPointsTickProvider<num> createTickProvider(ChartContext context) {
return new EndPointsTickProvider<num>();
}
@override
bool operator ==(Object other) => other is NumericEndPointsTickProviderSpec;
}
/// [TickProviderSpec] that allows you to specific the ticks to be used.
@immutable
class StaticNumericTickProviderSpec implements NumericTickProviderSpec {
final List<TickSpec<num>> tickSpecs;
const StaticNumericTickProviderSpec(this.tickSpecs);
@override
StaticTickProvider<num> createTickProvider(ChartContext context) =>
new StaticTickProvider<num>(tickSpecs);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is StaticNumericTickProviderSpec && tickSpecs == other.tickSpecs);
@override
int get hashCode => tickSpecs.hashCode;
}
@immutable
class BasicNumericTickFormatterSpec implements NumericTickFormatterSpec {
final MeasureFormatter formatter;
final NumberFormat numberFormat;
/// Simple [TickFormatterSpec] that delegates formatting to the given
/// [NumberFormat].
const BasicNumericTickFormatterSpec(this.formatter) : numberFormat = null;
const BasicNumericTickFormatterSpec.fromNumberFormat(this.numberFormat)
: formatter = null;
/// A formatter will be created with the number format if it is not null.
/// Otherwise, it will create one with the [MeasureFormatter] callback.
@override
NumericTickFormatter createTickFormatter(ChartContext context) {
return numberFormat != null
? new NumericTickFormatter.fromNumberFormat(numberFormat)
: new NumericTickFormatter(formatter: formatter);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is BasicNumericTickFormatterSpec &&
formatter == other.formatter &&
numberFormat == other.numberFormat);
}
@override
int get hashCode {
int hashcode = formatter.hashCode;
hashcode = (hashcode * 37) * numberFormat.hashCode;
return hashcode;
}
}

View File

@@ -0,0 +1,139 @@
// 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:meta/meta.dart' show immutable;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show Axis, OrdinalAxis, OrdinalViewport;
import '../ordinal_tick_provider.dart' show OrdinalTickProvider;
import '../static_tick_provider.dart' show StaticTickProvider;
import '../tick_formatter.dart' show OrdinalTickFormatter;
import 'axis_spec.dart'
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
import 'tick_spec.dart' show TickSpec;
/// [AxisSpec] specialized for ordinal/non-continuous axes typically for bars.
@immutable
class OrdinalAxisSpec extends AxisSpec<String> {
/// Sets viewport for this Axis.
///
/// If pan / zoom behaviors are set, this is the initial viewport.
final OrdinalViewport viewport;
/// Creates a [AxisSpec] that specialized for ordinal domain charts.
///
/// [renderSpec] spec used to configure how the ticks and labels
/// actually render. Possible values are [GridlineRendererSpec],
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
/// given to the RenderSpec is of type [String] when using this spec.
/// [tickProviderSpec] spec used to configure what ticks are generated.
/// [tickFormatterSpec] spec used to configure how the tick labels are
/// formatted.
/// [showAxisLine] override to force the axis to draw the axis line.
const OrdinalAxisSpec({
RenderSpec<String> renderSpec,
OrdinalTickProviderSpec tickProviderSpec,
OrdinalTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
this.viewport,
}) : super(
renderSpec: renderSpec,
tickProviderSpec: tickProviderSpec,
tickFormatterSpec: tickFormatterSpec,
showAxisLine: showAxisLine);
@override
configure(Axis<String> axis, ChartContext context,
GraphicsFactory graphicsFactory) {
super.configure(axis, context, graphicsFactory);
if (axis is OrdinalAxis && viewport != null) {
axis.setScaleViewport(viewport);
}
}
@override
OrdinalAxis createAxis() => new OrdinalAxis();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is OrdinalAxisSpec &&
viewport == other.viewport &&
super == (other));
}
@override
int get hashCode {
int hashcode = super.hashCode;
hashcode = (hashcode * 37) + viewport.hashCode;
return hashcode;
}
}
abstract class OrdinalTickProviderSpec extends TickProviderSpec<String> {}
abstract class OrdinalTickFormatterSpec extends TickFormatterSpec<String> {}
@immutable
class BasicOrdinalTickProviderSpec implements OrdinalTickProviderSpec {
const BasicOrdinalTickProviderSpec();
@override
OrdinalTickProvider createTickProvider(ChartContext context) =>
new OrdinalTickProvider();
@override
bool operator ==(Object other) => other is BasicOrdinalTickProviderSpec;
@override
int get hashCode => 37;
}
/// [TickProviderSpec] that allows you to specific the ticks to be used.
@immutable
class StaticOrdinalTickProviderSpec implements OrdinalTickProviderSpec {
final List<TickSpec<String>> tickSpecs;
const StaticOrdinalTickProviderSpec(this.tickSpecs);
@override
StaticTickProvider<String> createTickProvider(ChartContext context) =>
new StaticTickProvider<String>(tickSpecs);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is StaticOrdinalTickProviderSpec && tickSpecs == other.tickSpecs);
@override
int get hashCode => tickSpecs.hashCode;
}
@immutable
class BasicOrdinalTickFormatterSpec implements OrdinalTickFormatterSpec {
const BasicOrdinalTickFormatterSpec();
@override
OrdinalTickFormatter createTickFormatter(ChartContext context) =>
new OrdinalTickFormatter();
@override
bool operator ==(Object other) => other is BasicOrdinalTickFormatterSpec;
@override
int get hashCode => 37;
}

View File

@@ -0,0 +1,54 @@
// 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:meta/meta.dart' show immutable;
import 'package:intl/intl.dart';
import '../numeric_extents.dart' show NumericExtents;
import 'axis_spec.dart' show AxisSpec, RenderSpec;
import 'numeric_axis_spec.dart'
show
BasicNumericTickFormatterSpec,
BasicNumericTickProviderSpec,
NumericAxisSpec,
NumericTickProviderSpec,
NumericTickFormatterSpec;
/// Convenience [AxisSpec] specialized for numeric percentage axes.
@immutable
class PercentAxisSpec extends NumericAxisSpec {
/// Creates a [NumericAxisSpec] that is specialized for percentage data.
PercentAxisSpec({
RenderSpec<num> renderSpec,
NumericTickProviderSpec tickProviderSpec,
NumericTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
NumericExtents viewport,
}) : super(
renderSpec: renderSpec,
tickProviderSpec: tickProviderSpec ??
const BasicNumericTickProviderSpec(dataIsInWholeNumbers: false),
tickFormatterSpec: tickFormatterSpec ??
new BasicNumericTickFormatterSpec.fromNumberFormat(
new NumberFormat.percentPattern()),
showAxisLine: showAxisLine,
viewport: viewport ?? const NumericExtents(0.0, 1.0));
@override
bool operator ==(Object other) =>
other is PercentAxisSpec &&
viewport == other.viewport &&
super == (other);
}

View File

@@ -0,0 +1,32 @@
// 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 'axis_spec.dart' show TextStyleSpec;
/// Definition for a tick.
///
/// Used to define a tick that is used by static tick provider.
class TickSpec<D> {
final D value;
final String label;
final TextStyleSpec style;
/// [value] the value of this tick
/// [label] optional label for this tick. If not set, uses the tick formatter
/// of the axis.
/// [style] optional style for this tick. If not set, uses the style of the
/// axis.
const TickSpec(this.value, {this.label, this.style});
}

View File

@@ -0,0 +1,106 @@
// 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:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'numeric_scale.dart' show NumericScale;
import 'scale.dart' show MutableScale;
import 'spec/tick_spec.dart' show TickSpec;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
import 'tick_provider.dart' show TickProvider, TickHint;
import 'time/date_time_scale.dart' show DateTimeScale;
/// A strategy that uses the ticks provided and only assigns positioning.
///
/// The [TextStyle] is not overridden during tick draw strategy decorateTicks.
/// If it is null, then the default is used.
class StaticTickProvider<D> extends TickProvider<D> {
final List<TickSpec<D>> tickSpec;
StaticTickProvider(this.tickSpec);
@override
List<Tick<D>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required MutableScale<D> scale,
@required TickFormatter<D> formatter,
@required Map<D, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<D> tickHint,
}) {
final ticks = <Tick<D>>[];
bool allTicksHaveLabels = true;
for (TickSpec<D> spec in tickSpec) {
// When static ticks are being used with a numeric axis, extend the axis
// with the values specified.
if (scale is NumericScale || scale is DateTimeScale) {
scale.addDomain(spec.value);
}
// Save off whether all ticks have labels.
allTicksHaveLabels = allTicksHaveLabels && (spec.label != null);
}
// Use the formatter's label if the tick spec does not provide one.
List<String> formattedValues;
if (allTicksHaveLabels == false) {
formattedValues = formatter.format(
tickSpec.map((spec) => spec.value).toList(), formatterValueCache,
stepSize: scale.domainStepSize);
}
for (var i = 0; i < tickSpec.length; i++) {
final spec = tickSpec[i];
// We still check if the spec is within the viewport because we do not
// extend the axis for OrdinalScale.
if (scale.compareDomainValueToViewport(spec.value) == 0) {
final tick = new Tick<D>(
value: spec.value,
textElement: graphicsFactory
.createTextElement(spec.label ?? formattedValues[i]),
locationPx: scale[spec.value]);
if (spec.style != null) {
tick.textElement.textStyle = graphicsFactory.createTextPaint()
..fontFamily = spec.style.fontFamily
..fontSize = spec.style.fontSize
..color = spec.style.color;
}
ticks.add(tick);
}
}
// Allow draw strategy to decorate the ticks.
tickDrawStrategy.decorateTicks(ticks);
return ticks;
}
@override
bool operator ==(other) =>
other is StaticTickProvider && tickSpec == other.tickSpec;
@override
int get hashCode => tickSpec.hashCode;
}

View File

@@ -0,0 +1,47 @@
// 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:meta/meta.dart';
import '../../../common/text_element.dart';
/// A labeled point on an axis.
///
/// [D] is the type of the value this tick is associated with.
class Tick<D> {
/// The value that this tick represents
final D value;
/// [TextElement] for this tick.
TextElement textElement;
/// Location on the axis where this tick is rendered (in canvas coordinates).
double locationPx;
/// Offset of the label for this tick from its location.
///
/// This is a vertical offset for ticks on a vertical axis, or horizontal
/// offset for ticks on a horizontal axis.
double labelOffsetPx;
Tick(
{@required this.value,
@required this.textElement,
this.locationPx,
this.labelOffsetPx});
@override
String toString() => 'Tick(value: $value, locationPx: $locationPx, '
'labelOffsetPx: $labelOffsetPx)';
}

View File

@@ -0,0 +1,107 @@
// 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:intl/intl.dart';
import '../../common/datum_details.dart' show MeasureFormatter;
// TODO: Break out into separate files.
/// A strategy used for converting domain values of the ticks into Strings.
///
/// [D] is the domain type.
abstract class TickFormatter<D> {
const TickFormatter();
/// Formats a list of tick values.
List<String> format(List<D> tickValues, Map<D, String> cache, {num stepSize});
}
abstract class SimpleTickFormatterBase<D> implements TickFormatter<D> {
const SimpleTickFormatterBase();
@override
List<String> format(List<D> tickValues, Map<D, String> cache,
{num stepSize}) =>
tickValues.map((D value) {
// Try to use the cached formats first.
String formattedString = cache[value];
if (formattedString == null) {
formattedString = formatValue(value);
cache[value] = formattedString;
}
return formattedString;
}).toList();
/// Formats a single tick value.
String formatValue(D value);
}
/// A strategy that converts tick labels using toString().
class OrdinalTickFormatter extends SimpleTickFormatterBase<String> {
const OrdinalTickFormatter();
@override
String formatValue(String value) => value;
@override
bool operator ==(other) => other is OrdinalTickFormatter;
@override
int get hashCode => 31;
}
/// A strategy for formatting the labels on numeric ticks using [NumberFormat].
///
/// The default format is [NumberFormat.decimalPattern].
class NumericTickFormatter extends SimpleTickFormatterBase<num> {
final MeasureFormatter formatter;
NumericTickFormatter._internal(this.formatter);
/// Construct a a new [NumericTickFormatter].
///
/// [formatter] optionally specify a formatter to be used. Defaults to using
/// [NumberFormat.decimalPattern] if none is specified.
factory NumericTickFormatter({MeasureFormatter formatter}) {
formatter ??= _getFormatter(new NumberFormat.decimalPattern());
return new NumericTickFormatter._internal(formatter);
}
/// Constructs a new [NumericTickFormatter] that formats using [numberFormat].
factory NumericTickFormatter.fromNumberFormat(NumberFormat numberFormat) {
return new NumericTickFormatter._internal(_getFormatter(numberFormat));
}
/// Constructs a new formatter that uses [NumberFormat.compactCurrency].
factory NumericTickFormatter.compactSimpleCurrency() {
return new NumericTickFormatter._internal(
_getFormatter(new NumberFormat.compactCurrency()));
}
/// Returns a [MeasureFormatter] that calls format on [numberFormat].
static MeasureFormatter _getFormatter(NumberFormat numberFormat) {
return (num value) => numberFormat.format(value);
}
@override
String formatValue(num value) => formatter(value);
@override
bool operator ==(other) =>
other is NumericTickFormatter && formatter == other.formatter;
@override
int get hashCode => formatter.hashCode;
}

View File

@@ -0,0 +1,103 @@
// 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:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'scale.dart' show MutableScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
/// A strategy for selecting values for axis ticks based on the domain values.
///
/// [D] is the domain type.
abstract class TickProvider<D> {
/// Returns a list of ticks in value order that should be displayed.
///
/// This method should not return null. If no ticks are desired an empty list
/// should be returned.
///
/// [graphicsFactory] The graphics factory used for text measurement.
/// [scale] The scale of the data.
/// [formatter] The formatter to use for generating tick labels.
/// [orientation] Orientation of this axis ticks.
/// [tickDrawStrategy] Draw strategy for ticks.
/// [viewportExtensionEnabled] allow extending the viewport for 'niced' ticks.
/// [tickHint] tick values for provider to calculate a desired tick range.
List<Tick<D>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required covariant MutableScale<D> scale,
@required TickFormatter<D> formatter,
@required Map<D, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<D> tickHint,
});
}
/// A base tick provider.
abstract class BaseTickProvider<D> implements TickProvider<D> {
const BaseTickProvider();
/// Create ticks from [domainValues].
List<Tick<D>> createTicks(
List<D> domainValues, {
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required MutableScale<D> scale,
@required TickFormatter<D> formatter,
@required Map<D, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
num stepSize,
}) {
final ticks = <Tick<D>>[];
final labels =
formatter.format(domainValues, formatterValueCache, stepSize: stepSize);
for (var i = 0; i < domainValues.length; i++) {
final value = domainValues[i];
final tick = new Tick(
value: value,
textElement: graphicsFactory.createTextElement(labels[i]),
locationPx: scale[value]);
ticks.add(tick);
}
// Allow draw strategy to decorate the ticks.
tickDrawStrategy.decorateTicks(ticks);
return ticks;
}
}
/// A hint for the tick provider to determine step size and tick count.
class TickHint<D> {
/// The starting hint tick value.
final D start;
/// The ending hint tick value.
final D end;
/// Number of ticks.
final int tickCount;
TickHint(this.start, this.end, {this.tickCount});
}

View File

@@ -0,0 +1,177 @@
// 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:meta/meta.dart' show required;
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import '../tick.dart' show Tick;
import '../tick_formatter.dart' show TickFormatter;
import '../tick_provider.dart' show TickProvider, TickHint;
import 'date_time_scale.dart' show DateTimeScale;
import 'day_time_stepper.dart' show DayTimeStepper;
import 'hour_time_stepper.dart' show HourTimeStepper;
import 'minute_time_stepper.dart' show MinuteTimeStepper;
import 'month_time_stepper.dart' show MonthTimeStepper;
import 'time_range_tick_provider.dart' show TimeRangeTickProvider;
import 'time_range_tick_provider_impl.dart' show TimeRangeTickProviderImpl;
import 'year_time_stepper.dart' show YearTimeStepper;
/// Tick provider for date and time.
///
/// When determining the ticks for a given domain, the provider will use choose
/// one of the internal tick providers appropriate to the size of the data's
/// domain range. It does this in an attempt to ensure there are at least 3
/// ticks, before jumping to the next more fine grain provider. The 3 tick
/// minimum is not a hard rule as some of the ticks might be eliminated because
/// of collisions, but the data was within the targeted range.
///
/// Once a tick provider is chosen the selection of ticks is done by the child
/// tick provider.
class AutoAdjustingDateTimeTickProvider implements TickProvider<DateTime> {
/// List of tick providers to be selected from.
final List<TimeRangeTickProvider> _potentialTickProviders;
AutoAdjustingDateTimeTickProvider._internal(
List<TimeRangeTickProvider> tickProviders)
: _potentialTickProviders = tickProviders;
/// Creates a default [AutoAdjustingDateTimeTickProvider] for day and time.
factory AutoAdjustingDateTimeTickProvider.createDefault(
DateTimeFactory dateTimeFactory) {
return new AutoAdjustingDateTimeTickProvider._internal([
createYearTickProvider(dateTimeFactory),
createMonthTickProvider(dateTimeFactory),
createDayTickProvider(dateTimeFactory),
createHourTickProvider(dateTimeFactory),
createMinuteTickProvider(dateTimeFactory)
]);
}
/// Creates a default [AutoAdjustingDateTimeTickProvider] for day only.
factory AutoAdjustingDateTimeTickProvider.createWithoutTime(
DateTimeFactory dateTimeFactory) {
return new AutoAdjustingDateTimeTickProvider._internal([
createYearTickProvider(dateTimeFactory),
createMonthTickProvider(dateTimeFactory),
createDayTickProvider(dateTimeFactory)
]);
}
/// Creates [AutoAdjustingDateTimeTickProvider] with custom tick providers.
///
/// [potentialTickProviders] must have at least one [TimeRangeTickProvider]
/// and this list of tick providers are used in the order they are provided.
factory AutoAdjustingDateTimeTickProvider.createWith(
List<TimeRangeTickProvider> potentialTickProviders) {
if (potentialTickProviders == null || potentialTickProviders.isEmpty) {
throw new ArgumentError('At least one TimeRangeTickProvider is required');
}
return new AutoAdjustingDateTimeTickProvider._internal(
potentialTickProviders);
}
/// Generates a list of ticks for the given data which should not collide
/// unless the range is not large enough.
@override
List<Tick<DateTime>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required DateTimeScale scale,
@required TickFormatter<DateTime> formatter,
@required Map<DateTime, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<DateTime> tickHint,
}) {
List<TimeRangeTickProvider> tickProviders;
/// If tick hint is provided, use the closest tick provider, otherwise
/// look through the tick providers for one that provides sufficient ticks
/// for the viewport.
if (tickHint != null) {
tickProviders = [_getClosestTickProvider(tickHint)];
} else {
tickProviders = _potentialTickProviders;
}
final lastTickProvider = tickProviders.last;
final viewport = scale.viewportDomain;
for (final tickProvider in tickProviders) {
final isLastProvider = (tickProvider == lastTickProvider);
if (isLastProvider ||
tickProvider.providesSufficientTicksForRange(viewport)) {
return tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
orientation: orientation,
);
}
}
return <Tick<DateTime>>[];
}
/// Find the closest tick provider based on the tick hint.
TimeRangeTickProvider _getClosestTickProvider(TickHint<DateTime> tickHint) {
final stepSize = ((tickHint.end.difference(tickHint.start).inMilliseconds) /
(tickHint.tickCount - 1))
.round();
int minDifference;
TimeRangeTickProvider closestTickProvider;
for (final tickProvider in _potentialTickProviders) {
final difference =
(stepSize - tickProvider.getClosestStepSize(stepSize)).abs();
if (minDifference == null || minDifference > difference) {
minDifference = difference;
closestTickProvider = tickProvider;
}
}
return closestTickProvider;
}
static TimeRangeTickProvider createYearTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new YearTimeStepper(dateTimeFactory));
static TimeRangeTickProvider createMonthTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new MonthTimeStepper(dateTimeFactory));
static TimeRangeTickProvider createDayTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new DayTimeStepper(dateTimeFactory));
static TimeRangeTickProvider createHourTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new HourTimeStepper(dateTimeFactory));
static TimeRangeTickProvider createMinuteTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new MinuteTimeStepper(dateTimeFactory));
}

View File

@@ -0,0 +1,141 @@
// 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 '../../../../common/date_time_factory.dart';
import 'date_time_extents.dart' show DateTimeExtents;
import 'time_stepper.dart'
show TimeStepper, TimeStepIteratorFactory, TimeStepIterator;
/// A base stepper for operating with DateTimeFactory and time range steps.
abstract class BaseTimeStepper implements TimeStepper {
/// The factory to generate a DateTime object.
///
/// This is needed because Dart's DateTime does not handle time zone.
/// There is a time zone aware library that we could use that implements the
/// DateTime interface.
final DateTimeFactory dateTimeFactory;
_TimeStepIteratorFactoryImpl _stepsIterable;
BaseTimeStepper(this.dateTimeFactory);
/// Get the step time before or on the given [time] from [tickIncrement].
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement);
/// Get the next step time after [time] from [tickIncrement].
DateTime getNextStepTime(DateTime time, int tickIncrement);
@override
int getStepCountBetween(DateTimeExtents timeExtent, int tickIncrement) {
checkTickIncrement(tickIncrement);
final min = timeExtent.start;
final max = timeExtent.end;
var time = getStepTimeAfterInclusive(min, tickIncrement);
var cnt = 0;
while (time.compareTo(max) <= 0) {
cnt++;
time = getNextStepTime(time, tickIncrement);
}
return cnt;
}
@override
TimeStepIteratorFactory getSteps(DateTimeExtents timeExtent) {
// Keep the steps iterable unless time extent changes, so the same iterator
// can be used and reset for different increments.
if (_stepsIterable == null || _stepsIterable.timeExtent != timeExtent) {
_stepsIterable = new _TimeStepIteratorFactoryImpl(timeExtent, this);
}
return _stepsIterable;
}
@override
DateTimeExtents updateBoundingSteps(DateTimeExtents timeExtent) {
final stepBefore = getStepTimeBeforeInclusive(timeExtent.start, 1);
final stepAfter = getStepTimeAfterInclusive(timeExtent.end, 1);
return new DateTimeExtents(start: stepBefore, end: stepAfter);
}
DateTime getStepTimeAfterInclusive(DateTime time, int tickIncrement) {
final boundedStart = getStepTimeBeforeInclusive(time, tickIncrement);
if (boundedStart == time) {
return boundedStart;
}
return getNextStepTime(boundedStart, tickIncrement);
}
}
class _TimeStepIteratorImpl implements TimeStepIterator {
final DateTime extentStartTime;
final DateTime extentEndTime;
final BaseTimeStepper stepper;
DateTime _current;
int _tickIncrement = 1;
_TimeStepIteratorImpl(
this.extentStartTime, this.extentEndTime, this.stepper) {
reset(_tickIncrement);
}
@override
bool moveNext() {
if (_current == null) {
_current =
stepper.getStepTimeAfterInclusive(extentStartTime, _tickIncrement);
} else {
_current = stepper.getNextStepTime(_current, _tickIncrement);
}
return _current.compareTo(extentEndTime) <= 0;
}
@override
DateTime get current => _current;
@override
TimeStepIterator reset(int tickIncrement) {
checkTickIncrement(tickIncrement);
_tickIncrement = tickIncrement;
_current = null;
return this;
}
}
class _TimeStepIteratorFactoryImpl extends TimeStepIteratorFactory {
final DateTimeExtents timeExtent;
final _TimeStepIteratorImpl _timeStepIterator;
_TimeStepIteratorFactoryImpl._internal(
_TimeStepIteratorImpl timeStepIterator, this.timeExtent)
: _timeStepIterator = timeStepIterator;
factory _TimeStepIteratorFactoryImpl(
DateTimeExtents timeExtent, BaseTimeStepper stepper) {
final startTime = timeExtent.start;
final endTime = timeExtent.end;
return new _TimeStepIteratorFactoryImpl._internal(
new _TimeStepIteratorImpl(startTime, endTime, stepper), timeExtent);
}
@override
TimeStepIterator get iterator => _timeStepIterator;
}
void checkTickIncrement(int tickIncrement) {
/// tickIncrement must be greater than 0
assert(tickIncrement > 0);
}

View File

@@ -0,0 +1,41 @@
// 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 '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../axis.dart' show Axis;
import '../tick_formatter.dart' show TickFormatter;
import '../tick_provider.dart' show TickProvider;
import 'auto_adjusting_date_time_tick_provider.dart'
show AutoAdjustingDateTimeTickProvider;
import 'date_time_extents.dart' show DateTimeExtents;
import 'date_time_scale.dart' show DateTimeScale;
import 'date_time_tick_formatter.dart' show DateTimeTickFormatter;
class DateTimeAxis extends Axis<DateTime> {
DateTimeAxis(DateTimeFactory dateTimeFactory,
{TickProvider tickProvider, TickFormatter tickFormatter})
: super(
tickProvider: tickProvider ??
new AutoAdjustingDateTimeTickProvider.createDefault(
dateTimeFactory),
tickFormatter:
tickFormatter ?? new DateTimeTickFormatter(dateTimeFactory),
scale: new DateTimeScale(dateTimeFactory),
);
void setScaleViewport(DateTimeExtents viewport) {
(mutableScale as DateTimeScale).viewportDomain = viewport;
}
}

View File

@@ -0,0 +1,33 @@
// 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:meta/meta.dart' show required;
import '../scale.dart' show Extents;
class DateTimeExtents extends Extents<DateTime> {
final DateTime start;
final DateTime end;
DateTimeExtents({@required this.start, @required this.end});
@override
bool operator ==(other) {
return other is DateTimeExtents && start == other.start && end == other.end;
}
@override
int get hashCode => (start.hashCode + (end.hashCode * 37));
}

View File

@@ -0,0 +1,138 @@
// 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 '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../linear/linear_scale.dart' show LinearScale;
import '../numeric_extents.dart' show NumericExtents;
import '../scale.dart'
show MutableScale, StepSizeConfig, RangeBandConfig, ScaleOutputExtent;
import 'date_time_extents.dart' show DateTimeExtents;
/// [DateTimeScale] is a wrapper for [LinearScale].
/// [DateTime] values are converted to millisecondsSinceEpoch and passed to the
/// [LinearScale].
class DateTimeScale extends MutableScale<DateTime> {
final DateTimeFactory dateTimeFactory;
final LinearScale _linearScale;
DateTimeScale(this.dateTimeFactory) : _linearScale = new LinearScale();
DateTimeScale._copy(DateTimeScale other)
: dateTimeFactory = other.dateTimeFactory,
_linearScale = other._linearScale.copy();
@override
num operator [](DateTime domainValue) =>
_linearScale[domainValue.millisecondsSinceEpoch];
@override
DateTime reverse(double pixelLocation) =>
dateTimeFactory.createDateTimeFromMilliSecondsSinceEpoch(
_linearScale.reverse(pixelLocation).round());
@override
void resetDomain() {
_linearScale.resetDomain();
}
@override
set stepSizeConfig(StepSizeConfig config) {
_linearScale.stepSizeConfig = config;
}
@override
StepSizeConfig get stepSizeConfig => _linearScale.stepSizeConfig;
@override
set rangeBandConfig(RangeBandConfig barGroupWidthConfig) {
_linearScale.rangeBandConfig = barGroupWidthConfig;
}
@override
void setViewportSettings(double viewportScale, double viewportTranslatePx) {
_linearScale.setViewportSettings(viewportScale, viewportTranslatePx);
}
@override
set range(ScaleOutputExtent extent) {
_linearScale.range = extent;
}
@override
void addDomain(DateTime domainValue) {
_linearScale.addDomain(domainValue.millisecondsSinceEpoch);
}
@override
void resetViewportSettings() {
_linearScale.resetViewportSettings();
}
DateTimeExtents get viewportDomain {
final extents = _linearScale.viewportDomain;
return new DateTimeExtents(
start: dateTimeFactory
.createDateTimeFromMilliSecondsSinceEpoch(extents.min.toInt()),
end: dateTimeFactory
.createDateTimeFromMilliSecondsSinceEpoch(extents.max.toInt()));
}
set viewportDomain(DateTimeExtents extents) {
_linearScale.viewportDomain = new NumericExtents(
extents.start.millisecondsSinceEpoch,
extents.end.millisecondsSinceEpoch);
}
@override
DateTimeScale copy() => new DateTimeScale._copy(this);
@override
double get viewportTranslatePx => _linearScale.viewportTranslatePx;
@override
double get viewportScalingFactor => _linearScale.viewportScalingFactor;
@override
bool isRangeValueWithinViewport(double rangeValue) =>
_linearScale.isRangeValueWithinViewport(rangeValue);
@override
int compareDomainValueToViewport(DateTime domainValue) => _linearScale
.compareDomainValueToViewport(domainValue.millisecondsSinceEpoch);
@override
double get rangeBand => _linearScale.rangeBand;
@override
double get stepSize => _linearScale.stepSize;
@override
double get domainStepSize => _linearScale.domainStepSize;
@override
RangeBandConfig get rangeBandConfig => _linearScale.rangeBandConfig;
@override
int get rangeWidth => _linearScale.rangeWidth;
@override
ScaleOutputExtent get range => _linearScale.range;
@override
bool canTranslate(DateTime domainValue) =>
_linearScale.canTranslate(domainValue.millisecondsSinceEpoch);
NumericExtents get dataExtent => _linearScale.dataExtent;
}

View File

@@ -0,0 +1,218 @@
// 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:meta/meta.dart' show required;
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../tick_formatter.dart' show TickFormatter;
import 'hour_tick_formatter.dart' show HourTickFormatter;
import 'time_tick_formatter.dart' show TimeTickFormatter;
import 'time_tick_formatter_impl.dart'
show CalendarField, TimeTickFormatterImpl;
/// A [TickFormatter] that formats date/time values based on minimum difference
/// between subsequent ticks.
///
/// This formatter assumes that the Tick values passed in are sorted in
/// increasing order.
///
/// This class is setup with a list of formatters that format the input ticks at
/// a given time resolution. The time resolution which will accurately display
/// the difference between 2 subsequent ticks is picked. Each time resolution
/// can be setup with a [TimeTickFormatter], which is used to format ticks as
/// regular or transition ticks based on whether the tick has crossed the time
/// boundary defined in the [TimeTickFormatter].
class DateTimeTickFormatter implements TickFormatter<DateTime> {
static const int SECOND = 1000;
static const int MINUTE = 60 * SECOND;
static const int HOUR = 60 * MINUTE;
static const int DAY = 24 * HOUR;
/// Used for the case when there is only one formatter.
static const int ANY = -1;
final Map<int, TimeTickFormatter> _timeFormatters;
/// Creates a [DateTimeTickFormatter] that works well with time tick provider
/// classes.
///
/// The default formatter makes assumptions on border cases that time tick
/// providers will still provide ticks that make sense. Example: Tick provider
/// does not provide ticks with 23 hour intervals. For custom tick providers
/// where these assumptions are not correct, please create a custom
/// [TickFormatter].
factory DateTimeTickFormatter(DateTimeFactory dateTimeFactory,
{Map<int, TimeTickFormatter> overrides}) {
final Map<int, TimeTickFormatter> map = {
MINUTE: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'mm',
transitionFormat: 'h mm',
transitionField: CalendarField.hourOfDay),
HOUR: new HourTickFormatter(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'h',
transitionFormat: 'MMM d ha',
noonFormat: 'ha'),
23 * HOUR: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'd',
transitionFormat: 'MMM d',
transitionField: CalendarField.month),
28 * DAY: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'MMM',
transitionFormat: 'MMM yyyy',
transitionField: CalendarField.year),
364 * DAY: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'yyyy',
transitionFormat: 'yyyy',
transitionField: CalendarField.year),
};
// Allow the user to override some of the defaults.
if (overrides != null) {
map.addAll(overrides);
}
return new DateTimeTickFormatter._internal(map);
}
/// Creates a [DateTimeTickFormatter] without the time component.
factory DateTimeTickFormatter.withoutTime(DateTimeFactory dateTimeFactory) {
return new DateTimeTickFormatter._internal({
23 * HOUR: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'd',
transitionFormat: 'MMM d',
transitionField: CalendarField.month),
28 * DAY: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'MMM',
transitionFormat: 'MMM yyyy',
transitionField: CalendarField.year),
365 * DAY: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'yyyy',
transitionFormat: 'yyyy',
transitionField: CalendarField.year),
});
}
/// Creates a [DateTimeTickFormatter] that formats all ticks the same.
///
/// Only use this formatter for data with fixed intervals, otherwise use the
/// default, or build from scratch.
///
/// [formatter] The format for all ticks.
factory DateTimeTickFormatter.uniform(TimeTickFormatter formatter) {
return new DateTimeTickFormatter._internal({ANY: formatter});
}
/// Creates a [DateTimeTickFormatter] that formats ticks with [formatters].
///
/// The formatters are expected to be provided with keys in increasing order.
factory DateTimeTickFormatter.withFormatters(
Map<int, TimeTickFormatter> formatters) {
// Formatters must be non empty.
if (formatters == null || formatters.isEmpty) {
throw new ArgumentError('At least one TimeTickFormatter is required.');
}
return new DateTimeTickFormatter._internal(formatters);
}
DateTimeTickFormatter._internal(this._timeFormatters) {
// If there is only one formatter, just use this one and skip this check.
if (_timeFormatters.length == 1) {
return;
}
_checkPositiveAndSorted(_timeFormatters.keys);
}
@override
List<String> format(List<DateTime> tickValues, Map<DateTime, String> cache,
{@required num stepSize}) {
final tickLabels = <String>[];
if (tickValues.isEmpty) {
return tickLabels;
}
// Find the formatter that is the largest interval that has enough
// resolution to describe the difference between ticks. If no such formatter
// exists pick the highest res one.
var formatter = _timeFormatters[_timeFormatters.keys.first];
var formatterFound = false;
if (_timeFormatters.keys.first == ANY) {
formatterFound = true;
} else {
int minTimeBetweenTicks = stepSize.toInt();
// TODO: Skip the formatter if the formatter's step size is
// smaller than the minimum step size of the data.
var keys = _timeFormatters.keys.iterator;
while (keys.moveNext() && !formatterFound) {
if (keys.current > minTimeBetweenTicks) {
formatterFound = true;
} else {
formatter = _timeFormatters[keys.current];
}
}
}
// Format the ticks.
final tickValuesIt = tickValues.iterator;
var tickValue = (tickValuesIt..moveNext()).current;
var prevTickValue = tickValue;
tickLabels.add(formatter.formatFirstTick(tickValue));
while (tickValuesIt.moveNext()) {
tickValue = tickValuesIt.current;
if (formatter.isTransition(tickValue, prevTickValue)) {
tickLabels.add(formatter.formatTransitionTick(tickValue));
} else {
tickLabels.add(formatter.formatSimpleTick(tickValue));
}
prevTickValue = tickValue;
}
return tickLabels;
}
static void _checkPositiveAndSorted(Iterable<int> values) {
final valuesIterator = values.iterator;
var prev = (valuesIterator..moveNext()).current;
var isSorted = true;
// Only need to check the first value, because the values after are expected
// to be greater.
if (prev <= 0) {
throw new ArgumentError('Formatter keys must be positive');
}
while (valuesIterator.moveNext() && isSorted) {
isSorted = prev < valuesIterator.current;
prev = valuesIterator.current;
}
if (!isSorted) {
throw new ArgumentError(
'Formatters must be sorted with keys in increasing order');
}
}
}

View File

@@ -0,0 +1,81 @@
// 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 '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart' show BaseTimeStepper;
/// Day stepper.
class DayTimeStepper extends BaseTimeStepper {
// TODO: Remove the 14 day increment if we add week stepper.
static const _defaultIncrements = const [1, 2, 3, 7, 14];
static const _hoursInDay = 24;
final List<int> _allowedTickIncrements;
DayTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory DayTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment option.
assert(allowedTickIncrements.isNotEmpty);
// All increments must be > 0.
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
return new DayTimeStepper._internal(dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => _hoursInDay * 3600 * 1000;
@override
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Get the step time before or on the given [time] from [tickIncrement].
///
/// Increments are based off the beginning of the month.
/// Ex. 5 day increments in a month is 1,6,11,16,21,26,31
/// Ex. Time is Aug 20, increment is 1 day. Returns Aug 20.
/// Ex. Time is Aug 20, increment is 2 days. Returns Aug 19 because 2 day
/// increments in a month is 1,3,5,7,9,11,13,15,17,19,21....
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final dayRemainder = (time.day - 1) % tickIncrement;
// Subtract an extra hour in case stepping through a daylight saving change.
final dayBefore = dayRemainder > 0
? time.subtract(new Duration(hours: (_hoursInDay * dayRemainder) - 1))
: time;
// Explicitly leaving off hours and beyond to truncate to start of day.
final stepBefore = dateTimeFactory.createDateTime(
dayBefore.year, dayBefore.month, dayBefore.day);
return stepBefore;
}
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
// Add an extra hour in case stepping through a daylight saving change.
final stepAfter =
time.add(new Duration(hours: (_hoursInDay * tickIncrement) + 1));
// Explicitly leaving off hours and beyond to truncate to start of day.
return dateTimeFactory.createDateTime(
stepAfter.year, stepAfter.month, stepAfter.day);
}
}

View File

@@ -0,0 +1,45 @@
// 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:intl/intl.dart' show DateFormat;
import 'package:meta/meta.dart' show required;
import '../../../../common/date_time_factory.dart';
import 'time_tick_formatter_impl.dart'
show CalendarField, TimeTickFormatterImpl;
/// Hour specific tick formatter which will format noon differently.
class HourTickFormatter extends TimeTickFormatterImpl {
DateFormat _noonFormat;
HourTickFormatter(
{@required DateTimeFactory dateTimeFactory,
@required String simpleFormat,
@required String transitionFormat,
@required String noonFormat})
: super(
dateTimeFactory: dateTimeFactory,
simpleFormat: simpleFormat,
transitionFormat: transitionFormat,
transitionField: CalendarField.date) {
_noonFormat = dateTimeFactory.createDateFormat(noonFormat);
}
@override
String formatSimpleTick(DateTime date) {
return (date.hour == 12)
? _noonFormat.format(date)
: super.formatSimpleTick(date);
}
}

View File

@@ -0,0 +1,88 @@
// 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 '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart' show BaseTimeStepper;
/// Hour stepper.
class HourTimeStepper extends BaseTimeStepper {
static const _defaultIncrements = const [1, 2, 3, 4, 6, 12, 24];
static const _hoursInDay = 24;
static const _millisecondsInHour = 3600 * 1000;
final List<int> _allowedTickIncrements;
HourTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory HourTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment option.
assert(allowedTickIncrements.isNotEmpty);
// All increments must be between 1 and 24 inclusive.
assert(allowedTickIncrements
.any((increment) => increment <= 0 || increment > 24) ==
false);
return new HourTimeStepper._internal(
dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => _millisecondsInHour;
@override
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Get the step time before or on the given [time] from [tickIncrement].
///
/// Guarantee a step at the start of the next day.
/// Ex. Time is Aug 20 10 AM, increment is 1 hour. Returns 10 AM.
/// Ex. Time is Aug 20 6 AM, increment is 4 hours. Returns 4 AM.
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final nextDay = dateTimeFactory
.createDateTime(time.year, time.month, time.day)
.add(new Duration(hours: _hoursInDay + 1));
final nextDayStart = dateTimeFactory.createDateTime(
nextDay.year, nextDay.month, nextDay.day);
final hoursToNextDay =
((nextDayStart.millisecondsSinceEpoch - time.millisecondsSinceEpoch) /
_millisecondsInHour)
.ceil();
final hoursRemainder = hoursToNextDay % tickIncrement;
final rewindHours =
hoursRemainder == 0 ? 0 : tickIncrement - hoursRemainder;
final stepBefore = dateTimeFactory.createDateTime(
time.year, time.month, time.day, time.hour - rewindHours);
return stepBefore;
}
/// Get next step time.
///
/// [time] is expected to be a [DateTime] with the hour at start of the hour.
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
return time.add(new Duration(hours: tickIncrement));
}
}

View File

@@ -0,0 +1,78 @@
// 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 '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart';
/// Minute stepper where ticks generated aligns with the hour.
class MinuteTimeStepper extends BaseTimeStepper {
static const _defaultIncrements = const [5, 10, 15, 20, 30];
static const _millisecondsInMinute = 60 * 1000;
final List<int> _allowedTickIncrements;
MinuteTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory MinuteTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment
assert(allowedTickIncrements.isNotEmpty);
// Increment must be between 1 and 60 inclusive.
assert(allowedTickIncrements
.any((increment) => increment <= 0 || increment > 60) ==
false);
return new MinuteTimeStepper._internal(
dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => _millisecondsInMinute;
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Picks a tick start time that guarantees the start of the hour is included.
///
/// Ex. Time is 3:46, increments is 5 minutes, step before is 3:45, because
/// we can guarantee a step at 4:00.
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final nextHourStart = time.millisecondsSinceEpoch +
(60 - time.minute) * _millisecondsInMinute;
final minutesToNextHour =
((nextHourStart - time.millisecondsSinceEpoch) / _millisecondsInMinute)
.ceil();
final minRemainder = minutesToNextHour % tickIncrement;
final rewindMinutes = minRemainder == 0 ? 0 : tickIncrement - minRemainder;
final stepBefore = dateTimeFactory.createDateTimeFromMilliSecondsSinceEpoch(
time.millisecondsSinceEpoch - rewindMinutes * _millisecondsInMinute);
return stepBefore;
}
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
return time.add(new Duration(minutes: tickIncrement));
}
}

View File

@@ -0,0 +1,77 @@
// 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 '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart' show BaseTimeStepper;
/// Month stepper.
class MonthTimeStepper extends BaseTimeStepper {
static const _defaultIncrements = const [1, 2, 3, 4, 6, 12];
final List<int> _allowedTickIncrements;
MonthTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory MonthTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment option.
assert(allowedTickIncrements.isNotEmpty);
// All increments must be > 0.
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
return new MonthTimeStepper._internal(
dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => 30 * 24 * 3600 * 1000;
@override
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Guarantee a step ending in the last month of the year.
///
/// If date is 2017 Oct and increments is 6, the step before is 2017 June.
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final monthRemainder = time.month % tickIncrement;
var newMonth = (time.month - monthRemainder) % DateTime.monthsPerYear;
// Handles the last month of the year (December) edge case.
// Ex. When month is December and increment is 1
if (time.month == DateTime.monthsPerYear && newMonth == 0) {
newMonth = DateTime.monthsPerYear;
}
final newYear =
time.year - (monthRemainder / DateTime.monthsPerYear).floor();
return dateTimeFactory.createDateTime(newYear, newMonth);
}
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
final incrementedMonth = time.month + tickIncrement;
final newMonth = incrementedMonth % DateTime.monthsPerYear;
final newYear =
time.year + (incrementedMonth / DateTime.monthsPerYear).floor();
return dateTimeFactory.createDateTime(newYear, newMonth);
}
}

View File

@@ -0,0 +1,29 @@
// 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 '../tick_provider.dart' show BaseTickProvider;
import '../time/date_time_extents.dart' show DateTimeExtents;
/// Provides ticks for a particular time unit.
///
/// Used by [AutoAdjustingDateTimeTickProvider].
abstract class TimeRangeTickProvider extends BaseTickProvider<DateTime> {
/// Returns if this tick provider will produce a sufficient number of ticks
/// for [domainExtents].
bool providesSufficientTicksForRange(DateTimeExtents domainExtents);
/// Find the closet step size, from provided step size, in milliseconds.
int getClosestStepSize(int stepSize);
}

View File

@@ -0,0 +1,129 @@
// 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:meta/meta.dart' show required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import '../tick.dart' show Tick;
import '../tick_formatter.dart' show TickFormatter;
import '../tick_provider.dart' show TickHint;
import 'date_time_extents.dart' show DateTimeExtents;
import 'date_time_scale.dart' show DateTimeScale;
import 'time_range_tick_provider.dart' show TimeRangeTickProvider;
import 'time_stepper.dart' show TimeStepper;
// Contains all the common code for the time range tick providers.
class TimeRangeTickProviderImpl extends TimeRangeTickProvider {
final int requiredMinimumTicks;
final TimeStepper timeStepper;
TimeRangeTickProviderImpl(this.timeStepper, {this.requiredMinimumTicks = 3});
@override
bool providesSufficientTicksForRange(DateTimeExtents domainExtents) {
final cnt = timeStepper.getStepCountBetween(domainExtents, 1);
return cnt >= requiredMinimumTicks;
}
/// Find the closet step size, from provided step size, in milliseconds.
@override
int getClosestStepSize(int stepSize) {
return timeStepper.typicalStepSizeMs *
_getClosestIncrementFromStepSize(stepSize);
}
// Find the increment that is closest to the step size.
int _getClosestIncrementFromStepSize(int stepSize) {
int minDifference;
int closestIncrement;
for (int increment in timeStepper.allowedTickIncrements) {
final difference =
(stepSize - (timeStepper.typicalStepSizeMs * increment)).abs();
if (minDifference == null || minDifference > difference) {
minDifference = difference;
closestIncrement = increment;
}
}
return closestIncrement;
}
@override
List<Tick<DateTime>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required DateTimeScale scale,
@required TickFormatter<DateTime> formatter,
@required Map<DateTime, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<DateTime> tickHint,
}) {
List<Tick<DateTime>> currentTicks;
final tickValues = <DateTime>[];
final timeStepIt = timeStepper.getSteps(scale.viewportDomain).iterator;
// Try different tickIncrements and choose the first that has no collisions.
// If none exist use the last one which should have the fewest ticks and
// hope that the renderer will resolve collisions.
//
// If a tick hint was provided, use the tick hint to search for the closest
// increment and use that.
List<int> allowedTickIncrements;
if (tickHint != null) {
final stepSize = tickHint.end.difference(tickHint.start).inMilliseconds;
allowedTickIncrements = [_getClosestIncrementFromStepSize(stepSize)];
} else {
allowedTickIncrements = timeStepper.allowedTickIncrements;
}
for (int i = 0; i < allowedTickIncrements.length; i++) {
// Create tick values with a specified increment.
final tickIncrement = allowedTickIncrements[i];
tickValues.clear();
timeStepIt.reset(tickIncrement);
while (timeStepIt.moveNext()) {
tickValues.add(timeStepIt.current);
}
// Create ticks
currentTicks = createTicks(tickValues,
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
stepSize: timeStepper.typicalStepSizeMs * tickIncrement);
// Request collision check from draw strategy.
final collisionReport =
tickDrawStrategy.collides(currentTicks, orientation);
if (!collisionReport.ticksCollide) {
// Return the first non colliding ticks.
return currentTicks;
}
}
// If all ticks collide, return the last generated ticks.
return currentTicks;
}
}

View File

@@ -0,0 +1,60 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'date_time_extents.dart' show DateTimeExtents;
/// Represents the step/tick information for the given time range.
abstract class TimeStepper {
/// Get new bounding extents to the ticks that would contain the given
/// timeExtents.
DateTimeExtents updateBoundingSteps(DateTimeExtents timeExtents);
/// Returns the number steps/ticks are between the given extents inclusive.
///
/// Does not extend the extents to the bounding ticks.
int getStepCountBetween(DateTimeExtents timeExtents, int tickIncrement);
/// Generates an Iterable for iterating over the time steps bounded by the
/// given timeExtents. The desired tickIncrement can be set on the returned
/// [TimeStepIteratorFactory].
TimeStepIteratorFactory getSteps(DateTimeExtents timeExtents);
/// Returns the typical stepSize for this stepper assuming increment by 1.
int get typicalStepSizeMs;
/// An ordered list of step increments that makes sense given the step.
///
/// Example: hours may increment by 1, 2, 3, 4, 6, 12. It doesn't make sense
/// to increment hours by 7.
List<int> get allowedTickIncrements;
}
/// Iterator with a reset function that can be used multiple times to avoid
/// object instantiation during the Android layout/draw phases.
abstract class TimeStepIterator extends Iterator<DateTime> {
/// Reset the iterator and set the tickIncrement to the specified value.
///
/// This method is provided so that the same iterator instance can be used for
/// different tick increments, avoiding object allocation during Android
/// layout/draw phases.
TimeStepIterator reset(int tickIncrement);
}
/// Factory that creates TimeStepIterator with the set tickIncrement value.
abstract class TimeStepIteratorFactory extends Iterable {
/// Get iterator and optionally set the tickIncrement.
@override
TimeStepIterator get iterator;
}

View File

@@ -0,0 +1,31 @@
// 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.
/// Formatter of [DateTime] ticks
abstract class TimeTickFormatter {
/// Format for tick that is the first in a set of ticks.
String formatFirstTick(DateTime date);
/// Format for a 'simple' tick.
///
/// Ex. Not a first tick or transition tick.
String formatSimpleTick(DateTime date);
/// Format for a transitional tick.
String formatTransitionTick(DateTime date);
/// Returns true if tick is a transitional tick.
bool isTransition(DateTime tickValue, DateTime prevTickValue);
}

View File

@@ -0,0 +1,100 @@
// 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:intl/intl.dart' show DateFormat;
import 'package:meta/meta.dart' show required;
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'time_tick_formatter.dart' show TimeTickFormatter;
/// Formatter that can format simple and transition time ticks differently.
class TimeTickFormatterImpl implements TimeTickFormatter {
DateFormat _simpleFormat;
DateFormat _transitionFormat;
final CalendarField transitionField;
/// Create time tick formatter.
///
/// [dateTimeFactory] factory to use to generate the [DateFormat].
/// [simpleFormat] format to use for most ticks.
/// [transitionFormat] format to use when the time unit transitions.
/// For example showing the month with the date for Jan 1.
/// [transitionField] the calendar field that indicates transition.
TimeTickFormatterImpl(
{@required DateTimeFactory dateTimeFactory,
@required String simpleFormat,
@required String transitionFormat,
this.transitionField}) {
_simpleFormat = dateTimeFactory.createDateFormat(simpleFormat);
_transitionFormat = dateTimeFactory.createDateFormat(transitionFormat);
}
@override
String formatFirstTick(DateTime date) => _transitionFormat.format(date);
@override
String formatSimpleTick(DateTime date) => _simpleFormat.format(date);
@override
String formatTransitionTick(DateTime date) => _transitionFormat.format(date);
@override
bool isTransition(DateTime tickValue, DateTime prevTickValue) {
// Transition is always false if no transition field is specified.
if (transitionField == null) {
return false;
}
final prevTransitionFieldValue =
getCalendarField(prevTickValue, transitionField);
final transitionFieldValue = getCalendarField(tickValue, transitionField);
return prevTransitionFieldValue != transitionFieldValue;
}
/// Gets the calendar field for [dateTime].
int getCalendarField(DateTime dateTime, CalendarField field) {
int value;
switch (field) {
case CalendarField.year:
value = dateTime.year;
break;
case CalendarField.month:
value = dateTime.month;
break;
case CalendarField.date:
value = dateTime.day;
break;
case CalendarField.hourOfDay:
value = dateTime.hour;
break;
case CalendarField.minute:
value = dateTime.minute;
break;
case CalendarField.second:
value = dateTime.second;
break;
}
return value;
}
}
enum CalendarField {
year,
month,
date,
hourOfDay,
minute,
second,
}

View File

@@ -0,0 +1,63 @@
// 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 '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart' show BaseTimeStepper;
/// Year stepper.
class YearTimeStepper extends BaseTimeStepper {
static const _defaultIncrements = const [1, 2, 5, 10, 50, 100, 500, 1000];
final List<int> _allowedTickIncrements;
YearTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory YearTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment option.
assert(allowedTickIncrements.isNotEmpty);
// All increments must be > 0.
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
return new YearTimeStepper._internal(
dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => 365 * 24 * 3600 * 1000;
@override
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Guarantees the increment is a factor of the tick value.
///
/// Example: 2017, tick increment of 10, step before is 2010.
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final yearRemainder = time.year % tickIncrement;
return dateTimeFactory.createDateTime(time.year - yearRemainder);
}
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
return dateTimeFactory.createDateTime(time.year + tickIncrement);
}
}

View File

@@ -0,0 +1,468 @@
// 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:collection' show LinkedHashMap;
import 'dart:math' show Point;
import 'package:meta/meta.dart' show protected;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../../data/series.dart' show Series;
import '../bar/bar_renderer.dart' show BarRenderer;
import '../common/base_chart.dart' show BaseChart;
import '../common/chart_context.dart' show ChartContext;
import '../common/datum_details.dart' show DatumDetails;
import '../common/processed_series.dart' show MutableSeries;
import '../common/selection_model/selection_model.dart' show SelectionModelType;
import '../common/series_renderer.dart' show SeriesRenderer;
import '../layout/layout_config.dart' show LayoutConfig, MarginSpec;
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
import 'axis/axis.dart'
show
Axis,
AxisOrientation,
OrdinalAxis,
NumericAxis,
domainAxisKey,
measureAxisIdKey,
measureAxisKey;
import 'axis/draw_strategy/gridline_draw_strategy.dart'
show GridlineRendererSpec;
import 'axis/draw_strategy/none_draw_strategy.dart' show NoneDrawStrategy;
import 'axis/draw_strategy/small_tick_draw_strategy.dart'
show SmallTickRendererSpec;
import 'axis/spec/axis_spec.dart' show AxisSpec;
class NumericCartesianChart extends CartesianChart<num> {
NumericCartesianChart(
{bool vertical,
LayoutConfig layoutConfig,
NumericAxis primaryMeasureAxis,
NumericAxis secondaryMeasureAxis,
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
: super(
vertical: vertical,
layoutConfig: layoutConfig,
domainAxis: new NumericAxis(),
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes);
@protected
void initDomainAxis() {
_domainAxis.tickDrawStrategy = new SmallTickRendererSpec<num>()
.createDrawStrategy(context, graphicsFactory);
}
}
class OrdinalCartesianChart extends CartesianChart<String> {
OrdinalCartesianChart(
{bool vertical,
LayoutConfig layoutConfig,
NumericAxis primaryMeasureAxis,
NumericAxis secondaryMeasureAxis,
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
: super(
vertical: vertical,
layoutConfig: layoutConfig,
domainAxis: new OrdinalAxis(),
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes);
@protected
void initDomainAxis() {
_domainAxis
..tickDrawStrategy = new SmallTickRendererSpec<String>()
.createDrawStrategy(context, graphicsFactory);
}
}
abstract class CartesianChart<D> extends BaseChart<D> {
static final _defaultLayoutConfig = new LayoutConfig(
topSpec: new MarginSpec.fromPixel(minPixel: 20),
bottomSpec: new MarginSpec.fromPixel(minPixel: 20),
leftSpec: new MarginSpec.fromPixel(minPixel: 20),
rightSpec: new MarginSpec.fromPixel(minPixel: 20),
);
bool vertical;
/// The current domain axis for this chart.
Axis<D> _domainAxis;
/// Temporarily stores the new domain axis that is passed in the constructor
/// and the new domain axis created when [domainAxisSpec] is set to a new
/// spec.
///
/// This step is necessary because the axis cannot be fully configured until
/// [context] is available. [configurationChanged] is called after [context]
/// is available and [_newDomainAxis] will be set to [_domainAxis] and then
/// reset back to null.
Axis<D> _newDomainAxis;
/// The current domain axis spec that was used to configure [_domainAxis].
///
/// This is kept to check if the axis spec has changed when [domainAxisSpec]
/// is set.
AxisSpec<D> _domainAxisSpec;
/// Temporarily stores the new domain axis spec that is passed in when
/// [domainAxisSpec] is set and is different from [_domainAxisSpec]. This spec
/// is then applied to the new domain axis when [configurationChanged] is
/// called.
AxisSpec<D> _newDomainAxisSpec;
final Axis<num> _primaryMeasureAxis;
final Axis<num> _secondaryMeasureAxis;
final LinkedHashMap<String, NumericAxis> _disjointMeasureAxes;
/// If set to true, the vertical axis will render the opposite of the default
/// direction.
bool flipVerticalAxisOutput = false;
bool _usePrimaryMeasureAxis = false;
bool _useSecondaryMeasureAxis = false;
CartesianChart(
{bool vertical,
LayoutConfig layoutConfig,
Axis<D> domainAxis,
NumericAxis primaryMeasureAxis,
NumericAxis secondaryMeasureAxis,
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
: vertical = vertical ?? true,
// [domainAxis] will be set to the new axis in [configurationChanged].
_newDomainAxis = domainAxis,
_primaryMeasureAxis = primaryMeasureAxis ?? new NumericAxis(),
_secondaryMeasureAxis = secondaryMeasureAxis ?? new NumericAxis(),
_disjointMeasureAxes = disjointMeasureAxes ?? <String, NumericAxis>{},
super(layoutConfig: layoutConfig ?? _defaultLayoutConfig) {
// As a convenience for chart configuration, set the paint order on any axis
// that is missing one.
_primaryMeasureAxis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
_secondaryMeasureAxis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
});
}
void init(ChartContext context, GraphicsFactory graphicsFactory) {
super.init(context, graphicsFactory);
_primaryMeasureAxis.context = context;
_primaryMeasureAxis.tickDrawStrategy = new GridlineRendererSpec<num>()
.createDrawStrategy(context, graphicsFactory);
_secondaryMeasureAxis.context = context;
_secondaryMeasureAxis.tickDrawStrategy = new GridlineRendererSpec<num>()
.createDrawStrategy(context, graphicsFactory);
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis.context = context;
axis.tickDrawStrategy =
new NoneDrawStrategy<num>(context, graphicsFactory);
});
}
Axis get domainAxis => _domainAxis;
/// Allows the chart to configure the domain axis when it is created.
@protected
void initDomainAxis();
/// Create a new domain axis and save the new spec to be applied during
/// [configurationChanged].
set domainAxisSpec(AxisSpec axisSpec) {
if (_domainAxisSpec != axisSpec) {
_newDomainAxis = createDomainAxisFromSpec(axisSpec);
_newDomainAxisSpec = axisSpec;
}
}
/// Creates the domain axis spec from provided axis spec.
@protected
Axis<D> createDomainAxisFromSpec(AxisSpec<D> axisSpec) {
return axisSpec.createAxis();
}
@override
void configurationChanged() {
if (_newDomainAxis != null) {
if (_domainAxis != null) {
removeView(_domainAxis);
}
_domainAxis = _newDomainAxis;
_domainAxis
..context = context
..layoutPaintOrder = LayoutViewPaintOrder.domainAxis;
initDomainAxis();
addView(_domainAxis);
_newDomainAxis = null;
}
if (_newDomainAxisSpec != null) {
_domainAxisSpec = _newDomainAxisSpec;
_newDomainAxisSpec.configure(_domainAxis, context, graphicsFactory);
_newDomainAxisSpec = null;
}
}
/// Gets the measure axis matching the provided id.
///
/// If none is provided, this returns the primary measure axis.
Axis getMeasureAxis({String axisId}) {
Axis axis;
if (axisId == Axis.secondaryMeasureAxisId) {
axis = _secondaryMeasureAxis;
} else if (axisId == Axis.primaryMeasureAxisId) {
axis = _primaryMeasureAxis;
} else if (_disjointMeasureAxes[axisId] != null) {
axis = _disjointMeasureAxes[axisId];
}
// If no valid axisId was provided, fall back to primary axis.
axis ??= _primaryMeasureAxis;
return axis;
}
// TODO: Change measure axis spec to create new measure axis.
/// Sets the primary measure axis for the chart, rendered on the start side of
/// the domain axis.
set primaryMeasureAxisSpec(AxisSpec axisSpec) {
axisSpec.configure(_primaryMeasureAxis, context, graphicsFactory);
}
/// Sets the secondary measure axis for the chart, rendered on the end side of
/// the domain axis.
set secondaryMeasureAxisSpec(AxisSpec axisSpec) {
axisSpec.configure(_secondaryMeasureAxis, context, graphicsFactory);
}
/// Sets a map of disjoint measure axes for the chart.
///
/// Disjoint measure axes can be used to scale a sub-set of series on the
/// chart independently from the primary and secondary axes. The general use
/// case for this type of chart is to show differences in the trends of the
/// data, without comparing their absolute values.
///
/// Disjoint axes will not render any tick or gridline elements. With
/// independent scales, there would be a lot of collision in labels were they
/// to do so.
///
/// If any series is rendered with a disjoint axis, it is highly recommended
/// to render all series with disjoint axes. Otherwise, the chart may be
/// visually misleading.
///
/// A [LinkedHashMap] is used to ensure consistent ordering when painting the
/// axes.
set disjointMeasureAxisSpecs(LinkedHashMap<String, AxisSpec> axisSpecs) {
axisSpecs.forEach((String axisId, AxisSpec axisSpec) {
axisSpec.configure(
_disjointMeasureAxes[axisId], context, graphicsFactory);
});
}
@override
MutableSeries<D> makeSeries(Series<dynamic, D> series) {
MutableSeries<D> s = super.makeSeries(series);
s.measureOffsetFn ??= (_) => 0;
// Setup the Axes
s.setAttr(domainAxisKey, domainAxis);
s.setAttr(measureAxisKey,
getMeasureAxis(axisId: series.getAttribute(measureAxisIdKey)));
return s;
}
@override
SeriesRenderer<D> makeDefaultRenderer() {
return new BarRenderer()..rendererId = SeriesRenderer.defaultRendererId;
}
@override
Map<String, List<MutableSeries<D>>> preprocessSeries(
List<MutableSeries<D>> seriesList) {
var rendererToSeriesList = super.preprocessSeries(seriesList);
// Check if primary or secondary measure axis is being used.
for (final series in seriesList) {
final measureAxisId = series.getAttr(measureAxisIdKey);
_usePrimaryMeasureAxis = _usePrimaryMeasureAxis ||
(measureAxisId == null || measureAxisId == Axis.primaryMeasureAxisId);
_useSecondaryMeasureAxis = _useSecondaryMeasureAxis ||
(measureAxisId == Axis.secondaryMeasureAxisId);
}
// Add or remove the primary axis view.
if (_usePrimaryMeasureAxis) {
addView(_primaryMeasureAxis);
} else {
removeView(_primaryMeasureAxis);
}
// Add or remove the secondary axis view.
if (_useSecondaryMeasureAxis) {
addView(_secondaryMeasureAxis);
} else {
removeView(_secondaryMeasureAxis);
}
// Add all disjoint axis views so that their range will be configured.
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
addView(axis);
});
// Reset stale values from previous draw cycles.
domainAxis.resetDomains();
_primaryMeasureAxis.resetDomains();
_secondaryMeasureAxis.resetDomains();
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis.resetDomains();
});
final reverseAxisDirection = context != null && context.isRtl;
if (vertical) {
domainAxis
..axisOrientation = AxisOrientation.bottom
..reverseOutputRange = reverseAxisDirection;
_primaryMeasureAxis
..axisOrientation = (reverseAxisDirection
? AxisOrientation.right
: AxisOrientation.left)
..reverseOutputRange = flipVerticalAxisOutput;
_secondaryMeasureAxis
..axisOrientation = (reverseAxisDirection
? AxisOrientation.left
: AxisOrientation.right)
..reverseOutputRange = flipVerticalAxisOutput;
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis
..axisOrientation = (reverseAxisDirection
? AxisOrientation.left
: AxisOrientation.right)
..reverseOutputRange = flipVerticalAxisOutput;
});
} else {
domainAxis
..axisOrientation = (reverseAxisDirection
? AxisOrientation.right
: AxisOrientation.left)
..reverseOutputRange = flipVerticalAxisOutput;
_primaryMeasureAxis
..axisOrientation = AxisOrientation.bottom
..reverseOutputRange = reverseAxisDirection;
_secondaryMeasureAxis
..axisOrientation = AxisOrientation.top
..reverseOutputRange = reverseAxisDirection;
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis
..axisOrientation = AxisOrientation.top
..reverseOutputRange = reverseAxisDirection;
});
}
// Have each renderer configure the axes with their domain and measure
// values.
rendererToSeriesList
.forEach((String rendererId, List<MutableSeries<D>> seriesList) {
getSeriesRenderer(rendererId).configureDomainAxes(seriesList);
getSeriesRenderer(rendererId).configureMeasureAxes(seriesList);
});
return rendererToSeriesList;
}
@override
void onSkipLayout() {
// Update ticks only when skipping layout.
domainAxis.updateTicks();
if (_usePrimaryMeasureAxis) {
_primaryMeasureAxis.updateTicks();
}
if (_useSecondaryMeasureAxis) {
_secondaryMeasureAxis.updateTicks();
}
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis.updateTicks();
});
super.onSkipLayout();
}
@override
void onPostLayout(Map<String, List<MutableSeries<D>>> rendererToSeriesList) {
fireOnAxisConfigured();
super.onPostLayout(rendererToSeriesList);
}
/// Returns a list of datum details from selection model of [type].
@override
List<DatumDetails<D>> getDatumDetails(SelectionModelType type) {
final entries = <DatumDetails<D>>[];
getSelectionModel(type).selectedDatum.forEach((seriesDatum) {
final series = seriesDatum.series;
final datum = seriesDatum.datum;
final datumIndex = seriesDatum.index;
final domain = series.domainFn(datumIndex);
final measure = series.measureFn(datumIndex);
final rawMeasure = series.rawMeasureFn(datumIndex);
final color = series.colorFn(datumIndex);
final domainPosition = series.getAttr(domainAxisKey).getLocation(domain);
final measurePosition =
series.getAttr(measureAxisKey).getLocation(measure);
final chartPosition = new Point<double>(
vertical ? domainPosition : measurePosition,
vertical ? measurePosition : domainPosition);
entries.add(new DatumDetails(
datum: datum,
domain: domain,
measure: measure,
rawMeasure: rawMeasure,
series: series,
color: color,
chartPosition: chartPosition));
});
return entries;
}
}

View File

@@ -0,0 +1,264 @@
// 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:meta/meta.dart';
import '../../common/symbol_renderer.dart' show SymbolRenderer;
import '../../data/series.dart' show AccessorFn;
import '../common/base_chart.dart' show BaseChart;
import '../common/processed_series.dart' show MutableSeries;
import '../common/series_renderer.dart' show BaseSeriesRenderer, SeriesRenderer;
import 'axis/axis.dart' show Axis, domainAxisKey, measureAxisKey;
import 'cartesian_chart.dart' show CartesianChart;
abstract class CartesianRenderer<D> extends SeriesRenderer<D> {
void configureDomainAxes(List<MutableSeries<D>> seriesList);
void configureMeasureAxes(List<MutableSeries<D>> seriesList);
}
abstract class BaseCartesianRenderer<D> extends BaseSeriesRenderer<D>
implements CartesianRenderer<D> {
bool _renderingVertically = true;
BaseCartesianRenderer(
{@required String rendererId,
@required int layoutPaintOrder,
SymbolRenderer symbolRenderer})
: super(
rendererId: rendererId,
layoutPaintOrder: layoutPaintOrder,
symbolRenderer: symbolRenderer);
@override
void onAttach(BaseChart<D> chart) {
super.onAttach(chart);
_renderingVertically = (chart as CartesianChart).vertical;
}
bool get renderingVertically => _renderingVertically;
@override
void configureDomainAxes(List<MutableSeries<D>> seriesList) {
seriesList.forEach((MutableSeries<D> series) {
if (series.data.isEmpty) {
return;
}
final domainAxis = series.getAttr(domainAxisKey);
final domainFn = series.domainFn;
final domainLowerBoundFn = series.domainLowerBoundFn;
final domainUpperBoundFn = series.domainUpperBoundFn;
if (domainAxis == null) {
return;
}
if (renderingVertically) {
for (int i = 0; i < series.data.length; i++) {
domainAxis.addDomainValue(domainFn(i));
if (domainLowerBoundFn != null && domainUpperBoundFn != null) {
final domainLowerBound = domainLowerBoundFn(i);
final domainUpperBound = domainUpperBoundFn(i);
if (domainLowerBound != null && domainUpperBound != null) {
domainAxis.addDomainValue(domainLowerBound);
domainAxis.addDomainValue(domainUpperBound);
}
}
}
} else {
// When rendering horizontally, domains are displayed from top to bottom
// in order to match visual display in legend.
for (int i = series.data.length - 1; i >= 0; i--) {
domainAxis.addDomainValue(domainFn(i));
if (domainLowerBoundFn != null && domainUpperBoundFn != null) {
final domainLowerBound = domainLowerBoundFn(i);
final domainUpperBound = domainUpperBoundFn(i);
if (domainLowerBound != null && domainUpperBound != null) {
domainAxis.addDomainValue(domainLowerBound);
domainAxis.addDomainValue(domainUpperBound);
}
}
}
}
});
}
@override
void configureMeasureAxes(List<MutableSeries<D>> seriesList) {
seriesList.forEach((MutableSeries<D> series) {
if (series.data.isEmpty) {
return;
}
final domainAxis = series.getAttr(domainAxisKey);
final domainFn = series.domainFn;
if (domainAxis == null) {
return;
}
final measureAxis = series.getAttr(measureAxisKey);
if (measureAxis == null) {
return;
}
// Only add the measure values for datum who's domain is within the
// domainAxis viewport.
int startIndex =
findNearestViewportStart(domainAxis, domainFn, series.data);
int endIndex = findNearestViewportEnd(domainAxis, domainFn, series.data);
addMeasureValuesFor(series, measureAxis, startIndex, endIndex);
});
}
void addMeasureValuesFor(
MutableSeries<D> series, Axis measureAxis, int startIndex, int endIndex) {
final measureFn = series.measureFn;
final measureOffsetFn = series.measureOffsetFn;
final measureLowerBoundFn = series.measureLowerBoundFn;
final measureUpperBoundFn = series.measureUpperBoundFn;
for (int i = startIndex; i <= endIndex; i++) {
final measure = measureFn(i);
final measureOffset = measureOffsetFn(i);
if (measure != null && measureOffset != null) {
measureAxis.addDomainValue(measure + measureOffset);
if (measureLowerBoundFn != null && measureUpperBoundFn != null) {
measureAxis.addDomainValue(measureLowerBoundFn(i) + measureOffset);
measureAxis.addDomainValue(measureUpperBoundFn(i) + measureOffset);
}
}
}
}
@visibleForTesting
int findNearestViewportStart(
Axis domainAxis, AccessorFn<D> domainFn, List data) {
if (data.isEmpty) {
return null;
}
// Quick optimization for full viewport (likely).
if (domainAxis.compareDomainValueToViewport(domainFn(0)) == 0) {
return 0;
}
var start = 1; // Index zero was already checked for above.
var end = data.length - 1;
// Binary search for the start of the viewport.
while (end >= start) {
int searchIndex = ((end - start) / 2).floor() + start;
int prevIndex = searchIndex - 1;
var comparisonValue =
domainAxis.compareDomainValueToViewport(domainFn(searchIndex));
var prevComparisonValue =
domainAxis.compareDomainValueToViewport(domainFn(prevIndex));
// Found start?
if (prevComparisonValue == -1 && comparisonValue == 0) {
return searchIndex;
}
// Straddling viewport?
// Return previous index as the nearest start of the viewport.
if (comparisonValue == 1 && prevComparisonValue == -1) {
return (searchIndex - 1);
}
// Before start? Update startIndex
if (comparisonValue == -1) {
start = searchIndex + 1;
} else {
// Middle or after viewport? Update endIndex
end = searchIndex - 1;
}
}
// Binary search would reach this point for the edge cases where the domain
// specified is prior or after the domain viewport.
// If domain is prior to the domain viewport, return the first index as the
// nearest viewport start.
// If domain is after the domain viewport, return the last index as the
// nearest viewport start.
final lastComparison =
domainAxis.compareDomainValueToViewport(domainFn(data.length - 1));
return lastComparison == 1 ? (data.length - 1) : 0;
}
@visibleForTesting
int findNearestViewportEnd(
Axis domainAxis, AccessorFn<D> domainFn, List data) {
if (data.isEmpty) {
return null;
}
var start = 1;
var end = data.length - 1;
// Quick optimization for full viewport (likely).
if (domainAxis.compareDomainValueToViewport(domainFn(end)) == 0) {
return end;
}
end = end - 1; // Last index was already checked for above.
// Binary search for the start of the viewport.
while (end >= start) {
int searchIndex = ((end - start) / 2).floor() + start;
int prevIndex = searchIndex - 1;
int comparisonValue =
domainAxis.compareDomainValueToViewport(domainFn(searchIndex));
int prevComparisonValue =
domainAxis.compareDomainValueToViewport(domainFn(prevIndex));
// Found end?
if (prevComparisonValue == 0 && comparisonValue == 1) {
return prevIndex;
}
// Straddling viewport?
// Return the current index as the start of the viewport.
if (comparisonValue == 1 && prevComparisonValue == -1) {
return searchIndex;
}
// After end? Update endIndex
if (comparisonValue == 1) {
end = searchIndex - 1;
} else {
// Middle or before viewport? Update startIndex
start = searchIndex + 1;
}
}
// Binary search would reach this point for the edge cases where the domain
// specified is prior or after the domain viewport.
// If domain is prior to the domain viewport, return the first index as the
// nearest viewport end.
// If domain is after the domain viewport, return the last index as the
// nearest viewport end.
final lastComparison =
domainAxis.compareDomainValueToViewport(domainFn(data.length - 1));
return lastComparison == 1 ? (data.length - 1) : 0;
}
}

View File

@@ -0,0 +1,712 @@
// 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, Point;
import 'package:meta/meta.dart' show protected;
import '../../common/gesture_listener.dart' show GestureListener;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/proxy_gesture_listener.dart' show ProxyGestureListener;
import '../../data/series.dart' show Series;
import '../layout/layout_config.dart' show LayoutConfig;
import '../layout/layout_manager.dart' show LayoutManager;
import '../layout/layout_manager_impl.dart' show LayoutManagerImpl;
import '../layout/layout_view.dart' show LayoutView;
import 'behavior/chart_behavior.dart' show ChartBehavior;
import 'chart_canvas.dart' show ChartCanvas;
import 'chart_context.dart' show ChartContext;
import 'datum_details.dart' show DatumDetails;
import 'processed_series.dart' show MutableSeries;
import 'selection_model/selection_model.dart'
show MutableSelectionModel, SelectionModelType;
import 'series_datum.dart' show SeriesDatum;
import 'series_renderer.dart' show SeriesRenderer, rendererIdKey, rendererKey;
typedef BehaviorCreator = ChartBehavior<D> Function<D>();
abstract class BaseChart<D> {
ChartContext context;
/// Internal use only.
GraphicsFactory graphicsFactory;
LayoutManager _layoutManager;
int _chartWidth;
int _chartHeight;
Duration transition = const Duration(milliseconds: 300);
double animationPercent;
bool _animationsTemporarilyDisabled = false;
/// List of series that were passed into the previous draw call.
///
/// This list will be used when redraw is called, to reset the state of all
/// behaviors to the original list.
List<MutableSeries<D>> _originalSeriesList;
/// List of series that are currently drawn on the chart.
///
/// This list should be used by interactive behaviors between chart draw
/// cycles. It may be filtered or modified by some behaviors during the
/// initial draw cycle (e.g. a [Legend] may hide some series).
List<MutableSeries<D>> _currentSeriesList;
Set<String> _usingRenderers = new Set<String>();
Map<String, List<MutableSeries<D>>> _rendererToSeriesList;
final _seriesRenderers = <String, SeriesRenderer<D>>{};
/// Map of named chart behaviors attached to this chart.
final _behaviorRoleMap = <String, ChartBehavior<D>>{};
final _behaviorStack = <ChartBehavior<D>>[];
final _behaviorTappableMap = <String, ChartBehavior<D>>{};
/// Whether or not the chart will respond to tap events.
///
/// This will generally be true if there is a behavior attached to the chart
/// that does something with tap events, such as "click to select data."
bool get isTappable => _behaviorTappableMap.isNotEmpty;
final _gestureProxy = new ProxyGestureListener();
final _selectionModels = <SelectionModelType, MutableSelectionModel<D>>{};
/// Whether data should be selected by nearest domain distance, or by relative
/// distance.
///
/// This should generally be true for chart types that are intended to be
/// aggregated by domain, and false for charts that plot arbitrary x,y data.
/// Scatter plots, for example, may have many overlapping data with the same
/// domain value.
bool get selectNearestByDomain => true;
final _lifecycleListeners = <LifecycleListener<D>>[];
BaseChart({LayoutConfig layoutConfig}) {
_layoutManager = new LayoutManagerImpl(config: layoutConfig);
}
void init(ChartContext context, GraphicsFactory graphicsFactory) {
this.context = context;
// When graphics factory is updated, update all the views.
if (this.graphicsFactory != graphicsFactory) {
this.graphicsFactory = graphicsFactory;
_layoutManager.applyToViews(
(LayoutView view) => view.graphicsFactory = graphicsFactory);
}
configurationChanged();
}
/// Finish configuring components that require context and graphics factory.
///
/// Some components require context and graphics factory to be set again when
/// configuration has changed but the configuration is set prior to the
/// chart first calling init with the context.
void configurationChanged() {}
int get chartWidth => _chartWidth;
int get chartHeight => _chartHeight;
//
// Gesture proxy methods
//
ProxyGestureListener get gestureProxy => _gestureProxy;
/// Add a [GestureListener] to this chart.
GestureListener addGestureListener(GestureListener listener) {
_gestureProxy.add(listener);
return listener;
}
/// Remove a [GestureListener] from this chart.
void removeGestureListener(GestureListener listener) {
_gestureProxy.remove(listener);
}
LifecycleListener addLifecycleListener(LifecycleListener<D> listener) {
_lifecycleListeners.add(listener);
return listener;
}
bool removeLifecycleListener(LifecycleListener<D> listener) =>
_lifecycleListeners.remove(listener);
/// Returns MutableSelectionModel for the given type. Lazy creates one upon first
/// request.
MutableSelectionModel<D> getSelectionModel(SelectionModelType type) {
return _selectionModels.putIfAbsent(
type, () => new MutableSelectionModel<D>());
}
/// Returns a list of datum details from selection model of [type].
List<DatumDetails<D>> getDatumDetails(SelectionModelType type);
//
// Renderer methods
//
set defaultRenderer(SeriesRenderer<D> renderer) {
renderer.rendererId = SeriesRenderer.defaultRendererId;
addSeriesRenderer(renderer);
}
SeriesRenderer<D> get defaultRenderer =>
getSeriesRenderer(SeriesRenderer.defaultRendererId);
void addSeriesRenderer(SeriesRenderer renderer) {
String rendererId = renderer.rendererId;
SeriesRenderer<D> previousRenderer = _seriesRenderers[rendererId];
if (previousRenderer != null) {
removeView(previousRenderer);
previousRenderer.onDetach(this);
}
addView(renderer);
renderer.onAttach(this);
_seriesRenderers[rendererId] = renderer;
}
SeriesRenderer<D> getSeriesRenderer(String rendererId) {
SeriesRenderer<D> renderer = _seriesRenderers[rendererId];
// Special case, if we are asking for the default and we haven't made it
// yet, then make it now.
if (renderer == null) {
if (rendererId == SeriesRenderer.defaultRendererId) {
renderer = makeDefaultRenderer();
defaultRenderer = renderer;
}
}
// TODO: throw error if couldn't find renderer by id?
return renderer;
}
SeriesRenderer<D> makeDefaultRenderer();
bool pointWithinRenderer(Point<double> chartPosition) {
return _usingRenderers.any((String rendererId) =>
getSeriesRenderer(rendererId)
.componentBounds
.containsPoint(chartPosition));
}
/// Retrieves the datum details that are nearest to the given [drawAreaPoint].
///
/// [drawAreaPoint] represents a point in the chart, such as a point that was
/// clicked/tapped on by a user.
///
/// [selectAcrossAllDrawAreaComponents] specifies whether nearest data
/// selection should be done across the combined draw area of all components
/// with series draw areas, or just the chart's primary draw area bounds.
List<DatumDetails<D>> getNearestDatumDetailPerSeries(
Point<double> drawAreaPoint, bool selectAcrossAllDrawAreaComponents) {
// Optionally grab the combined draw area bounds of all components. If this
// is disabled, then we expect each series renderer to filter out the event
// if [chartPoint] is located outside of its own component bounds.
final boundsOverride =
selectAcrossAllDrawAreaComponents ? drawableLayoutAreaBounds : null;
final details = <DatumDetails<D>>[];
_usingRenderers.forEach((String rendererId) {
details.addAll(getSeriesRenderer(rendererId)
.getNearestDatumDetailPerSeries(
drawAreaPoint, selectNearestByDomain, boundsOverride));
});
details.sort((DatumDetails<D> a, DatumDetails<D> b) {
// Sort so that the nearest one is first.
// Special sort, sort by domain distance first, then by measure distance.
if (selectNearestByDomain) {
int domainDiff = a.domainDistance.compareTo(b.domainDistance);
if (domainDiff == 0) {
return a.measureDistance.compareTo(b.measureDistance);
}
return domainDiff;
} else {
return a.relativeDistance.compareTo(b.relativeDistance);
}
});
return details;
}
/// Retrieves the datum details for the current chart selection.
///
/// [selectionModelType] specifies the type of the selection model to use.
List<DatumDetails<D>> getSelectedDatumDetails(
SelectionModelType selectionModelType) {
final details = <DatumDetails<D>>[];
if (_currentSeriesList == null) {
return details;
}
final selectionModel = getSelectionModel(selectionModelType);
if (selectionModel == null || !selectionModel.hasDatumSelection) {
return details;
}
// Pass each selected datum to the appropriate series renderer to get full
// details appropriate to its series type.
for (SeriesDatum<D> seriesDatum in selectionModel.selectedDatum) {
final rendererId = seriesDatum.series.getAttr(rendererIdKey);
details.add(
getSeriesRenderer(rendererId).getDetailsForSeriesDatum(seriesDatum));
}
return details;
}
//
// Behavior methods
//
/// Helper method to create a behavior with congruent types.
///
/// This invokes the provides helper with type parameters that match this
/// chart.
ChartBehavior<D> createBehavior(BehaviorCreator creator) => creator<D>();
/// Attaches a behavior to the chart.
///
/// Setting a new behavior with the same role as a behavior already attached
/// to the chart will replace the old behavior. The old behavior's removeFrom
/// method will be called before we attach the new behavior.
void addBehavior(ChartBehavior<D> behavior) {
final role = behavior.role;
if (role != null && _behaviorRoleMap[role] != behavior) {
// Remove any old behavior with the same role.
removeBehavior(_behaviorRoleMap[role]);
// Add the new behavior.
_behaviorRoleMap[role] = behavior;
}
// Add the behavior if it wasn't already added.
if (!_behaviorStack.contains(behavior)) {
_behaviorStack.add(behavior);
behavior.attachTo(this);
}
}
/// Removes a behavior from the chart.
///
/// Returns true if a behavior was removed, otherwise returns false.
bool removeBehavior(ChartBehavior<D> behavior) {
if (behavior == null) {
return false;
}
final role = behavior?.role;
if (role != null && _behaviorRoleMap[role] == behavior) {
_behaviorRoleMap.remove(role);
}
// Make sure the removed behavior is no longer registered for tap events.
unregisterTappable(behavior);
final wasAttached = _behaviorStack.remove(behavior);
behavior.removeFrom(this);
return wasAttached;
}
/// Tells the chart that this behavior responds to tap events.
///
/// This should only be called after [behavior] has been attached to the chart
/// via [addBehavior].
void registerTappable(ChartBehavior<D> behavior) {
final role = behavior.role;
if (role != null &&
_behaviorRoleMap[role] == behavior &&
_behaviorTappableMap[role] != behavior) {
_behaviorTappableMap[role] = behavior;
}
}
/// Tells the chart that this behavior no longer responds to tap events.
void unregisterTappable(ChartBehavior<D> behavior) {
final role = behavior?.role;
if (role != null && _behaviorTappableMap[role] == behavior) {
_behaviorTappableMap.remove(role);
}
}
/// Returns a list of behaviors that have been added.
List<ChartBehavior<D>> get behaviors => new List.unmodifiable(_behaviorStack);
//
// Layout methods
//
void measure(int width, int height) {
if (_rendererToSeriesList != null) {
_layoutManager.measure(width, height);
}
}
void layout(int width, int height) {
if (_rendererToSeriesList != null) {
layoutInternal(width, height);
onPostLayout(_rendererToSeriesList);
}
}
void layoutInternal(int width, int height) {
_chartWidth = width;
_chartHeight = height;
_layoutManager.layout(width, height);
}
void addView(LayoutView view) {
if (_layoutManager.isAttached(view) == false) {
view.graphicsFactory = graphicsFactory;
_layoutManager.addView(view);
}
}
void removeView(LayoutView view) {
_layoutManager.removeView(view);
}
/// Returns whether or not [point] is within the draw area bounds.
bool withinDrawArea(Point<num> point) {
return _layoutManager.withinDrawArea(point);
}
/// Returns the bounds of the chart draw area.
Rectangle<int> get drawAreaBounds => _layoutManager.drawAreaBounds;
int get marginBottom => _layoutManager.marginBottom;
int get marginLeft => _layoutManager.marginLeft;
int get marginRight => _layoutManager.marginRight;
int get marginTop => _layoutManager.marginTop;
/// Returns the combined bounds of the chart draw area and all layout
/// components that draw series data.
Rectangle<int> get drawableLayoutAreaBounds =>
_layoutManager.drawableLayoutAreaBounds;
//
// Draw methods
//
void draw(List<Series<dynamic, D>> seriesList) {
// Clear the selection model when [seriesList] changes.
for (final selectionModel in _selectionModels.values) {
selectionModel.clearSelection(notifyListeners: false);
}
var processedSeriesList =
new List<MutableSeries<D>>.from(seriesList.map(makeSeries));
// Allow listeners to manipulate the seriesList.
fireOnDraw(processedSeriesList);
// Set an index on the series list.
// This can be used by listeners of selection to determine the order of
// series, because the selection details are not returned in this order.
int seriesIndex = 0;
processedSeriesList.forEach((series) => series.seriesIndex = seriesIndex++);
// Initially save a reference to processedSeriesList. After drawInternal
// finishes, we expect _currentSeriesList to contain a new, possibly
// modified list.
_currentSeriesList = processedSeriesList;
// Store off processedSeriesList for use later during redraw calls. This
// list will not reflect any modifications that were made to
// _currentSeriesList by behaviors during the draw cycle.
_originalSeriesList = processedSeriesList;
drawInternal(processedSeriesList, skipAnimation: false, skipLayout: false);
}
/// Redraws and re-lays-out the chart using the previously rendered layout
/// dimensions.
void redraw({bool skipAnimation = false, bool skipLayout = false}) {
drawInternal(_originalSeriesList,
skipAnimation: skipAnimation, skipLayout: skipLayout);
// Trigger layout and actually redraw the chart.
if (!skipLayout) {
measure(_chartWidth, _chartHeight);
layout(_chartWidth, _chartHeight);
} else {
onSkipLayout();
}
}
void drawInternal(List<MutableSeries<D>> seriesList,
{bool skipAnimation, bool skipLayout}) {
seriesList = seriesList
.map((MutableSeries<D> series) => new MutableSeries<D>.clone(series))
.toList();
// TODO: Handle exiting renderers.
_animationsTemporarilyDisabled = skipAnimation;
configureSeries(seriesList);
// Allow listeners to manipulate the processed seriesList.
fireOnPreprocess(seriesList);
_rendererToSeriesList = preprocessSeries(seriesList);
// Allow listeners to manipulate the processed seriesList.
fireOnPostprocess(seriesList);
_currentSeriesList = seriesList;
}
List<MutableSeries<D>> get currentSeriesList => _currentSeriesList;
MutableSeries<D> makeSeries(Series<dynamic, D> series) {
final s = new MutableSeries<D>(series);
// Setup the Renderer
final rendererId =
series.getAttribute(rendererIdKey) ?? SeriesRenderer.defaultRendererId;
s.setAttr(rendererIdKey, rendererId);
s.setAttr(rendererKey, getSeriesRenderer(rendererId));
return s;
}
/// Preprocess series to assign missing color functions.
void configureSeries(List<MutableSeries<D>> seriesList) {
Map<String, List<MutableSeries<D>>> rendererToSeriesList = {};
// Build map of rendererIds to SeriesLists. This map can't be re-used later
// in the preprocessSeries call because some behaviors might alter the
// seriesList.
seriesList.forEach((MutableSeries<D> series) {
String rendererId = series.getAttr(rendererIdKey);
rendererToSeriesList.putIfAbsent(rendererId, () => []).add(series);
});
// Have each renderer add missing color functions to their seriesLists.
rendererToSeriesList
.forEach((String rendererId, List<MutableSeries<D>> seriesList) {
getSeriesRenderer(rendererId).configureSeries(seriesList);
});
}
/// Preprocess series to allow stacking and other mutations.
///
/// Build a map of rendererId to series.
Map<String, List<MutableSeries<D>>> preprocessSeries(
List<MutableSeries<D>> seriesList) {
Map<String, List<MutableSeries<D>>> rendererToSeriesList = {};
var unusedRenderers = _usingRenderers;
_usingRenderers = new Set<String>();
// Build map of rendererIds to SeriesLists.
seriesList.forEach((MutableSeries<D> series) {
String rendererId = series.getAttr(rendererIdKey);
rendererToSeriesList.putIfAbsent(rendererId, () => []).add(series);
_usingRenderers.add(rendererId);
unusedRenderers.remove(rendererId);
});
// Allow unused renderers to render out content.
unusedRenderers
.forEach((String rendererId) => rendererToSeriesList[rendererId] = []);
// Have each renderer preprocess their seriesLists.
rendererToSeriesList
.forEach((String rendererId, List<MutableSeries<D>> seriesList) {
getSeriesRenderer(rendererId).preprocessSeries(seriesList);
});
return rendererToSeriesList;
}
void onSkipLayout() {
onPostLayout(_rendererToSeriesList);
}
void onPostLayout(Map<String, List<MutableSeries<D>>> rendererToSeriesList) {
// Update each renderer with
rendererToSeriesList
.forEach((String rendererId, List<MutableSeries<D>> seriesList) {
getSeriesRenderer(rendererId).update(seriesList, animatingThisDraw);
});
// Request animation
if (animatingThisDraw) {
animationPercent = 0.0;
context.requestAnimation(this.transition);
} else {
animationPercent = 1.0;
context.requestPaint();
}
_animationsTemporarilyDisabled = false;
}
void paint(ChartCanvas canvas) {
canvas.drawingView = 'BaseView';
_layoutManager.paintOrderedViews.forEach((LayoutView view) {
canvas.drawingView = view.runtimeType.toString();
view.paint(canvas, animatingThisDraw ? animationPercent : 1.0);
});
canvas.drawingView = 'PostRender';
fireOnPostrender(canvas);
canvas.drawingView = null;
if (animationPercent == 1.0) {
fireOnAnimationComplete();
}
}
bool get animatingThisDraw => (transition != null &&
transition.inMilliseconds > 0 &&
!_animationsTemporarilyDisabled);
@protected
fireOnDraw(List<MutableSeries<D>> seriesList) {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onData != null) {
listener.onData(seriesList);
}
});
}
@protected
fireOnPreprocess(List<MutableSeries<D>> seriesList) {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onPreprocess != null) {
listener.onPreprocess(seriesList);
}
});
}
@protected
fireOnPostprocess(List<MutableSeries<D>> seriesList) {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onPostprocess != null) {
listener.onPostprocess(seriesList);
}
});
}
@protected
fireOnAxisConfigured() {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onAxisConfigured != null) {
listener.onAxisConfigured();
}
});
}
@protected
fireOnPostrender(ChartCanvas canvas) {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onPostrender != null) {
listener.onPostrender(canvas);
}
});
}
@protected
fireOnAnimationComplete() {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onAnimationComplete != null) {
listener.onAnimationComplete();
}
});
}
/// Called to free up any resources due to chart going away.
destroy() {
// Walk them in add order to support behaviors that remove other behaviors.
for (var i = 0; i < _behaviorStack.length; i++) {
_behaviorStack[i].removeFrom(this);
}
_behaviorStack.clear();
_behaviorRoleMap.clear();
_selectionModels.values.forEach((MutableSelectionModel selectionModel) =>
selectionModel.clearAllListeners());
}
}
class LifecycleListener<D> {
/// Called when new data is drawn to the chart (not a redraw).
///
/// This step is good for processing the data (running averages, percentage of
/// first, etc). It can also be used to add Series of data (trend line) or
/// remove a line as mentioned above, removing Series.
final LifecycleSeriesListCallback onData;
/// Called for every redraw given the original SeriesList resulting from the
/// previous onData.
///
/// This step is good for injecting default attributes on the Series before
/// the renderers process the data (ex: before stacking measures).
final LifecycleSeriesListCallback onPreprocess;
/// Called after the chart and renderers get a chance to process the data but
/// before the axes process them.
///
/// This step is good if you need to alter the Series measure values after the
/// renderers have processed them (ex: after stacking measures).
final LifecycleSeriesListCallback onPostprocess;
/// Called after the Axes have been configured.
/// This step is good if you need to use the axes to get any cartesian
/// location information. At this point Axes should be immutable and stable.
final LifecycleEmptyCallback onAxisConfigured;
/// Called after the chart is done rendering passing along the canvas allowing
/// a behavior or other listener to render on top of the chart.
///
/// This is a convenience callback, however if there is any significant canvas
/// interaction or stacking needs, it is preferred that a AplosView/ChartView
/// is added to the chart instead to fully participate in the view stacking.
final LifecycleCanvasCallback onPostrender;
/// Called after animation hits 100%. This allows a behavior or other listener
/// to chain animations to create a multiple step animation transition.
final LifecycleEmptyCallback onAnimationComplete;
LifecycleListener(
{this.onData,
this.onPreprocess,
this.onPostprocess,
this.onAxisConfigured,
this.onPostrender,
this.onAnimationComplete});
}
typedef LifecycleSeriesListCallback<D>(List<MutableSeries<D>> seriesList);
typedef LifecycleCanvasCallback(ChartCanvas canvas);
typedef LifecycleEmptyCallback();

View File

@@ -0,0 +1,97 @@
// 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 '../../../../common/gesture_listener.dart' show GestureListener;
import '../../base_chart.dart' show BaseChart;
import '../chart_behavior.dart' show ChartBehavior;
import 'a11y_node.dart' show A11yNode;
/// The gesture to use for triggering explore mode.
enum ExploreModeTrigger {
pressHold,
tap,
}
/// Chart behavior for adding A11y information.
abstract class A11yExploreBehavior<D> implements ChartBehavior<D> {
/// The gesture that activates explore mode. Defaults to long press.
///
/// Turning on explore mode asks this [A11yExploreBehavior] to generate nodes within
/// this chart.
final ExploreModeTrigger exploreModeTrigger;
/// Minimum width of the bounding box for the a11y focus.
///
/// Must be 1 or higher because invisible semantic nodes should not be added.
final double minimumWidth;
/// Optionally notify the OS when explore mode is enabled.
final String exploreModeEnabledAnnouncement;
/// Optionally notify the OS when explore mode is disabled.
final String exploreModeDisabledAnnouncement;
BaseChart<D> _chart;
GestureListener _listener;
bool _exploreModeOn = false;
A11yExploreBehavior({
this.exploreModeTrigger = ExploreModeTrigger.pressHold,
double minimumWidth,
this.exploreModeEnabledAnnouncement,
this.exploreModeDisabledAnnouncement,
}) : minimumWidth = minimumWidth ?? 1.0 {
assert(this.minimumWidth >= 1.0);
switch (exploreModeTrigger) {
case ExploreModeTrigger.pressHold:
_listener = new GestureListener(onLongPress: _toggleExploreMode);
break;
case ExploreModeTrigger.tap:
_listener = new GestureListener(onTap: _toggleExploreMode);
break;
}
}
bool _toggleExploreMode(_) {
if (_exploreModeOn) {
_exploreModeOn = false;
// Ask native platform to turn off explore mode.
_chart.context.disableA11yExploreMode(
announcement: exploreModeDisabledAnnouncement);
} else {
_exploreModeOn = true;
// Ask native platform to turn on explore mode.
_chart.context.enableA11yExploreMode(createA11yNodes(),
announcement: exploreModeEnabledAnnouncement);
}
return true;
}
/// Returns a list of A11yNodes for this chart.
List<A11yNode> createA11yNodes();
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addGestureListener(_listener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeGestureListener(_listener);
}
}

View File

@@ -0,0 +1,32 @@
// 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;
typedef void OnFocus();
/// Container for accessibility data.
class A11yNode {
/// The bounding box for this node.
final Rectangle<int> boundingBox;
/// The textual description of this node.
final String label;
/// Callback when the A11yNode is focused by the native platform
OnFocus onFocus;
A11yNode(this.label, this.boundingBox, {this.onFocus});
}

View File

@@ -0,0 +1,195 @@
// 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:meta/meta.dart' show required;
import '../../../cartesian/axis/axis.dart' show ImmutableAxis, domainAxisKey;
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import '../../series_datum.dart' show SeriesDatum;
import 'a11y_explore_behavior.dart'
show A11yExploreBehavior, ExploreModeTrigger;
import 'a11y_node.dart' show A11yNode, OnFocus;
/// Returns a string for a11y vocalization from a list of series datum.
typedef String VocalizationCallback<D>(List<SeriesDatum<D>> seriesDatums);
/// A simple vocalization that returns the domain value to string.
String domainVocalization<D>(List<SeriesDatum<D>> seriesDatums) {
final datumIndex = seriesDatums.first.index;
final domainFn = seriesDatums.first.series.domainFn;
final domain = domainFn(datumIndex);
return domain.toString();
}
/// Behavior that generates semantic nodes for each domain.
class DomainA11yExploreBehavior<D> extends A11yExploreBehavior<D> {
final VocalizationCallback _vocalizationCallback;
LifecycleListener<D> _lifecycleListener;
CartesianChart<D> _chart;
List<MutableSeries<D>> _seriesList;
DomainA11yExploreBehavior(
{VocalizationCallback vocalizationCallback,
ExploreModeTrigger exploreModeTrigger,
double minimumWidth,
String exploreModeEnabledAnnouncement,
String exploreModeDisabledAnnouncement})
: _vocalizationCallback = vocalizationCallback ?? domainVocalization,
super(
exploreModeTrigger: exploreModeTrigger,
minimumWidth: minimumWidth,
exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement,
exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement) {
_lifecycleListener =
new LifecycleListener<D>(onPostprocess: _updateSeriesList);
}
@override
List<A11yNode> createA11yNodes() {
final nodes = <_DomainA11yNode>[];
// Update the selection model when the a11y node has focus.
final selectionModel = _chart.getSelectionModel(SelectionModelType.info);
final domainSeriesDatum = <D, List<SeriesDatum<D>>>{};
for (MutableSeries<D> series in _seriesList) {
for (var index = 0; index < series.data.length; index++) {
final datum = series.data[index];
D domain = series.domainFn(index);
domainSeriesDatum[domain] ??= <SeriesDatum<D>>[];
domainSeriesDatum[domain].add(new SeriesDatum<D>(series, datum));
}
}
domainSeriesDatum.forEach((D domain, List<SeriesDatum<D>> seriesDatums) {
final a11yDescription = _vocalizationCallback(seriesDatums);
final firstSeries = seriesDatums.first.series;
final domainAxis = firstSeries.getAttr(domainAxisKey) as ImmutableAxis<D>;
final location = domainAxis.getLocation(domain);
/// If the step size is smaller than the minimum width, use minimum.
final stepSize = (domainAxis.stepSize > minimumWidth)
? domainAxis.stepSize
: minimumWidth;
nodes.add(new _DomainA11yNode(a11yDescription,
location: location,
stepSize: stepSize,
chartDrawBounds: _chart.drawAreaBounds,
isRtl: _chart.context.isRtl,
renderVertically: _chart.vertical,
onFocus: () => selectionModel.updateSelection(seriesDatums, [])));
});
// The screen reader navigates the nodes based on the order it is returned.
// So if the chart is RTL, then the nodes should be ordered with the right
// most domain first.
//
// If the chart has multiple series and one series is missing the domain
// and it was added later, we still want the domains to be in order.
nodes.sort();
return nodes;
}
void _updateSeriesList(List<MutableSeries<D>> seriesList) {
_seriesList = seriesList;
}
@override
void attachTo(BaseChart<D> chart) {
// Domain selection behavior only works for cartesian charts.
assert(chart is CartesianChart);
_chart = chart as CartesianChart;
chart.addLifecycleListener(_lifecycleListener);
super.attachTo(chart);
}
@override
void removeFrom(BaseChart chart) {
chart.removeLifecycleListener(_lifecycleListener);
}
@override
String get role => 'DomainA11yExplore-${exploreModeTrigger}';
}
/// A11yNode with domain specific information.
class _DomainA11yNode extends A11yNode implements Comparable<_DomainA11yNode> {
// Save location, RTL, and is render vertically for sorting
final double location;
final bool isRtl;
final bool renderVertically;
factory _DomainA11yNode(String label,
{@required double location,
@required double stepSize,
@required Rectangle<int> chartDrawBounds,
@required bool isRtl,
@required bool renderVertically,
OnFocus onFocus}) {
Rectangle<int> boundingBox;
if (renderVertically) {
var left = (location - stepSize / 2).round();
var top = chartDrawBounds.top;
var width = stepSize.round();
var height = chartDrawBounds.height;
boundingBox = new Rectangle(left, top, width, height);
} else {
var left = chartDrawBounds.left;
var top = (location - stepSize / 2).round();
var width = chartDrawBounds.width;
var height = stepSize.round();
boundingBox = new Rectangle(left, top, width, height);
}
return new _DomainA11yNode._internal(label, boundingBox,
location: location,
isRtl: isRtl,
renderVertically: renderVertically,
onFocus: onFocus);
}
_DomainA11yNode._internal(String label, Rectangle<int> boundingBox,
{@required this.location,
@required this.isRtl,
@required this.renderVertically,
OnFocus onFocus})
: super(label, boundingBox, onFocus: onFocus);
@override
int compareTo(_DomainA11yNode other) {
// Ordered by smaller location first, unless rendering vertically and RTL,
// then flip to sort by larger location first.
int result = location.compareTo(other.location);
if (renderVertically && isRtl && result != 0) {
result = -result;
}
return result;
}
}

View File

@@ -0,0 +1,235 @@
// 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 '../../../../data/series.dart' show AttributeKey;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../behavior/chart_behavior.dart' show ChartBehavior;
import '../../processed_series.dart' show MutableSeries;
const percentInjectedKey =
const AttributeKey<bool>('PercentInjector.percentInjected');
/// Chart behavior that can inject series or domain percentages into each datum.
///
/// [totalType] configures the type of total to be calculated.
///
/// The measure values of each datum will be replaced by the percent of the
/// total measure value that each represents. The "raw" measure accessor
/// function on [MutableSeries] can still be used to get the original values.
///
/// Note that the results for measureLowerBound and measureUpperBound are not
/// currently well defined when converted into percentage values. This behavior
/// will replace them as percents to prevent bad axis results, but no effort is
/// made to bound them to within a "0 to 100%" data range.
///
/// Note that if the chart has a [Legend] that is capable of hiding series data,
/// then this behavior must be added after the [Legend] to ensure that it
/// calculates values after series have been potentially removed from the list.
class PercentInjector<D> implements ChartBehavior<D> {
LifecycleListener<D> _lifecycleListener;
/// The type of data total to be calculated.
final PercentInjectorTotalType totalType;
/// Constructs a [PercentInjector].
///
/// [totalType] configures the type of data total to be calculated.
PercentInjector({this.totalType = PercentInjectorTotalType.domain}) {
// Set up chart draw cycle listeners.
_lifecycleListener =
new LifecycleListener<D>(onPreprocess: _preProcess, onData: _onData);
}
@override
void attachTo(BaseChart<D> chart) {
chart.addLifecycleListener(_lifecycleListener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeLifecycleListener(_lifecycleListener);
}
/// Resets the state of the behavior when new data is drawn on the chart.
void _onData(List<MutableSeries<D>> seriesList) {
// Reset tracking of percentage injection for new data.
seriesList.forEach((MutableSeries series) {
series.setAttr(percentInjectedKey, false);
});
}
/// Injects percent of domain and/or series accessor functions into each
/// series.
///
/// These are injected in the preProcess phase in case other behaviors modify
/// the [seriesList] between chart redraws.
void _preProcess(List<MutableSeries<D>> seriesList) {
var percentInjected = true;
seriesList.forEach((MutableSeries series) {
percentInjected = percentInjected && series.getAttr(percentInjectedKey);
});
if (percentInjected) {
return;
}
switch (totalType) {
case PercentInjectorTotalType.domain:
case PercentInjectorTotalType.domainBySeriesCategory:
final totalsByDomain = <String, num>{};
final useSeriesCategory =
totalType == PercentInjectorTotalType.domainBySeriesCategory;
// Walk the series and compute the domain total. Series total is
// automatically computed by [MutableSeries].
seriesList.forEach((MutableSeries series) {
final seriesCategory = series.seriesCategory;
final rawMeasureFn = series.rawMeasureFn;
final domainFn = series.domainFn;
for (var index = 0; index < series.data.length; index++) {
final domain = domainFn(index);
var measure = rawMeasureFn(index);
measure ??= 0.0;
final key = useSeriesCategory
? '${seriesCategory}__${domain.toString()}'
: '${domain.toString()}';
if (totalsByDomain[key] != null) {
totalsByDomain[key] = totalsByDomain[key] + measure;
} else {
totalsByDomain[key] = measure;
}
}
});
// Add percent of domain and series accessor functions.
seriesList.forEach((MutableSeries series) {
// Replace the default measure accessor with one that computes the
// percentage.
series.measureFn = (int index) {
final measure = series.rawMeasureFn(index);
if (measure == null || measure == 0.0) {
return 0.0;
}
final domain = series.domainFn(index);
final key = useSeriesCategory
? '${series.seriesCategory}__${domain.toString()}'
: '${domain.toString()}';
return measure / totalsByDomain[key];
};
// Replace the default measure lower bound accessor with one that
// computes the percentage.
if (series.measureLowerBoundFn != null) {
series.measureLowerBoundFn = (int index) {
final measureLowerBound = series.rawMeasureLowerBoundFn(index);
if (measureLowerBound == null || measureLowerBound == 0.0) {
return 0.0;
}
final domain = series.domainFn(index);
final key = useSeriesCategory
? '${series.seriesCategory}__${domain.toString()}'
: '${domain.toString()}';
return measureLowerBound / totalsByDomain[key];
};
}
// Replace the default measure upper bound accessor with one that
// computes the percentage.
if (series.measureUpperBoundFn != null) {
series.measureUpperBoundFn = (int index) {
final measureUpperBound = series.rawMeasureUpperBoundFn(index);
if (measureUpperBound == null || measureUpperBound == 0.0) {
return 0.0;
}
final domain = series.domainFn(index);
final key = useSeriesCategory
? '${series.seriesCategory}__${domain.toString()}'
: '${domain.toString()}';
return measureUpperBound / totalsByDomain[key];
};
}
series.setAttr(percentInjectedKey, true);
});
break;
case PercentInjectorTotalType.series:
seriesList.forEach((MutableSeries series) {
// Replace the default measure accessor with one that computes the
// percentage.
series.measureFn = (int index) =>
series.rawMeasureFn(index) / series.seriesMeasureTotal;
// Replace the default measure lower bound accessor with one that
// computes the percentage.
if (series.measureLowerBoundFn != null) {
series.measureLowerBoundFn = (int index) =>
series.rawMeasureLowerBoundFn(index) /
series.seriesMeasureTotal;
}
// Replace the default measure upper bound accessor with one that
// computes the percentage.
if (series.measureUpperBoundFn != null) {
series.measureUpperBoundFn = (int index) =>
series.rawMeasureUpperBoundFn(index) /
series.seriesMeasureTotal;
}
series.setAttr(percentInjectedKey, true);
});
break;
default:
throw new ArgumentError('Unsupported totalType: ${totalType}');
}
}
@override
String get role => 'PercentInjector';
}
/// Describes the type of data total that will be calculated by PercentInjector.
///
/// [domain] calculates the percentage of each datum's measure value out of the
/// total measure values for all data that share the same domain value.
///
/// [domainBySeriesCategory] calculates the percentage of each datum's measure
/// value out of the total measure values for all data that share the same
/// domain value and seriesCategory value. This should be enabled if the data
/// will be rendered by a series renderer that groups data by both domain and
/// series category, such as the "grouped stacked" mode of [BarRenderer].
///
/// [series] calculates the percentage of each datum's measure value out of the
/// total measure values for all data in that datum's series.
enum PercentInjectorTotalType { domain, domainBySeriesCategory, series }

View File

@@ -0,0 +1,66 @@
// 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 '../base_chart.dart';
/// Interface for adding behavior to a chart.
///
/// For example pan and zoom are implemented via behavior strategies.
abstract class ChartBehavior<D> {
String get role;
/// Injects the behavior into a chart.
void attachTo(BaseChart<D> chart);
/// Removes the behavior from a chart.
void removeFrom(BaseChart<D> chart);
}
/// Position of a component within the chart layout.
///
/// Outside positions are [top], [bottom], [start], and [end].
///
/// [top] component positioned at the top, with the chart positioned below the
/// component and height reduced by the height of the component.
/// [bottom] component positioned below the chart, and the chart's height is
/// reduced by the height of the component.
/// [start] component is positioned at the left of the chart (or the right if
/// RTL), the chart's width is reduced by the width of the component.
/// [end] component is positioned at the right of the chart (or the left if
/// RTL), the chart's width is reduced by the width of the component.
/// [inside] component is layered on top of the chart.
enum BehaviorPosition {
top,
bottom,
start,
end,
inside,
}
/// Justification for components positioned outside [BehaviorPosition].
enum OutsideJustification {
startDrawArea,
start,
middleDrawArea,
middle,
endDrawArea,
end,
}
/// Justification for components positioned [BehaviorPosition.inside].
enum InsideJustification {
topStart,
topEnd,
}

View File

@@ -0,0 +1,836 @@
// 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:meta/meta.dart';
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../../common/text_element.dart'
show MaxWidthStrategy, TextDirection, TextElement;
import '../../../../common/text_style.dart' show TextStyle;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
ViewMeasuredSizes;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../behavior/chart_behavior.dart'
show BehaviorPosition, ChartBehavior, OutsideJustification;
import '../../chart_canvas.dart' show ChartCanvas;
/// Chart behavior that adds title text to a chart. An optional second line of
/// text may be rendered as a sub-title.
///
/// Titles will by default be rendered as the outermost component in the chart
/// margin.
class ChartTitle<D> implements ChartBehavior<D> {
static const _defaultBehaviorPosition = BehaviorPosition.top;
static const _defaultMaxWidthStrategy = MaxWidthStrategy.ellipsize;
static const _defaultTitleDirection = ChartTitleDirection.auto;
static const _defaultTitleOutsideJustification = OutsideJustification.middle;
static final _defaultTitleStyle =
new TextStyleSpec(fontSize: 18, color: StyleFactory.style.tickColor);
static final _defaultSubTitleStyle =
new TextStyleSpec(fontSize: 14, color: StyleFactory.style.tickColor);
static const _defaultInnerPadding = 10;
static const _defaultTitlePadding = 18;
static const _defaultOuterPadding = 10;
/// Stores all of the configured properties of the behavior.
_ChartTitleConfig _config;
BaseChart<D> _chart;
_ChartTitleLayoutView _view;
LifecycleListener<D> _lifecycleListener;
/// Constructs a [ChartTitle].
///
/// [title] contains the text for the chart title.
ChartTitle(String title,
{BehaviorPosition behaviorPosition,
int innerPadding,
int layoutMinSize,
int layoutPreferredSize,
int outerPadding,
MaxWidthStrategy maxWidthStrategy,
ChartTitleDirection titleDirection,
OutsideJustification titleOutsideJustification,
int titlePadding,
TextStyleSpec titleStyleSpec,
String subTitle,
TextStyleSpec subTitleStyleSpec}) {
_config = new _ChartTitleConfig()
..behaviorPosition = behaviorPosition ?? _defaultBehaviorPosition
..innerPadding = innerPadding ?? _defaultInnerPadding
..layoutMinSize = layoutMinSize
..layoutPreferredSize = layoutPreferredSize
..outerPadding = outerPadding ?? _defaultOuterPadding
..maxWidthStrategy = maxWidthStrategy ?? _defaultMaxWidthStrategy
..title = title
..titleDirection = titleDirection ?? _defaultTitleDirection
..titleOutsideJustification =
titleOutsideJustification ?? _defaultTitleOutsideJustification
..titlePadding = titlePadding ?? _defaultTitlePadding
..titleStyleSpec = titleStyleSpec ?? _defaultTitleStyle
..subTitle = subTitle
..subTitleStyleSpec = subTitleStyleSpec ?? _defaultSubTitleStyle;
_lifecycleListener =
new LifecycleListener<D>(onAxisConfigured: _updateViewData);
}
/// Layout position for the title.
BehaviorPosition get behaviorPosition => _config.behaviorPosition;
set behaviorPosition(BehaviorPosition behaviorPosition) {
_config.behaviorPosition = behaviorPosition;
}
/// Minimum size of the legend component. Optional.
///
/// If the legend is positioned in the top or bottom margin, then this
/// configures the legend's height. If positioned in the start or end
/// position, this configures the legend's width.
int get layoutMinSize => _config.layoutMinSize;
set layoutMinSize(int layoutMinSize) {
_config.layoutMinSize = layoutMinSize;
}
/// Preferred size of the legend component. Defaults to 0.
///
/// If the legend is positioned in the top or bottom margin, then this
/// configures the legend's height. If positioned in the start or end
/// position, this configures the legend's width.
int get layoutPreferredSize => _config.layoutPreferredSize;
set layoutPreferredSize(int layoutPreferredSize) {
_config.layoutPreferredSize = layoutPreferredSize;
}
/// Strategy for handling title text that is too large to fit. Defaults to
/// truncating the text with ellipses.
MaxWidthStrategy get maxWidthStrategy => _config.maxWidthStrategy;
set maxWidthStrategy(MaxWidthStrategy maxWidthStrategy) {
_config.maxWidthStrategy = maxWidthStrategy;
}
/// Primary text for the title.
String get title => _config.title;
set title(String title) {
_config.title = title;
}
/// Direction of the chart title text.
///
/// This defaults to horizontal for a title in the top or bottom
/// [behaviorPosition], or vertical for start or end [behaviorPosition].
ChartTitleDirection get titleDirection => _config.titleDirection;
set titleDirection(ChartTitleDirection titleDirection) {
_config.titleDirection = titleDirection;
}
/// Justification of the title text if it is positioned outside of the draw
/// area.
OutsideJustification get titleOutsideJustification =>
_config.titleOutsideJustification;
set titleOutsideJustification(
OutsideJustification titleOutsideJustification) {
_config.titleOutsideJustification = titleOutsideJustification;
}
/// Space between the title and sub-title text, if defined.
///
/// This padding is not used if no sub-title is provided.
int get titlePadding => _config.titlePadding;
set titlePadding(int titlePadding) {
_config.titlePadding = titlePadding;
}
/// Style of the [title] text.
TextStyleSpec get titleStyleSpec => _config.titleStyleSpec;
set titleStyleSpec(TextStyleSpec titleStyleSpec) {
_config.titleStyleSpec = titleStyleSpec;
}
/// Secondary text for the sub-title.
///
/// [subTitle] is rendered on a second line below the [title], and may be
/// styled differently.
String get subTitle => _config.subTitle;
set subTitle(String subTitle) {
_config.subTitle = subTitle;
}
/// Style of the [subTitle] text.
TextStyleSpec get subTitleStyleSpec => _config.subTitleStyleSpec;
set subTitleStyleSpec(TextStyleSpec subTitleStyleSpec) {
_config.subTitleStyleSpec = subTitleStyleSpec;
}
/// Space between the "inside" of the chart, and the title behavior itself.
///
/// This padding is applied to all the edge of the title that is in the
/// direction of the draw area. For a top positioned title, this is applied
/// to the bottom edge. [outerPadding] is applied to the top, left, and right
/// edges.
///
/// If a sub-title is defined, this is the space between the sub-title text
/// and the inside of the chart. Otherwise, it is the space between the title
/// text and the inside of chart.
int get innerPadding => _config.innerPadding;
set innerPadding(int innerPadding) {
_config.innerPadding = innerPadding;
}
/// Space between the "outside" of the chart, and the title behavior itself.
///
/// This padding is applied to all 3 edges of the title that are not in the
/// direction of the draw area. For a top positioned title, this is applied
/// to the top, left, and right edges. [innerPadding] is applied to the
/// bottom edge.
int get outerPadding => _config.outerPadding;
set outerPadding(int outerPadding) {
_config.outerPadding = outerPadding;
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
_view = new _ChartTitleLayoutView<D>(
layoutPaintOrder: LayoutViewPaintOrder.chartTitle,
config: _config,
chart: _chart);
chart.addView(_view);
chart.addLifecycleListener(_lifecycleListener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeView(_view);
chart.removeLifecycleListener(_lifecycleListener);
_chart = null;
}
void _updateViewData() {
_view.config = _config;
}
@override
String get role => 'ChartTitle-${_config?.behaviorPosition}';
bool get isRtl => _chart.context.isRtl;
}
/// Layout view component for [ChartTitle].
class _ChartTitleLayoutView<D> extends LayoutView {
LayoutViewConfig _layoutConfig;
LayoutViewConfig get layoutConfig => _layoutConfig;
/// Stores all of the configured properties of the behavior.
_ChartTitleConfig _config;
BaseChart<D> chart;
bool get isRtl => chart?.context?.isRtl ?? false;
Rectangle<int> _componentBounds;
Rectangle<int> _drawAreaBounds;
GraphicsFactory _graphicsFactory;
/// Cached layout element for the title text.
///
/// This is used to prevent expensive Flutter painter layout calls on every
/// animation frame during the paint cycle. It should never be cached during
/// layout measurement.
TextElement _titleTextElement;
/// Cached layout element for the sub-title text.
///
/// This is used to prevent expensive Flutter painter layout calls on every
/// animation frame during the paint cycle. It should never be cached during
/// layout measurement.
TextElement _subTitleTextElement;
_ChartTitleLayoutView(
{@required int layoutPaintOrder,
@required _ChartTitleConfig config,
@required this.chart})
: this._config = config {
// Set inside body to resolve [_layoutPosition].
_layoutConfig = new LayoutViewConfig(
paintOrder: layoutPaintOrder,
position: _layoutPosition,
positionOrder: LayoutViewPositionOrder.chartTitle);
}
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
/// Sets the configuration for the title behavior.
set config(_ChartTitleConfig config) {
_config = config;
layoutConfig.position = _layoutPosition;
}
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
int minWidth;
int minHeight;
int preferredWidth = 0;
int preferredHeight = 0;
// Always assume that we need outer padding and title padding, but only add
// in the sub-title padding if we have one. Title is required, but sub-title
// is optional.
final totalPadding = _config.outerPadding +
_config.innerPadding +
(_config.subTitle != null ? _config.titlePadding : 0.0);
// Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
// The [GraphicsFactory] is needed so it can't be created earlier.
final textStyle = _getTextStyle(graphicsFactory, _config.titleStyleSpec);
final textElement = graphicsFactory.createTextElement(_config.title)
..maxWidthStrategy = _config.maxWidthStrategy
..textStyle = textStyle;
final subTitleTextStyle =
_getTextStyle(graphicsFactory, _config.subTitleStyleSpec);
final subTitleTextElement =
graphicsFactory.createTextElement(_config.subTitle)
..maxWidthStrategy = _config.maxWidthStrategy
..textStyle = subTitleTextStyle;
final resolvedTitleDirection = _resolvedTitleDirection;
switch (_config.behaviorPosition) {
case BehaviorPosition.bottom:
case BehaviorPosition.top:
final textHeight =
(resolvedTitleDirection == ChartTitleDirection.vertical
? textElement.measurement.horizontalSliceWidth
: textElement.measurement.verticalSliceWidth)
.round();
final subTitleTextHeight = _config.subTitle != null
? (resolvedTitleDirection == ChartTitleDirection.vertical
? subTitleTextElement.measurement.horizontalSliceWidth
: subTitleTextElement.measurement.verticalSliceWidth)
.round()
: 0;
final measuredHeight =
(textHeight + subTitleTextHeight + totalPadding).round();
minHeight = _config.layoutMinSize != null
? min(_config.layoutMinSize, measuredHeight)
: measuredHeight;
preferredWidth = maxWidth;
preferredHeight = _config.layoutPreferredSize != null
? min(_config.layoutPreferredSize, maxHeight)
: measuredHeight;
break;
case BehaviorPosition.end:
case BehaviorPosition.start:
final textWidth =
(resolvedTitleDirection == ChartTitleDirection.vertical
? textElement.measurement.verticalSliceWidth
: textElement.measurement.horizontalSliceWidth)
.round();
final subTitleTextWidth = _config.subTitle != null
? (resolvedTitleDirection == ChartTitleDirection.vertical
? subTitleTextElement.measurement.verticalSliceWidth
: subTitleTextElement.measurement.horizontalSliceWidth)
.round()
: 0;
final measuredWidth =
(textWidth + subTitleTextWidth + totalPadding).round();
minWidth = _config.layoutMinSize != null
? min(_config.layoutMinSize, measuredWidth)
: measuredWidth;
preferredWidth = _config.layoutPreferredSize != null
? min(_config.layoutPreferredSize, maxWidth)
: measuredWidth;
preferredHeight = maxHeight;
break;
case BehaviorPosition.inside:
preferredWidth = _drawAreaBounds != null
? min(_drawAreaBounds.width, maxWidth)
: maxWidth;
preferredHeight = _drawAreaBounds != null
? min(_drawAreaBounds.height, maxHeight)
: maxHeight;
break;
}
// Reset the cached text elements used during the paint step.
_resetTextElementCache();
return new ViewMeasuredSizes(
minWidth: minWidth,
minHeight: minHeight,
preferredWidth: preferredWidth,
preferredHeight: preferredHeight);
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
this._componentBounds = componentBounds;
this._drawAreaBounds = drawAreaBounds;
// Reset the cached text elements used during the paint step.
_resetTextElementCache();
}
@override
void paint(ChartCanvas canvas, double animationPercent) {
final resolvedTitleDirection = _resolvedTitleDirection;
var titleHeight = 0.0;
var subTitleHeight = 0.0;
// First, measure the height of the title and sub-title.
if (_config.title != null) {
// Chart titles do not animate. As an optimization for Flutter, cache the
// [TextElement] to avoid an expensive painter layout operation on
// subsequent animation frames.
if (_titleTextElement == null) {
// Create [TextStyle] from [TextStyleSpec] to be used by all the
// elements. The [GraphicsFactory] is needed so it can't be created
// earlier.
final textStyle =
_getTextStyle(graphicsFactory, _config.titleStyleSpec);
_titleTextElement = graphicsFactory.createTextElement(_config.title)
..maxWidthStrategy = _config.maxWidthStrategy
..textStyle = textStyle;
_titleTextElement.maxWidth =
resolvedTitleDirection == ChartTitleDirection.horizontal
? _componentBounds.width
: _componentBounds.height;
}
// Get the height of the title so that we can off-set both text elements.
titleHeight = _titleTextElement.measurement.verticalSliceWidth;
}
if (_config.subTitle != null) {
// Chart titles do not animate. As an optimization for Flutter, cache the
// [TextElement] to avoid an expensive painter layout operation on
// subsequent animation frames.
if (_subTitleTextElement == null) {
// Create [TextStyle] from [TextStyleSpec] to be used by all the
// elements. The [GraphicsFactory] is needed so it can't be created
// earlier.
final textStyle =
_getTextStyle(graphicsFactory, _config.subTitleStyleSpec);
_subTitleTextElement =
graphicsFactory.createTextElement(_config.subTitle)
..maxWidthStrategy = _config.maxWidthStrategy
..textStyle = textStyle;
_subTitleTextElement.maxWidth =
resolvedTitleDirection == ChartTitleDirection.horizontal
? _componentBounds.width
: _componentBounds.height;
}
// Get the height of the sub-title so that we can off-set both text
// elements.
subTitleHeight = _subTitleTextElement.measurement.verticalSliceWidth;
}
// Draw a title if the text is not empty.
if (_config.title != null) {
final labelPoint = _getLabelPosition(
true,
_componentBounds,
resolvedTitleDirection,
_titleTextElement,
titleHeight,
subTitleHeight);
if (labelPoint != null) {
final rotation = resolvedTitleDirection == ChartTitleDirection.vertical
? -pi / 2
: 0.0;
canvas.drawText(_titleTextElement, labelPoint.x, labelPoint.y,
rotation: rotation);
}
}
// Draw a sub-title if the text is not empty.
if (_config.subTitle != null) {
final labelPoint = _getLabelPosition(
false,
_componentBounds,
resolvedTitleDirection,
_subTitleTextElement,
titleHeight,
subTitleHeight);
if (labelPoint != null) {
final rotation = resolvedTitleDirection == ChartTitleDirection.vertical
? -pi / 2
: 0.0;
canvas.drawText(_subTitleTextElement, labelPoint.x, labelPoint.y,
rotation: rotation);
}
}
}
/// Resets the cached text elements used during the paint step.
void _resetTextElementCache() {
_titleTextElement = null;
_subTitleTextElement = null;
}
/// Get the direction of the title, resolving "auto" position into the
/// appropriate direction for the position of the behavior.
ChartTitleDirection get _resolvedTitleDirection {
var resolvedTitleDirection = _config.titleDirection;
if (resolvedTitleDirection == ChartTitleDirection.auto) {
switch (_config.behaviorPosition) {
case BehaviorPosition.bottom:
case BehaviorPosition.inside:
case BehaviorPosition.top:
resolvedTitleDirection = ChartTitleDirection.horizontal;
break;
case BehaviorPosition.end:
case BehaviorPosition.start:
resolvedTitleDirection = ChartTitleDirection.vertical;
break;
}
}
return resolvedTitleDirection;
}
/// Get layout position from chart title position.
LayoutPosition get _layoutPosition {
LayoutPosition position;
switch (_config.behaviorPosition) {
case BehaviorPosition.bottom:
position = LayoutPosition.Bottom;
break;
case BehaviorPosition.end:
position = isRtl ? LayoutPosition.Left : LayoutPosition.Right;
break;
case BehaviorPosition.inside:
position = LayoutPosition.DrawArea;
break;
case BehaviorPosition.start:
position = isRtl ? LayoutPosition.Right : LayoutPosition.Left;
break;
case BehaviorPosition.top:
position = LayoutPosition.Top;
break;
}
// If we have a "full" [OutsideJustification], convert the layout position
// to the "full" form.
if (_config.titleOutsideJustification == OutsideJustification.start ||
_config.titleOutsideJustification == OutsideJustification.middle ||
_config.titleOutsideJustification == OutsideJustification.end) {
switch (position) {
case LayoutPosition.Bottom:
position = LayoutPosition.FullBottom;
break;
case LayoutPosition.Left:
position = LayoutPosition.FullLeft;
break;
case LayoutPosition.Top:
position = LayoutPosition.FullTop;
break;
case LayoutPosition.Right:
position = LayoutPosition.FullRight;
break;
// Ignore other positions, like DrawArea.
default:
break;
}
}
return position;
}
/// Gets the resolved location for a label element.
Point<int> _getLabelPosition(
bool isPrimaryTitle,
Rectangle<num> bounds,
ChartTitleDirection titleDirection,
TextElement textElement,
double titleHeight,
double subTitleHeight) {
switch (_config.behaviorPosition) {
case BehaviorPosition.bottom:
case BehaviorPosition.top:
return _getHorizontalLabelPosition(isPrimaryTitle, bounds,
titleDirection, textElement, titleHeight, subTitleHeight);
break;
case BehaviorPosition.start:
case BehaviorPosition.end:
return _getVerticalLabelPosition(isPrimaryTitle, bounds, titleDirection,
textElement, titleHeight, subTitleHeight);
break;
case BehaviorPosition.inside:
break;
}
return null;
}
/// Gets the resolved location for a title in the top or bottom margin.
Point<int> _getHorizontalLabelPosition(
bool isPrimaryTitle,
Rectangle<num> bounds,
ChartTitleDirection titleDirection,
TextElement textElement,
double titleHeight,
double subTitleHeight) {
int labelX = 0;
int labelY = 0;
switch (_config.titleOutsideJustification) {
case OutsideJustification.middle:
case OutsideJustification.middleDrawArea:
final textWidth =
(isRtl ? 1 : -1) * textElement.measurement.horizontalSliceWidth / 2;
labelX = (bounds.left + bounds.width / 2 + textWidth).round();
textElement.textDirection =
isRtl ? TextDirection.rtl : TextDirection.ltr;
break;
case OutsideJustification.end:
case OutsideJustification.endDrawArea:
case OutsideJustification.start:
case OutsideJustification.startDrawArea:
final alignLeft = isRtl
? (_config.titleOutsideJustification == OutsideJustification.end ||
_config.titleOutsideJustification ==
OutsideJustification.endDrawArea)
: (_config.titleOutsideJustification ==
OutsideJustification.start ||
_config.titleOutsideJustification ==
OutsideJustification.startDrawArea);
// Don't apply outer padding if we are aligned to the draw area.
final padding = (_config.titleOutsideJustification ==
OutsideJustification.endDrawArea ||
_config.titleOutsideJustification ==
OutsideJustification.startDrawArea)
? 0.0
: _config.outerPadding;
if (alignLeft) {
labelX = (bounds.left + padding).round();
textElement.textDirection = TextDirection.ltr;
} else {
labelX = (bounds.right - padding).round();
textElement.textDirection = TextDirection.rtl;
}
break;
}
// labelY is always relative to the component bounds.
if (_config.behaviorPosition == BehaviorPosition.bottom) {
final padding = _config.innerPadding +
(isPrimaryTitle ? 0 : _config.titlePadding + titleHeight);
labelY = (bounds.top + padding).round();
} else {
var padding = 0.0 + _config.innerPadding;
if (isPrimaryTitle) {
padding +=
((subTitleHeight > 0 ? _config.titlePadding + subTitleHeight : 0) +
titleHeight);
} else {
padding += subTitleHeight;
}
labelY = (bounds.bottom - padding).round();
}
return new Point<int>(labelX, labelY);
}
/// Gets the resolved location for a title in the left or right margin.
Point<int> _getVerticalLabelPosition(
bool isPrimaryTitle,
Rectangle<num> bounds,
ChartTitleDirection titleDirection,
TextElement textElement,
double titleHeight,
double subTitleHeight) {
int labelX = 0;
int labelY = 0;
switch (_config.titleOutsideJustification) {
case OutsideJustification.middle:
case OutsideJustification.middleDrawArea:
final textWidth =
(isRtl ? -1 : 1) * textElement.measurement.horizontalSliceWidth / 2;
labelY = (bounds.top + bounds.height / 2 + textWidth).round();
textElement.textDirection =
isRtl ? TextDirection.rtl : TextDirection.ltr;
break;
case OutsideJustification.end:
case OutsideJustification.endDrawArea:
case OutsideJustification.start:
case OutsideJustification.startDrawArea:
final alignLeft = isRtl
? (_config.titleOutsideJustification == OutsideJustification.end ||
_config.titleOutsideJustification ==
OutsideJustification.endDrawArea)
: (_config.titleOutsideJustification ==
OutsideJustification.start ||
_config.titleOutsideJustification ==
OutsideJustification.startDrawArea);
// Don't apply outer padding if we are aligned to the draw area.
final padding = (_config.titleOutsideJustification ==
OutsideJustification.endDrawArea ||
_config.titleOutsideJustification ==
OutsideJustification.startDrawArea)
? 0.0
: _config.outerPadding;
if (alignLeft) {
labelY = (bounds.bottom - padding).round();
textElement.textDirection = TextDirection.ltr;
} else {
labelY = (bounds.top + padding).round();
textElement.textDirection = TextDirection.rtl;
}
break;
}
// labelX is always relative to the component bounds.
if (_layoutPosition == LayoutPosition.Right ||
_layoutPosition == LayoutPosition.FullRight) {
final padding = _config.outerPadding +
(isPrimaryTitle ? 0 : _config.titlePadding + titleHeight);
labelX = (bounds.left + padding).round();
} else {
final padding = _config.outerPadding +
titleHeight +
(isPrimaryTitle
? (subTitleHeight > 0 ? _config.titlePadding + subTitleHeight : 0)
: 0.0);
labelX = (bounds.right - padding).round();
}
return new Point<int>(labelX, labelY);
}
// Helper function that converts [TextStyleSpec] to [TextStyle].
TextStyle _getTextStyle(
GraphicsFactory graphicsFactory, TextStyleSpec labelSpec) {
return graphicsFactory.createTextPaint()
..color = labelSpec?.color ?? StyleFactory.style.tickColor
..fontFamily = labelSpec?.fontFamily
..fontSize = labelSpec?.fontSize ?? 18;
}
@override
Rectangle<int> get componentBounds => this._drawAreaBounds;
@override
bool get isSeriesRenderer => false;
}
/// Configuration object for [ChartTitle].
class _ChartTitleConfig {
BehaviorPosition behaviorPosition;
int layoutMinSize;
int layoutPreferredSize;
MaxWidthStrategy maxWidthStrategy;
String title;
ChartTitleDirection titleDirection;
OutsideJustification titleOutsideJustification;
TextStyleSpec titleStyleSpec;
String subTitle;
TextStyleSpec subTitleStyleSpec;
int innerPadding;
int titlePadding;
int outerPadding;
}
/// Direction of the title text on the chart.
enum ChartTitleDirection {
/// Automatically assign a direction based on the [RangeAnnotationAxisType].
///
/// [horizontal] for measure axes, or [vertical] for domain axes.
auto,
/// Text flows parallel to the x axis.
horizontal,
/// Text flows parallel to the y axis.
vertical,
}

View File

@@ -0,0 +1,83 @@
// 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 '../base_chart.dart' show BaseChart, LifecycleListener;
import '../processed_series.dart' show MutableSeries;
import '../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import 'chart_behavior.dart' show ChartBehavior;
/// Chart behavior that monitors the specified [SelectionModel] and darkens the
/// color for selected data.
///
/// This is typically used for bars and pies to highlight segments.
///
/// It is used in combination with SelectNearest to update the selection model
/// and expand selection out to the domain value.
class DomainHighlighter<D> implements ChartBehavior<D> {
final SelectionModelType selectionModelType;
BaseChart<D> _chart;
LifecycleListener<D> _lifecycleListener;
DomainHighlighter([this.selectionModelType = SelectionModelType.info]) {
_lifecycleListener =
new LifecycleListener<D>(onPostprocess: _updateColorFunctions);
}
void _selectionChanged(SelectionModel selectionModel) {
_chart.redraw(skipLayout: true, skipAnimation: true);
}
void _updateColorFunctions(List<MutableSeries<D>> seriesList) {
SelectionModel selectionModel =
_chart.getSelectionModel(selectionModelType);
seriesList.forEach((MutableSeries<D> series) {
final origColorFn = series.colorFn;
if (origColorFn != null) {
series.colorFn = (int index) {
final origColor = origColorFn(index);
if (selectionModel.isDatumSelected(series, index)) {
return origColor.darker;
} else {
return origColor;
}
};
}
});
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addLifecycleListener(_lifecycleListener);
chart
.getSelectionModel(selectionModelType)
.addSelectionChangedListener(_selectionChanged);
}
@override
void removeFrom(BaseChart chart) {
chart
.getSelectionModel(selectionModelType)
.removeSelectionChangedListener(_selectionChanged);
chart.removeLifecycleListener(_lifecycleListener);
}
@override
String get role => 'domainHighlight-${selectionModelType.toString()}';
}

View File

@@ -0,0 +1,74 @@
// 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 '../base_chart.dart' show BaseChart, LifecycleListener;
import '../processed_series.dart' show MutableSeries;
import '../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import '../series_datum.dart' show SeriesDatumConfig;
import 'chart_behavior.dart' show ChartBehavior;
/// Behavior that sets initial selection.
class InitialSelection<D> implements ChartBehavior<D> {
final SelectionModelType selectionModelType;
/// List of series id of initially selected series.
final List<String> selectedSeriesConfig;
/// List of [SeriesDatumConfig] that represents the initially selected datums.
final List<SeriesDatumConfig> selectedDataConfig;
BaseChart<D> _chart;
LifecycleListener<D> _lifecycleListener;
bool _firstDraw = true;
// TODO : When the series changes, if the user does not also
// change the index the wrong item could be highlighted.
InitialSelection(
{this.selectionModelType = SelectionModelType.info,
this.selectedDataConfig,
this.selectedSeriesConfig}) {
_lifecycleListener = new LifecycleListener<D>(onData: _setInitialSelection);
}
void _setInitialSelection(List<MutableSeries<D>> seriesList) {
if (!_firstDraw) {
return;
}
_firstDraw = false;
final immutableModel = new SelectionModel<D>.fromConfig(
selectedDataConfig, selectedSeriesConfig, seriesList);
_chart.getSelectionModel(selectionModelType).updateSelection(
immutableModel.selectedDatum, immutableModel.selectedSeries,
notifyListeners: false);
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addLifecycleListener(_lifecycleListener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeLifecycleListener(_lifecycleListener);
_chart = null;
}
@override
String get role => 'InitialSelection-${selectionModelType.toString()}}';
}

View File

@@ -0,0 +1,104 @@
// 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 '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import 'legend.dart';
import 'legend_entry_generator.dart';
import 'per_datum_legend_entry_generator.dart';
/// Datum legend behavior for charts.
///
/// By default this behavior creates one legend entry per datum in the first
/// series rendered on the chart.
///
/// TODO: Allows for hovering over a datum in legend to highlight
/// corresponding datum in draw area.
///
/// TODO: Implement tap to hide individual data in the series.
class DatumLegend<D> extends Legend<D> {
/// Whether or not the series legend should show measures on datum selection.
bool _showMeasures;
DatumLegend({
SelectionModelType selectionModelType,
LegendEntryGenerator<D> legendEntryGenerator,
MeasureFormatter measureFormatter,
MeasureFormatter secondaryMeasureFormatter,
bool showMeasures,
LegendDefaultMeasure legendDefaultMeasure,
TextStyleSpec entryTextStyle,
}) : super(
selectionModelType: selectionModelType ?? SelectionModelType.info,
legendEntryGenerator:
legendEntryGenerator ?? new PerDatumLegendEntryGenerator(),
entryTextStyle: entryTextStyle) {
// Call the setters that include the setting for default.
this.showMeasures = showMeasures;
this.legendDefaultMeasure = legendDefaultMeasure;
this.measureFormatter = measureFormatter;
this.secondaryMeasureFormatter = secondaryMeasureFormatter;
}
/// Whether or not the legend should show measures.
///
/// By default this is false, measures are not shown. When set to true, the
/// default behavior is to show measure only if there is selected data.
/// Please set [legendDefaultMeasure] to something other than none to enable
/// showing measures when there is no selection.
///
/// If [showMeasure] is set to null, it is changed to the default of false.
bool get showMeasures => _showMeasures;
set showMeasures(bool showMeasures) {
_showMeasures = showMeasures ?? false;
}
/// Option to show measures when selection is null.
///
/// By default this is set to none, so no measures are shown when there is
/// no selection.
///
/// If [legendDefaultMeasure] is set to null, it is changed to the default of
/// none.
LegendDefaultMeasure get legendDefaultMeasure =>
legendEntryGenerator.legendDefaultMeasure;
set legendDefaultMeasure(LegendDefaultMeasure legendDefaultMeasure) {
legendEntryGenerator.legendDefaultMeasure =
legendDefaultMeasure ?? LegendDefaultMeasure.none;
}
/// Formatter for measure values.
///
/// This is optional. The default formatter formats measure values with
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
/// returned.
set measureFormatter(MeasureFormatter formatter) {
legendEntryGenerator.measureFormatter =
formatter ?? defaultLegendMeasureFormatter;
}
/// Formatter for measure values of series that uses the secondary axis.
///
/// This is optional. The default formatter formats measure values with
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
/// returned.
set secondaryMeasureFormatter(MeasureFormatter formatter) {
legendEntryGenerator.secondaryMeasureFormatter =
formatter ?? defaultLegendMeasureFormatter;
}
}

View File

@@ -0,0 +1,433 @@
// 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:meta/meta.dart' show protected;
import 'package:intl/intl.dart';
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPositionOrder,
LayoutViewPaintOrder,
ViewMeasuredSizes;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../chart_canvas.dart' show ChartCanvas;
import '../../chart_context.dart' show ChartContext;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import '../chart_behavior.dart'
show
BehaviorPosition,
ChartBehavior,
InsideJustification,
OutsideJustification;
import 'legend_entry.dart';
import 'legend_entry_generator.dart';
/// Legend behavior for charts.
///
/// Since legends are desired to be customizable, building and displaying the
/// visual content of legends is done on the native platforms. This allows users
/// to specify customized content for legends using the native platform (ex. for
/// Flutter, using widgets).
abstract class Legend<D> implements ChartBehavior<D>, LayoutView {
final SelectionModelType selectionModelType;
final legendState = new LegendState<D>();
final LegendEntryGenerator<D> legendEntryGenerator;
String _title;
BaseChart _chart;
LifecycleListener<D> _lifecycleListener;
Rectangle<int> _componentBounds;
Rectangle<int> _drawAreaBounds;
GraphicsFactory _graphicsFactory;
BehaviorPosition _behaviorPosition = BehaviorPosition.end;
OutsideJustification _outsideJustification =
OutsideJustification.startDrawArea;
InsideJustification _insideJustification = InsideJustification.topStart;
LegendCellPadding _cellPadding;
LegendCellPadding _legendPadding;
TextStyleSpec _titleTextStyle;
LegendTapHandling _legendTapHandling = LegendTapHandling.hide;
List<MutableSeries<D>> _currentSeriesList;
/// Save this in order to check if series list have changed and regenerate
/// the legend entries.
List<MutableSeries<D>> _postProcessSeriesList;
static final _decimalPattern = new NumberFormat.decimalPattern();
/// Default measure formatter for legends.
@protected
String defaultLegendMeasureFormatter(num value) {
return (value == null) ? '' : _decimalPattern.format(value);
}
Legend({this.selectionModelType, this.legendEntryGenerator, entryTextStyle}) {
_lifecycleListener = new LifecycleListener(
onPostprocess: _postProcess, onPreprocess: _preProcess, onData: onData);
legendEntryGenerator.entryTextStyle = entryTextStyle;
}
String get title => _title;
/// Sets title text to display before legend entries.
set title(String title) {
_title = title;
}
BehaviorPosition get behaviorPosition => _behaviorPosition;
set behaviorPosition(BehaviorPosition behaviorPosition) {
_behaviorPosition = behaviorPosition;
}
OutsideJustification get outsideJustification => _outsideJustification;
set outsideJustification(OutsideJustification outsideJustification) {
_outsideJustification = outsideJustification;
}
InsideJustification get insideJustification => _insideJustification;
set insideJustification(InsideJustification insideJustification) {
_insideJustification = insideJustification;
}
LegendCellPadding get cellPadding => _cellPadding;
set cellPadding(LegendCellPadding cellPadding) {
_cellPadding = cellPadding;
}
LegendCellPadding get legendPadding => _legendPadding;
set legendPadding(LegendCellPadding legendPadding) {
_legendPadding = legendPadding;
}
LegendTapHandling get legendTapHandling => _legendTapHandling;
/// Text style of the legend entry text.
TextStyleSpec get entryTextStyle => legendEntryGenerator.entryTextStyle;
set entryTextStyle(TextStyleSpec entryTextStyle) {
legendEntryGenerator.entryTextStyle = entryTextStyle;
}
/// Text style of the legend title text.
TextStyleSpec get titleTextStyle => _titleTextStyle;
set titleTextStyle(TextStyleSpec titleTextStyle) {
_titleTextStyle = titleTextStyle;
}
/// Configures the behavior of the legend when the user taps/clicks on an
/// entry. Defaults to no behavior.
///
/// Tapping on a legend entry will update the data visible on the chart. For
/// example, when [LegendTapHandling.hide] is configured, the series or datum
/// associated with that entry will be removed from the chart. Tapping on that
/// entry a second time will make the data visible again.
set legendTapHandling(LegendTapHandling legendTapHandling) {
_legendTapHandling = legendTapHandling;
}
/// Resets any hidden series data when new data is drawn on the chart.
@protected
void onData(List<MutableSeries<D>> seriesList) {}
/// Store off a copy of the series list for use when we render the legend.
void _preProcess(List<MutableSeries<D>> seriesList) {
_currentSeriesList = new List.from(seriesList);
preProcessSeriesList(seriesList);
}
/// Overridable method that may be used by concrete [Legend] instances to
/// manipulate the series list.
@protected
void preProcessSeriesList(List<MutableSeries<D>> seriesList) {}
/// Build LegendEntries from list of series.
void _postProcess(List<MutableSeries<D>> seriesList) {
// Get the selection model directly from chart on post process.
//
// This is because if initial selection is set as a behavior, it will be
// handled during onData. onData is prior to this behavior's postProcess
// call, so the selection will have changed prior to the entries being
// generated.
final selectionModel = chart.getSelectionModel(selectionModelType);
// Update entries if the selection model is different because post
// process is called on each draw cycle, so this is called on each animation
// frame and we don't want to update and request the native platform to
// rebuild if nothing has changed.
//
// Also update legend entries if the series list has changed.
if (legendState._selectionModel != selectionModel ||
_postProcessSeriesList != seriesList) {
legendState._legendEntries =
legendEntryGenerator.getLegendEntries(_currentSeriesList);
legendState._selectionModel = selectionModel;
_postProcessSeriesList = seriesList;
_updateLegendEntries();
}
}
// need to handle when series data changes, selection should be reset
/// Update the legend state with [selectionModel] and request legend update.
void _selectionChanged(SelectionModel selectionModel) {
legendState._selectionModel = selectionModel;
_updateLegendEntries();
}
ChartContext get chartContext => _chart.context;
/// Internally update legend entries, before calling [updateLegend] that
/// notifies the native platform.
void _updateLegendEntries() {
legendEntryGenerator.updateLegendEntries(legendState._legendEntries,
legendState._selectionModel, chart.currentSeriesList);
updateLegend();
}
/// Requires override to show in native platform
void updateLegend() {}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addLifecycleListener(_lifecycleListener);
chart
.getSelectionModel(selectionModelType)
.addSelectionChangedListener(_selectionChanged);
chart.addView(this);
}
@override
void removeFrom(BaseChart chart) {
chart
.getSelectionModel(selectionModelType)
.removeSelectionChangedListener(_selectionChanged);
chart.removeLifecycleListener(_lifecycleListener);
chart.removeView(this);
}
@protected
BaseChart get chart => _chart;
@override
String get role => 'legend-${selectionModelType.toString()}';
bool get isRtl => _chart.context.isRtl;
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
@override
LayoutViewConfig get layoutConfig {
return new LayoutViewConfig(
position: _layoutPosition,
positionOrder: LayoutViewPositionOrder.legend,
paintOrder: LayoutViewPaintOrder.legend);
}
/// Get layout position from legend position.
LayoutPosition get _layoutPosition {
LayoutPosition position;
switch (_behaviorPosition) {
case BehaviorPosition.bottom:
position = LayoutPosition.Bottom;
break;
case BehaviorPosition.end:
position = isRtl ? LayoutPosition.Left : LayoutPosition.Right;
break;
case BehaviorPosition.inside:
position = LayoutPosition.DrawArea;
break;
case BehaviorPosition.start:
position = isRtl ? LayoutPosition.Right : LayoutPosition.Left;
position = isRtl ? LayoutPosition.Right : LayoutPosition.Left;
break;
case BehaviorPosition.top:
position = LayoutPosition.Top;
break;
}
return position;
}
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
// Native child classes should override this method to return real
// measurements.
return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
_componentBounds = componentBounds;
_drawAreaBounds = drawAreaBounds;
updateLegend();
}
@override
void paint(ChartCanvas canvas, double animationPercent) {}
@override
Rectangle<int> get componentBounds => _componentBounds;
@override
bool get isSeriesRenderer => false;
// Gets the draw area bounds for native legend content to position itself
// accordingly.
Rectangle<int> get drawAreaBounds => _drawAreaBounds;
}
/// Stores legend data used by native legend content builder.
class LegendState<D> {
List<LegendEntry<D>> _legendEntries;
SelectionModel _selectionModel;
List<LegendEntry<D>> get legendEntries => _legendEntries;
SelectionModel get selectionModel => _selectionModel;
}
/// Stores legend cell padding, in percents or pixels.
///
/// If a percent is specified, it takes precedence over a flat pixel value.
class LegendCellPadding {
final double bottomPct;
final double bottomPx;
final double leftPct;
final double leftPx;
final double rightPct;
final double rightPx;
final double topPct;
final double topPx;
/// Creates padding in percents from the left, top, right, and bottom.
const LegendCellPadding.fromLTRBPct(
this.leftPct, this.topPct, this.rightPct, this.bottomPct)
: leftPx = null,
topPx = null,
rightPx = null,
bottomPx = null;
/// Creates padding in pixels from the left, top, right, and bottom.
const LegendCellPadding.fromLTRBPx(
this.leftPx, this.topPx, this.rightPx, this.bottomPx)
: leftPct = null,
topPct = null,
rightPct = null,
bottomPct = null;
/// Creates padding in percents from the top, right, bottom, and left.
const LegendCellPadding.fromTRBLPct(
this.topPct, this.rightPct, this.bottomPct, this.leftPct)
: topPx = null,
rightPx = null,
bottomPx = null,
leftPx = null;
/// Creates padding in pixels from the top, right, bottom, and left.
const LegendCellPadding.fromTRBLPx(
this.topPx, this.rightPx, this.bottomPx, this.leftPx)
: topPct = null,
rightPct = null,
bottomPct = null,
leftPct = null;
/// Creates cell padding where all the offsets are `value` in percent.
///
/// ## Sample code
///
/// Typical eight percent margin on all sides:
///
/// ```dart
/// const LegendCellPadding.allPct(8.0)
/// ```
const LegendCellPadding.allPct(double value)
: leftPct = value,
topPct = value,
rightPct = value,
bottomPct = value,
leftPx = null,
topPx = null,
rightPx = null,
bottomPx = null;
/// Creates cell padding where all the offsets are `value` in pixels.
///
/// ## Sample code
///
/// Typical eight-pixel margin on all sides:
///
/// ```dart
/// const LegendCellPadding.allPx(8.0)
/// ```
const LegendCellPadding.allPx(double value)
: leftPx = value,
topPx = value,
rightPx = value,
bottomPx = value,
leftPct = null,
topPct = null,
rightPct = null,
bottomPct = null;
double bottom(num height) =>
bottomPct != null ? bottomPct * height : bottomPx;
double left(num width) => leftPct != null ? leftPct * width : leftPx;
double right(num width) => rightPct != null ? rightPct * width : rightPx;
double top(num height) => topPct != null ? topPct * height : topPx;
}
/// Options for behavior of tapping/clicking on entries in the legend.
enum LegendTapHandling {
/// No associated behavior.
none,
/// Hide elements on the chart associated with this legend entry.
hide,
}

View File

@@ -0,0 +1,85 @@
// 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 '../../../../common/color.dart';
import '../../../../common/symbol_renderer.dart';
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../processed_series.dart' show ImmutableSeries;
import '../../series_renderer.dart' show rendererKey;
/// Holder for the information used for a legend row.
///
/// [T] the datum class type for the series passed in.
/// [D] the domain class type for the datum.
class LegendEntry<D> {
final String label;
final ImmutableSeries<D> series;
final dynamic datum;
final int datumIndex;
final D domain;
final Color color;
final TextStyleSpec textStyle;
double value;
String formattedValue;
bool isSelected;
/// Zero based index for the row where this legend appears in the legend.
int rowNumber;
/// Zero based index for the column where this legend appears in the legend.
int columnNumber;
/// Total number of rows in the legend.
int rowCount;
/// Total number of columns in the legend.
int columnCount;
/// Indicates whether this is in the first row of a tabular layout.
bool inFirstRow;
/// Indicates whether this is in the first column of a tabular layout.
bool inFirstColumn;
/// Indicates whether this is in the last row of a tabular layout.
bool inLastRow;
/// Indicates whether this is in the last column of a tabular layout.
bool inLastColumn;
// TODO: Forward the default formatters from series and allow for
// native legends to provide separate formatters.
LegendEntry(this.series, this.label,
{this.datum,
this.datumIndex,
this.domain,
this.value,
this.color,
this.textStyle,
this.isSelected = false,
this.rowNumber,
this.columnNumber,
this.rowCount,
this.columnCount,
this.inFirstRow,
this.inFirstColumn,
this.inLastRow,
this.inLastColumn});
/// Get the native symbol renderer stored in the series.
SymbolRenderer get symbolRenderer =>
series.getAttr(rendererKey).symbolRenderer;
}

View File

@@ -0,0 +1,68 @@
// 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 '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart';
import 'legend_entry.dart';
/// A strategy for generating a list of [LegendEntry] based on the series drawn.
///
/// [D] the domain class type for the datum.
abstract class LegendEntryGenerator<D> {
/// Generates a list of legend entries based on the series drawn on the chart.
///
/// [seriesList] Processed series list.
List<LegendEntry<D>> getLegendEntries(List<MutableSeries<D>> seriesList);
/// Update the list of legend entries based on the selection model.
///
/// [legendEntries] Existing legend entries to update.
/// [selectionModel] Selection model to query selected state.
/// [seriesList] Processed series list.
void updateLegendEntries(List<LegendEntry<D>> legendEntries,
SelectionModel<D> selectionModel, List<MutableSeries<D>> seriesList);
MeasureFormatter get measureFormatter;
set measureFormatter(MeasureFormatter formatter);
MeasureFormatter get secondaryMeasureFormatter;
set secondaryMeasureFormatter(MeasureFormatter formatter);
LegendDefaultMeasure get legendDefaultMeasure;
set legendDefaultMeasure(LegendDefaultMeasure noSelectionMeasure);
TextStyleSpec get entryTextStyle;
set entryTextStyle(TextStyleSpec entryTextStyle);
}
/// Options for calculating what measures are shown when there is no selection.
enum LegendDefaultMeasure {
// No measures are shown where there is no selection.
none,
// Sum of all measure values for the series.
sum,
// Average of all measure values for the series.
average,
// The first measure value of the series.
firstValue,
// The last measure value of the series.
lastValue,
}

View File

@@ -0,0 +1,146 @@
// 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:collection' show HashSet;
import '../../../cartesian/axis/axis.dart' show Axis, measureAxisIdKey;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../processed_series.dart' show ImmutableSeries, MutableSeries;
import '../../selection_model/selection_model.dart';
import 'legend_entry.dart';
import 'legend_entry_generator.dart';
/// A strategy for generating a list of [LegendEntry] per series data drawn.
///
/// [D] the domain class type for the datum.
class PerDatumLegendEntryGenerator<D> implements LegendEntryGenerator<D> {
TextStyleSpec entryTextStyle;
MeasureFormatter measureFormatter;
MeasureFormatter secondaryMeasureFormatter;
/// Option for showing measures when there is no selection.
LegendDefaultMeasure legendDefaultMeasure;
@override
List<LegendEntry<D>> getLegendEntries(List<MutableSeries<D>> seriesList) {
final legendEntries = <LegendEntry<D>>[];
final series = seriesList[0];
for (var i = 0; i < series.data.length; i++) {
legendEntries.add(new LegendEntry<D>(
series, series.domainFn(i).toString(),
color: series.colorFn(i),
datum: series.data[i],
datumIndex: i,
textStyle: entryTextStyle));
}
// Update with measures only if showing measure on no selection.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
_updateFromSeriesList(legendEntries, seriesList);
}
return legendEntries;
}
@override
void updateLegendEntries(List<LegendEntry<D>> legendEntries,
SelectionModel<D> selectionModel, List<MutableSeries<D>> seriesList) {
if (selectionModel.hasAnySelection) {
_updateFromSelection(legendEntries, selectionModel);
} else {
// Update with measures only if showing measure on no selection.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
_updateFromSeriesList(legendEntries, seriesList);
} else {
_resetLegendEntryMeasures(legendEntries);
}
}
}
/// Update legend entries with measures of the selected datum
void _updateFromSelection(
List<LegendEntry<D>> legendEntries, SelectionModel<D> selectionModel) {
// Given that each legend entry only has one datum associated with it, any
// option for [legendDefaultMeasure] essentially boils down to just showing
// the measure value.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
for (var entry in legendEntries) {
final series = entry.series;
final measure = series.measureFn(entry.datumIndex);
entry.value = measure.toDouble();
entry.formattedValue = _getFormattedMeasureValue(series, measure);
entry.isSelected = selectionModel.selectedSeries
.any((selectedSeries) => series.id == selectedSeries.id);
}
}
}
void _resetLegendEntryMeasures(List<LegendEntry<D>> legendEntries) {
for (LegendEntry<D> entry in legendEntries) {
entry.value = null;
entry.formattedValue = null;
entry.isSelected = false;
}
}
/// Update each legend entry by calculating measure values in [seriesList].
///
/// This method calculates the legend's measure value to show when there is no
/// selection. The type of calculation is based on the [legendDefaultMeasure]
/// value.
void _updateFromSeriesList(
List<LegendEntry<D>> legendEntries, List<MutableSeries<D>> seriesList) {
// Given that each legend entry only has one datum associated with it, any
// option for [legendDefaultMeasure] essentially boils down to just showing
// the measure value.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
for (var entry in legendEntries) {
final series = entry.series;
final measure = series.measureFn(entry.datumIndex);
entry.value = measure.toDouble();
entry.formattedValue = _getFormattedMeasureValue(series, measure);
entry.isSelected = false;
}
}
}
/// Formats the measure value using the appropriate measure formatter
/// function for the series.
String _getFormattedMeasureValue(ImmutableSeries series, num measure) {
return (series.getAttr(measureAxisIdKey) == Axis.secondaryMeasureAxisId)
? secondaryMeasureFormatter(measure)
: measureFormatter(measure);
}
@override
bool operator ==(Object other) {
return other is PerDatumLegendEntryGenerator &&
measureFormatter == other.measureFormatter &&
secondaryMeasureFormatter == other.secondaryMeasureFormatter &&
legendDefaultMeasure == other.legendDefaultMeasure &&
entryTextStyle == other.entryTextStyle;
}
@override
int get hashCode {
int hashcode = measureFormatter?.hashCode ?? 0;
hashcode = (hashcode * 37) + secondaryMeasureFormatter.hashCode;
hashcode = (hashcode * 37) + legendDefaultMeasure.hashCode;
hashcode = (hashcode * 37) + entryTextStyle.hashCode;
return hashcode;
}
}

View File

@@ -0,0 +1,190 @@
// 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:collection' show HashSet;
import '../../../cartesian/axis/axis.dart' show Axis, measureAxisIdKey;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart';
import '../../series_datum.dart' show SeriesDatum;
import 'legend_entry.dart';
import 'legend_entry_generator.dart';
/// A strategy for generating a list of [LegendEntry] per series drawn.
///
/// [D] the domain class type for the datum.
class PerSeriesLegendEntryGenerator<D> implements LegendEntryGenerator<D> {
TextStyleSpec entryTextStyle;
MeasureFormatter measureFormatter;
MeasureFormatter secondaryMeasureFormatter;
/// Option for showing measures when there is no selection.
LegendDefaultMeasure legendDefaultMeasure;
@override
List<LegendEntry<D>> getLegendEntries(List<MutableSeries<D>> seriesList) {
final legendEntries = seriesList
.map((series) => new LegendEntry<D>(series, series.displayName,
color: series.colorFn(0), textStyle: entryTextStyle))
.toList();
// Update with measures only if showing measure on no selection.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
_updateFromSeriesList(legendEntries, seriesList);
}
return legendEntries;
}
@override
void updateLegendEntries(List<LegendEntry<D>> legendEntries,
SelectionModel<D> selectionModel, List<MutableSeries<D>> seriesList) {
if (selectionModel.hasAnySelection) {
_updateFromSelection(legendEntries, selectionModel);
} else {
// Update with measures only if showing measure on no selection.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
_updateFromSeriesList(legendEntries, seriesList);
} else {
_resetLegendEntryMeasures(legendEntries);
}
}
}
/// Update legend entries with measures of the selected datum
void _updateFromSelection(
List<LegendEntry<D>> legendEntries, SelectionModel<D> selectionModel) {
// Map of series ID to the total selected measure value for that series.
final seriesAndMeasure = <String, num>{};
// Hash set of series ID's that use the secondary measure axis
final secondaryAxisSeriesIDs = new HashSet<String>();
for (SeriesDatum<D> selectedDatum in selectionModel.selectedDatum) {
final series = selectedDatum.series;
final seriesId = series.id;
final measure = series.measureFn(selectedDatum.index) ?? 0;
seriesAndMeasure[seriesId] = seriesAndMeasure.containsKey(seriesId)
? seriesAndMeasure[seriesId] + measure
: measure;
if (series.getAttr(measureAxisIdKey) == Axis.secondaryMeasureAxisId) {
secondaryAxisSeriesIDs.add(seriesId);
}
}
for (var entry in legendEntries) {
final seriesId = entry.series.id;
final measureValue = seriesAndMeasure[seriesId]?.toDouble();
final formattedValue = secondaryAxisSeriesIDs.contains(seriesId)
? secondaryMeasureFormatter(measureValue)
: measureFormatter(measureValue);
entry.value = measureValue;
entry.formattedValue = formattedValue;
entry.isSelected = selectionModel.selectedSeries
.any((selectedSeries) => entry.series.id == selectedSeries.id);
}
}
void _resetLegendEntryMeasures(List<LegendEntry<D>> legendEntries) {
for (LegendEntry<D> entry in legendEntries) {
entry.value = null;
entry.formattedValue = null;
entry.isSelected = false;
}
}
/// Update each legend entry by calculating measure values in [seriesList].
///
/// This method calculates the legend's measure value to show when there is no
/// selection. The type of calculation is based on the [legendDefaultMeasure]
/// value.
void _updateFromSeriesList(
List<LegendEntry<D>> legendEntries, List<MutableSeries<D>> seriesList) {
// Helper function to sum up the measure values
num getMeasureTotal(MutableSeries<D> series) {
var measureTotal = 0.0;
for (var i = 0; i < series.data.length; i++) {
measureTotal += series.measureFn(i);
}
return measureTotal;
}
// Map of series ID to the calculated measure for that series.
final seriesAndMeasure = <String, double>{};
// Map of series ID and the formatted measure for that series.
final seriesAndFormattedMeasure = <String, String>{};
for (MutableSeries<D> series in seriesList) {
final seriesId = series.id;
num calculatedMeasure;
switch (legendDefaultMeasure) {
case LegendDefaultMeasure.sum:
calculatedMeasure = getMeasureTotal(series);
break;
case LegendDefaultMeasure.average:
calculatedMeasure = getMeasureTotal(series) / series.data.length;
break;
case LegendDefaultMeasure.firstValue:
calculatedMeasure = series.measureFn(0);
break;
case LegendDefaultMeasure.lastValue:
calculatedMeasure = series.measureFn(series.data.length - 1);
break;
case LegendDefaultMeasure.none:
// [calculatedMeasure] intentionally left null, since we do not want
// to show any measures.
break;
}
seriesAndMeasure[seriesId] = calculatedMeasure?.toDouble();
seriesAndFormattedMeasure[seriesId] =
(series.getAttr(measureAxisIdKey) == Axis.secondaryMeasureAxisId)
? secondaryMeasureFormatter(calculatedMeasure)
: measureFormatter(calculatedMeasure);
}
for (var entry in legendEntries) {
final seriesId = entry.series.id;
entry.value = seriesAndMeasure[seriesId];
entry.formattedValue = seriesAndFormattedMeasure[seriesId];
entry.isSelected = false;
}
}
@override
bool operator ==(Object other) {
return other is PerSeriesLegendEntryGenerator &&
measureFormatter == other.measureFormatter &&
secondaryMeasureFormatter == other.secondaryMeasureFormatter &&
legendDefaultMeasure == other.legendDefaultMeasure &&
entryTextStyle == other.entryTextStyle;
}
@override
int get hashCode {
int hashcode = measureFormatter?.hashCode ?? 0;
hashcode = (hashcode * 37) + secondaryMeasureFormatter.hashCode;
hashcode = (hashcode * 37) + legendDefaultMeasure.hashCode;
hashcode = (hashcode * 37) + entryTextStyle.hashCode;
return hashcode;
}
}

View File

@@ -0,0 +1,171 @@
// 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:meta/meta.dart' show protected;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import 'legend.dart';
import 'legend_entry_generator.dart';
import 'per_series_legend_entry_generator.dart';
// TODO: Allows for hovering over a series in legend to highlight
// corresponding series in draw area.
/// Series legend behavior for charts.
///
/// By default this behavior creates a legend entry per series.
class SeriesLegend<D> extends Legend<D> {
/// List of currently hidden series, by ID.
final _hiddenSeriesList = new Set<String>();
/// List of series IDs that should be hidden by default.
List<String> _defaultHiddenSeries;
/// Whether or not the series legend should show measures on datum selection.
bool _showMeasures;
SeriesLegend({
SelectionModelType selectionModelType,
LegendEntryGenerator<D> legendEntryGenerator,
MeasureFormatter measureFormatter,
MeasureFormatter secondaryMeasureFormatter,
bool showMeasures,
LegendDefaultMeasure legendDefaultMeasure,
TextStyleSpec entryTextStyle,
}) : super(
selectionModelType: selectionModelType ?? SelectionModelType.info,
legendEntryGenerator:
legendEntryGenerator ?? new PerSeriesLegendEntryGenerator(),
entryTextStyle: entryTextStyle) {
// Call the setters that include the setting for default.
this.showMeasures = showMeasures;
this.legendDefaultMeasure = legendDefaultMeasure;
this.measureFormatter = measureFormatter;
this.secondaryMeasureFormatter = secondaryMeasureFormatter;
}
/// Sets a list of series IDs that should be hidden by default on first chart
/// draw.
///
/// This will also reset the current list of hidden series, filling it in with
/// the new default list.
set defaultHiddenSeries(List<String> defaultHiddenSeries) {
_defaultHiddenSeries = defaultHiddenSeries;
_hiddenSeriesList.clear();
if (_defaultHiddenSeries != null) {
_defaultHiddenSeries.forEach(hideSeries);
}
}
/// Gets a list of series IDs that should be hidden by default on first chart
/// draw.
List<String> get defaultHiddenSeries => _defaultHiddenSeries;
/// Whether or not the legend should show measures.
///
/// By default this is false, measures are not shown. When set to true, the
/// default behavior is to show measure only if there is selected data.
/// Please set [legendDefaultMeasure] to something other than none to enable
/// showing measures when there is no selection.
///
/// If [showMeasure] is set to null, it is changed to the default of false.
bool get showMeasures => _showMeasures;
set showMeasures(bool showMeasures) {
_showMeasures = showMeasures ?? false;
}
/// Option to show measures when selection is null.
///
/// By default this is set to none, so no measures are shown when there is
/// no selection.
///
/// If [legendDefaultMeasure] is set to null, it is changed to the default of
/// none.
LegendDefaultMeasure get legendDefaultMeasure =>
legendEntryGenerator.legendDefaultMeasure;
set legendDefaultMeasure(LegendDefaultMeasure legendDefaultMeasure) {
legendEntryGenerator.legendDefaultMeasure =
legendDefaultMeasure ?? LegendDefaultMeasure.none;
}
/// Formatter for measure values.
///
/// This is optional. The default formatter formats measure values with
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
/// returned.
set measureFormatter(MeasureFormatter formatter) {
legendEntryGenerator.measureFormatter =
formatter ?? defaultLegendMeasureFormatter;
}
/// Formatter for measure values of series that uses the secondary axis.
///
/// This is optional. The default formatter formats measure values with
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
/// returned.
set secondaryMeasureFormatter(MeasureFormatter formatter) {
legendEntryGenerator.secondaryMeasureFormatter =
formatter ?? defaultLegendMeasureFormatter;
}
/// Remove series IDs from the currently hidden list if those series have been
/// removed from the chart data. The goal is to allow any metric that is
/// removed from a chart, and later re-added to it, to be visible to the user.
@override
void onData(List<MutableSeries<D>> seriesList) {
// If a series was removed from the chart, remove it from our current list
// of hidden series.
final seriesIds = seriesList.map((MutableSeries<D> series) => series.id);
_hiddenSeriesList.removeWhere((String id) => !seriesIds.contains(id));
}
@override
void preProcessSeriesList(List<MutableSeries<D>> seriesList) {
seriesList.removeWhere((MutableSeries<D> series) {
return _hiddenSeriesList.contains(series.id);
});
}
/// Hides the data for a series on the chart by [seriesId].
///
/// The entry in the legend for this series will be grayed out to indicate
/// that it is hidden.
@protected
void hideSeries(String seriesId) {
_hiddenSeriesList.add(seriesId);
}
/// Shows the data for a series on the chart by [seriesId].
///
/// The entry in the legend for this series will be returned to its normal
/// color if it was previously hidden.
@protected
void showSeries(String seriesId) {
_hiddenSeriesList.removeWhere((String id) => id == seriesId);
}
/// Returns whether or not a given series [seriesId] is currently hidden.
bool isSeriesHidden(String seriesId) {
return _hiddenSeriesList.contains(seriesId);
}
}

View File

@@ -0,0 +1,697 @@
// 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:collection' show LinkedHashMap;
import 'dart:math' show max, min, Point, Rectangle;
import 'package:meta/meta.dart';
import '../../../common/color.dart' show Color;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/style/style_factory.dart' show StyleFactory;
import '../../../common/symbol_renderer.dart'
show CircleSymbolRenderer, SymbolRenderer;
import '../../cartesian/axis/axis.dart'
show ImmutableAxis, domainAxisKey, measureAxisKey;
import '../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
ViewMeasuredSizes;
import '../base_chart.dart' show BaseChart, LifecycleListener;
import '../chart_canvas.dart' show ChartCanvas, getAnimatedColor;
import '../datum_details.dart' show DatumDetails;
import '../processed_series.dart' show ImmutableSeries;
import '../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import 'chart_behavior.dart' show ChartBehavior;
/// Chart behavior that monitors the specified [SelectionModel] and renders a
/// dot for selected data.
///
/// Vertical or horizontal follow lines can optionally be drawn underneath the
/// rendered dots. Follow lines will be drawn in the combined area of the chart
/// draw area, and the draw area for any layout components that provide a
/// series draw area (e.g. [SymbolAnnotationRenderer]).
///
/// This is typically used for line charts to highlight segments.
///
/// It is used in combination with SelectNearest to update the selection model
/// and expand selection out to the domain value.
class LinePointHighlighter<D> implements ChartBehavior<D> {
final SelectionModelType selectionModelType;
/// Default radius of the dots if the series has no radius mapping function.
///
/// When no radius mapping function is provided, this value will be used as
/// is. [radiusPaddingPx] will not be added to [defaultRadiusPx].
final double defaultRadiusPx;
/// Additional radius value added to the radius of the selected data.
///
/// This value is only used when the series has a radius mapping function
/// defined.
final double radiusPaddingPx;
/// Whether or not to draw horizontal follow lines through the selected
/// points.
///
/// Defaults to drawing no horizontal follow lines.
final LinePointHighlighterFollowLineType showHorizontalFollowLine;
/// Whether or not to draw vertical follow lines through the selected points.
///
/// Defaults to drawing a vertical follow line only for the nearest datum.
final LinePointHighlighterFollowLineType showVerticalFollowLine;
/// The dash pattern to be used for drawing the line.
///
/// To disable dash pattern (to draw a solid line), pass in an empty list.
/// This is because if dashPattern is null or not set, it defaults to [1,3].
final List<int> dashPattern;
/// Whether or not follow lines should be drawn across the entire chart draw
/// area, or just from the axis to the point.
///
/// When disabled, measure follow lines will be drawn from the primary measure
/// axis to the point. In RTL mode, this means from the right-hand axis. In
/// LTR mode, from the left-hand axis.
final bool drawFollowLinesAcrossChart;
/// Renderer used to draw the highlighted points.
final SymbolRenderer symbolRenderer;
BaseChart<D> _chart;
_LinePointLayoutView _view;
LifecycleListener<D> _lifecycleListener;
/// Store a map of data drawn on the chart, mapped by series name.
///
/// [LinkedHashMap] is used to render the series on the canvas in the same
/// order as the data was provided by the selection model.
var _seriesPointMap = LinkedHashMap<String, _AnimatedPoint<D>>();
// Store a list of points that exist in the series data.
//
// This list will be used to remove any [_AnimatedPoint] that were rendered in
// previous draw cycles, but no longer have a corresponding datum in the new
// data.
final _currentKeys = <String>[];
LinePointHighlighter(
{SelectionModelType selectionModelType,
double defaultRadiusPx,
double radiusPaddingPx,
LinePointHighlighterFollowLineType showHorizontalFollowLine,
LinePointHighlighterFollowLineType showVerticalFollowLine,
List<int> dashPattern,
bool drawFollowLinesAcrossChart,
SymbolRenderer symbolRenderer})
: selectionModelType = selectionModelType ?? SelectionModelType.info,
defaultRadiusPx = defaultRadiusPx ?? 4.0,
radiusPaddingPx = radiusPaddingPx ?? 2.0,
showHorizontalFollowLine =
showHorizontalFollowLine ?? LinePointHighlighterFollowLineType.none,
showVerticalFollowLine = showVerticalFollowLine ??
LinePointHighlighterFollowLineType.nearest,
dashPattern = dashPattern ?? [1, 3],
drawFollowLinesAcrossChart = drawFollowLinesAcrossChart ?? true,
symbolRenderer = symbolRenderer ?? new CircleSymbolRenderer() {
_lifecycleListener =
new LifecycleListener<D>(onAxisConfigured: _updateViewData);
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
_view = new _LinePointLayoutView<D>(
chart: chart,
layoutPaintOrder: LayoutViewPaintOrder.linePointHighlighter,
showHorizontalFollowLine: showHorizontalFollowLine,
showVerticalFollowLine: showVerticalFollowLine,
dashPattern: dashPattern,
drawFollowLinesAcrossChart: drawFollowLinesAcrossChart,
symbolRenderer: symbolRenderer);
if (chart is CartesianChart) {
// Only vertical rendering is supported by this behavior.
assert((chart as CartesianChart).vertical);
}
chart.addView(_view);
chart.addLifecycleListener(_lifecycleListener);
chart
.getSelectionModel(selectionModelType)
.addSelectionChangedListener(_selectionChanged);
}
@override
void removeFrom(BaseChart chart) {
chart.removeView(_view);
chart
.getSelectionModel(selectionModelType)
.removeSelectionChangedListener(_selectionChanged);
chart.removeLifecycleListener(_lifecycleListener);
}
void _selectionChanged(SelectionModel selectionModel) {
_chart.redraw(skipLayout: true, skipAnimation: true);
}
void _updateViewData() {
_currentKeys.clear();
final selectedDatumDetails =
_chart.getSelectedDatumDetails(selectionModelType);
// Create a new map each time to ensure that we have it sorted in the
// selection model order. This preserves the "nearestDetail" ordering, so
// that we render follow lines in the proper place.
final newSeriesMap = <String, _AnimatedPoint<D>>{};
for (DatumDetails<D> detail in selectedDatumDetails) {
if (detail == null) {
continue;
}
final series = detail.series;
final datum = detail.datum;
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final lineKey = series.id;
double radiusPx = (detail.radiusPx != null)
? detail.radiusPx.toDouble() + radiusPaddingPx
: defaultRadiusPx;
final pointKey = '${lineKey}::${detail.domain}';
// If we already have a point for that key, use it.
_AnimatedPoint<D> animatingPoint;
if (_seriesPointMap.containsKey(pointKey)) {
animatingPoint = _seriesPointMap[pointKey];
} else {
// Create a new point and have it animate in from axis.
final point = new _DatumPoint<D>(
datum: datum,
domain: detail.domain,
series: series,
x: domainAxis.getLocation(detail.domain),
y: measureAxis.getLocation(0.0));
animatingPoint = new _AnimatedPoint<D>(
key: pointKey, overlaySeries: series.overlaySeries)
..setNewTarget(new _PointRendererElement<D>()
..point = point
..color = detail.color
..fillColor = detail.fillColor
..radiusPx = radiusPx
..measureAxisPosition = measureAxis.getLocation(0.0)
..strokeWidthPx = detail.strokeWidthPx
..symbolRenderer = detail.symbolRenderer);
}
newSeriesMap[pointKey] = animatingPoint;
// Create a new line using the final point locations.
final point = new _DatumPoint<D>(
datum: datum,
domain: detail.domain,
series: series,
x: detail.chartPosition.x,
y: detail.chartPosition.y);
// Update the set of points that still exist in the series data.
_currentKeys.add(pointKey);
// Get the point element we are going to setup.
final pointElement = new _PointRendererElement<D>()
..point = point
..color = detail.color
..fillColor = detail.fillColor
..radiusPx = radiusPx
..measureAxisPosition = measureAxis.getLocation(0.0)
..strokeWidthPx = detail.strokeWidthPx
..symbolRenderer = detail.symbolRenderer;
animatingPoint.setNewTarget(pointElement);
}
// Animate out points that don't exist anymore.
_seriesPointMap.forEach((String key, _AnimatedPoint<D> point) {
if (_currentKeys.contains(point.key) != true) {
point.animateOut();
newSeriesMap[point.key] = point;
}
});
_seriesPointMap = newSeriesMap;
_view.seriesPointMap = _seriesPointMap;
}
@override
String get role => 'LinePointHighlighter-${selectionModelType.toString()}';
}
class _LinePointLayoutView<D> extends LayoutView {
final LayoutViewConfig layoutConfig;
final LinePointHighlighterFollowLineType showHorizontalFollowLine;
final LinePointHighlighterFollowLineType showVerticalFollowLine;
final BaseChart<D> chart;
final List<int> dashPattern;
Rectangle<int> _drawAreaBounds;
Rectangle<int> get drawBounds => _drawAreaBounds;
final bool drawFollowLinesAcrossChart;
final SymbolRenderer symbolRenderer;
GraphicsFactory _graphicsFactory;
/// Store a map of series drawn on the chart, mapped by series name.
///
/// [LinkedHashMap] is used to render the series on the canvas in the same
/// order as the data was given to the chart.
LinkedHashMap<String, _AnimatedPoint<D>> _seriesPointMap;
_LinePointLayoutView({
@required this.chart,
@required int layoutPaintOrder,
@required this.showHorizontalFollowLine,
@required this.showVerticalFollowLine,
@required this.symbolRenderer,
this.dashPattern,
this.drawFollowLinesAcrossChart,
}) : this.layoutConfig = new LayoutViewConfig(
paintOrder: LayoutViewPaintOrder.linePointHighlighter,
position: LayoutPosition.DrawArea,
positionOrder: layoutPaintOrder);
set seriesPointMap(LinkedHashMap<String, _AnimatedPoint<D>> value) {
_seriesPointMap = value;
}
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
return null;
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
this._drawAreaBounds = drawAreaBounds;
}
@override
void paint(ChartCanvas canvas, double animationPercent) {
if (_seriesPointMap == null) {
return;
}
// Clean up the lines that no longer exist.
if (animationPercent == 1.0) {
final keysToRemove = <String>[];
_seriesPointMap.forEach((String key, _AnimatedPoint<D> point) {
if (point.animatingOut) {
keysToRemove.add(key);
}
});
keysToRemove.forEach((String key) => _seriesPointMap.remove(key));
}
final points = <_PointRendererElement<D>>[];
_seriesPointMap.forEach((String key, _AnimatedPoint<D> point) {
points.add(point.getCurrentPoint(animationPercent));
});
// Build maps of the position where the follow lines should stop for each
// selected data point.
final endPointPerValueVertical = <int, int>{};
final endPointPerValueHorizontal = <int, int>{};
for (_PointRendererElement<D> pointElement in points) {
if (pointElement.point.x == null || pointElement.point.y == null) {
continue;
}
final roundedX = pointElement.point.x.round();
final roundedY = pointElement.point.y.round();
// Get the Y value closest to the top of the chart for this X position.
if (endPointPerValueVertical[roundedX] == null) {
endPointPerValueVertical[roundedX] = roundedY;
} else {
// In the nearest case, we rely on the selected data always starting
// with the nearest point. In this case, we don't care about the rest of
// the selected data positions.
if (showVerticalFollowLine !=
LinePointHighlighterFollowLineType.nearest) {
endPointPerValueVertical[roundedX] =
min(endPointPerValueVertical[roundedX], roundedY);
}
}
// Get the X value closest to the "end" side of the chart for this Y
// position.
if (endPointPerValueHorizontal[roundedY] == null) {
endPointPerValueHorizontal[roundedY] = roundedX;
} else {
// In the nearest case, we rely on the selected data always starting
// with the nearest point. In this case, we don't care about the rest of
// the selected data positions.
if (showHorizontalFollowLine !=
LinePointHighlighterFollowLineType.nearest) {
endPointPerValueHorizontal[roundedY] =
max(endPointPerValueHorizontal[roundedY], roundedX);
}
}
}
var shouldShowHorizontalFollowLine = showHorizontalFollowLine ==
LinePointHighlighterFollowLineType.all ||
showHorizontalFollowLine == LinePointHighlighterFollowLineType.nearest;
var shouldShowVerticalFollowLine = showVerticalFollowLine ==
LinePointHighlighterFollowLineType.all ||
showVerticalFollowLine == LinePointHighlighterFollowLineType.nearest;
// Keep track of points for which we've already drawn lines.
final paintedHorizontalLinePositions = <num>[];
final paintedVerticalLinePositions = <num>[];
final drawBounds = chart.drawableLayoutAreaBounds;
final rtl = chart.context.isRtl;
// Draw the follow lines first, below all of the highlight shapes.
for (_PointRendererElement<D> pointElement in points) {
if (pointElement.point.x == null || pointElement.point.y == null) {
continue;
}
final roundedX = pointElement.point.x.round();
final roundedY = pointElement.point.y.round();
// Draw the horizontal follow line.
if (shouldShowHorizontalFollowLine &&
!paintedHorizontalLinePositions.contains(roundedY)) {
int leftBound;
int rightBound;
if (drawFollowLinesAcrossChart) {
// RTL and LTR both go across the whole draw area.
leftBound = drawBounds.left;
rightBound = drawBounds.left + drawBounds.width;
} else {
final x = endPointPerValueHorizontal[roundedY];
// RTL goes from the point to the right edge. LTR goes from the left
// edge to the point.
leftBound = rtl ? x : drawBounds.left;
rightBound = rtl ? drawBounds.left + drawBounds.width : x;
}
canvas.drawLine(
points: [
new Point<num>(leftBound, pointElement.point.y),
new Point<num>(rightBound, pointElement.point.y),
],
stroke: StyleFactory.style.linePointHighlighterColor,
strokeWidthPx: 1.0,
dashPattern: [1, 3]);
if (showHorizontalFollowLine ==
LinePointHighlighterFollowLineType.nearest) {
shouldShowHorizontalFollowLine = false;
}
paintedHorizontalLinePositions.add(roundedY);
}
// Draw the vertical follow line.
if (shouldShowVerticalFollowLine &&
!paintedVerticalLinePositions.contains(roundedX)) {
final topBound = drawFollowLinesAcrossChart
? drawBounds.top
: endPointPerValueVertical[roundedX];
canvas.drawLine(
points: [
new Point<num>(pointElement.point.x, topBound),
new Point<num>(
pointElement.point.x, drawBounds.top + drawBounds.height),
],
stroke: StyleFactory.style.linePointHighlighterColor,
strokeWidthPx: 1.0,
dashPattern: dashPattern);
if (showVerticalFollowLine ==
LinePointHighlighterFollowLineType.nearest) {
shouldShowVerticalFollowLine = false;
}
paintedVerticalLinePositions.add(roundedX);
}
if (!shouldShowHorizontalFollowLine && !shouldShowVerticalFollowLine) {
break;
}
}
// Draw the highlight shapes on top of all follow lines.
for (_PointRendererElement<D> pointElement in points) {
if (pointElement.point.x == null || pointElement.point.y == null) {
continue;
}
final bounds = new Rectangle<double>(
pointElement.point.x - pointElement.radiusPx,
pointElement.point.y - pointElement.radiusPx,
pointElement.radiusPx * 2,
pointElement.radiusPx * 2);
// Draw the highlight dot. Use the [SymbolRenderer] from the datum if one
// is defined.
(pointElement.symbolRenderer ?? symbolRenderer).paint(canvas, bounds,
fillColor: pointElement.fillColor,
strokeColor: pointElement.color,
strokeWidthPx: pointElement.strokeWidthPx);
}
}
@override
Rectangle<int> get componentBounds => this._drawAreaBounds;
@override
bool get isSeriesRenderer => false;
}
class _DatumPoint<D> extends Point<double> {
final dynamic datum;
final D domain;
final ImmutableSeries<D> series;
_DatumPoint({this.datum, this.domain, this.series, double x, double y})
: super(x, y);
factory _DatumPoint.from(_DatumPoint<D> other, [double x, double y]) {
return new _DatumPoint<D>(
datum: other.datum,
domain: other.domain,
series: other.series,
x: x ?? other.x,
y: y ?? other.y);
}
}
class _PointRendererElement<D> {
_DatumPoint<D> point;
Color color;
Color fillColor;
double radiusPx;
double measureAxisPosition;
double strokeWidthPx;
SymbolRenderer symbolRenderer;
_PointRendererElement<D> clone() {
return new _PointRendererElement<D>()
..point = this.point
..color = this.color
..fillColor = this.fillColor
..measureAxisPosition = this.measureAxisPosition
..radiusPx = this.radiusPx
..strokeWidthPx = this.strokeWidthPx
..symbolRenderer = this.symbolRenderer;
}
void updateAnimationPercent(_PointRendererElement previous,
_PointRendererElement target, double animationPercent) {
final targetPoint = target.point;
final previousPoint = previous.point;
final x = _lerpDouble(previousPoint.x, targetPoint.x, animationPercent);
final y = _lerpDouble(previousPoint.y, targetPoint.y, animationPercent);
point = new _DatumPoint<D>.from(targetPoint, x, y);
color = getAnimatedColor(previous.color, target.color, animationPercent);
fillColor = getAnimatedColor(
previous.fillColor, target.fillColor, animationPercent);
radiusPx =
_lerpDouble(previous.radiusPx, target.radiusPx, animationPercent);
if (target.strokeWidthPx != null && previous.strokeWidthPx != null) {
strokeWidthPx = (((target.strokeWidthPx - previous.strokeWidthPx) *
animationPercent) +
previous.strokeWidthPx);
} else {
strokeWidthPx = null;
}
}
/// Linear interpolation for doubles.
///
/// If either [a] or [b] is null, return null.
/// This is different than Flutter's lerpDouble method, we want to return null
/// instead of assuming it is 0.0.
double _lerpDouble(double a, double b, double t) {
if (a == null || b == null) return null;
return a + (b - a) * t;
}
}
class _AnimatedPoint<D> {
final String key;
final bool overlaySeries;
_PointRendererElement<D> _previousPoint;
_PointRendererElement<D> _targetPoint;
_PointRendererElement<D> _currentPoint;
// Flag indicating whether this point is being animated out of the chart.
bool animatingOut = false;
_AnimatedPoint({@required this.key, @required this.overlaySeries});
/// Animates a point that was removed from the series out of the view.
///
/// This should be called in place of "setNewTarget" for points that represent
/// data that has been removed from the series.
///
/// Animates the height of the point down to the measure axis position
/// (position of 0).
void animateOut() {
final newTarget = _currentPoint.clone();
// Set the target measure value to the axis position for all points.
final targetPoint = newTarget.point;
final newPoint = new _DatumPoint<D>.from(targetPoint, targetPoint.x,
newTarget.measureAxisPosition.roundToDouble());
newTarget.point = newPoint;
// Animate the radius to 0 so that we don't get a lingering point after
// animation is done.
newTarget.radiusPx = 0.0;
setNewTarget(newTarget);
animatingOut = true;
}
void setNewTarget(_PointRendererElement<D> newTarget) {
animatingOut = false;
_currentPoint ??= newTarget.clone();
_previousPoint = _currentPoint.clone();
_targetPoint = newTarget;
}
_PointRendererElement<D> getCurrentPoint(double animationPercent) {
if (animationPercent == 1.0 || _previousPoint == null) {
_currentPoint = _targetPoint;
_previousPoint = _targetPoint;
return _currentPoint;
}
_currentPoint.updateAnimationPercent(
_previousPoint, _targetPoint, animationPercent);
return _currentPoint;
}
}
/// Type of follow line(s) to draw.
enum LinePointHighlighterFollowLineType {
/// Draw a follow line for only the nearest point in the selection.
nearest,
/// Draw no follow lines.
none,
/// Draw a follow line for every point in the selection.
all,
}
/// Helper class that exposes fewer private internal properties for unit tests.
@visibleForTesting
class LinePointHighlighterTester<D> {
final LinePointHighlighter<D> behavior;
LinePointHighlighterTester(this.behavior);
int getSelectionLength() {
return behavior._seriesPointMap.length;
}
bool isDatumSelected(D datum) {
var contains = false;
behavior._seriesPointMap.forEach((String key, _AnimatedPoint<D> point) {
if (point._currentPoint.point.datum == datum) {
contains = true;
return;
}
});
return contains;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,127 @@
// 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 '../../../../common/gesture_listener.dart' show GestureListener;
import '../../base_chart.dart' show BaseChart;
import '../../behavior/chart_behavior.dart' show ChartBehavior;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import 'selection_trigger.dart' show SelectionTrigger;
/// Chart behavior that listens to tap event trigges and locks the specified
/// [SelectionModel]. This is used to prevent further updates to the selection
/// model, until it is unlocked again.
///
/// SelectionModels that can be updated:
/// info - To view the details of the selected items (ie: hover for web).
/// action - To select an item as an input, drill, or other selection.
///
/// You can add one LockSelection for each model type that you are updating.
/// Any previous LockSelection behavior for that selection model will be
/// removed.
class LockSelection<D> implements ChartBehavior<D> {
GestureListener _listener;
/// Type of selection model that should be updated by input events.
final SelectionModelType selectionModelType;
/// Type of input event that should trigger selection.
final SelectionTrigger eventTrigger = SelectionTrigger.tap;
BaseChart<D> _chart;
LockSelection({this.selectionModelType = SelectionModelType.info}) {
// Setup the appropriate gesture listening.
switch (this.eventTrigger) {
case SelectionTrigger.tap:
_listener =
new GestureListener(onTapTest: _onTapTest, onTap: _onSelect);
break;
default:
throw new ArgumentError('LockSelection does not support the event '
'trigger "${this.eventTrigger}"');
break;
}
}
bool _onTapTest(Point<double> chartPoint) {
// If the tap is within the drawArea, then claim the event from others.
return _chart.pointWithinRenderer(chartPoint);
}
bool _onSelect(Point<double> chartPoint, [double ignored]) {
// Skip events that occur outside the drawArea for any series renderer.
if (!_chart.pointWithinRenderer(chartPoint)) {
return false;
}
final selectionModel = _chart.getSelectionModel(selectionModelType);
// Do nothing if the chart has no selection model.
if (selectionModel == null) {
return false;
}
// Do not lock the selection model if there is no selection. Locking nothing
// would result in a very confusing user interface as the user tries to
// interact with content on the chart.
if (!selectionModel.locked && !selectionModel.hasAnySelection) {
return false;
}
// Toggle the lock state.
selectionModel.locked = !selectionModel.locked;
// If the model was just unlocked, clear the selection to dismiss any stale
// behavior elements. A new hovercard/etc. will appear after the user
// triggers a new gesture.
if (!selectionModel.locked) {
selectionModel.clearSelection();
}
return false;
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addGestureListener(_listener);
// TODO: Update this dynamically based on tappable location.
switch (this.eventTrigger) {
case SelectionTrigger.tap:
case SelectionTrigger.tapAndDrag:
case SelectionTrigger.pressHold:
case SelectionTrigger.longPressHold:
chart.registerTappable(this);
break;
case SelectionTrigger.hover:
default:
chart.unregisterTappable(this);
break;
}
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeGestureListener(_listener);
chart.unregisterTappable(this);
_chart = null;
}
@override
String get role => 'LockSelection-${selectionModelType.toString()}}';
}

View File

@@ -0,0 +1,302 @@
// 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 '../../../../common/gesture_listener.dart' show GestureListener;
import '../../base_chart.dart' show BaseChart;
import '../../behavior/chart_behavior.dart' show ChartBehavior;
import '../../datum_details.dart' show DatumDetails;
import '../../processed_series.dart' show ImmutableSeries;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import '../../series_datum.dart' show SeriesDatum;
import 'selection_trigger.dart' show SelectionTrigger;
/// Chart behavior that listens to the given eventTrigger and updates the
/// specified [SelectionModel]. This is used to pair input events to behaviors
/// that listen to selection changes.
///
/// Input event types:
/// hover (default) - Mouse over/near data.
/// tap - Mouse/Touch on/near data.
/// pressHold - Mouse/Touch and drag across the data instead of panning.
/// longPressHold - Mouse/Touch for a while in one place then drag across the
/// data.
///
/// SelectionModels that can be updated:
/// info - To view the details of the selected items (ie: hover for web).
/// action - To select an item as an input, drill, or other selection.
///
/// Other options available
/// [expandToDomain] - All data points that match the domain value of the
/// closest data point from each Series will be included in the selection.
/// The selection is limited to the hovered component area unless
/// [selectAcrossAllSeriesRendererComponents] is set to true. (Default:
/// true)
/// [selectAcrossAllSeriesRendererComponents] - Events in any component that
/// draw Series data will propagate to other components that draw Series
/// data to get a union of points that match across all series renderer
/// components. This is useful when components in the margins draw series
/// data and a selection is supposed to bridge the two adjacent
/// components. (Default: true)
/// [selectClosestSeries] - If true, the closest Series itself will be marked
/// as selected in addition to the datum. This is useful for features like
/// highlighting the closest Series. (Default: true)
///
/// You can add one SelectNearest for each model type that you are updating.
/// Any previous SelectNearest behavior for that selection model will be
/// removed.
class SelectNearest<D> implements ChartBehavior<D> {
GestureListener _listener;
/// Type of selection model that should be updated by input events.
final SelectionModelType selectionModelType;
/// Type of input event that should trigger selection.
final SelectionTrigger eventTrigger;
/// Whether or not all data points that match the domain value of the closest
/// data point from each Series will be included in the selection.
///
/// The selection is limited to the hovered component area unless
/// [selectAcrossAllSeriesRendererComponents] is set to true.
final bool expandToDomain;
/// Whether or not events in any component that draw Series data will
/// propagate to other components that draw Series data to get a union of
/// points that match across all series renderer components.
///
/// This is useful when components in the margins draw series data and a
/// selection is supposed to bridge the two adjacent components.
final bool selectAcrossAllSeriesRendererComponents;
/// Whether or not the closest Series itself will be marked as selected in
/// addition to the datum.
final bool selectClosestSeries;
/// The farthest away a domain value can be from the mouse position on the
/// domain axis before we'll ignore the datum.
///
/// This allows sparse data to not get selected until the mouse is some
/// reasonable distance. Defaults to no maximum distance.
final int maximumDomainDistancePx;
BaseChart<D> _chart;
bool _delaySelect = false;
SelectNearest(
{this.selectionModelType = SelectionModelType.info,
this.expandToDomain = true,
this.selectAcrossAllSeriesRendererComponents = true,
this.selectClosestSeries = true,
this.eventTrigger = SelectionTrigger.hover,
this.maximumDomainDistancePx}) {
// Setup the appropriate gesture listening.
switch (this.eventTrigger) {
case SelectionTrigger.tap:
_listener =
new GestureListener(onTapTest: _onTapTest, onTap: _onSelect);
break;
case SelectionTrigger.tapAndDrag:
_listener = new GestureListener(
onTapTest: _onTapTest,
onTap: _onSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
);
break;
case SelectionTrigger.pressHold:
_listener = new GestureListener(
onTapTest: _onTapTest,
onLongPress: _onSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDeselectAll);
break;
case SelectionTrigger.longPressHold:
_listener = new GestureListener(
onTapTest: _onTapTest,
onLongPress: _onLongPressSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDeselectAll);
break;
case SelectionTrigger.hover:
default:
_listener = new GestureListener(onHover: _onSelect);
break;
}
}
bool _onTapTest(Point<double> chartPoint) {
// If the tap is within the drawArea, then claim the event from others.
_delaySelect = eventTrigger == SelectionTrigger.longPressHold;
return _chart.pointWithinRenderer(chartPoint);
}
bool _onLongPressSelect(Point<double> chartPoint) {
_delaySelect = false;
return _onSelect(chartPoint);
}
bool _onSelect(Point<double> chartPoint, [double ignored]) {
// If the selection is delayed (waiting for long press), then quit early.
if (_delaySelect) {
return false;
}
var details = _chart.getNearestDatumDetailPerSeries(
chartPoint, selectAcrossAllSeriesRendererComponents);
final seriesList = <ImmutableSeries<D>>[];
var seriesDatumList = <SeriesDatum<D>>[];
if (details != null && details.isNotEmpty) {
details.sort((a, b) => a.domainDistance.compareTo(b.domainDistance));
if (maximumDomainDistancePx == null ||
details[0].domainDistance <= maximumDomainDistancePx) {
seriesDatumList = expandToDomain
? _expandToDomain(details.first)
: [new SeriesDatum<D>(details.first.series, details.first.datum)];
// Filter out points from overlay series.
seriesDatumList
.removeWhere((SeriesDatum<D> datum) => datum.series.overlaySeries);
if (selectClosestSeries && seriesList.isEmpty) {
if (details.first.series.overlaySeries) {
// If the closest "details" was from an overlay series, grab the
// closest remaining series instead. In this case, we need to sort a
// copy of the list by domain distance because we do not want to
// re-order the actual return values here.
final sortedSeriesDatumList =
new List<SeriesDatum<D>>.from(seriesDatumList);
sortedSeriesDatumList.sort((a, b) =>
a.datum.domainDistance.compareTo(b.datum.domainDistance));
seriesList.add(sortedSeriesDatumList.first.series);
} else {
seriesList.add(details.first.series);
}
}
}
}
return _chart
.getSelectionModel(selectionModelType)
.updateSelection(seriesDatumList, seriesList);
}
bool _onDeselectAll(_, __, ___) {
// If the selection is delayed (waiting for long press), then quit early.
if (_delaySelect) {
return false;
}
_chart
.getSelectionModel(selectionModelType)
.updateSelection(<SeriesDatum<D>>[], <ImmutableSeries<D>>[]);
return false;
}
List<SeriesDatum<D>> _expandToDomain(DatumDetails<D> nearestDetails) {
// Make sure that the "nearest" datum is at the top of the list.
final data = <SeriesDatum<D>>[
new SeriesDatum(nearestDetails.series, nearestDetails.datum)
];
final nearestDomain = nearestDetails.domain;
for (ImmutableSeries<D> series in _chart.currentSeriesList) {
final domainFn = series.domainFn;
final domainLowerBoundFn = series.domainLowerBoundFn;
final domainUpperBoundFn = series.domainUpperBoundFn;
final testBounds =
domainLowerBoundFn != null && domainUpperBoundFn != null;
for (var i = 0; i < series.data.length; i++) {
final datum = series.data[i];
final domain = domainFn(i);
// Don't re-add the nearest details.
if (nearestDetails.series == series && nearestDetails.datum == datum) {
continue;
}
if (domain == nearestDomain) {
data.add(new SeriesDatum(series, datum));
} else if (testBounds) {
final domainLowerBound = domainLowerBoundFn(i);
final domainUpperBound = domainUpperBoundFn(i);
var addDatum = false;
if (domainLowerBound != null && domainUpperBound != null) {
if (domain is int) {
addDatum = (domainLowerBound as int) <= (nearestDomain as int) &&
(nearestDomain as int) <= (domainUpperBound as int);
} else if (domain is double) {
addDatum =
(domainLowerBound as double) <= (nearestDomain as double) &&
(nearestDomain as double) <= (domainUpperBound as double);
} else if (domain is DateTime) {
addDatum = domainLowerBound == nearestDomain ||
domainUpperBound == nearestDomain ||
((domainLowerBound as DateTime)
.isBefore(nearestDomain as DateTime) &&
(nearestDomain as DateTime)
.isBefore(domainUpperBound as DateTime));
}
}
if (addDatum) {
data.add(new SeriesDatum(series, datum));
}
}
}
}
return data;
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addGestureListener(_listener);
// TODO: Update this dynamically based on tappable location.
switch (this.eventTrigger) {
case SelectionTrigger.tap:
case SelectionTrigger.tapAndDrag:
case SelectionTrigger.pressHold:
case SelectionTrigger.longPressHold:
chart.registerTappable(this);
break;
case SelectionTrigger.hover:
default:
chart.unregisterTappable(this);
break;
}
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeGestureListener(_listener);
chart.unregisterTappable(this);
_chart = null;
}
@override
String get role => 'SelectNearest-${selectionModelType.toString()}}';
}

View File

@@ -0,0 +1,22 @@
// 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.
enum SelectionTrigger {
hover,
tap,
tapAndDrag,
pressHold,
longPressHold,
}

View File

@@ -0,0 +1,816 @@
// 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:meta/meta.dart';
import '../../../../common/color.dart' show Color;
import '../../../../common/gesture_listener.dart' show GestureListener;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/math.dart' show clamp;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../../common/symbol_renderer.dart'
show RectSymbolRenderer, SymbolRenderer;
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
ViewMeasuredSizes;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../behavior/chart_behavior.dart' show ChartBehavior;
import '../../chart_canvas.dart' show ChartCanvas, getAnimatedColor;
import '../selection/selection_trigger.dart' show SelectionTrigger;
/// Chart behavior that adds a slider widget to a chart. When the slider is
/// dropped after drag, it will report its domain position and nearest datum
/// value. This behavior only supports charts that use continuous scales.
///
/// Input event types:
/// tapAndDrag - Mouse/Touch on the handle and drag across the chart.
/// pressHold - Mouse/Touch on the handle and drag across the chart instead of
/// panning.
/// longPressHold - Mouse/Touch for a while on the handle, then drag across
/// the data.
class Slider<D> implements ChartBehavior<D> {
_SliderLayoutView _view;
GestureListener _gestureListener;
LifecycleListener<D> _lifecycleListener;
SliderEventListener<D> _sliderEventListener;
/// The order to paint slider on the canvas.
///
/// The smaller number is drawn first. This value should be relative to
/// LayoutPaintViewOrder.slider (e.g. LayoutViewPaintOrder.slider + 1).
int layoutPaintOrder;
/// Type of input event for the slider.
///
/// Input event types:
/// tapAndDrag - Mouse/Touch on the handle and drag across the chart.
/// pressHold - Mouse/Touch on the handle and drag across the chart instead
/// of panning.
/// longPressHold - Mouse/Touch for a while on the handle, then drag across
/// the data.
final SelectionTrigger eventTrigger;
/// Renderer for the handle. Defaults to a rectangle.
SymbolRenderer _handleRenderer;
/// Custom role ID for this slider
String _roleId;
/// Whether or not the slider will snap onto the nearest datum (by domain
/// distance) when dragged.
final bool snapToDatum;
/// Color and size styles for the slider.
SliderStyle _style;
CartesianChart<D> _chart;
/// Rendering data for the slider line and handle.
_AnimatedSlider _sliderHandle;
bool _delaySelect = false;
bool _handleDrag = false;
/// Current location of the slider line.
Point<int> _domainCenterPoint;
/// Previous location of the slider line.
///
/// This is used to track changes in the position of the slider caused by new
/// data being drawn on the chart.
Point<int> _previousDomainCenterPoint;
/// Bounding box for the slider drag handle.
Rectangle<int> _handleBounds;
/// Domain value of the current slider position.
///
/// This is saved in terms of domain instead of chart position so that we can
/// adjust the slider automatically when the chart is resized.
D _domainValue;
/// Event to fire during the chart's onPostrender event.
///
/// This should be set any time the state of the slider has changed.
SliderListenerDragState _dragStateToFireOnPostRender;
/// Constructs a [Slider].
///
/// [eventTrigger] sets the type of gesture handled by the slider.
///
/// [handleRenderer] draws a handle for the slider. Defaults to a rectangle.
///
/// [initialDomainValue] sets the initial position of the slider in domain
/// units. The default is the center of the chart.
///
/// [onChangeCallback] will be called when the position of the slider
/// changes during a drag event.
///
/// [roleId] optional custom role ID for the slider. This can be used to allow
/// multiple [Slider] behaviors on the same chart. Normally, there can only be
/// one slider (per event trigger type) on a chart. This setting allows for
/// configuring multiple independent sliders.
///
/// [snapToDatum] configures the slider to snap snap onto the nearest datum
/// (by domain distance) when dragged. By default, the slider can be
/// positioned anywhere along the domain axis.
///
/// [style] configures the color and sizing of the slider line and handle.
///
/// [layoutPaintOrder] configures the order in which the behavior should be
/// painted. This value should be relative to LayoutPaintViewOrder.slider.
/// (e.g. LayoutViewPaintOrder.slider + 1).
Slider(
{this.eventTrigger = SelectionTrigger.tapAndDrag,
SymbolRenderer handleRenderer,
D initialDomainValue,
SliderListenerCallback<D> onChangeCallback,
String roleId,
this.snapToDatum = false,
SliderStyle style,
this.layoutPaintOrder = LayoutViewPaintOrder.slider}) {
_handleRenderer = handleRenderer ?? new RectSymbolRenderer();
_roleId = roleId ?? '';
_style = style ?? new SliderStyle();
_domainValue = initialDomainValue;
if (_domainValue != null) {
_dragStateToFireOnPostRender = SliderListenerDragState.initial;
}
// Setup the appropriate gesture listening.
switch (this.eventTrigger) {
case SelectionTrigger.tapAndDrag:
_gestureListener = new GestureListener(
onTapTest: _onTapTest,
onTap: _onSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDragEnd);
break;
case SelectionTrigger.pressHold:
_gestureListener = new GestureListener(
onTapTest: _onTapTest,
onLongPress: _onSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDragEnd);
break;
case SelectionTrigger.longPressHold:
_gestureListener = new GestureListener(
onTapTest: _onTapTest,
onLongPress: _onLongPressSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDragEnd);
break;
default:
throw new ArgumentError('Slider does not support the event trigger '
'"${this.eventTrigger}"');
break;
}
// Set up chart draw cycle listeners.
_lifecycleListener = new LifecycleListener<D>(
onData: _setInitialDragState,
onAxisConfigured: _updateViewData,
onPostrender: _fireChangeEvent,
);
// Set up slider event listeners.
_sliderEventListener =
new SliderEventListener<D>(onChange: onChangeCallback);
}
bool _onTapTest(Point<double> chartPoint) {
_delaySelect = eventTrigger == SelectionTrigger.longPressHold;
_handleDrag = _sliderContainsPoint(chartPoint);
return _handleDrag;
}
bool _onLongPressSelect(Point<double> chartPoint) {
_delaySelect = false;
return _onSelect(chartPoint);
}
bool _onSelect(Point<double> chartPoint, [double ignored]) {
// Skip events that occur outside the drawArea for any series renderer.
// If the selection is delayed (waiting for long press), then quit early.
if (!_handleDrag || _delaySelect) {
return false;
}
// Move the slider line along the domain axis, without adjusting the measure
// position.
final positionChanged = _moveSliderToPoint(chartPoint);
if (positionChanged) {
_dragStateToFireOnPostRender = SliderListenerDragState.drag;
_chart.redraw(skipAnimation: true, skipLayout: true);
}
return true;
}
bool _onDragEnd(Point<double> chartPoint, __, ___) {
// If the selection is delayed (waiting for long press), then quit early.
if (_delaySelect) {
return false;
}
_handleDrag = false;
// If snapToDatum is enabled, use the x position of the nearest datum
// instead of the mouse point.
if (snapToDatum) {
final details = _chart.getNearestDatumDetailPerSeries(chartPoint, true);
if (details.isNotEmpty && details[0].chartPosition.x != null) {
// Only trigger an animating draw cycle if we need to move the slider.
if (_domainValue != details[0].domain) {
_moveSliderToDomain(details[0].domain);
// Always fire the end event to notify listeners that the gesture is
// over.
_dragStateToFireOnPostRender = SliderListenerDragState.end;
_chart.redraw(skipAnimation: false, skipLayout: true);
}
}
} else {
// Move the slider line along the domain axis, without adjusting the
// measure position.
_moveSliderToPoint(chartPoint);
// Always fire the end event to notify listeners that the gesture is
// over.
_dragStateToFireOnPostRender = SliderListenerDragState.end;
_chart.redraw(skipAnimation: true, skipLayout: true);
}
return false;
}
bool _sliderContainsPoint(Point<double> chartPoint) {
return _handleBounds.containsPoint(chartPoint);
}
/// Sets the drag state to "initial" when new data is drawn on the chart.
void _setInitialDragState(_) {
_dragStateToFireOnPostRender = SliderListenerDragState.initial;
}
void _updateViewData() {
_sliderHandle ??= new _AnimatedSlider();
// If not set in the constructor, initial position for the handle is the
// center of the draw area.
_domainValue ??= _chart.domainAxis
.getDomain(_view.drawBounds.left + _view.drawBounds.width / 2)
.round();
// Possibly move the slider, if the axis values have changed since the last
// chart draw.
_moveSliderToDomain(_domainValue);
// Move the handle to the current event position.
final element = new _SliderElement()
..domainCenterPoint =
new Point<int>(_domainCenterPoint.x, _domainCenterPoint.y)
..buttonBounds = new Rectangle<int>(_handleBounds.left, _handleBounds.top,
_handleBounds.width, _handleBounds.height)
..fill = _style.fillColor
..stroke = _style.strokeColor
..strokeWidthPx = _style.strokeWidthPx;
_sliderHandle.setNewTarget(element);
_view.sliderHandle = _sliderHandle;
}
/// Fires a [SliderListenerDragState] change event if needed.
void _fireChangeEvent(_) {
if (SliderListenerDragState == null ||
_sliderEventListener.onChange == null) {
return;
}
SliderListenerDragState dragState = _dragStateToFireOnPostRender;
// Initial drag state event should only be fired if the slider has moved
// since the last draw. We always set the initial drag state event when new
// data was drawn on the chart, since we might need to move the slider if
// the axis range changed.
if (dragState == SliderListenerDragState.initial &&
_previousDomainCenterPoint == _domainCenterPoint) {
dragState = null;
}
// Reset state.
_dragStateToFireOnPostRender = null;
_previousDomainCenterPoint = _domainCenterPoint;
// Bail out if the event was cancelled.
if (dragState == null) {
return;
}
// Fire the event.
_sliderEventListener.onChange(
new Point<int>(_domainCenterPoint.x, _domainCenterPoint.y),
_domainValue,
_roleId,
dragState);
}
/// Moves the slider along the domain axis to [point].
///
/// If [point] exists beyond either edge of the draw area, it will be bound to
/// the nearest edge.
///
/// Updates [_domainValue] with the domain value located at [point]. For
/// ordinal axes, this might technically result in a domain value whose center
/// point lies slightly outside the draw area.
///
/// Updates [_domainCenterPoint] and [_handleBounds] with the new position of
/// the slider.
///
/// Returns whether or not the position actually changed. This will generally
/// be false if the mouse was dragged outside of the domain axis viewport.
bool _moveSliderToPoint(Point<double> point) {
var positionChanged = false;
if (_chart != null) {
final viewBounds = _view.componentBounds;
// Clamp the position to the edge of the viewport.
final position = clamp(point.x, viewBounds.left, viewBounds.right);
positionChanged = (_previousDomainCenterPoint != null &&
position != _previousDomainCenterPoint.x);
// Reset the domain value if the position was outside of the chart.
_domainValue = _chart.domainAxis.getDomain(position.toDouble());
if (_domainCenterPoint != null) {
_domainCenterPoint =
new Point<int>(position.round(), _domainCenterPoint.y);
} else {
_domainCenterPoint = new Point<int>(
position.round(), (viewBounds.top + viewBounds.height / 2).round());
}
num handleReferenceY;
switch (_style.handlePosition) {
case SliderHandlePosition.middle:
handleReferenceY = _domainCenterPoint.y;
break;
case SliderHandlePosition.top:
handleReferenceY = viewBounds.top;
break;
default:
throw new ArgumentError('Slider does not support the handle position '
'"${_style.handlePosition}"');
}
// Move the slider handle along the domain axis.
_handleBounds = new Rectangle<int>(
(_domainCenterPoint.x -
_style.handleSize.width / 2 +
_style.handleOffset.x)
.round(),
(handleReferenceY -
_style.handleSize.height / 2 +
_style.handleOffset.y)
.round(),
_style.handleSize.width,
_style.handleSize.height);
}
return positionChanged;
}
/// Moves the slider along the domain axis to the location of [domain].
///
/// If [domain] exists beyond either edge of the draw area, the position will
/// be bound to the nearest edge.
///
/// Updates [_domainValue] with the location of [domain]. For ordinal axes,
/// this might result in a different domain value if the range band of
/// [domain] is completely outside of the viewport.
///
/// Updates [_domainCenterPoint] and [_handleBounds] with the new position of
/// the slider.
///
/// Returns whether or not the position actually changed. This will generally
/// be false if the mouse was dragged outside of the domain axis viewport.
bool _moveSliderToDomain(D domain) {
final x = _chart.domainAxis.getLocation(domain);
return _moveSliderToPoint(new Point<double>(x, 0.0));
}
/// Programmatically moves the slider to the location of [domain] on the
/// domain axis.
///
/// If [domain] exists beyond either edge of the draw area, the position will
/// be bound to the nearest edge of the chart. The slider's current domain
/// value state will reflect the domain value at the edge of the chart. For
/// ordinal axes, this might result in a domain value whose range band is
/// partially located beyond the edge of the chart.
///
/// This does nothing if the domain matches the current domain location.
///
/// [SliderEventListener] callbacks will be fired to indicate that the slider
/// has moved.
///
/// [skipAnimation] controls whether or not the slider will animate. Animation
/// is disabled by default.
void moveSliderToDomain(D domain, {bool skipAnimation = true}) {
// Nothing to do if we are unattached to a chart or asked to move to the
// current location.
if (_chart == null || domain == _domainValue) {
return;
}
final positionChanged = _moveSliderToDomain(domain);
if (positionChanged) {
_dragStateToFireOnPostRender = SliderListenerDragState.end;
_chart.redraw(skipAnimation: skipAnimation, skipLayout: true);
}
}
@override
void attachTo(BaseChart<D> chart) {
if (!(chart is CartesianChart)) {
throw new ArgumentError(
'Slider can only be attached to a cartesian chart.');
}
_chart = chart as CartesianChart;
// Only vertical rendering is supported by this behavior.
assert(_chart.vertical);
_view = new _SliderLayoutView<D>(
layoutPaintOrder: layoutPaintOrder, handleRenderer: _handleRenderer);
chart.addView(_view);
chart.addGestureListener(_gestureListener);
chart.addLifecycleListener(_lifecycleListener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeView(_view);
chart.removeGestureListener(_gestureListener);
chart.removeLifecycleListener(_lifecycleListener);
_chart = null;
}
@override
String get role => 'Slider-${eventTrigger.toString()}-${_roleId}';
}
/// Style configuration for a [Slider] behavior.
class SliderStyle {
/// Fill color of the handle of the slider.
Color fillColor;
/// Allows users to specify both x-position and y-position offset values that
/// determines where the slider handle will be rendered. The offset will be
/// calculated relative to its default position at the vertical and horizontal
/// center of the slider line.
Point<double> handleOffset;
/// The vertical position for the slider handle.
SliderHandlePosition handlePosition;
/// Specifies the size of the slider handle.
Rectangle<int> handleSize;
/// Stroke width of the slider line and the slider handle.
double strokeWidthPx;
/// Stroke color of the slider line and hte slider handle
Color strokeColor = StyleFactory.style.sliderStrokeColor;
SliderStyle(
{Color fillColor,
this.handleOffset = const Point<double>(0.0, 0.0),
this.handleSize = const Rectangle<int>(0, 0, 10, 20),
Color strokeColor,
this.handlePosition = SliderHandlePosition.middle,
this.strokeWidthPx = 2.0}) {
this.fillColor = fillColor ?? StyleFactory.style.sliderFillColor;
this.strokeColor = strokeColor ?? StyleFactory.style.sliderStrokeColor;
}
@override
bool operator ==(Object o) {
return o is SliderStyle &&
fillColor == o.fillColor &&
handleOffset == o.handleOffset &&
handleSize == o.handleSize &&
strokeWidthPx == o.strokeWidthPx &&
strokeColor == o.strokeColor;
}
@override
int get hashCode {
int hashcode = fillColor?.hashCode ?? 0;
hashcode = (hashcode * 37) + handleOffset?.hashCode ?? 0;
hashcode = (hashcode * 37) + handleSize?.hashCode ?? 0;
hashcode = (hashcode * 37) + strokeWidthPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + strokeColor?.hashCode ?? 0;
hashcode = (hashcode * 37) + handlePosition?.hashCode ?? 0;
return hashcode;
}
}
/// Describes the vertical position of the slider handle on the slider.
///
/// [middle] indicates the handle should be half-way between the top and bottom
/// of the chart in the middle of the slider line.
///
/// [top] indicates the slider should be rendered relative to the top of the
/// chart.
enum SliderHandlePosition { middle, top }
/// Layout view component for [Slider].
class _SliderLayoutView<D> extends LayoutView {
final LayoutViewConfig layoutConfig;
Rectangle<int> _drawAreaBounds;
Rectangle<int> get drawBounds => _drawAreaBounds;
GraphicsFactory _graphicsFactory;
/// Renderer for the handle. Defaults to a rectangle.
SymbolRenderer _handleRenderer;
/// Rendering data for the slider line and handle.
_AnimatedSlider _sliderHandle;
_SliderLayoutView(
{@required int layoutPaintOrder, @required SymbolRenderer handleRenderer})
: this.layoutConfig = new LayoutViewConfig(
paintOrder: layoutPaintOrder,
position: LayoutPosition.DrawArea,
positionOrder: LayoutViewPositionOrder.drawArea),
_handleRenderer = handleRenderer;
set sliderHandle(_AnimatedSlider value) {
_sliderHandle = value;
}
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
return null;
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
this._drawAreaBounds = drawAreaBounds;
}
@override
void paint(ChartCanvas canvas, double animationPercent) {
final sliderElement = _sliderHandle.getCurrentSlider(animationPercent);
canvas.drawLine(
points: [
new Point<num>(
sliderElement.domainCenterPoint.x, _drawAreaBounds.top),
new Point<num>(
sliderElement.domainCenterPoint.x, _drawAreaBounds.bottom),
],
stroke: sliderElement.stroke,
strokeWidthPx: sliderElement.strokeWidthPx);
_handleRenderer.paint(canvas, sliderElement.buttonBounds,
fillColor: sliderElement.fill,
strokeColor: sliderElement.stroke,
strokeWidthPx: sliderElement.strokeWidthPx);
}
@override
Rectangle<int> get componentBounds => this._drawAreaBounds;
@override
bool get isSeriesRenderer => false;
}
/// Rendering information for a slider control element.
class _SliderElement<D> {
Point<int> domainCenterPoint;
Rectangle<int> buttonBounds;
Color fill;
Color stroke;
double strokeWidthPx;
_SliderElement<D> clone() {
return new _SliderElement<D>()
..domainCenterPoint = this.domainCenterPoint
..buttonBounds = this.buttonBounds
..fill = this.fill
..stroke = this.stroke
..strokeWidthPx = this.strokeWidthPx;
}
void updateAnimationPercent(
_SliderElement previous, _SliderElement target, double animationPercent) {
final _SliderElement localPrevious = previous;
final _SliderElement localTarget = target;
final previousPoint = localPrevious.domainCenterPoint;
final targetPoint = localTarget.domainCenterPoint;
final x = ((targetPoint.x - previousPoint.x) * animationPercent) +
previousPoint.x;
final y = ((targetPoint.y - previousPoint.y) * animationPercent) +
previousPoint.y;
domainCenterPoint = new Point<int>(x.round(), y.round());
final previousBounds = localPrevious.buttonBounds;
final targetBounds = localTarget.buttonBounds;
final top = ((targetBounds.top - previousBounds.top) * animationPercent) +
previousBounds.top;
final right =
((targetBounds.right - previousBounds.right) * animationPercent) +
previousBounds.right;
final bottom =
((targetBounds.bottom - previousBounds.bottom) * animationPercent) +
previousBounds.bottom;
final left =
((targetBounds.left - previousBounds.left) * animationPercent) +
previousBounds.left;
buttonBounds = new Rectangle<int>(left.round(), top.round(),
(right - left).round(), (bottom - top).round());
fill = getAnimatedColor(previous.fill, target.fill, animationPercent);
stroke = getAnimatedColor(previous.stroke, target.stroke, animationPercent);
strokeWidthPx =
(((target.strokeWidthPx - previous.strokeWidthPx) * animationPercent) +
previous.strokeWidthPx);
}
}
/// Animates the slider control element of the behavior between different
/// states.
class _AnimatedSlider<D> {
_SliderElement<D> _previousSlider;
_SliderElement<D> _targetSlider;
_SliderElement<D> _currentSlider;
// Flag indicating whether this point is being animated out of the chart.
bool animatingOut = false;
_AnimatedSlider();
/// Animates a point that was removed from the series out of the view.
///
/// This should be called in place of "setNewTarget" for points that represent
/// data that has been removed from the series.
///
/// Animates the width of the slider down to 0.
void animateOut() {
final newTarget = _currentSlider.clone();
// Animate the button bounds inwards horizontally towards a 0 width box.
final targetBounds = newTarget.buttonBounds;
final top = targetBounds.top;
final right = targetBounds.left + targetBounds.width / 2;
final bottom = targetBounds.bottom;
final left = right;
newTarget.buttonBounds = new Rectangle<int>(left.round(), top.round(),
(right - left).round(), (bottom - top).round());
// Animate the stroke width to 0 so that we don't get a lingering line after
// animation is done.
newTarget.strokeWidthPx = 0.0;
setNewTarget(newTarget);
animatingOut = true;
}
void setNewTarget(_SliderElement<D> newTarget) {
animatingOut = false;
_currentSlider ??= newTarget.clone();
_previousSlider = _currentSlider.clone();
_targetSlider = newTarget;
}
_SliderElement<D> getCurrentSlider(double animationPercent) {
if (animationPercent == 1.0 || _previousSlider == null) {
_currentSlider = _targetSlider;
_previousSlider = _targetSlider;
return _currentSlider;
}
_currentSlider.updateAnimationPercent(
_previousSlider, _targetSlider, animationPercent);
return _currentSlider;
}
}
/// Event handler for slider events.
class SliderEventListener<D> {
/// Called when the position of the slider has changed during a drag event.
final SliderListenerCallback<D> onChange;
SliderEventListener({this.onChange});
}
/// Callback function for [Slider] drag events.
///
/// [point] is the current position of the slider line. [point.x] is the domain
/// position, and [point.y] is the position of the center of the line on the
/// measure axis.
///
/// [domain] is the domain value at the slider position.
///
/// [dragState] indicates the current state of a drag event.
typedef SliderListenerCallback<D>(Point<int> point, D domain, String roleId,
SliderListenerDragState dragState);
/// Describes the current state of a slider change as a result of a drag event.
///
/// [initial] indicates that the slider was set to an initial position when new
/// data was drawn on a chart. This will be fired if an initialDomainValue is
/// passed to [Slider]. It will also be fired if the position of the slider
/// changes as a result of new data being drawn on the chart.
///
/// [drag] indicates that the slider is being moved as a result of drag events.
/// When this is passed, the drag event is still active. Once the drag event is
/// completed, an [end] event will be fired.
///
/// [end] indicates that a drag event has been completed. This usually occurs
/// after one or more [drag] events. An [end] event will also be fired if
/// [Slider.moveSliderToDomain] is called, but there will be no preceding [drag]
/// events in this case.
enum SliderListenerDragState { initial, drag, end }
/// Helper class that exposes fewer private internal properties for unit tests.
@visibleForTesting
class SliderTester<D> {
final Slider<D> behavior;
SliderTester(this.behavior);
Point<int> get domainCenterPoint => behavior._domainCenterPoint;
D get domainValue => behavior._domainValue;
Rectangle<int> get handleBounds => behavior._handleBounds;
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
behavior._view.layout(componentBounds, drawAreaBounds);
}
_SliderLayoutView get view => behavior._view;
}

View File

@@ -0,0 +1,75 @@
// 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 '../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../base_chart.dart' show BaseChart;
import '../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import 'chart_behavior.dart' show ChartBehavior;
/// Chart behavior that centers the viewport on the selected domain.
///
/// It is used in combination with SelectNearest to update the selection model
/// and notify this behavior to update the viewport on selection change.
///
/// This behavior can only be used on [CartesianChart].
class SlidingViewport<D> implements ChartBehavior<D> {
final SelectionModelType selectionModelType;
CartesianChart<D> _chart;
SlidingViewport([this.selectionModelType = SelectionModelType.info]);
void _selectionChanged(SelectionModel selectionModel) {
if (selectionModel.hasAnySelection == false) {
return;
}
// Calculate current viewport center and determine the translate pixels
// needed based on the selected domain value's location and existing amount
// of translate pixels.
final domainAxis = _chart.domainAxis;
final selectedDatum = selectionModel.selectedDatum.first;
final domainLocation = domainAxis
.getLocation(selectedDatum.series.domainFn(selectedDatum.index));
final viewportCenter =
domainAxis.range.start + (domainAxis.range.width / 2);
final translatePx =
domainAxis.viewportTranslatePx + (viewportCenter - domainLocation);
domainAxis.setViewportSettings(
domainAxis.viewportScalingFactor, translatePx);
_chart.redraw();
}
@override
void attachTo(BaseChart<D> chart) {
assert(chart is CartesianChart);
_chart = chart as CartesianChart<D>;
chart
.getSelectionModel(selectionModelType)
.addSelectionChangedListener(_selectionChanged);
}
@override
void removeFrom(BaseChart chart) {
chart
.getSelectionModel(selectionModelType)
.removeSelectionChangedListener(_selectionChanged);
}
@override
String get role => 'slidingViewport-${selectionModelType.toString()}';
}

View File

@@ -0,0 +1,264 @@
// 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:meta/meta.dart' show protected;
import '../../../../common/gesture_listener.dart' show GestureListener;
import '../../../cartesian/axis/axis.dart' show Axis;
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../chart_behavior.dart' show ChartBehavior;
/// Adds initial hint behavior for [CartesianChart].
///
/// This behavior animates to the final viewport from an initial translate and
/// or scale factor.
abstract class InitialHintBehavior<D> implements ChartBehavior<D> {
/// Listens for drag gestures.
GestureListener _listener;
/// Chart lifecycle listener to setup hint animation.
LifecycleListener<D> _lifecycleListener;
@override
String get role => 'InitialHint';
/// The chart to which the behavior is attached.
CartesianChart<D> _chart;
@protected
CartesianChart<D> get chart => _chart;
Duration _hintDuration = new Duration(milliseconds: 3000);
/// The amount of time to animate to the desired viewport.
///
/// If no duration is passed in, the default of 3000 ms is used.
@protected
Duration get hintDuration => _hintDuration;
set hintDuration(Duration duration) {
_hintDuration = duration;
}
double _maxHintTranslate = 0.0;
// TODO: Translation animation only works for ordinal axis.
/// The maximum amount ordinal values to shift the viewport for the the hint
/// animation.
///
/// Positive numbers shift the viewport to the right and negative to the left.
/// The default is no translation.
@protected
double get maxHintTranslate => _maxHintTranslate;
set maxHintTranslate(double maxHintTranslate) {
_maxHintTranslate = maxHintTranslate;
}
double _maxHintScaleFactor;
/// The amount the domain axis will be scaled for the start of the hint.
///
/// A value of 1.0 means the viewport is completely zoomed out (all domains
/// are in the viewport). If a value is provided, it cannot be less than 1.0.
///
/// By default maxHintScaleFactor is not set.
@protected
double get maxHintScaleFactor => _maxHintScaleFactor;
set maxHintScaleFactor(double maxHintScaleFactor) {
assert(maxHintScaleFactor != null && maxHintScaleFactor >= 1.0);
_maxHintScaleFactor = maxHintScaleFactor;
}
/// Flag to indicate that hint animation controller has already been set up.
///
/// This is to ensure that the hint is only set up on the first draw.
bool _hintSetupCompleted = false;
/// Flag to indicate that the first call to axis configured is completed.
///
/// This is to ensure that the initial and target viewport translate and scale
/// factor is only calculated on the first axis configuration.
bool _firstAxisConfigured = false;
double _initialViewportTranslatePx;
double _initialViewportScalingFactor;
double _targetViewportTranslatePx;
double _targetViewportScalingFactor;
InitialHintBehavior() {
_listener = new GestureListener(onTapTest: onTapTest);
_lifecycleListener = new LifecycleListener<D>(
onAxisConfigured: _onAxisConfigured,
onAnimationComplete: _onAnimationComplete);
}
@override
attachTo(BaseChart<D> chart) {
if (!(chart is CartesianChart)) {
throw new ArgumentError(
'InitialHintBehavior can only be attached to a CartesianChart');
}
_chart = chart;
_chart.addGestureListener(_listener);
_chart.addLifecycleListener(_lifecycleListener);
}
@override
removeFrom(BaseChart<D> chart) {
if (!(chart is CartesianChart)) {
throw new ArgumentError(
'InitialHintBehavior can only be removed from a CartesianChart');
}
stopHintAnimation();
_chart = chart;
_chart.removeGestureListener(_listener);
_chart.removeLifecycleListener(_lifecycleListener);
_chart = null;
}
@protected
bool onTapTest(Point<double> localPosition) {
if (_chart == null) {
return false;
}
// If the user taps the chart, stop the hint animation immediately.
stopHintAnimation();
return _chart.withinDrawArea(localPosition);
}
/// Calculate the animation's initial and target viewport and scale factor
/// and shift the viewport to the start.
void _onAxisConfigured() {
if (_firstAxisConfigured == false) {
_firstAxisConfigured = true;
final domainAxis = chart.domainAxis;
// TODO: Translation animation only works for axis with a
// rangeband type that returns a non zero step size. If two rows have
// the same domain value, step size could also equal 0.
assert(domainAxis.stepSize != 0.0);
// Save the target viewport and scale factor from axis, because the
// viewport can be set by the user using AxisSpec.
_targetViewportTranslatePx = domainAxis.viewportTranslatePx;
_targetViewportScalingFactor = domainAxis.viewportScalingFactor;
// Calculate the amount to translate from the target viewport.
final translateAmount = domainAxis.stepSize * maxHintTranslate;
_initialViewportTranslatePx =
_targetViewportTranslatePx - translateAmount;
_initialViewportScalingFactor =
maxHintScaleFactor ?? _targetViewportScalingFactor;
domainAxis.setViewportSettings(
_initialViewportScalingFactor, _initialViewportTranslatePx);
chart.redraw(skipAnimation: true, skipLayout: false);
}
}
/// Start the hint animation, only start the animation on the very first draw.
void _onAnimationComplete() {
if (_hintSetupCompleted == false) {
_hintSetupCompleted = true;
startHintAnimation();
}
}
/// Setup and start the hint animation.
///
/// Animation controller to be handled by the native platform.
@protected
void startHintAnimation() {
// When panning starts, measure tick provider should not update ticks.
// This is still needed because axis internally updates the tick location
// after the tick provider generates the ticks. If we do not tell the axis
// not to update the location of the measure axes, the measure axis will
// change during the hint animation and make values jump back and forth.
_chart.getMeasureAxis().lockAxis = true;
_chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis = true;
}
/// Stop hint animation
@protected
void stopHintAnimation() {
// When panning is completed, unlock the measure axis.
_chart.getMeasureAxis().lockAxis = false;
_chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis =
false;
}
/// Animation hint percent, to be returned by the native platform.
@protected
double get hintAnimationPercent;
/// Shift domain viewport on hint animation ticks.
@protected
void onHintTick() {
final percent = hintAnimationPercent;
final scaleFactor = _lerpDouble(
_initialViewportScalingFactor, _targetViewportScalingFactor, percent);
double translatePx = _lerpDouble(
_initialViewportTranslatePx, _targetViewportTranslatePx, percent);
// If there is a scale factor animation, need to scale the translatePx so
// the animation appears to be zooming in on the viewport when there is no
// [maxHintTranslate] provided.
//
// If there is a translate hint, the animation will still first zoom in
// and then translate the [maxHintTranslate] amount.
if (_initialViewportScalingFactor != _targetViewportScalingFactor) {
translatePx = translatePx * percent;
}
final domainAxis = chart.domainAxis;
domainAxis.setViewportSettings(scaleFactor, translatePx,
drawAreaWidth: chart.drawAreaBounds.width);
if (percent >= 1.0) {
stopHintAnimation();
chart.redraw();
} else {
chart.redraw(skipAnimation: true, skipLayout: true);
}
}
/// Linear interpolation for doubles.
double _lerpDouble(double a, double b, double t) {
if (a == null && b == null) return null;
a ??= 0.0;
b ??= 0.0;
return a + (b - a) * t;
}
}

Some files were not shown because too many files have changed in this diff Show More