diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 000000000..51955fa2a --- /dev/null +++ b/web/.gitignore @@ -0,0 +1 @@ +!**/pubspec.lock diff --git a/web/_tool/peanut_post_build.dart b/web/_tool/peanut_post_build.dart new file mode 100644 index 000000000..b40c7e197 --- /dev/null +++ b/web/_tool/peanut_post_build.dart @@ -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 args) { + final buildDir = args[0]; + final fileMap = + (jsonDecode(args[1]) as Map).cast(); + + 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() + .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('', '\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 => ''' +
+ + $name + + $name +
+ ${_indent(content, 2)} +
+
+'''; +} + +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 = ''' + +'''; + +String _indent(String content, int spaces) => + LineSplitter.split(content).join('\n' + ' ' * spaces); + +const _itemsReplace = r''; + +String _tocTemplate(Iterable<_Demo> items) => ''' + + + + ${_indent(_analytics, 2)} + Examples + + + + + +

Flutter for web samples

+ Sample source code +
+ $_itemsReplace +
+ + + +''' + .replaceAll(_itemsReplace, _indent(items.map((d) => d.html).join('\n'), 4)); diff --git a/web/_tool/pubspec.lock b/web/_tool/pubspec.lock new file mode 100644 index 000000000..49e488aba --- /dev/null +++ b/web/_tool/pubspec.lock @@ -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" diff --git a/web/_tool/pubspec.yaml b/web/_tool/pubspec.yaml new file mode 100644 index 000000000..08ed30f37 --- /dev/null +++ b/web/_tool/pubspec.yaml @@ -0,0 +1,6 @@ +name: tool +publish_to: none + +dependencies: + markdown: ^2.0.3 + path: ^1.6.2 diff --git a/web/_tool/verify_packages.dart b/web/_tool/verify_packages.dart new file mode 100644 index 000000000..f09f763eb --- /dev/null +++ b/web/_tool/verify_packages.dart @@ -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 = []; + 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 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 _run( + String workingDir, String commandName, List 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 _listPackageDirs(Directory dir) sync* { + if (File('${dir.path}/pubspec.yaml').existsSync()) { + yield dir.path; + } else { + for (var subDir in dir + .listSync(followLinks: false) + .whereType() + .where((d) => !Uri.file(d.path).pathSegments.last.startsWith('.'))) { + yield* _listPackageDirs(subDir); + } + } +} diff --git a/web/charts/common/CHANGELOG.md b/web/charts/common/CHANGELOG.md new file mode 100644 index 000000000..f5597bf94 --- /dev/null +++ b/web/charts/common/CHANGELOG.md @@ -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. This is a breaking change. +* BarTargetLineRendererConfig is no longer default of type String, please change current usage to BarTargetLineRendererConfig. 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()'. +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. diff --git a/web/charts/common/LICENSE b/web/charts/common/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/web/charts/common/LICENSE @@ -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. diff --git a/web/charts/common/README.md b/web/charts/common/README.md new file mode 100644 index 000000000..f7e5eee6d --- /dev/null +++ b/web/charts/common/README.md @@ -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. diff --git a/web/charts/common/lib/common.dart b/web/charts/common/lib/common.dart new file mode 100644 index 000000000..12de87b2b --- /dev/null +++ b/web/charts/common/lib/common.dart @@ -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; diff --git a/web/charts/common/lib/src/chart/bar/bar_chart.dart b/web/charts/common/lib/src/chart/bar/bar_chart.dart new file mode 100644 index 000000000..791572f78 --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/bar_chart.dart @@ -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 disjointMeasureAxes}) + : super( + vertical: vertical, + layoutConfig: layoutConfig, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes); + + @override + SeriesRenderer makeDefaultRenderer() { + return new BarRenderer() + ..rendererId = SeriesRenderer.defaultRendererId; + } +} diff --git a/web/charts/common/lib/src/chart/bar/bar_label_decorator.dart b/web/charts/common/lib/src/chart/bar/bar_label_decorator.dart new file mode 100644 index 000000000..41d381561 --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/bar_label_decorator.dart @@ -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 extends BarRendererDecorator { + // 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> 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 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, +} diff --git a/web/charts/common/lib/src/chart/bar/bar_lane_renderer.dart b/web/charts/common/lib/src/chart/bar/bar_lane_renderer.dart new file mode 100644 index 000000000..072922a6b --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/bar_lane_renderer.dart @@ -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('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 extends BarRenderer { + 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>>(); + + /// 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(); + + 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> seriesList) { + super.preprocessSeries(seriesList); + + _allMeasuresForDomainNullMap.clear(); + + seriesList.forEach((MutableSeries series) { + final domainFn = series.domainFn; + final measureFn = series.rawMeasureFn; + + final domainValues = new Set(); + + 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> seriesList, bool isAnimatingThisDraw) { + super.update(seriesList, isAnimatingThisDraw); + + // Add gray bars to render under every bar stack. + seriesList.forEach((ImmutableSeries series) { + Set domainValues = series.getAttr(domainValuesKey) as Set; + + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + 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.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, () => >[]); + + // 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(), + 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(), + 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; + final measureAxis = + seriesList[0].getAttr(measureAxisKey) as ImmutableAxis; + + 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.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, () => >[]); + + // 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(), + 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(), + 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 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> 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> barElements = barStack + .map((AnimatedBar animatingBar) => + animatingBar.getCurrentBar(animationPercent)) + .toList(); + + paintBar(canvas, animationPercent, barElements); + }); + + super.paint(canvas, animationPercent); + } +} diff --git a/web/charts/common/lib/src/chart/bar/bar_lane_renderer_config.dart b/web/charts/common/lib/src/chart/bar/bar_lane_renderer_config.dart new file mode 100644 index 000000000..3f6795944 --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/bar_lane_renderer_config.dart @@ -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 { + /// 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 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 build() { + return new BarLaneRenderer( + 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; + } +} diff --git a/web/charts/common/lib/src/chart/bar/bar_renderer.dart b/web/charts/common/lib/src/chart/bar/bar_renderer.dart new file mode 100644 index 000000000..92096ce1d --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/bar_renderer.dart @@ -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 + extends BaseBarRenderer, AnimatedBar> { + /// 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> seriesList) { + assignMissingColors(getOrderedSeriesList(seriesList), + emptyCategoryUsesSinglePalette: true); + } + + DatumDetails addPositionToDetailsForSeriesDatum( + DatumDetails details, SeriesDatum seriesDatum) { + final series = details.series; + + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + + 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 chartPosition; + + if (renderingVertically) { + chartPosition = new Point( + (bounds.left + (bounds.width / 2)).toDouble(), bounds.top.toDouble()); + } else { + chartPosition = new Point( + isRtl ? bounds.left.toDouble() : bounds.right.toDouble(), + (bounds.top + (bounds.height / 2)).toDouble()); + } + + return new DatumDetails.from(details, chartPosition: chartPosition); + } + + @override + BarRendererElement getBaseDetails(dynamic datum, int index) { + return new BarRendererElement(); + } + + 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 makeAnimatedBar( + {String key, + ImmutableSeries series, + List dashPattern, + dynamic datum, + Color color, + BarRendererElement details, + D domainValue, + ImmutableAxis domainAxis, + int domainWidth, + num measureValue, + num measureOffsetValue, + ImmutableAxis measureAxis, + double measureAxisPosition, + Color fillColor, + FillPatternType fillPattern, + double strokeWidthPx, + int barGroupIndex, + double previousBarGroupWeight, + double barGroupWeight, + int numBarGroups, + bool measureIsNull, + bool measureIsNegative}) { + return new AnimatedBar( + 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 makeBarRendererElement( + {Color color, + List dashPattern, + BarRendererElement details, + D domainValue, + ImmutableAxis domainAxis, + int domainWidth, + num measureValue, + num measureOffsetValue, + ImmutableAxis measureAxis, + double measureAxisPosition, + Color fillColor, + FillPatternType fillPattern, + double strokeWidthPx, + int barGroupIndex, + double previousBarGroupWeight, + double barGroupWeight, + int numBarGroups, + bool measureIsNull, + bool measureIsNegative}) { + return new BarRendererElement() + ..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> barElements) { + final bars = []; + + // 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( + bar.bounds.left, + max( + 0, + bar.bounds.top + + (measureIsNegative ? _stackedBarPadding : 0)), + bar.bounds.width, + max(0, bar.bounds.height - _stackedBarPadding), + ) + : new Rectangle( + 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 _getBarStackBounds(Rectangle 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 _getBarBounds( + D domainValue, + ImmutableAxis domainAxis, + int domainWidth, + num measureValue, + num measureOffsetValue, + ImmutableAxis 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 bounds; + if (this.renderingVertically) { + // Rectangle clamps to zero width/height + bounds = new Rectangle(domainStart, measureEnd, + domainEnd - domainStart, measureStart - measureEnd); + } else { + // Rectangle clamps to zero width/height + bounds = new Rectangle(min(measureStart, measureEnd), domainStart, + (measureEnd - measureStart).abs(), domainEnd - domainStart); + } + return bounds; + } + + @override + Rectangle getBoundsForBar(BarRendererElement bar) => bar.bounds; +} + +abstract class ImmutableBarRendererElement { + ImmutableSeries get series; + + dynamic get datum; + + int get index; + + Rectangle get bounds; +} + +class BarRendererElement extends BaseBarRendererElement + implements ImmutableBarRendererElement { + ImmutableSeries series; + Rectangle 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(left.round(), top.round(), + (right - left).round(), (bottom - top).round()); + + roundPx = localTarget.roundPx; + + super.updateAnimationPercent(previous, target, animationPercent); + } +} + +class AnimatedBar extends BaseAnimatedBar> { + AnimatedBar( + {@required String key, + @required dynamic datum, + @required ImmutableSeries 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( + localTarget.bounds.left + (localTarget.bounds.width / 2).round(), + localTarget.measureAxisPosition.round(), + 0, + 0); + } + + BarRendererElement getCurrentBar(double animationPercent) { + final BarRendererElement 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 clone(BarRendererElement bar) => + new BarRendererElement.clone(bar); +} diff --git a/web/charts/common/lib/src/chart/bar/bar_renderer_config.dart b/web/charts/common/lib/src/chart/bar/bar_renderer_config.dart new file mode 100644 index 000000000..daf770d8e --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/bar_renderer_config.dart @@ -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 extends BaseBarRendererConfig { + /// 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 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 build() { + return new BarRenderer(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); +} diff --git a/web/charts/common/lib/src/chart/bar/bar_renderer_decorator.dart b/web/charts/common/lib/src/chart/bar/bar_renderer_decorator.dart new file mode 100644 index 000000000..ecf551528 --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/bar_renderer_decorator.dart @@ -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 { + const BarRendererDecorator(); + + void decorate(Iterable> barElements, + ChartCanvas canvas, GraphicsFactory graphicsFactory, + {@required Rectangle drawBounds, + @required double animationPercent, + @required bool renderingVertically, + bool rtl = false}); +} diff --git a/web/charts/common/lib/src/chart/bar/bar_target_line_renderer.dart b/web/charts/common/lib/src/chart/bar/bar_target_line_renderer.dart new file mode 100644 index 000000000..a43ee3ee3 --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/bar_target_line_renderer.dart @@ -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 extends BaseBarRenderer> { + /// 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 config, + String rendererId = 'barTargetLine'}) { + config ??= new BarTargetLineRendererConfig(); + return new BarTargetLineRenderer._internal( + config: config, rendererId: rendererId); + } + + BarTargetLineRenderer._internal( + {BarTargetLineRendererConfig config, String rendererId}) + : super( + config: config, + rendererId: rendererId, + layoutPaintOrder: config.layoutPaintOrder); + + @override + void configureSeries(List> seriesList) { + seriesList.forEach((MutableSeries series) { + series.colorFn ??= (_) => _color; + series.fillColorFn ??= (_) => _color; + }); + } + + DatumDetails addPositionToDetailsForSeriesDatum( + DatumDetails details, SeriesDatum seriesDatum) { + final series = details.series; + + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + + 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 chartPosition; + + if (renderingVertically) { + chartPosition = new Point( + (points[0].x + (points[1].x - points[0].x) / 2).toDouble(), + points[0].y.toDouble()); + } else { + chartPosition = new Point(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 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 makeAnimatedBar( + {String key, + ImmutableSeries series, + dynamic datum, + Color color, + List dashPattern, + _BarTargetLineRendererElement details, + D domainValue, + ImmutableAxis domainAxis, + int domainWidth, + num measureValue, + num measureOffsetValue, + ImmutableAxis 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 dashPattern, + _BarTargetLineRendererElement details, + D domainValue, + ImmutableAxis domainAxis, + int domainWidth, + num measureValue, + num measureOffsetValue, + ImmutableAxis 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> _getTargetLinePoints( + D domainValue, + ImmutableAxis domainAxis, + int domainWidth, + num measureValue, + num measureOffsetValue, + ImmutableAxis 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 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> points; + if (renderingVertically) { + points = [ + new Point(domainStart, measureStart), + new Point(domainEnd, measureStart) + ]; + } else { + points = [ + new Point(measureStart, domainStart), + new Point(measureStart, domainEnd) + ]; + } + return points; + } + + @override + Rectangle getBoundsForBar(_BarTargetLineRendererElement bar) { + final points = bar.points; + int top; + int bottom; + int left; + int right; + points.forEach((Point 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(left, top, right - left, bottom - top); + } +} + +class _BarTargetLineRendererElement extends BaseBarRendererElement { + List> points; + bool roundEndCaps; + + _BarTargetLineRendererElement(); + + _BarTargetLineRendererElement.clone(_BarTargetLineRendererElement other) + : super.clone(other) { + points = new List>.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 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 previousPoint; + if (previousPoints.length - 1 >= pointIndex) { + previousPoint = previousPoints[pointIndex]; + lastPoint = previousPoint; + } else { + previousPoint = new Point(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(x.round(), y.round()); + } else { + points.add(new Point(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 + extends BaseAnimatedBar { + _AnimatedBarTargetLine( + {@required String key, + @required dynamic datum, + @required ImmutableSeries series, + @required D domainValue}) + : super(key: key, datum: datum, series: series, domainValue: domainValue); + + @override + animateElementToMeasureAxisPosition(BaseBarRendererElement target) { + final _BarTargetLineRendererElement localTarget = target; + + final newPoints = >[]; + for (var index = 0; index < localTarget.points.length; index++) { + final targetPoint = localTarget.points[index]; + + newPoints.add(new Point( + targetPoint.x, localTarget.measureAxisPosition.round())); + } + localTarget.points = newPoints; + } + + @override + _BarTargetLineRendererElement clone(_BarTargetLineRendererElement bar) => + new _BarTargetLineRendererElement.clone(bar); +} diff --git a/web/charts/common/lib/src/chart/bar/bar_target_line_renderer_config.dart b/web/charts/common/lib/src/chart/bar/bar_target_line_renderer_config.dart new file mode 100644 index 000000000..c1e011bfc --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/bar_target_line_renderer_config.dart @@ -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 extends BaseBarRendererConfig { + /// 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 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 weightPattern}) + : super( + customRendererId: customRendererId, + dashPattern: dashPattern, + groupingType: groupingType, + layoutPaintOrder: layoutPaintOrder, + minBarLengthPx: minBarLengthPx, + strokeWidthPx: strokeWidthPx, + symbolRenderer: symbolRenderer ?? new LineSymbolRenderer(), + weightPattern: weightPattern, + ); + + @override + BarTargetLineRenderer build() { + return new BarTargetLineRenderer( + 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; + } +} diff --git a/web/charts/common/lib/src/chart/bar/base_bar_renderer.dart b/web/charts/common/lib/src/chart/bar/base_bar_renderer.dart new file mode 100644 index 000000000..63e90a8f4 --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/base_bar_renderer.dart @@ -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('BarRenderer.barGroupIndex'); + +const barGroupCountKey = const AttributeKey('BarRenderer.barGroupCount'); + +const barGroupWeightKey = + const AttributeKey('BarRenderer.barGroupWeight'); + +const previousBarGroupWeightKey = + const AttributeKey('BarRenderer.previousBarGroupWeight'); + +const stackKeyKey = const AttributeKey('BarRenderer.stackKey'); + +const barElementsKey = + const AttributeKey>('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> extends BaseCartesianRenderer { + final BaseBarRendererConfig config; + + @protected + BaseChart 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>(); + + // 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 = []; + + /// Stores a list of stack keys for each group key. + final _currentGroupsStackKeys = new LinkedHashMap>(); + + /// Optimization for getNearest to avoid scanning all data if possible. + ImmutableAxis _prevDomainAxis; + + BaseBarRenderer( + {@required this.config, String rendererId, int layoutPaintOrder}) + : super( + rendererId: rendererId, + layoutPaintOrder: layoutPaintOrder, + symbolRenderer: + config?.symbolRenderer ?? new RoundedRectSymbolRenderer(), + ); + + @override + void preprocessSeries(List> 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 series) { + var elements = []; + + 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 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 _calculateBarWeights(int numBarGroups) { + // Set up bar weights for each series as a ratio of the total weight. + final weights = []; + + 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> 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> seriesList, bool isAnimatingThisDraw) { + _currentKeys.clear(); + _currentGroupsStackKeys.clear(); + + final orderedSeriesList = getOrderedSeriesList(seriesList); + + orderedSeriesList.forEach((final ImmutableSeries series) { + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final domainFn = series.domainFn; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + 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()) + .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 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 series, + dynamic datum, + int barGroupIndex, + double previousBarGroupWeight, + double barGroupWeight, + Color color, + List dashPattern, + R details, + D domainValue, + ImmutableAxis domainAxis, + int domainWidth, + num measureValue, + num measureOffsetValue, + ImmutableAxis 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 dashPattern, + R details, + D domainValue, + ImmutableAxis domainAxis, + int domainWidth, + num measureValue, + num measureOffsetValue, + ImmutableAxis measureAxis, + double measureAxisPosition, + int numBarGroups, + Color fillColor, + FillPatternType fillPattern, + double strokeWidthPx, + bool measureIsNull, + bool measureIsNegative}); + + @override + void onAttach(BaseChart 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(); + + _barStackMap.forEach((String key, List 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 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 barElements); + + @override + List> getNearestDatumDetailPerSeries( + Point chartPoint, bool byDomain, Rectangle boundsOverride) { + var nearest = >[]; + + // 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 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 ??= >[]; + + // Note: the details are already sorted by domain & measure distance in + // base chart. + return nearest; + } + + Rectangle getBoundsForBar(R bar); + + @protected + List> _getSegmentsForDomainValue(D domainValue, + {bool where(BaseAnimatedBar bar)}) { + final matchingSegments = >[]; + + // [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> _getVerticalDetailsForDomainValue( + D domainValue, Point chartPoint) { + return new List>.from(_getSegmentsForDomainValue( + domainValue, + where: (BaseAnimatedBar bar) => !bar.series.overlaySeries) + .map>((BaseAnimatedBar 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( + clamp(chartPoint.x, barBounds.left, barBounds.right).toDouble(), + clamp(chartPoint.y, barBounds.top, barBounds.bottom).toDouble()); + + final relativeDistance = chartPoint.distanceTo(nearestPoint); + + return new DatumDetails( + series: bar.series, + datum: bar.datum, + domain: bar.domainValue, + domainDistance: segmentDomainDistance, + measureDistance: segmentMeasureDistance, + relativeDistance: relativeDistance, + ); + })); + } + + List> _getHorizontalDetailsForDomainValue( + D domainValue, Point chartPoint) { + return new List>.from(_getSegmentsForDomainValue( + domainValue, + where: (BaseAnimatedBar bar) => !bar.series.overlaySeries) + .map((BaseAnimatedBar 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( + 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 getOrderedSeriesList( + List 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 extends Iterable { + final List seriesList; + + _ReversedSeriesIterable(this.seriesList); + + @override + Iterator 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 extends Iterator { + final List _list; + final _visitIndex = []; + int _current; + + _ReversedSeriesIterator(List list) : _list = list { + // In the order of the list, save the category and the indices of the series + // with the same category. + final categoryAndSeriesIndexMap = >{}; + for (var i = 0; i < list.length; i++) { + categoryAndSeriesIndexMap + .putIfAbsent(list[i].seriesCategory, () => []) + .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]]; +} diff --git a/web/charts/common/lib/src/chart/bar/base_bar_renderer_config.dart b/web/charts/common/lib/src/chart/bar/base_bar_renderer_config.dart new file mode 100644 index 000000000..a9ae3334a --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/base_bar_renderer_config.dart @@ -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 extends LayoutViewConfig + implements SeriesRendererConfig { + final String customRendererId; + + final SymbolRenderer symbolRenderer; + + /// Dash pattern for the stroke line around the edges of the bar. + final List 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 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 } diff --git a/web/charts/common/lib/src/chart/bar/base_bar_renderer_element.dart b/web/charts/common/lib/src/chart/bar/base_bar_renderer_element.dart new file mode 100644 index 000000000..d49140fbb --- /dev/null +++ b/web/charts/common/lib/src/chart/bar/base_bar_renderer_element.dart @@ -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 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 { + final String key; + dynamic datum; + ImmutableSeries 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); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/axis.dart b/web/charts/common/lib/src/chart/cartesian/axis/axis.dart new file mode 100644 index 000000000..a76dcf5a1 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/axis.dart @@ -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('Axis.measureAxisId'); +const measureAxisKey = const AttributeKey('Axis.measureAxis'); +const domainAxisKey = const AttributeKey('Axis.domainAxis'); + +/// Orientation of an Axis. +enum AxisOrientation { top, right, bottom, left } + +abstract class ImmutableAxis { + /// 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 extends ImmutableAxis implements LayoutView { + static const primaryMeasureAxisId = 'primaryMeasureAxisId'; + static const secondaryMeasureAxisId = 'secondaryMeasureAxisId'; + + final MutableScale _scale; + + /// [Scale] of this axis. + @protected + MutableScale get scale => _scale; + + /// Previous [Scale] of this axis, used to calculate tick animation. + MutableScale _previousScale; + + TickProvider _tickProvider; + + /// [TickProvider] for this axis. + TickProvider get tickProvider => _tickProvider; + + set tickProvider(TickProvider tickProvider) { + _tickProvider = tickProvider; + } + + /// [TickFormatter] for this axis. + TickFormatter _tickFormatter; + + set tickFormatter(TickFormatter formatter) { + if (_tickFormatter != formatter) { + _tickFormatter = formatter; + _formatterValueCache.clear(); + } + } + + TickFormatter get tickFormatter => _tickFormatter; + final _formatterValueCache = {}; + + /// [TickDrawStrategy] for this axis. + TickDrawStrategy 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 _providedTicks; + + /// Ticks used by the axis for drawing. + final _axisTicks = >[]; + + Rectangle _componentBounds; + Rectangle _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 tickProvider, + TickFormatter tickFormatter, + MutableScale scale}) + : this._scale = scale, + this._tickProvider = tickProvider, + this._tickFormatter = tickFormatter; + + @protected + MutableScale 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 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(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 componentBounds, Rectangle 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 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 { + NumericAxis({TickProvider 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 { + 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 componentBounds, Rectangle 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 { + final Axis _axis; + + AxisTester(this._axis); + + List> get axisTicks => _axis._axisTicks; + + MutableScale get scale => _axis._scale; + + List get axisValues => axisTicks.map((t) => t.value).toList(); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/axis_tick.dart b/web/charts/common/lib/src/chart/cartesian/axis/axis_tick.dart new file mode 100644 index 000000000..58f60a533 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/axis_tick.dart @@ -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 extends Tick implements Comparable> { + /// 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 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 other) { + return _targetLocation.compareTo(other._targetLocation); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/collision_report.dart b/web/charts/common/lib/src/chart/cartesian/axis/collision_report.dart new file mode 100644 index 000000000..82141c414 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/collision_report.dart @@ -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 ticks; + + final bool alternateTicksUsed; + + CollisionReport( + {@required this.ticksCollide, + @required this.ticks, + bool alternateTicksUsed}) + : alternateTicksUsed = alternateTicksUsed ?? false; + + CollisionReport.empty() + : ticksCollide = false, + ticks = [], + alternateTicksUsed = false; +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart new file mode 100644 index 000000000..64d54d011 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart @@ -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 implements RenderSpec { + 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 implements TickDrawStrategy { + 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> ticks) { + for (Tick 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> 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> 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> 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 axisBounds) { + Point start; + Point 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 tick, + {@required AxisOrientation orientation, + @required Rectangle axisBounds, + @required Rectangle 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, +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/gridline_draw_strategy.dart b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/gridline_draw_strategy.dart new file mode 100644 index 000000000..b94ffc75b --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/gridline_draw_strategy.dart @@ -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 extends SmallTickRendererSpec { + 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 createDrawStrategy( + ChartContext context, GraphicsFactory graphicsFactory) => + new GridlineTickDrawStrategy(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 extends BaseTickDrawStrategy { + 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 tick, + {@required AxisOrientation orientation, + @required Rectangle axisBounds, + @required Rectangle drawAreaBounds, + @required bool isFirst, + @required bool isLast}) { + Point lineStart; + Point 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); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/none_draw_strategy.dart b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/none_draw_strategy.dart new file mode 100644 index 000000000..781bcc806 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/none_draw_strategy.dart @@ -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 extends RenderSpec { + final LineStyleSpec axisLineStyle; + + const NoneRenderSpec({this.axisLineStyle}); + + @override + TickDrawStrategy createDrawStrategy( + ChartContext context, GraphicsFactory graphicFactory) => + new NoneDrawStrategy(context, graphicFactory, + axisLineStyleSpec: axisLineStyle); + + @override + bool operator ==(Object other) => + identical(this, other) || other is NoneRenderSpec; + + @override + int get hashCode => 0; +} + +class NoneDrawStrategy implements TickDrawStrategy { + 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 ticks, AxisOrientation orientation) => + new CollisionReport(ticksCollide: false, ticks: ticks); + + @override + void decorateTicks(List 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 axisBounds) { + Point start; + Point 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 tick, + {@required AxisOrientation orientation, + @required Rectangle axisBounds, + @required Rectangle drawAreaBounds, + @required bool isFirst, + @required bool isLast}) {} + + @override + ViewMeasuredSizes measureHorizontallyDrawnTicks( + List ticks, int maxWidth, int maxHeight) { + return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0); + } + + @override + ViewMeasuredSizes measureVerticallyDrawnTicks( + List ticks, int maxWidth, int maxHeight) { + return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/small_tick_draw_strategy.dart b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/small_tick_draw_strategy.dart new file mode 100644 index 000000000..b7db57e45 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/small_tick_draw_strategy.dart @@ -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 extends BaseRenderSpec { + 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 createDrawStrategy( + ChartContext context, GraphicsFactory graphicsFactory) => + new SmallTickDrawStrategy(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 extends BaseTickDrawStrategy { + 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 tick, + {@required AxisOrientation orientation, + @required Rectangle axisBounds, + @required Rectangle drawAreaBounds, + @required bool isFirst, + @required bool isLast}) { + Point tickStart; + Point 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); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/tick_draw_strategy.dart b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/tick_draw_strategy.dart new file mode 100644 index 000000000..7824fb388 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/draw_strategy/tick_draw_strategy.dart @@ -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 { + /// 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> ticks); + + /// Returns a [CollisionReport] indicating if there are any collisions. + CollisionReport collides(List> ticks, AxisOrientation orientation); + + /// Returns measurement of ticks drawn vertically. + ViewMeasuredSizes measureVerticallyDrawnTicks( + List> ticks, int maxWidth, int maxHeight); + + /// Returns measurement of ticks drawn horizontally. + ViewMeasuredSizes measureHorizontallyDrawnTicks( + List> 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 tick, + {@required AxisOrientation orientation, + @required Rectangle axisBounds, + @required Rectangle drawAreaBounds, + @required bool isFirst, + @required bool isLast}); + + void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation, + Rectangle axisBounds); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/end_points_tick_provider.dart b/web/charts/common/lib/src/chart/cartesian/axis/end_points_tick_provider.dart new file mode 100644 index 000000000..17426f960 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/end_points_tick_provider.dart @@ -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 extends BaseTickProvider { + @override + List> getTicks({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required MutableScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required AxisOrientation orientation, + bool viewportExtensionEnabled = false, + TickHint tickHint, + }) { + final ticks = >[]; + + // 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 tickHint, MutableScale 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 tickHint, MutableScale 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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/linear/bucketing_numeric_axis.dart b/web/charts/common/lib/src/chart/cartesian/axis/linear/bucketing_numeric_axis.dart new file mode 100644 index 000000000..d138b372e --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/linear/bucketing_numeric_axis.dart @@ -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]; + } + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/linear/bucketing_numeric_tick_provider.dart b/web/charts/common/lib/src/chart/cartesian/axis/linear/bucketing_numeric_tick_provider.dart new file mode 100644 index 000000000..36c35f962 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/linear/bucketing_numeric_tick_provider.dart @@ -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> getTicks({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required NumericScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required AxisOrientation orientation, + bool viewportExtensionEnabled = false, + TickHint 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( + 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(>[thresholdTick]); + + // Filter out ticks that sit below the threshold. + ticks.removeWhere((Tick 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 { + /// All values smaller than the threshold will be formatted into an empty + /// string. + num threshold; + + SimpleTickFormatterBase 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); + } + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale.dart b/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale.dart new file mode 100644 index 000000000..54a0289ed --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale.dart @@ -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. +/// +///

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. +/// +///

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. +/// +///

[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. +/// +///

[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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale_domain_info.dart b/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale_domain_info.dart new file mode 100644 index 000000000..b1836c47b --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale_domain_info.dart @@ -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); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale_function.dart b/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale_function.dart new file mode 100644 index 000000000..57a659487 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale_function.dart @@ -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. + /// + ///

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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale_viewport.dart b/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale_viewport.dart new file mode 100644 index 000000000..198d06b8e --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/linear/linear_scale_viewport.dart @@ -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); + } + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/numeric_extents.dart b/web/charts/common/lib/src/chart/cartesian/axis/numeric_extents.dart new file mode 100644 index 000000000..f8e4234b1 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/numeric_extents.dart @@ -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 { + 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 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); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/numeric_scale.dart b/web/charts/common/lib/src/chart/cartesian/axis/numeric_scale.dart new file mode 100644 index 000000000..1007a8314 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/numeric_scale.dart @@ -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 { + /// 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); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/numeric_tick_provider.dart b/web/charts/common/lib/src/chart/cartesian/axis/numeric_tick_provider.dart new file mode 100644 index 000000000..02d174696 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/numeric_tick_provider.dart @@ -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 { + /// 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 dataToAxisUnitConverter = + const IdentityConverter(); + + // 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 steps) { + assert(steps != null && steps.isNotEmpty); + steps.sort(); + + final stepSet = new Set.from(steps); + _allowedSteps = new List(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> _getTicksFromHint({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required NumericScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required TickHint 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> getTicks({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required NumericScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required AxisOrientation orientation, + bool viewportExtensionEnabled = false, + TickHint tickHint, + }) { + List> 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 _getTickValues(_TickStepInfo steps, int tickCount) { + final tickValues = new List(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); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/ordinal_extents.dart b/web/charts/common/lib/src/chart/cartesian/axis/ordinal_extents.dart new file mode 100644 index 000000000..8f5c2637e --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/ordinal_extents.dart @@ -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 { + final List _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 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 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); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/ordinal_scale.dart b/web/charts/common/lib/src/chart/cartesian/axis/ordinal_scale.dart new file mode 100644 index 000000000..b5a548d5c --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/ordinal_scale.dart @@ -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 { + /// 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; +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/ordinal_scale_domain_info.dart b/web/charts/common/lib/src/chart/cartesian/axis/ordinal_scale_domain_info.dart new file mode 100644 index 000000000..58963da60 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/ordinal_scale_domain_info.dart @@ -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(); + + /// A list of domain values kept to support [getDomainAtIndex]. + final _domainList = []; + + 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 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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/ordinal_tick_provider.dart b/web/charts/common/lib/src/chart/cartesian/axis/ordinal_tick_provider.dart new file mode 100644 index 000000000..5b5b57750 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/ordinal_tick_provider.dart @@ -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 { + const OrdinalTickProvider(); + + @override + List> getTicks({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required List domainValues, + @required OrdinalScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required AxisOrientation orientation, + bool viewportExtensionEnabled = false, + TickHint 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; +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/scale.dart b/web/charts/common/lib/src/chart/cartesian/axis/scale.dart new file mode 100644 index 000000000..176241808 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/scale.dart @@ -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). +/// +///

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 { + /// 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 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 extends Scale { + /// 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. +/// +///

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. + /// + ///

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 {} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/simple_ordinal_scale.dart b/web/charts/common/lib/src/chart/cartesian/axis/simple_ordinal_scale.dart new file mode 100644 index 000000000..9440c4e81 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/simple_ordinal_scale.dart @@ -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); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/spec/axis_spec.dart b/web/charts/common/lib/src/chart/cartesian/axis/spec/axis_spec.dart new file mode 100644 index 000000000..6127c7a9d --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/spec/axis_spec.dart @@ -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 { + final bool showAxisLine; + final RenderSpec renderSpec; + final TickProviderSpec tickProviderSpec; + final TickFormatterSpec tickFormatterSpec; + + const AxisSpec({ + this.renderSpec, + this.tickProviderSpec, + this.tickFormatterSpec, + this.showAxisLine, + }); + + factory AxisSpec.from( + AxisSpec other, { + RenderSpec renderSpec, + TickProviderSpec tickProviderSpec, + TickFormatterSpec tickFormatterSpec, + bool showAxisLine, + }) { + return new AxisSpec( + renderSpec: renderSpec ?? other.renderSpec, + tickProviderSpec: tickProviderSpec ?? other.tickProviderSpec, + tickFormatterSpec: tickFormatterSpec ?? other.tickFormatterSpec, + showAxisLine: showAxisLine ?? other.showAxisLine, + ); + } + + configure( + Axis 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 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 { + TickProvider createTickProvider(ChartContext context); +} + +@immutable +abstract class TickFormatterSpec { + TickFormatter createTickFormatter(ChartContext context); +} + +@immutable +abstract class RenderSpec { + const RenderSpec(); + + TickDrawStrategy 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 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, +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/spec/bucketing_axis_spec.dart b/web/charts/common/lib/src/chart/cartesian/axis/spec/bucketing_axis_spec.dart new file mode 100644 index 000000000..abdf4ac4b --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/spec/bucketing_axis_spec.dart @@ -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 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 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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/spec/date_time_axis_spec.dart b/web/charts/common/lib/src/chart/cartesian/axis/spec/date_time_axis_spec.dart new file mode 100644 index 000000000..8c9969dd8 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/spec/date_time_axis_spec.dart @@ -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 { + /// 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 + /// 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 renderSpec, + DateTimeTickProviderSpec tickProviderSpec, + DateTimeTickFormatterSpec tickFormatterSpec, + bool showAxisLine, + this.viewport, + }) : super( + renderSpec: renderSpec, + tickProviderSpec: tickProviderSpec, + tickFormatterSpec: tickFormatterSpec, + showAxisLine: showAxisLine); + + @override + configure(Axis axis, ChartContext context, + GraphicsFactory graphicsFactory) { + super.configure(axis, context, graphicsFactory); + + if (axis is DateTimeAxis && viewport != null) { + axis.setScaleViewport(viewport); + } + } + + Axis 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 {} + +abstract class DateTimeTickFormatterSpec extends TickFormatterSpec {} + +/// [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 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 createTickProvider(ChartContext context) { + return new EndPointsTickProvider(); + } + + @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> tickSpecs; + + const StaticDateTimeTickProviderSpec(this.tickSpecs); + + @override + StaticTickProvider createTickProvider(ChartContext context) => + new StaticTickProvider(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 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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/spec/end_points_time_axis_spec.dart b/web/charts/common/lib/src/chart/cartesian/axis/spec/end_points_time_axis_spec.dart new file mode 100644 index 000000000..706c38973 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/spec/end_points_time_axis_spec.dart @@ -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 + /// 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 renderSpec, + DateTimeTickProviderSpec tickProviderSpec, + DateTimeTickFormatterSpec tickFormatterSpec, + bool showAxisLine, + DateTimeExtents viewport, + bool usingBarRenderer = false, + }) : super( + renderSpec: renderSpec ?? + const SmallTickRendererSpec( + 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)); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/spec/numeric_axis_spec.dart b/web/charts/common/lib/src/chart/cartesian/axis/spec/numeric_axis_spec.dart new file mode 100644 index 000000000..cb8ad80fe --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/spec/numeric_axis_spec.dart @@ -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 { + /// 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 + /// 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 renderSpec, + NumericTickProviderSpec tickProviderSpec, + NumericTickFormatterSpec tickFormatterSpec, + bool showAxisLine, + this.viewport, + }) : super( + renderSpec: renderSpec, + tickProviderSpec: tickProviderSpec, + tickFormatterSpec: tickFormatterSpec, + showAxisLine: showAxisLine); + + factory NumericAxisSpec.from( + NumericAxisSpec other, { + RenderSpec 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 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 {} + +abstract class NumericTickFormatterSpec extends TickFormatterSpec {} + +@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 createTickProvider(ChartContext context) { + return new EndPointsTickProvider(); + } + + @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> tickSpecs; + + const StaticNumericTickProviderSpec(this.tickSpecs); + + @override + StaticTickProvider createTickProvider(ChartContext context) => + new StaticTickProvider(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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/spec/ordinal_axis_spec.dart b/web/charts/common/lib/src/chart/cartesian/axis/spec/ordinal_axis_spec.dart new file mode 100644 index 000000000..f3443d3f0 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/spec/ordinal_axis_spec.dart @@ -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 { + /// 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 + /// 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 renderSpec, + OrdinalTickProviderSpec tickProviderSpec, + OrdinalTickFormatterSpec tickFormatterSpec, + bool showAxisLine, + this.viewport, + }) : super( + renderSpec: renderSpec, + tickProviderSpec: tickProviderSpec, + tickFormatterSpec: tickFormatterSpec, + showAxisLine: showAxisLine); + + @override + configure(Axis 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 {} + +abstract class OrdinalTickFormatterSpec extends TickFormatterSpec {} + +@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> tickSpecs; + + const StaticOrdinalTickProviderSpec(this.tickSpecs); + + @override + StaticTickProvider createTickProvider(ChartContext context) => + new StaticTickProvider(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; +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/spec/percent_axis_spec.dart b/web/charts/common/lib/src/chart/cartesian/axis/spec/percent_axis_spec.dart new file mode 100644 index 000000000..d58000dbd --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/spec/percent_axis_spec.dart @@ -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 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); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/spec/tick_spec.dart b/web/charts/common/lib/src/chart/cartesian/axis/spec/tick_spec.dart new file mode 100644 index 000000000..1f81c05ba --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/spec/tick_spec.dart @@ -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 { + 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}); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/static_tick_provider.dart b/web/charts/common/lib/src/chart/cartesian/axis/static_tick_provider.dart new file mode 100644 index 000000000..f3fa0af6b --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/static_tick_provider.dart @@ -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 extends TickProvider { + final List> tickSpec; + + StaticTickProvider(this.tickSpec); + + @override + List> getTicks({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required MutableScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required AxisOrientation orientation, + bool viewportExtensionEnabled = false, + TickHint tickHint, + }) { + final ticks = >[]; + + bool allTicksHaveLabels = true; + + for (TickSpec 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 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( + 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; +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/tick.dart b/web/charts/common/lib/src/chart/cartesian/axis/tick.dart new file mode 100644 index 000000000..907f2f6b9 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/tick.dart @@ -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 { + /// 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)'; +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/tick_formatter.dart b/web/charts/common/lib/src/chart/cartesian/axis/tick_formatter.dart new file mode 100644 index 000000000..5698cc065 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/tick_formatter.dart @@ -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 { + const TickFormatter(); + + /// Formats a list of tick values. + List format(List tickValues, Map cache, {num stepSize}); +} + +abstract class SimpleTickFormatterBase implements TickFormatter { + const SimpleTickFormatterBase(); + + @override + List format(List tickValues, Map 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 { + 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 { + 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; +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/tick_provider.dart b/web/charts/common/lib/src/chart/cartesian/axis/tick_provider.dart new file mode 100644 index 000000000..9a6f3ed75 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/tick_provider.dart @@ -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 { + /// 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> getTicks({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required covariant MutableScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required AxisOrientation orientation, + bool viewportExtensionEnabled = false, + TickHint tickHint, + }); +} + +/// A base tick provider. +abstract class BaseTickProvider implements TickProvider { + const BaseTickProvider(); + + /// Create ticks from [domainValues]. + List> createTicks( + List domainValues, { + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required MutableScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + num stepSize, + }) { + final ticks = >[]; + 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 { + /// 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}); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/auto_adjusting_date_time_tick_provider.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/auto_adjusting_date_time_tick_provider.dart new file mode 100644 index 000000000..2fbdc4fb2 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/auto_adjusting_date_time_tick_provider.dart @@ -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 { + /// List of tick providers to be selected from. + final List _potentialTickProviders; + + AutoAdjustingDateTimeTickProvider._internal( + List 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 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> getTicks({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required DateTimeScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required AxisOrientation orientation, + bool viewportExtensionEnabled = false, + TickHint tickHint, + }) { + List 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 >[]; + } + + /// Find the closest tick provider based on the tick hint. + TimeRangeTickProvider _getClosestTickProvider(TickHint 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)); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/base_time_stepper.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/base_time_stepper.dart new file mode 100644 index 000000000..7bba76d31 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/base_time_stepper.dart @@ -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); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_axis.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_axis.dart new file mode 100644 index 000000000..6d32958c2 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_axis.dart @@ -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 { + 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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_extents.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_extents.dart new file mode 100644 index 000000000..f69875c09 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_extents.dart @@ -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 { + 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)); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_scale.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_scale.dart new file mode 100644 index 000000000..f0312a078 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_scale.dart @@ -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 { + 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; +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_tick_formatter.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_tick_formatter.dart new file mode 100644 index 000000000..57408965c --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/date_time_tick_formatter.dart @@ -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 { + 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 _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 overrides}) { + final Map 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 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 format(List tickValues, Map cache, + {@required num stepSize}) { + final tickLabels = []; + 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 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'); + } + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/day_time_stepper.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/day_time_stepper.dart new file mode 100644 index 000000000..efd52c4c5 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/day_time_stepper.dart @@ -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 _allowedTickIncrements; + + DayTimeStepper._internal( + DateTimeFactory dateTimeFactory, List increments) + : _allowedTickIncrements = increments, + super(dateTimeFactory); + + factory DayTimeStepper(DateTimeFactory dateTimeFactory, + {List 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 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); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/hour_tick_formatter.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/hour_tick_formatter.dart new file mode 100644 index 000000000..ef262c3dc --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/hour_tick_formatter.dart @@ -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); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/hour_time_stepper.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/hour_time_stepper.dart new file mode 100644 index 000000000..7e66a1360 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/hour_time_stepper.dart @@ -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 _allowedTickIncrements; + + HourTimeStepper._internal( + DateTimeFactory dateTimeFactory, List increments) + : _allowedTickIncrements = increments, + super(dateTimeFactory); + + factory HourTimeStepper(DateTimeFactory dateTimeFactory, + {List 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 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)); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/minute_time_stepper.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/minute_time_stepper.dart new file mode 100644 index 000000000..da362a2bd --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/minute_time_stepper.dart @@ -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 _allowedTickIncrements; + + MinuteTimeStepper._internal( + DateTimeFactory dateTimeFactory, List increments) + : _allowedTickIncrements = increments, + super(dateTimeFactory); + + factory MinuteTimeStepper(DateTimeFactory dateTimeFactory, + {List 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 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)); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/month_time_stepper.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/month_time_stepper.dart new file mode 100644 index 000000000..86dd78d83 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/month_time_stepper.dart @@ -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 _allowedTickIncrements; + + MonthTimeStepper._internal( + DateTimeFactory dateTimeFactory, List increments) + : _allowedTickIncrements = increments, + super(dateTimeFactory); + + factory MonthTimeStepper(DateTimeFactory dateTimeFactory, + {List 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 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); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/time_range_tick_provider.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/time_range_tick_provider.dart new file mode 100644 index 000000000..b768d4883 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/time_range_tick_provider.dart @@ -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 { + /// 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); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/time_range_tick_provider_impl.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/time_range_tick_provider_impl.dart new file mode 100644 index 000000000..32455af06 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/time_range_tick_provider_impl.dart @@ -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> getTicks({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required DateTimeScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required AxisOrientation orientation, + bool viewportExtensionEnabled = false, + TickHint tickHint, + }) { + List> currentTicks; + final tickValues = []; + 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 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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/time_stepper.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/time_stepper.dart new file mode 100644 index 000000000..480f7284f --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/time_stepper.dart @@ -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 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 { + /// 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; +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/time_tick_formatter.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/time_tick_formatter.dart new file mode 100644 index 000000000..cb13e486d --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/time_tick_formatter.dart @@ -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); +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/time_tick_formatter_impl.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/time_tick_formatter_impl.dart new file mode 100644 index 000000000..b5696d94e --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/time_tick_formatter_impl.dart @@ -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, +} diff --git a/web/charts/common/lib/src/chart/cartesian/axis/time/year_time_stepper.dart b/web/charts/common/lib/src/chart/cartesian/axis/time/year_time_stepper.dart new file mode 100644 index 000000000..e9a39e135 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/axis/time/year_time_stepper.dart @@ -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 _allowedTickIncrements; + + YearTimeStepper._internal( + DateTimeFactory dateTimeFactory, List increments) + : _allowedTickIncrements = increments, + super(dateTimeFactory); + + factory YearTimeStepper(DateTimeFactory dateTimeFactory, + {List 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 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); + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/cartesian_chart.dart b/web/charts/common/lib/src/chart/cartesian/cartesian_chart.dart new file mode 100644 index 000000000..e1c02dfc9 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/cartesian_chart.dart @@ -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 { + NumericCartesianChart( + {bool vertical, + LayoutConfig layoutConfig, + NumericAxis primaryMeasureAxis, + NumericAxis secondaryMeasureAxis, + LinkedHashMap disjointMeasureAxes}) + : super( + vertical: vertical, + layoutConfig: layoutConfig, + domainAxis: new NumericAxis(), + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes); + + @protected + void initDomainAxis() { + _domainAxis.tickDrawStrategy = new SmallTickRendererSpec() + .createDrawStrategy(context, graphicsFactory); + } +} + +class OrdinalCartesianChart extends CartesianChart { + OrdinalCartesianChart( + {bool vertical, + LayoutConfig layoutConfig, + NumericAxis primaryMeasureAxis, + NumericAxis secondaryMeasureAxis, + LinkedHashMap disjointMeasureAxes}) + : super( + vertical: vertical, + layoutConfig: layoutConfig, + domainAxis: new OrdinalAxis(), + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes); + + @protected + void initDomainAxis() { + _domainAxis + ..tickDrawStrategy = new SmallTickRendererSpec() + .createDrawStrategy(context, graphicsFactory); + } +} + +abstract class CartesianChart extends BaseChart { + 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 _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 _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 _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 _newDomainAxisSpec; + + final Axis _primaryMeasureAxis; + + final Axis _secondaryMeasureAxis; + + final LinkedHashMap _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 domainAxis, + NumericAxis primaryMeasureAxis, + NumericAxis secondaryMeasureAxis, + LinkedHashMap 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 ?? {}, + 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() + .createDrawStrategy(context, graphicsFactory); + + _secondaryMeasureAxis.context = context; + _secondaryMeasureAxis.tickDrawStrategy = new GridlineRendererSpec() + .createDrawStrategy(context, graphicsFactory); + + _disjointMeasureAxes.forEach((String axisId, NumericAxis axis) { + axis.context = context; + axis.tickDrawStrategy = + new NoneDrawStrategy(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 createDomainAxisFromSpec(AxisSpec 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 axisSpecs) { + axisSpecs.forEach((String axisId, AxisSpec axisSpec) { + axisSpec.configure( + _disjointMeasureAxes[axisId], context, graphicsFactory); + }); + } + + @override + MutableSeries makeSeries(Series series) { + MutableSeries 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 makeDefaultRenderer() { + return new BarRenderer()..rendererId = SeriesRenderer.defaultRendererId; + } + + @override + Map>> preprocessSeries( + List> 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> 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>> rendererToSeriesList) { + fireOnAxisConfigured(); + + super.onPostLayout(rendererToSeriesList); + } + + /// Returns a list of datum details from selection model of [type]. + @override + List> getDatumDetails(SelectionModelType type) { + final entries = >[]; + + 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( + 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; + } +} diff --git a/web/charts/common/lib/src/chart/cartesian/cartesian_renderer.dart b/web/charts/common/lib/src/chart/cartesian/cartesian_renderer.dart new file mode 100644 index 000000000..4a044e076 --- /dev/null +++ b/web/charts/common/lib/src/chart/cartesian/cartesian_renderer.dart @@ -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 extends SeriesRenderer { + void configureDomainAxes(List> seriesList); + + void configureMeasureAxes(List> seriesList); +} + +abstract class BaseCartesianRenderer extends BaseSeriesRenderer + implements CartesianRenderer { + bool _renderingVertically = true; + + BaseCartesianRenderer( + {@required String rendererId, + @required int layoutPaintOrder, + SymbolRenderer symbolRenderer}) + : super( + rendererId: rendererId, + layoutPaintOrder: layoutPaintOrder, + symbolRenderer: symbolRenderer); + + @override + void onAttach(BaseChart chart) { + super.onAttach(chart); + _renderingVertically = (chart as CartesianChart).vertical; + } + + bool get renderingVertically => _renderingVertically; + + @override + void configureDomainAxes(List> seriesList) { + seriesList.forEach((MutableSeries 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> seriesList) { + seriesList.forEach((MutableSeries 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 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 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 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; + } +} diff --git a/web/charts/common/lib/src/chart/common/base_chart.dart b/web/charts/common/lib/src/chart/common/base_chart.dart new file mode 100644 index 000000000..88e5f4ba6 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/base_chart.dart @@ -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 Function(); + +abstract class BaseChart { + 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> _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> _currentSeriesList; + + Set _usingRenderers = new Set(); + Map>> _rendererToSeriesList; + + final _seriesRenderers = >{}; + + /// Map of named chart behaviors attached to this chart. + final _behaviorRoleMap = >{}; + final _behaviorStack = >[]; + + final _behaviorTappableMap = >{}; + + /// 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 = >{}; + + /// 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 = >[]; + + 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 listener) { + _lifecycleListeners.add(listener); + return listener; + } + + bool removeLifecycleListener(LifecycleListener listener) => + _lifecycleListeners.remove(listener); + + /// Returns MutableSelectionModel for the given type. Lazy creates one upon first + /// request. + MutableSelectionModel getSelectionModel(SelectionModelType type) { + return _selectionModels.putIfAbsent( + type, () => new MutableSelectionModel()); + } + + /// Returns a list of datum details from selection model of [type]. + List> getDatumDetails(SelectionModelType type); + + // + // Renderer methods + // + + set defaultRenderer(SeriesRenderer renderer) { + renderer.rendererId = SeriesRenderer.defaultRendererId; + addSeriesRenderer(renderer); + } + + SeriesRenderer get defaultRenderer => + getSeriesRenderer(SeriesRenderer.defaultRendererId); + + void addSeriesRenderer(SeriesRenderer renderer) { + String rendererId = renderer.rendererId; + + SeriesRenderer previousRenderer = _seriesRenderers[rendererId]; + if (previousRenderer != null) { + removeView(previousRenderer); + previousRenderer.onDetach(this); + } + + addView(renderer); + renderer.onAttach(this); + _seriesRenderers[rendererId] = renderer; + } + + SeriesRenderer getSeriesRenderer(String rendererId) { + SeriesRenderer 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 makeDefaultRenderer(); + + bool pointWithinRenderer(Point 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> getNearestDatumDetailPerSeries( + Point 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 = >[]; + _usingRenderers.forEach((String rendererId) { + details.addAll(getSeriesRenderer(rendererId) + .getNearestDatumDetailPerSeries( + drawAreaPoint, selectNearestByDomain, boundsOverride)); + }); + + details.sort((DatumDetails a, DatumDetails 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> getSelectedDatumDetails( + SelectionModelType selectionModelType) { + final details = >[]; + + 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 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 createBehavior(BehaviorCreator creator) => creator(); + + /// 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 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 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 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 behavior) { + final role = behavior?.role; + if (role != null && _behaviorTappableMap[role] == behavior) { + _behaviorTappableMap.remove(role); + } + } + + /// Returns a list of behaviors that have been added. + List> 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 point) { + return _layoutManager.withinDrawArea(point); + } + + /// Returns the bounds of the chart draw area. + Rectangle 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 get drawableLayoutAreaBounds => + _layoutManager.drawableLayoutAreaBounds; + + // + // Draw methods + // + void draw(List> seriesList) { + // Clear the selection model when [seriesList] changes. + for (final selectionModel in _selectionModels.values) { + selectionModel.clearSelection(notifyListeners: false); + } + + var processedSeriesList = + new List>.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> seriesList, + {bool skipAnimation, bool skipLayout}) { + seriesList = seriesList + .map((MutableSeries series) => new MutableSeries.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> get currentSeriesList => _currentSeriesList; + + MutableSeries makeSeries(Series series) { + final s = new MutableSeries(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> seriesList) { + Map>> 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 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> seriesList) { + getSeriesRenderer(rendererId).configureSeries(seriesList); + }); + } + + /// Preprocess series to allow stacking and other mutations. + /// + /// Build a map of rendererId to series. + Map>> preprocessSeries( + List> seriesList) { + Map>> rendererToSeriesList = {}; + + var unusedRenderers = _usingRenderers; + _usingRenderers = new Set(); + + // Build map of rendererIds to SeriesLists. + seriesList.forEach((MutableSeries 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> seriesList) { + getSeriesRenderer(rendererId).preprocessSeries(seriesList); + }); + + return rendererToSeriesList; + } + + void onSkipLayout() { + onPostLayout(_rendererToSeriesList); + } + + void onPostLayout(Map>> rendererToSeriesList) { + // Update each renderer with + rendererToSeriesList + .forEach((String rendererId, List> 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> seriesList) { + _lifecycleListeners.forEach((LifecycleListener listener) { + if (listener.onData != null) { + listener.onData(seriesList); + } + }); + } + + @protected + fireOnPreprocess(List> seriesList) { + _lifecycleListeners.forEach((LifecycleListener listener) { + if (listener.onPreprocess != null) { + listener.onPreprocess(seriesList); + } + }); + } + + @protected + fireOnPostprocess(List> seriesList) { + _lifecycleListeners.forEach((LifecycleListener listener) { + if (listener.onPostprocess != null) { + listener.onPostprocess(seriesList); + } + }); + } + + @protected + fireOnAxisConfigured() { + _lifecycleListeners.forEach((LifecycleListener listener) { + if (listener.onAxisConfigured != null) { + listener.onAxisConfigured(); + } + }); + } + + @protected + fireOnPostrender(ChartCanvas canvas) { + _lifecycleListeners.forEach((LifecycleListener listener) { + if (listener.onPostrender != null) { + listener.onPostrender(canvas); + } + }); + } + + @protected + fireOnAnimationComplete() { + _lifecycleListeners.forEach((LifecycleListener 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 { + /// 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(List> seriesList); +typedef LifecycleCanvasCallback(ChartCanvas canvas); +typedef LifecycleEmptyCallback(); diff --git a/web/charts/common/lib/src/chart/common/behavior/a11y/a11y_explore_behavior.dart b/web/charts/common/lib/src/chart/common/behavior/a11y/a11y_explore_behavior.dart new file mode 100644 index 000000000..c448de1d9 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/a11y/a11y_explore_behavior.dart @@ -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 implements ChartBehavior { + /// 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 _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 createA11yNodes(); + + @override + void attachTo(BaseChart chart) { + _chart = chart; + chart.addGestureListener(_listener); + } + + @override + void removeFrom(BaseChart chart) { + chart.removeGestureListener(_listener); + } +} diff --git a/web/charts/common/lib/src/chart/common/behavior/a11y/a11y_node.dart b/web/charts/common/lib/src/chart/common/behavior/a11y/a11y_node.dart new file mode 100644 index 000000000..b79ebba80 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/a11y/a11y_node.dart @@ -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 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}); +} diff --git a/web/charts/common/lib/src/chart/common/behavior/a11y/domain_a11y_explore_behavior.dart b/web/charts/common/lib/src/chart/common/behavior/a11y/domain_a11y_explore_behavior.dart new file mode 100644 index 000000000..6232c4436 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/a11y/domain_a11y_explore_behavior.dart @@ -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(List> seriesDatums); + +/// A simple vocalization that returns the domain value to string. +String domainVocalization(List> 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 extends A11yExploreBehavior { + final VocalizationCallback _vocalizationCallback; + LifecycleListener _lifecycleListener; + CartesianChart _chart; + List> _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(onPostprocess: _updateSeriesList); + } + + @override + List createA11yNodes() { + final nodes = <_DomainA11yNode>[]; + + // Update the selection model when the a11y node has focus. + final selectionModel = _chart.getSelectionModel(SelectionModelType.info); + + final domainSeriesDatum = >>{}; + + for (MutableSeries series in _seriesList) { + for (var index = 0; index < series.data.length; index++) { + final datum = series.data[index]; + D domain = series.domainFn(index); + + domainSeriesDatum[domain] ??= >[]; + domainSeriesDatum[domain].add(new SeriesDatum(series, datum)); + } + } + + domainSeriesDatum.forEach((D domain, List> seriesDatums) { + final a11yDescription = _vocalizationCallback(seriesDatums); + + final firstSeries = seriesDatums.first.series; + final domainAxis = firstSeries.getAttr(domainAxisKey) as ImmutableAxis; + 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> seriesList) { + _seriesList = seriesList; + } + + @override + void attachTo(BaseChart 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 chartDrawBounds, + @required bool isRtl, + @required bool renderVertically, + OnFocus onFocus}) { + Rectangle 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 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; + } +} diff --git a/web/charts/common/lib/src/chart/common/behavior/calculation/percent_injector.dart b/web/charts/common/lib/src/chart/common/behavior/calculation/percent_injector.dart new file mode 100644 index 000000000..fd3bb0b21 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/calculation/percent_injector.dart @@ -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('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 implements ChartBehavior { + LifecycleListener _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(onPreprocess: _preProcess, onData: _onData); + } + + @override + void attachTo(BaseChart chart) { + chart.addLifecycleListener(_lifecycleListener); + } + + @override + void removeFrom(BaseChart chart) { + chart.removeLifecycleListener(_lifecycleListener); + } + + /// Resets the state of the behavior when new data is drawn on the chart. + void _onData(List> 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> 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 = {}; + + 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 } diff --git a/web/charts/common/lib/src/chart/common/behavior/chart_behavior.dart b/web/charts/common/lib/src/chart/common/behavior/chart_behavior.dart new file mode 100644 index 000000000..f1460f5f5 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/chart_behavior.dart @@ -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 { + String get role; + + /// Injects the behavior into a chart. + void attachTo(BaseChart chart); + + /// Removes the behavior from a chart. + void removeFrom(BaseChart 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, +} diff --git a/web/charts/common/lib/src/chart/common/behavior/chart_title/chart_title.dart b/web/charts/common/lib/src/chart/common/behavior/chart_title/chart_title.dart new file mode 100644 index 000000000..6df4e7c28 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/chart_title/chart_title.dart @@ -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 implements ChartBehavior { + 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 _chart; + + _ChartTitleLayoutView _view; + + LifecycleListener _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(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 chart) { + _chart = chart; + + _view = new _ChartTitleLayoutView( + layoutPaintOrder: LayoutViewPaintOrder.chartTitle, + config: _config, + chart: _chart); + + chart.addView(_view); + chart.addLifecycleListener(_lifecycleListener); + } + + @override + void removeFrom(BaseChart 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 extends LayoutView { + LayoutViewConfig _layoutConfig; + + LayoutViewConfig get layoutConfig => _layoutConfig; + + /// Stores all of the configured properties of the behavior. + _ChartTitleConfig _config; + + BaseChart chart; + + bool get isRtl => chart?.context?.isRtl ?? false; + + Rectangle _componentBounds; + Rectangle _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 componentBounds, Rectangle 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 _getLabelPosition( + bool isPrimaryTitle, + Rectangle 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 _getHorizontalLabelPosition( + bool isPrimaryTitle, + Rectangle 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(labelX, labelY); + } + + /// Gets the resolved location for a title in the left or right margin. + Point _getVerticalLabelPosition( + bool isPrimaryTitle, + Rectangle 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(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 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, +} diff --git a/web/charts/common/lib/src/chart/common/behavior/domain_highlighter.dart b/web/charts/common/lib/src/chart/common/behavior/domain_highlighter.dart new file mode 100644 index 000000000..1986bc34a --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/domain_highlighter.dart @@ -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 implements ChartBehavior { + final SelectionModelType selectionModelType; + + BaseChart _chart; + + LifecycleListener _lifecycleListener; + + DomainHighlighter([this.selectionModelType = SelectionModelType.info]) { + _lifecycleListener = + new LifecycleListener(onPostprocess: _updateColorFunctions); + } + + void _selectionChanged(SelectionModel selectionModel) { + _chart.redraw(skipLayout: true, skipAnimation: true); + } + + void _updateColorFunctions(List> seriesList) { + SelectionModel selectionModel = + _chart.getSelectionModel(selectionModelType); + seriesList.forEach((MutableSeries 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 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()}'; +} diff --git a/web/charts/common/lib/src/chart/common/behavior/initial_selection.dart b/web/charts/common/lib/src/chart/common/behavior/initial_selection.dart new file mode 100644 index 000000000..17200bf5e --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/initial_selection.dart @@ -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 implements ChartBehavior { + final SelectionModelType selectionModelType; + + /// List of series id of initially selected series. + final List selectedSeriesConfig; + + /// List of [SeriesDatumConfig] that represents the initially selected datums. + final List selectedDataConfig; + + BaseChart _chart; + LifecycleListener _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(onData: _setInitialSelection); + } + + void _setInitialSelection(List> seriesList) { + if (!_firstDraw) { + return; + } + _firstDraw = false; + + final immutableModel = new SelectionModel.fromConfig( + selectedDataConfig, selectedSeriesConfig, seriesList); + + _chart.getSelectionModel(selectionModelType).updateSelection( + immutableModel.selectedDatum, immutableModel.selectedSeries, + notifyListeners: false); + } + + @override + void attachTo(BaseChart chart) { + _chart = chart; + chart.addLifecycleListener(_lifecycleListener); + } + + @override + void removeFrom(BaseChart chart) { + chart.removeLifecycleListener(_lifecycleListener); + _chart = null; + } + + @override + String get role => 'InitialSelection-${selectionModelType.toString()}}'; +} diff --git a/web/charts/common/lib/src/chart/common/behavior/legend/datum_legend.dart b/web/charts/common/lib/src/chart/common/behavior/legend/datum_legend.dart new file mode 100644 index 000000000..277c52587 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/legend/datum_legend.dart @@ -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 extends Legend { + /// Whether or not the series legend should show measures on datum selection. + bool _showMeasures; + + DatumLegend({ + SelectionModelType selectionModelType, + LegendEntryGenerator 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; + } +} diff --git a/web/charts/common/lib/src/chart/common/behavior/legend/legend.dart b/web/charts/common/lib/src/chart/common/behavior/legend/legend.dart new file mode 100644 index 000000000..44c1d16f9 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/legend/legend.dart @@ -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 implements ChartBehavior, LayoutView { + final SelectionModelType selectionModelType; + final legendState = new LegendState(); + final LegendEntryGenerator legendEntryGenerator; + + String _title; + + BaseChart _chart; + LifecycleListener _lifecycleListener; + + Rectangle _componentBounds; + Rectangle _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> _currentSeriesList; + + /// Save this in order to check if series list have changed and regenerate + /// the legend entries. + List> _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> seriesList) {} + + /// Store off a copy of the series list for use when we render the legend. + void _preProcess(List> 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> seriesList) {} + + /// Build LegendEntries from list of series. + void _postProcess(List> 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 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 componentBounds, Rectangle drawAreaBounds) { + _componentBounds = componentBounds; + _drawAreaBounds = drawAreaBounds; + + updateLegend(); + } + + @override + void paint(ChartCanvas canvas, double animationPercent) {} + + @override + Rectangle get componentBounds => _componentBounds; + + @override + bool get isSeriesRenderer => false; + + // Gets the draw area bounds for native legend content to position itself + // accordingly. + Rectangle get drawAreaBounds => _drawAreaBounds; +} + +/// Stores legend data used by native legend content builder. +class LegendState { + List> _legendEntries; + SelectionModel _selectionModel; + + List> 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, +} diff --git a/web/charts/common/lib/src/chart/common/behavior/legend/legend_entry.dart b/web/charts/common/lib/src/chart/common/behavior/legend/legend_entry.dart new file mode 100644 index 000000000..b3824bfd6 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/legend/legend_entry.dart @@ -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 { + final String label; + final ImmutableSeries 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; +} diff --git a/web/charts/common/lib/src/chart/common/behavior/legend/legend_entry_generator.dart b/web/charts/common/lib/src/chart/common/behavior/legend/legend_entry_generator.dart new file mode 100644 index 000000000..fa0a8a7ad --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/legend/legend_entry_generator.dart @@ -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 { + /// Generates a list of legend entries based on the series drawn on the chart. + /// + /// [seriesList] Processed series list. + List> getLegendEntries(List> 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> legendEntries, + SelectionModel selectionModel, List> 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, +} diff --git a/web/charts/common/lib/src/chart/common/behavior/legend/per_datum_legend_entry_generator.dart b/web/charts/common/lib/src/chart/common/behavior/legend/per_datum_legend_entry_generator.dart new file mode 100644 index 000000000..08b9cf5f5 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/legend/per_datum_legend_entry_generator.dart @@ -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 implements LegendEntryGenerator { + TextStyleSpec entryTextStyle; + MeasureFormatter measureFormatter; + MeasureFormatter secondaryMeasureFormatter; + + /// Option for showing measures when there is no selection. + LegendDefaultMeasure legendDefaultMeasure; + + @override + List> getLegendEntries(List> seriesList) { + final legendEntries = >[]; + + final series = seriesList[0]; + for (var i = 0; i < series.data.length; i++) { + legendEntries.add(new LegendEntry( + 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> legendEntries, + SelectionModel selectionModel, List> 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> legendEntries, SelectionModel 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> legendEntries) { + for (LegendEntry 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> legendEntries, List> 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; + } +} diff --git a/web/charts/common/lib/src/chart/common/behavior/legend/per_series_legend_entry_generator.dart b/web/charts/common/lib/src/chart/common/behavior/legend/per_series_legend_entry_generator.dart new file mode 100644 index 000000000..43994c155 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/legend/per_series_legend_entry_generator.dart @@ -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 implements LegendEntryGenerator { + TextStyleSpec entryTextStyle; + MeasureFormatter measureFormatter; + MeasureFormatter secondaryMeasureFormatter; + + /// Option for showing measures when there is no selection. + LegendDefaultMeasure legendDefaultMeasure; + + @override + List> getLegendEntries(List> seriesList) { + final legendEntries = seriesList + .map((series) => new LegendEntry(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> legendEntries, + SelectionModel selectionModel, List> 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> legendEntries, SelectionModel selectionModel) { + // Map of series ID to the total selected measure value for that series. + final seriesAndMeasure = {}; + + // Hash set of series ID's that use the secondary measure axis + final secondaryAxisSeriesIDs = new HashSet(); + + for (SeriesDatum 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> legendEntries) { + for (LegendEntry 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> legendEntries, List> seriesList) { + // Helper function to sum up the measure values + num getMeasureTotal(MutableSeries 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 = {}; + // Map of series ID and the formatted measure for that series. + final seriesAndFormattedMeasure = {}; + + for (MutableSeries 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; + } +} diff --git a/web/charts/common/lib/src/chart/common/behavior/legend/series_legend.dart b/web/charts/common/lib/src/chart/common/behavior/legend/series_legend.dart new file mode 100644 index 000000000..21ca0c38b --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/legend/series_legend.dart @@ -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 extends Legend { + /// List of currently hidden series, by ID. + final _hiddenSeriesList = new Set(); + + /// List of series IDs that should be hidden by default. + List _defaultHiddenSeries; + + /// Whether or not the series legend should show measures on datum selection. + bool _showMeasures; + + SeriesLegend({ + SelectionModelType selectionModelType, + LegendEntryGenerator 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 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 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> seriesList) { + // If a series was removed from the chart, remove it from our current list + // of hidden series. + final seriesIds = seriesList.map((MutableSeries series) => series.id); + + _hiddenSeriesList.removeWhere((String id) => !seriesIds.contains(id)); + } + + @override + void preProcessSeriesList(List> seriesList) { + seriesList.removeWhere((MutableSeries 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); + } +} diff --git a/web/charts/common/lib/src/chart/common/behavior/line_point_highlighter.dart b/web/charts/common/lib/src/chart/common/behavior/line_point_highlighter.dart new file mode 100644 index 000000000..82c01fbad --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/line_point_highlighter.dart @@ -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 implements ChartBehavior { + 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 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 _chart; + + _LinePointLayoutView _view; + + LifecycleListener _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>(); + + // 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 = []; + + LinePointHighlighter( + {SelectionModelType selectionModelType, + double defaultRadiusPx, + double radiusPaddingPx, + LinePointHighlighterFollowLineType showHorizontalFollowLine, + LinePointHighlighterFollowLineType showVerticalFollowLine, + List 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(onAxisConfigured: _updateViewData); + } + + @override + void attachTo(BaseChart chart) { + _chart = chart; + + _view = new _LinePointLayoutView( + 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 = >{}; + + for (DatumDetails detail in selectedDatumDetails) { + if (detail == null) { + continue; + } + + final series = detail.series; + final datum = detail.datum; + + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + + 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 animatingPoint; + if (_seriesPointMap.containsKey(pointKey)) { + animatingPoint = _seriesPointMap[pointKey]; + } else { + // Create a new point and have it animate in from axis. + final point = new _DatumPoint( + datum: datum, + domain: detail.domain, + series: series, + x: domainAxis.getLocation(detail.domain), + y: measureAxis.getLocation(0.0)); + + animatingPoint = new _AnimatedPoint( + key: pointKey, overlaySeries: series.overlaySeries) + ..setNewTarget(new _PointRendererElement() + ..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( + 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() + ..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 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 extends LayoutView { + final LayoutViewConfig layoutConfig; + + final LinePointHighlighterFollowLineType showHorizontalFollowLine; + + final LinePointHighlighterFollowLineType showVerticalFollowLine; + + final BaseChart chart; + + final List dashPattern; + + Rectangle _drawAreaBounds; + + Rectangle 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> _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> 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 componentBounds, Rectangle 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 = []; + + _seriesPointMap.forEach((String key, _AnimatedPoint point) { + if (point.animatingOut) { + keysToRemove.add(key); + } + }); + + keysToRemove.forEach((String key) => _seriesPointMap.remove(key)); + } + + final points = <_PointRendererElement>[]; + _seriesPointMap.forEach((String key, _AnimatedPoint point) { + points.add(point.getCurrentPoint(animationPercent)); + }); + + // Build maps of the position where the follow lines should stop for each + // selected data point. + final endPointPerValueVertical = {}; + final endPointPerValueHorizontal = {}; + + for (_PointRendererElement 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 = []; + final paintedVerticalLinePositions = []; + + final drawBounds = chart.drawableLayoutAreaBounds; + + final rtl = chart.context.isRtl; + + // Draw the follow lines first, below all of the highlight shapes. + for (_PointRendererElement 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(leftBound, pointElement.point.y), + new Point(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(pointElement.point.x, topBound), + new Point( + 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 pointElement in points) { + if (pointElement.point.x == null || pointElement.point.y == null) { + continue; + } + + final bounds = new Rectangle( + 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 get componentBounds => this._drawAreaBounds; + + @override + bool get isSeriesRenderer => false; +} + +class _DatumPoint extends Point { + final dynamic datum; + final D domain; + final ImmutableSeries series; + + _DatumPoint({this.datum, this.domain, this.series, double x, double y}) + : super(x, y); + + factory _DatumPoint.from(_DatumPoint other, [double x, double y]) { + return new _DatumPoint( + datum: other.datum, + domain: other.domain, + series: other.series, + x: x ?? other.x, + y: y ?? other.y); + } +} + +class _PointRendererElement { + _DatumPoint point; + Color color; + Color fillColor; + double radiusPx; + double measureAxisPosition; + double strokeWidthPx; + SymbolRenderer symbolRenderer; + + _PointRendererElement clone() { + return new _PointRendererElement() + ..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.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 { + final String key; + final bool overlaySeries; + + _PointRendererElement _previousPoint; + _PointRendererElement _targetPoint; + _PointRendererElement _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.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 newTarget) { + animatingOut = false; + _currentPoint ??= newTarget.clone(); + _previousPoint = _currentPoint.clone(); + _targetPoint = newTarget; + } + + _PointRendererElement 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 { + final LinePointHighlighter behavior; + + LinePointHighlighterTester(this.behavior); + + int getSelectionLength() { + return behavior._seriesPointMap.length; + } + + bool isDatumSelected(D datum) { + var contains = false; + + behavior._seriesPointMap.forEach((String key, _AnimatedPoint point) { + if (point._currentPoint.point.datum == datum) { + contains = true; + return; + } + }); + + return contains; + } +} diff --git a/web/charts/common/lib/src/chart/common/behavior/range_annotation.dart b/web/charts/common/lib/src/chart/common/behavior/range_annotation.dart new file mode 100644 index 000000000..249205a4f --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/range_annotation.dart @@ -0,0 +1,1317 @@ +// 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 pi, 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/text_element.dart' + show MaxWidthStrategy, TextDirection, TextElement; +import '../../../common/text_style.dart' show TextStyle; +import '../../cartesian/axis/axis.dart' show Axis, ImmutableAxis; +import '../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec; +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 '../chart_canvas.dart' show ChartCanvas, getAnimatedColor; +import '../processed_series.dart' show MutableSeries; +import 'chart_behavior.dart' show ChartBehavior; + +/// Chart behavior that annotates domain ranges with a solid fill color. +/// +/// The annotations will be drawn underneath series data and chart axes. +/// +/// This is typically used for line charts to call out sections of the data +/// range. +/// +/// TODO: Support labels. +class RangeAnnotation implements ChartBehavior { + static const _defaultLabelAnchor = AnnotationLabelAnchor.end; + static const _defaultLabelDirection = AnnotationLabelDirection.auto; + static const _defaultLabelPosition = AnnotationLabelPosition.auto; + static const _defaultLabelPadding = 5; + static final _defaultLabelStyle = + new TextStyleSpec(fontSize: 12, color: Color.black); + static const _defaultStrokeWidthPx = 2.0; + + /// List of annotations to render on the chart. + final List annotations; + + /// Default color for annotations. + final Color defaultColor; + + /// Configures where to anchor annotation label text. + final AnnotationLabelAnchor defaultLabelAnchor; + + /// Direction of label text on the annotations. + final AnnotationLabelDirection defaultLabelDirection; + + /// Configures where to place labels relative to the annotation. + final AnnotationLabelPosition defaultLabelPosition; + + /// Configures the style of label text. + final TextStyleSpec defaultLabelStyleSpec; + + /// Configures the stroke width for line annotations. + final double defaultStrokeWidthPx; + + /// Whether or not the range of the axis should be extended to include the + /// annotation start and end values. + final bool extendAxis; + + /// Space before and after label text. + final int labelPadding; + + CartesianChart _chart; + + _RangeAnnotationLayoutView _view; + + LifecycleListener _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 given to the chart. + final _annotationMap = LinkedHashMap>(); + + // Store a list of annotations that exist in the current annotation list. + // + // This list will be used to remove any [_AnimatedAnnotation] that were + // rendered in previous draw cycles, but no longer have a corresponding datum + // in the new data. + final _currentKeys = []; + + RangeAnnotation(this.annotations, + {Color defaultColor, + AnnotationLabelAnchor defaultLabelAnchor, + AnnotationLabelDirection defaultLabelDirection, + AnnotationLabelPosition defaultLabelPosition, + TextStyleSpec defaultLabelStyleSpec, + bool extendAxis, + int labelPadding, + double defaultStrokeWidthPx}) + : defaultColor = StyleFactory.style.rangeAnnotationColor, + defaultLabelAnchor = defaultLabelAnchor ?? _defaultLabelAnchor, + defaultLabelDirection = defaultLabelDirection ?? _defaultLabelDirection, + defaultLabelPosition = defaultLabelPosition ?? _defaultLabelPosition, + defaultLabelStyleSpec = defaultLabelStyleSpec ?? _defaultLabelStyle, + extendAxis = extendAxis ?? true, + labelPadding = labelPadding ?? _defaultLabelPadding, + defaultStrokeWidthPx = defaultStrokeWidthPx ?? _defaultStrokeWidthPx { + _lifecycleListener = new LifecycleListener( + onPostprocess: _updateAxisRange, onAxisConfigured: _updateViewData); + } + + @override + void attachTo(BaseChart chart) { + if (!(chart is CartesianChart)) { + throw new ArgumentError( + 'RangeAnnotation can only be attached to a CartesianChart'); + } + + _chart = chart; + + _view = new _RangeAnnotationLayoutView( + defaultColor: defaultColor, labelPadding: labelPadding, chart: chart); + + chart.addView(_view); + + chart.addLifecycleListener(_lifecycleListener); + } + + @override + void removeFrom(BaseChart chart) { + chart.removeView(_view); + chart.removeLifecycleListener(_lifecycleListener); + + _view.chart = null; + } + + void _updateAxisRange(List> seriesList) { + // Extend the axis range if enabled. + if (extendAxis) { + final domainAxis = _chart.domainAxis; + + annotations.forEach((AnnotationSegment annotation) { + Axis axis; + + switch (annotation.axisType) { + case RangeAnnotationAxisType.domain: + axis = domainAxis; + break; + + case RangeAnnotationAxisType.measure: + // We expect an empty axisId to get us the primary measure axis. + axis = _chart.getMeasureAxis(axisId: annotation.axisId); + break; + } + + if (annotation is RangeAnnotationSegment) { + axis.addDomainValue(annotation.startValue); + axis.addDomainValue(annotation.endValue); + } else if (annotation is LineAnnotationSegment) { + axis.addDomainValue(annotation.value); + } + }); + } + } + + void _updateViewData() { + _currentKeys.clear(); + + annotations.forEach((AnnotationSegment annotation) { + Axis axis; + + switch (annotation.axisType) { + case RangeAnnotationAxisType.domain: + axis = _chart.domainAxis; + break; + + case RangeAnnotationAxisType.measure: + // We expect an empty axisId to get us the primary measure axis. + axis = _chart.getMeasureAxis(axisId: annotation.axisId); + break; + } + + final key = annotation.key; + + final color = annotation.color ?? defaultColor; + + final startLabel = annotation.startLabel; + final endLabel = annotation.endLabel; + final labelAnchor = annotation.labelAnchor ?? defaultLabelAnchor; + var labelDirection = annotation.labelDirection ?? defaultLabelDirection; + + if (labelDirection == AnnotationLabelDirection.auto) { + switch (annotation.axisType) { + case RangeAnnotationAxisType.domain: + labelDirection = AnnotationLabelDirection.vertical; + break; + + case RangeAnnotationAxisType.measure: + labelDirection = AnnotationLabelDirection.horizontal; + break; + } + } + + final labelPosition = annotation.labelPosition ?? defaultLabelPosition; + final labelStyleSpec = annotation.labelStyleSpec ?? defaultLabelStyleSpec; + + // Add line annotation settings. + final dashPattern = + annotation is LineAnnotationSegment ? annotation.dashPattern : null; + final strokeWidthPx = annotation is LineAnnotationSegment + ? annotation.strokeWidthPx ?? defaultLabelStyleSpec + : 0.0; + + final isRange = annotation is RangeAnnotationSegment; + + // The values can match the data type of the domain (D) or measure axis + // (num). + dynamic startValue; + dynamic endValue; + + if (annotation is RangeAnnotationSegment) { + startValue = annotation.startValue; + endValue = annotation.endValue; + } else if (annotation is LineAnnotationSegment) { + startValue = annotation.value; + endValue = annotation.value; + } + + final annotationDatum = + _getAnnotationDatum(startValue, endValue, axis, annotation.axisType); + + // If we already have a animatingAnnotation for that index, use it. + _AnimatedAnnotation animatingAnnotation; + if (_annotationMap.containsKey(key)) { + animatingAnnotation = _annotationMap[key]; + } else { + // Create a new annotation, positioned at the start and end values. + animatingAnnotation = new _AnimatedAnnotation(key: key) + ..setNewTarget(new _AnnotationElement() + ..annotation = annotationDatum + ..color = color + ..dashPattern = dashPattern + ..startLabel = startLabel + ..endLabel = endLabel + ..isRange = isRange + ..labelAnchor = labelAnchor + ..labelDirection = labelDirection + ..labelPosition = labelPosition + ..labelStyleSpec = labelStyleSpec + ..strokeWidthPx = strokeWidthPx); + + _annotationMap[key] = animatingAnnotation; + } + + // Update the set of annotations that still exist in the series data. + _currentKeys.add(key); + + // Get the annotation element we are going to setup. + final annotationElement = new _AnnotationElement() + ..annotation = annotationDatum + ..color = color + ..dashPattern = dashPattern + ..startLabel = startLabel + ..endLabel = endLabel + ..isRange = isRange + ..labelAnchor = labelAnchor + ..labelDirection = labelDirection + ..labelPosition = labelPosition + ..labelStyleSpec = labelStyleSpec + ..strokeWidthPx = strokeWidthPx; + + animatingAnnotation.setNewTarget(annotationElement); + }); + + // Animate out annotations that don't exist anymore. + _annotationMap.forEach((String key, _AnimatedAnnotation annotation) { + if (_currentKeys.contains(annotation.key) != true) { + annotation.animateOut(); + } + }); + + _view.annotationMap = _annotationMap; + } + + /// Generates a datum that describes an annotation. + /// + /// [startValue] and [endValue] are dynamic because they can be different data + /// types for domain and measure axes, e.g. DateTime and num for a TimeSeries + /// chart. + _DatumAnnotation _getAnnotationDatum(dynamic startValue, dynamic endValue, + ImmutableAxis axis, RangeAnnotationAxisType axisType) { + // Remove floating point rounding errors by rounding to 2 decimal places of + // precision. The difference in the canvas is negligible. + final startPosition = (axis.getLocation(startValue) * 100).round() / 100; + final endPosition = (axis.getLocation(endValue) * 100).round() / 100; + + return new _DatumAnnotation( + startPosition: startPosition, + endPosition: endPosition, + axisType: axisType); + } + + @override + String get role => 'RangeAnnotation'; +} + +class _RangeAnnotationLayoutView extends LayoutView { + final LayoutViewConfig layoutConfig; + + final Color defaultColor; + + final int labelPadding; + + CartesianChart chart; + + bool get isRtl => chart.context.isRtl; + + Rectangle _drawAreaBounds; + + Rectangle get drawBounds => _drawAreaBounds; + + 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> _annotationMap; + + _RangeAnnotationLayoutView({ + @required this.defaultColor, + @required this.labelPadding, + @required this.chart, + }) : this.layoutConfig = new LayoutViewConfig( + paintOrder: LayoutViewPaintOrder.rangeAnnotation, + position: LayoutPosition.DrawArea, + positionOrder: LayoutViewPositionOrder.drawArea); + + set annotationMap(LinkedHashMap> value) { + _annotationMap = 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 componentBounds, Rectangle drawAreaBounds) { + this._drawAreaBounds = drawAreaBounds; + } + + @override + void paint(ChartCanvas canvas, double animationPercent) { + if (_annotationMap == null) { + return; + } + + // Clean up the annotations that no longer exist. + if (animationPercent == 1.0) { + final keysToRemove = []; + + _annotationMap.forEach((String key, _AnimatedAnnotation annotation) { + if (annotation.animatingOut) { + keysToRemove.add(key); + } + }); + + keysToRemove.forEach((String key) => _annotationMap.remove(key)); + } + + _annotationMap.forEach((String key, _AnimatedAnnotation annotation) { + final annotationElement = + annotation.getCurrentAnnotation(animationPercent); + + // Calculate the bounds of a range annotation. + // + // This will still be used for line annotations to compute the position of + // labels. We always expect those to end up outside, since the bounds will + // have zero width or height. + final bounds = _getAnnotationBounds(annotationElement); + + if (annotationElement.isRange) { + // Draw the annotation. + canvas.drawRect(bounds, fill: annotationElement.color); + } else { + // Calculate the points for a line annotation. + final points = _getLineAnnotationPoints(annotationElement); + + // Draw the annotation. + canvas.drawLine( + dashPattern: annotationElement.dashPattern, + points: points, + stroke: annotationElement.color, + strokeWidthPx: annotationElement.strokeWidthPx); + } + + // Create [TextStyle] from [TextStyleSpec] to be used by all the elements. + // The [GraphicsFactory] is needed so it can't be created earlier. + final labelStyle = + _getTextStyle(graphicsFactory, annotationElement.labelStyleSpec); + + final rotation = + annotationElement.labelDirection == AnnotationLabelDirection.vertical + ? -pi / 2 + : 0.0; + + // Draw a start label if one is defined. + if (annotationElement.startLabel != null) { + final labelElement = + graphicsFactory.createTextElement(annotationElement.startLabel) + ..maxWidthStrategy = MaxWidthStrategy.ellipsize + ..textStyle = labelStyle; + + // Measure the label max width once if either type of label is defined. + labelElement.maxWidth = + _getLabelMaxWidth(bounds, annotationElement, labelElement); + + final labelPoint = + _getStartLabelPosition(bounds, annotationElement, labelElement); + + if (labelPoint != null) { + canvas.drawText(labelElement, labelPoint.x, labelPoint.y, + rotation: rotation); + } + } + + // Draw an end label if one is defined. + if (annotationElement.endLabel != null) { + final labelElement = + graphicsFactory.createTextElement(annotationElement.endLabel) + ..maxWidthStrategy = MaxWidthStrategy.ellipsize + ..textStyle = labelStyle; + + // Measure the label max width once if either type of label is defined. + labelElement.maxWidth = + _getLabelMaxWidth(bounds, annotationElement, labelElement); + + final labelPoint = + _getEndLabelPosition(bounds, annotationElement, labelElement); + + if (labelPoint != null) { + canvas.drawText(labelElement, labelPoint.x, labelPoint.y, + rotation: rotation); + } + } + }); + } + + /// Calculates the bounds of the annotation. + Rectangle _getAnnotationBounds(_AnnotationElement annotationElement) { + Rectangle bounds; + + switch (annotationElement.annotation.axisType) { + case RangeAnnotationAxisType.domain: + bounds = new Rectangle( + annotationElement.annotation.startPosition, + _drawAreaBounds.top, + annotationElement.annotation.endPosition - + annotationElement.annotation.startPosition, + _drawAreaBounds.height); + break; + + case RangeAnnotationAxisType.measure: + bounds = new Rectangle( + _drawAreaBounds.left, + annotationElement.annotation.endPosition, + _drawAreaBounds.width, + annotationElement.annotation.startPosition - + annotationElement.annotation.endPosition); + break; + } + + return bounds; + } + + /// Calculates the bounds of the annotation. + List _getLineAnnotationPoints( + _AnnotationElement annotationElement) { + final points = []; + + switch (annotationElement.annotation.axisType) { + case RangeAnnotationAxisType.domain: + points.add(new Point( + annotationElement.annotation.startPosition, _drawAreaBounds.top)); + points.add(new Point( + annotationElement.annotation.endPosition, _drawAreaBounds.bottom)); + break; + + case RangeAnnotationAxisType.measure: + points.add(new Point( + _drawAreaBounds.left, annotationElement.annotation.startPosition)); + points.add(new Point( + _drawAreaBounds.right, annotationElement.annotation.endPosition)); + break; + } + + return points; + } + + /// Measures the max label width of the annotation. + int _getLabelMaxWidth(Rectangle bounds, + _AnnotationElement annotationElement, TextElement labelElement) { + num maxWidth = 0; + + final calculatedLabelPosition = + _resolveAutoLabelPosition(bounds, annotationElement, labelElement); + + if (annotationElement.labelPosition == AnnotationLabelPosition.margin && + annotationElement.annotation.axisType == + RangeAnnotationAxisType.measure) { + switch (annotationElement.annotation.axisType) { + case RangeAnnotationAxisType.domain: + break; + + case RangeAnnotationAxisType.measure: + switch (annotationElement.labelAnchor) { + case AnnotationLabelAnchor.start: + maxWidth = chart.marginLeft - labelPadding; + break; + + case AnnotationLabelAnchor.end: + maxWidth = chart.marginRight - labelPadding; + break; + + case AnnotationLabelAnchor.middle: + break; + } + break; + } + } else { + if (calculatedLabelPosition == AnnotationLabelPosition.outside) { + maxWidth = annotationElement.labelDirection == + AnnotationLabelDirection.horizontal + ? drawBounds.width + : drawBounds.height; + } else { + maxWidth = annotationElement.labelDirection == + AnnotationLabelDirection.horizontal + ? bounds.width + : bounds.height; + } + } + + return (maxWidth).round(); + } + + /// Gets the resolved location for a start label element. + Point _getStartLabelPosition(Rectangle bounds, + _AnnotationElement annotationElement, TextElement labelElement) { + return _getLabelPosition(true, bounds, annotationElement, labelElement); + } + + /// Gets the resolved location for an end label element. + Point _getEndLabelPosition(Rectangle bounds, + _AnnotationElement annotationElement, TextElement labelElement) { + return _getLabelPosition(false, bounds, annotationElement, labelElement); + } + + /// Gets the resolved location for a label element. + Point _getLabelPosition(bool isStartLabel, Rectangle bounds, + _AnnotationElement annotationElement, TextElement labelElement) { + switch (annotationElement.annotation.axisType) { + case RangeAnnotationAxisType.domain: + return _getDomainLabelPosition( + isStartLabel, bounds, annotationElement, labelElement); + break; + + case RangeAnnotationAxisType.measure: + return _getMeasureLabelPosition( + isStartLabel, bounds, annotationElement, labelElement); + break; + } + return null; + } + + /// Gets the resolved location for a domain annotation label element. + Point _getDomainLabelPosition(bool isStartLabel, Rectangle bounds, + _AnnotationElement annotationElement, TextElement labelElement) { + if (annotationElement.labelDirection == AnnotationLabelDirection.vertical) { + return _getDomainLabelPositionVertical( + isStartLabel, bounds, annotationElement, labelElement); + } else { + return _getDomainLabelPositionHorizontal( + isStartLabel, bounds, annotationElement, labelElement); + } + } + + /// Gets the resolved location for a horizontal domain annotation label + /// element. + Point _getDomainLabelPositionHorizontal( + bool isStartLabel, + Rectangle bounds, + _AnnotationElement annotationElement, + TextElement labelElement) { + num labelX = 0; + num labelY = 0; + + final calculatedLabelPosition = + _resolveAutoLabelPosition(bounds, annotationElement, labelElement); + + switch (annotationElement.labelAnchor) { + case AnnotationLabelAnchor.middle: + labelY = bounds.top + + bounds.height / 2 - + labelElement.measurement.verticalSliceWidth / 2 - + labelPadding; + break; + + case AnnotationLabelAnchor.end: + if (annotationElement.labelPosition == AnnotationLabelPosition.margin) { + labelY = bounds.top - + labelElement.measurement.verticalSliceWidth - + labelPadding; + } else { + labelY = bounds.top + labelPadding; + } + break; + + case AnnotationLabelAnchor.start: + if (annotationElement.labelPosition == AnnotationLabelPosition.margin) { + labelY = bounds.bottom + labelPadding; + } else { + labelY = bounds.bottom - + labelElement.measurement.verticalSliceWidth - + labelPadding; + } + break; + } + + switch (calculatedLabelPosition) { + case AnnotationLabelPosition.margin: + case AnnotationLabelPosition.auto: + throw new ArgumentError(_unresolvedAutoMessage); + break; + + case AnnotationLabelPosition.outside: + if (isStartLabel) { + labelX = bounds.left - + labelElement.measurement.horizontalSliceWidth - + labelPadding; + } else { + labelX = bounds.right + labelPadding; + } + + labelElement.textDirection = + isRtl ? TextDirection.rtl : TextDirection.ltr; + break; + + case AnnotationLabelPosition.inside: + if (isStartLabel) { + labelX = bounds.left + labelPadding; + } else { + labelX = bounds.right - + labelElement.measurement.horizontalSliceWidth - + labelPadding; + } + + labelElement.textDirection = + isRtl ? TextDirection.rtl : TextDirection.ltr; + break; + } + + return new Point(labelX.round(), labelY.round()); + } + + /// Gets the resolved location for a vertical domain annotation label element. + Point _getDomainLabelPositionVertical( + bool isStartLabel, + Rectangle bounds, + _AnnotationElement annotationElement, + TextElement labelElement) { + num labelX = 0; + num labelY = 0; + + final calculatedLabelPosition = + _resolveAutoLabelPosition(bounds, annotationElement, labelElement); + + switch (annotationElement.labelAnchor) { + case AnnotationLabelAnchor.middle: + labelY = bounds.top + + bounds.height / 2 + + labelElement.measurement.horizontalSliceWidth / 2 + + labelPadding; + break; + + case AnnotationLabelAnchor.end: + if (annotationElement.labelPosition == AnnotationLabelPosition.margin) { + labelY = bounds.top + + labelElement.measurement.horizontalSliceWidth + + labelPadding; + } else { + labelY = bounds.top + + labelElement.measurement.horizontalSliceWidth + + labelPadding; + } + break; + + case AnnotationLabelAnchor.start: + if (annotationElement.labelPosition == AnnotationLabelPosition.margin) { + labelY = bounds.bottom + labelPadding; + } else { + labelY = bounds.bottom - + labelElement.measurement.horizontalSliceWidth - + labelPadding; + } + break; + } + + switch (calculatedLabelPosition) { + case AnnotationLabelPosition.margin: + case AnnotationLabelPosition.auto: + throw new ArgumentError(_unresolvedAutoMessage); + break; + + case AnnotationLabelPosition.outside: + if (isStartLabel) { + labelX = bounds.left - + labelElement.measurement.verticalSliceWidth - + labelPadding; + } else { + labelX = bounds.right + labelPadding; + } + + labelElement.textDirection = + isRtl ? TextDirection.rtl : TextDirection.ltr; + break; + + case AnnotationLabelPosition.inside: + if (isStartLabel) { + labelX = bounds.left + labelPadding; + } else { + labelX = bounds.right - + labelElement.measurement.verticalSliceWidth - + labelPadding; + } + + labelElement.textDirection = + isRtl ? TextDirection.rtl : TextDirection.ltr; + break; + } + + return new Point(labelX.round(), labelY.round()); + } + + /// Gets the resolved location for a measure annotation label element. + Point _getMeasureLabelPosition(bool isStartLabel, Rectangle bounds, + _AnnotationElement annotationElement, TextElement labelElement) { + if (annotationElement.labelDirection == AnnotationLabelDirection.vertical) { + return _getMeasureLabelPositionVertical( + isStartLabel, bounds, annotationElement, labelElement); + } else { + return _getMeasureLabelPositionHorizontal( + isStartLabel, bounds, annotationElement, labelElement); + } + } + + /// Gets the resolved location for a horizontal measure annotation label + /// element. + Point _getMeasureLabelPositionHorizontal( + bool isStartLabel, + Rectangle bounds, + _AnnotationElement annotationElement, + TextElement labelElement) { + num labelX = 0; + num labelY = 0; + + final calculatedLabelPosition = + _resolveAutoLabelPosition(bounds, annotationElement, labelElement); + + switch (annotationElement.labelAnchor) { + case AnnotationLabelAnchor.middle: + labelX = bounds.left + + bounds.width / 2 - + labelElement.measurement.horizontalSliceWidth / 2; + labelElement.textDirection = + isRtl ? TextDirection.rtl : TextDirection.ltr; + break; + + case AnnotationLabelAnchor.end: + case AnnotationLabelAnchor.start: + if (annotationElement.labelPosition == AnnotationLabelPosition.margin) { + final alignLeft = isRtl + ? (annotationElement.labelAnchor == AnnotationLabelAnchor.end) + : (annotationElement.labelAnchor == AnnotationLabelAnchor.start); + + if (alignLeft) { + labelX = bounds.left - labelPadding; + labelElement.textDirection = TextDirection.rtl; + } else { + labelX = bounds.right + labelPadding; + labelElement.textDirection = TextDirection.ltr; + } + } else { + final alignLeft = isRtl + ? (annotationElement.labelAnchor == AnnotationLabelAnchor.end) + : (annotationElement.labelAnchor == AnnotationLabelAnchor.start); + + if (alignLeft) { + labelX = bounds.left + labelPadding; + labelElement.textDirection = TextDirection.ltr; + } else { + labelX = bounds.right - labelPadding; + labelElement.textDirection = TextDirection.rtl; + } + } + break; + } + + switch (calculatedLabelPosition) { + case AnnotationLabelPosition.margin: + case AnnotationLabelPosition.auto: + throw new ArgumentError(_unresolvedAutoMessage); + break; + + case AnnotationLabelPosition.outside: + if (isStartLabel) { + labelY = bounds.bottom + labelPadding; + } else { + labelY = bounds.top - + labelElement.measurement.verticalSliceWidth - + labelPadding; + } + break; + + case AnnotationLabelPosition.inside: + if (isStartLabel) { + labelY = bounds.bottom - + labelElement.measurement.verticalSliceWidth - + labelPadding; + } else { + labelY = bounds.top + labelPadding; + } + break; + } + + return new Point(labelX.round(), labelY.round()); + } + + /// Gets the resolved location for a vertical measure annotation label + /// element. + Point _getMeasureLabelPositionVertical( + bool isStartLabel, + Rectangle bounds, + _AnnotationElement annotationElement, + TextElement labelElement) { + num labelX = 0; + num labelY = 0; + + final calculatedLabelPosition = + _resolveAutoLabelPosition(bounds, annotationElement, labelElement); + + switch (annotationElement.labelAnchor) { + case AnnotationLabelAnchor.middle: + labelX = bounds.left + + bounds.width / 2 - + labelElement.measurement.verticalSliceWidth / 2; + labelElement.textDirection = + isRtl ? TextDirection.rtl : TextDirection.ltr; + break; + + case AnnotationLabelAnchor.end: + case AnnotationLabelAnchor.start: + if (annotationElement.labelPosition == AnnotationLabelPosition.margin) { + final alignLeft = isRtl + ? (annotationElement.labelAnchor == AnnotationLabelAnchor.end) + : (annotationElement.labelAnchor == AnnotationLabelAnchor.start); + + if (alignLeft) { + labelX = bounds.left - + labelElement.measurement.verticalSliceWidth - + labelPadding; + labelElement.textDirection = TextDirection.ltr; + } else { + labelX = bounds.right + labelPadding; + labelElement.textDirection = TextDirection.ltr; + } + } else { + final alignLeft = isRtl + ? (annotationElement.labelAnchor == AnnotationLabelAnchor.end) + : (annotationElement.labelAnchor == AnnotationLabelAnchor.start); + + if (alignLeft) { + labelX = bounds.left + labelPadding; + labelElement.textDirection = TextDirection.ltr; + } else { + labelX = bounds.right - + labelElement.measurement.verticalSliceWidth - + labelPadding; + labelElement.textDirection = TextDirection.ltr; + } + } + break; + } + + switch (calculatedLabelPosition) { + case AnnotationLabelPosition.margin: + case AnnotationLabelPosition.auto: + throw new ArgumentError(_unresolvedAutoMessage); + break; + + case AnnotationLabelPosition.outside: + if (isStartLabel) { + labelY = bounds.bottom + + labelElement.measurement.horizontalSliceWidth + + labelPadding; + } else { + labelY = bounds.top - labelPadding; + } + break; + + case AnnotationLabelPosition.inside: + if (isStartLabel) { + labelY = bounds.bottom - labelPadding; + } else { + labelY = bounds.top + + labelElement.measurement.horizontalSliceWidth + + labelPadding; + } + break; + } + + return new Point(labelX.round(), labelY.round()); + } + + /// Resolves [AnnotationLabelPosition.auto] configuration for an annotation + /// into an inside or outside position, depending on the size of the + /// annotation and the chart draw area. + AnnotationLabelPosition _resolveAutoLabelPosition(Rectangle bounds, + _AnnotationElement annotationElement, TextElement labelElement) { + var calculatedLabelPosition = annotationElement.labelPosition; + if (calculatedLabelPosition == AnnotationLabelPosition.auto || + calculatedLabelPosition == AnnotationLabelPosition.margin) { + final isDomain = annotationElement.annotation.axisType == + RangeAnnotationAxisType.domain; + + final annotationBoundsSize = isDomain ? bounds.width : bounds.height; + + final drawBoundsSize = isDomain ? drawBounds.width : drawBounds.height; + + final isVertical = + annotationElement.labelDirection == AnnotationLabelDirection.vertical; + + final labelSize = isDomain && isVertical || !isDomain && !isVertical + ? labelElement.measurement.verticalSliceWidth + : labelElement.measurement.horizontalSliceWidth; + + // Get space available inside and outside the annotation. + final totalPadding = labelPadding * 2; + final insideBarWidth = annotationBoundsSize - totalPadding; + final outsideBarWidth = + drawBoundsSize - annotationBoundsSize - totalPadding; + + // A label fits if the space inside the annotation is >= outside + // annotation or if the length of the text fits and the space. This is + // because if the annotation has more space than the outside, it makes + // more sense to place the label inside the annotation, even if the + // entire label does not fit. + calculatedLabelPosition = + (insideBarWidth >= outsideBarWidth || labelSize < insideBarWidth) + ? AnnotationLabelPosition.inside + : AnnotationLabelPosition.outside; + } + + return calculatedLabelPosition; + } + + @override + Rectangle get componentBounds => this._drawAreaBounds; + + @override + bool get isSeriesRenderer => false; + + // 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; + } +} + +class _DatumAnnotation { + final double startPosition; + final double endPosition; + final RangeAnnotationAxisType axisType; + + _DatumAnnotation({this.startPosition, this.endPosition, this.axisType}); + + factory _DatumAnnotation.from(_DatumAnnotation other, + [double startPosition, double endPosition]) { + return new _DatumAnnotation( + startPosition: startPosition ?? other.startPosition, + endPosition: endPosition ?? other.endPosition, + axisType: other.axisType); + } +} + +class _AnnotationElement { + _DatumAnnotation annotation; + Color color; + String startLabel; + String endLabel; + bool isRange; + AnnotationLabelAnchor labelAnchor; + AnnotationLabelDirection labelDirection; + AnnotationLabelPosition labelPosition; + TextStyleSpec labelStyleSpec; + List dashPattern; + double strokeWidthPx; + + _AnnotationElement clone() { + return new _AnnotationElement() + ..annotation = new _DatumAnnotation.from(annotation) + ..color = color != null ? new Color.fromOther(color: color) : null + ..startLabel = this.startLabel + ..endLabel = this.endLabel + ..isRange = this.isRange + ..labelAnchor = this.labelAnchor + ..labelDirection = this.labelDirection + ..labelPosition = this.labelPosition + ..labelStyleSpec = this.labelStyleSpec + ..dashPattern = dashPattern + ..strokeWidthPx = this.strokeWidthPx; + } + + void updateAnimationPercent(_AnnotationElement previous, + _AnnotationElement target, double animationPercent) { + final targetAnnotation = target.annotation; + final previousAnnotation = previous.annotation; + + final startPosition = + ((targetAnnotation.startPosition - previousAnnotation.startPosition) * + animationPercent) + + previousAnnotation.startPosition; + + final endPosition = + ((targetAnnotation.endPosition - previousAnnotation.endPosition) * + animationPercent) + + previousAnnotation.endPosition; + + annotation = + new _DatumAnnotation.from(targetAnnotation, startPosition, endPosition); + + color = getAnimatedColor(previous.color, target.color, animationPercent); + + strokeWidthPx = + (((target.strokeWidthPx - previous.strokeWidthPx) * animationPercent) + + previous.strokeWidthPx); + } +} + +class _AnimatedAnnotation { + final String key; + + _AnnotationElement _previousAnnotation; + _AnnotationElement _targetAnnotation; + _AnnotationElement _currentAnnotation; + + // Flag indicating whether this annotation is being animated out of the chart. + bool animatingOut = false; + + _AnimatedAnnotation({@required this.key}); + + /// Animates an annotation that was removed from the list out of the view. + /// + /// This should be called in place of "setNewTarget" for annotations have been + /// removed from the list. + /// TODO: Needed? + void animateOut() { + final newTarget = _currentAnnotation.clone(); + + setNewTarget(newTarget); + animatingOut = true; + } + + void setNewTarget(_AnnotationElement newTarget) { + animatingOut = false; + _currentAnnotation ??= newTarget.clone(); + _previousAnnotation = _currentAnnotation.clone(); + _targetAnnotation = newTarget; + } + + _AnnotationElement getCurrentAnnotation(double animationPercent) { + if (animationPercent == 1.0 || _previousAnnotation == null) { + _currentAnnotation = _targetAnnotation; + _previousAnnotation = _targetAnnotation; + return _currentAnnotation; + } + + _currentAnnotation.updateAnimationPercent( + _previousAnnotation, _targetAnnotation, animationPercent); + + return _currentAnnotation; + } +} + +/// Helper class that exposes fewer private internal properties for unit tests. +@visibleForTesting +class RangeAnnotationTester { + final RangeAnnotation behavior; + + RangeAnnotationTester(this.behavior); + + set graphicsFactory(GraphicsFactory value) { + behavior._view._graphicsFactory = value; + } + + mockLayout(Rectangle bounds) { + behavior._view.layout(bounds, bounds); + } + + /// Checks if an annotation exists with the given position and color. + bool doesAnnotationExist( + {num startPosition, + num endPosition, + Color color, + List dashPattern, + String startLabel, + String endLabel, + AnnotationLabelAnchor labelAnchor, + AnnotationLabelDirection labelDirection, + AnnotationLabelPosition labelPosition}) { + var exists = false; + + behavior._annotationMap.forEach((String key, _AnimatedAnnotation a) { + final currentAnnotation = a._currentAnnotation; + final annotation = currentAnnotation.annotation; + + if (annotation.startPosition == startPosition && + annotation.endPosition == endPosition && + currentAnnotation.color == color && + currentAnnotation.startLabel == startLabel && + currentAnnotation.endLabel == endLabel && + currentAnnotation.labelAnchor == labelAnchor && + currentAnnotation.labelDirection == labelDirection && + currentAnnotation.labelPosition == labelPosition && + (!(currentAnnotation is LineAnnotationSegment) || + currentAnnotation.dashPattern == dashPattern)) { + exists = true; + return; + } + }); + + return exists; + } +} + +/// Base class for chart annotations. +abstract class AnnotationSegment { + final RangeAnnotationAxisType axisType; + final String axisId; + final Color color; + final String startLabel; + final String endLabel; + final AnnotationLabelAnchor labelAnchor; + final AnnotationLabelDirection labelDirection; + final AnnotationLabelPosition labelPosition; + final TextStyleSpec labelStyleSpec; + + String get key; + + AnnotationSegment(this.axisType, + {this.axisId, + this.color, + this.startLabel, + this.endLabel, + this.labelAnchor, + this.labelDirection, + this.labelPosition, + this.labelStyleSpec}); +} + +/// Data for a chart range annotation. +class RangeAnnotationSegment extends AnnotationSegment { + final D startValue; + final D endValue; + + RangeAnnotationSegment( + this.startValue, this.endValue, RangeAnnotationAxisType axisType, + {String axisId, + Color color, + String startLabel, + String endLabel, + AnnotationLabelAnchor labelAnchor, + AnnotationLabelDirection labelDirection, + AnnotationLabelPosition labelPosition, + TextStyleSpec labelStyleSpec}) + : super(axisType, + axisId: axisId, + color: color, + startLabel: startLabel, + endLabel: endLabel, + labelAnchor: labelAnchor, + labelDirection: labelDirection, + labelPosition: labelPosition, + labelStyleSpec: labelStyleSpec); + + @override + String get key => 'r::${axisType}::${axisId}::${startValue}::${endValue}'; +} + +/// Data for a chart line annotation. +class LineAnnotationSegment extends AnnotationSegment { + final D value; + final List dashPattern; + final double strokeWidthPx; + + LineAnnotationSegment(this.value, RangeAnnotationAxisType axisType, + {String axisId, + Color color, + String startLabel, + String endLabel, + AnnotationLabelAnchor labelAnchor, + AnnotationLabelDirection labelDirection, + AnnotationLabelPosition labelPosition, + TextStyleSpec labelStyleSpec, + this.dashPattern, + this.strokeWidthPx = 2.0}) + : super(axisType, + axisId: axisId, + color: color, + startLabel: startLabel, + endLabel: endLabel, + labelAnchor: labelAnchor, + labelDirection: labelDirection, + labelPosition: labelPosition, + labelStyleSpec: labelStyleSpec); + + @override + String get key => 'l::${axisType}::${axisId}::${value}'; +} + +/// Axis type for an annotation. +enum RangeAnnotationAxisType { + domain, + measure, +} + +/// Configures where to anchor the label. +enum AnnotationLabelAnchor { + /// Anchor to the starting side of the annotation range. + start, + + /// Anchor to the middle of the annotation range. + middle, + + /// Anchor to the ending side of the annotation range. + end, +} + +/// Direction of the label text on the chart. +enum AnnotationLabelDirection { + /// 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. + /// TODO[b/112553019]: Implement vertical text rendering of labels. + vertical, +} + +/// Configures where to place the label relative to the annotation. +enum AnnotationLabelPosition { + /// 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, + + /// Place the label outside of the draw area, in the chart margin. + /// + /// Labels will be rendered on the opposite side of the chart from the primary + /// axis. For measure annotations, this means the "end" side, opposite from + /// the "start" side where the primary measure axis is located. + /// + /// This should not be used for measure annotations if the chart has a + /// secondary measure axis. The annotation behaviors do not perform collision + /// detection with tick labels. + margin, +} + +const String _unresolvedAutoMessage = 'Unresolved AnnotationLabelPosition.auto'; diff --git a/web/charts/common/lib/src/chart/common/behavior/selection/lock_selection.dart b/web/charts/common/lib/src/chart/common/behavior/selection/lock_selection.dart new file mode 100644 index 000000000..93ed7c745 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/selection/lock_selection.dart @@ -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 implements ChartBehavior { + 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 _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 chartPoint) { + // If the tap is within the drawArea, then claim the event from others. + return _chart.pointWithinRenderer(chartPoint); + } + + bool _onSelect(Point 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 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 chart) { + chart.removeGestureListener(_listener); + chart.unregisterTappable(this); + _chart = null; + } + + @override + String get role => 'LockSelection-${selectionModelType.toString()}}'; +} diff --git a/web/charts/common/lib/src/chart/common/behavior/selection/select_nearest.dart b/web/charts/common/lib/src/chart/common/behavior/selection/select_nearest.dart new file mode 100644 index 000000000..09dc8a9f2 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/selection/select_nearest.dart @@ -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 implements ChartBehavior { + 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 _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 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 chartPoint) { + _delaySelect = false; + return _onSelect(chartPoint); + } + + bool _onSelect(Point 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 = >[]; + var seriesDatumList = >[]; + + 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(details.first.series, details.first.datum)]; + + // Filter out points from overlay series. + seriesDatumList + .removeWhere((SeriesDatum 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>.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(>[], >[]); + return false; + } + + List> _expandToDomain(DatumDetails nearestDetails) { + // Make sure that the "nearest" datum is at the top of the list. + final data = >[ + new SeriesDatum(nearestDetails.series, nearestDetails.datum) + ]; + final nearestDomain = nearestDetails.domain; + + for (ImmutableSeries 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 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 chart) { + chart.removeGestureListener(_listener); + chart.unregisterTappable(this); + _chart = null; + } + + @override + String get role => 'SelectNearest-${selectionModelType.toString()}}'; +} diff --git a/web/charts/common/lib/src/chart/common/behavior/selection/selection_trigger.dart b/web/charts/common/lib/src/chart/common/behavior/selection/selection_trigger.dart new file mode 100644 index 000000000..663c8d6ec --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/selection/selection_trigger.dart @@ -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, +} diff --git a/web/charts/common/lib/src/chart/common/behavior/slider/slider.dart b/web/charts/common/lib/src/chart/common/behavior/slider/slider.dart new file mode 100644 index 000000000..5d8c7d446 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/slider/slider.dart @@ -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 implements ChartBehavior { + _SliderLayoutView _view; + + GestureListener _gestureListener; + + LifecycleListener _lifecycleListener; + + SliderEventListener _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 _chart; + + /// Rendering data for the slider line and handle. + _AnimatedSlider _sliderHandle; + + bool _delaySelect = false; + + bool _handleDrag = false; + + /// Current location of the slider line. + Point _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 _previousDomainCenterPoint; + + /// Bounding box for the slider drag handle. + Rectangle _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 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( + onData: _setInitialDragState, + onAxisConfigured: _updateViewData, + onPostrender: _fireChangeEvent, + ); + + // Set up slider event listeners. + _sliderEventListener = + new SliderEventListener(onChange: onChangeCallback); + } + + bool _onTapTest(Point chartPoint) { + _delaySelect = eventTrigger == SelectionTrigger.longPressHold; + _handleDrag = _sliderContainsPoint(chartPoint); + return _handleDrag; + } + + bool _onLongPressSelect(Point chartPoint) { + _delaySelect = false; + return _onSelect(chartPoint); + } + + bool _onSelect(Point 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 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 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(_domainCenterPoint.x, _domainCenterPoint.y) + ..buttonBounds = new Rectangle(_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(_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 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(position.round(), _domainCenterPoint.y); + } else { + _domainCenterPoint = new Point( + 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( + (_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(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 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( + layoutPaintOrder: layoutPaintOrder, handleRenderer: _handleRenderer); + + chart.addView(_view); + chart.addGestureListener(_gestureListener); + chart.addLifecycleListener(_lifecycleListener); + } + + @override + void removeFrom(BaseChart 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 handleOffset; + + /// The vertical position for the slider handle. + SliderHandlePosition handlePosition; + + /// Specifies the size of the slider handle. + Rectangle 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(0.0, 0.0), + this.handleSize = const Rectangle(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 extends LayoutView { + final LayoutViewConfig layoutConfig; + + Rectangle _drawAreaBounds; + + Rectangle 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 componentBounds, Rectangle drawAreaBounds) { + this._drawAreaBounds = drawAreaBounds; + } + + @override + void paint(ChartCanvas canvas, double animationPercent) { + final sliderElement = _sliderHandle.getCurrentSlider(animationPercent); + + canvas.drawLine( + points: [ + new Point( + sliderElement.domainCenterPoint.x, _drawAreaBounds.top), + new Point( + 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 get componentBounds => this._drawAreaBounds; + + @override + bool get isSeriesRenderer => false; +} + +/// Rendering information for a slider control element. +class _SliderElement { + Point domainCenterPoint; + Rectangle buttonBounds; + Color fill; + Color stroke; + double strokeWidthPx; + + _SliderElement clone() { + return new _SliderElement() + ..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(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(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 { + _SliderElement _previousSlider; + _SliderElement _targetSlider; + _SliderElement _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(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 newTarget) { + animatingOut = false; + _currentSlider ??= newTarget.clone(); + _previousSlider = _currentSlider.clone(); + _targetSlider = newTarget; + } + + _SliderElement 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 { + /// Called when the position of the slider has changed during a drag event. + final SliderListenerCallback 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(Point 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 { + final Slider behavior; + + SliderTester(this.behavior); + + Point get domainCenterPoint => behavior._domainCenterPoint; + + D get domainValue => behavior._domainValue; + + Rectangle get handleBounds => behavior._handleBounds; + + void layout(Rectangle componentBounds, Rectangle drawAreaBounds) { + behavior._view.layout(componentBounds, drawAreaBounds); + } + + _SliderLayoutView get view => behavior._view; +} diff --git a/web/charts/common/lib/src/chart/common/behavior/sliding_viewport.dart b/web/charts/common/lib/src/chart/common/behavior/sliding_viewport.dart new file mode 100644 index 000000000..633db1f66 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/sliding_viewport.dart @@ -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 implements ChartBehavior { + final SelectionModelType selectionModelType; + + CartesianChart _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 chart) { + assert(chart is CartesianChart); + _chart = chart as CartesianChart; + chart + .getSelectionModel(selectionModelType) + .addSelectionChangedListener(_selectionChanged); + } + + @override + void removeFrom(BaseChart chart) { + chart + .getSelectionModel(selectionModelType) + .removeSelectionChangedListener(_selectionChanged); + } + + @override + String get role => 'slidingViewport-${selectionModelType.toString()}'; +} diff --git a/web/charts/common/lib/src/chart/common/behavior/zoom/initial_hint_behavior.dart b/web/charts/common/lib/src/chart/common/behavior/zoom/initial_hint_behavior.dart new file mode 100644 index 000000000..c3fdde9af --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/zoom/initial_hint_behavior.dart @@ -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 implements ChartBehavior { + /// Listens for drag gestures. + GestureListener _listener; + + /// Chart lifecycle listener to setup hint animation. + LifecycleListener _lifecycleListener; + + @override + String get role => 'InitialHint'; + + /// The chart to which the behavior is attached. + CartesianChart _chart; + + @protected + CartesianChart 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( + onAxisConfigured: _onAxisConfigured, + onAnimationComplete: _onAnimationComplete); + } + + @override + attachTo(BaseChart 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 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 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; + } +} diff --git a/web/charts/common/lib/src/chart/common/behavior/zoom/pan_and_zoom_behavior.dart b/web/charts/common/lib/src/chart/common/behavior/zoom/pan_and_zoom_behavior.dart new file mode 100644 index 000000000..05dc5f310 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/zoom/pan_and_zoom_behavior.dart @@ -0,0 +1,119 @@ +// 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, Point; + +import 'package:meta/meta.dart' show protected; + +import 'pan_behavior.dart'; +import 'panning_tick_provider.dart' show PanningTickProviderMode; + +/// Adds domain axis panning and zooming support to the chart. +/// +/// Zooming is supported for the web by mouse wheel events. Scrolling up zooms +/// the chart in, and scrolling down zooms the chart out. The chart can never be +/// zoomed out past the domain axis range. +/// +/// Zooming is supported by pinch gestures for mobile devices. +/// +/// Panning is supported by clicking and dragging the mouse for web, or tapping +/// and dragging on the chart for mobile devices. +class PanAndZoomBehavior extends PanBehavior { + @override + String get role => 'PanAndZoom'; + + /// Flag which is enabled to indicate that the user is "zooming" the chart. + bool _isZooming = false; + + @protected + bool get isZooming => _isZooming; + + /// Current zoom scaling factor for the behavior. + double _scalingFactor = 1.0; + + /// Minimum scalingFactor to prevent zooming out beyond the data range. + final _minScalingFactor = 1.0; + + /// Maximum scalingFactor to prevent zooming in so far that no data is + /// visible. + /// + /// TODO: Dynamic max based on data range? + final _maxScalingFactor = 5.0; + + @override + bool onDragStart(Point localPosition) { + if (chart == null) { + return false; + } + + super.onDragStart(localPosition); + + // Save the current scaling factor to make zoom events relative. + _scalingFactor = chart.domainAxis?.viewportScalingFactor; + _isZooming = true; + + return true; + } + + @override + bool onDragUpdate(Point localPosition, double scale) { + // Swipe gestures should be handled by the [PanBehavior]. + if (scale == 1.0) { + _isZooming = false; + return super.onDragUpdate(localPosition, scale); + } + + // No further events in this chain should be handled by [PanBehavior]. + cancelPanning(); + + if (!_isZooming || lastPosition == null || chart == null) { + return false; + } + + // Update the domain axis's viewport scale factor to zoom the chart. + final domainAxis = chart.domainAxis; + + if (domainAxis == null) { + return false; + } + + // This is set during onDragUpdate and NOT onDragStart because we don't yet + // know during onDragStart whether pan/zoom behavior is panning or zooming. + // During zoom in / zoom out, domain tick provider set to return existing + // cached ticks. + domainAxisTickProvider.mode = PanningTickProviderMode.useCachedTicks; + + // Clamp the scale to prevent zooming out beyond the range of the data, or + // zooming in so far that we show nothing useful. + final newScalingFactor = + min(max(_scalingFactor * scale, _minScalingFactor), _maxScalingFactor); + + domainAxis.setViewportSettings( + newScalingFactor, domainAxis.viewportTranslatePx, + drawAreaWidth: chart.drawAreaBounds.width); + + chart.redraw(skipAnimation: true, skipLayout: true); + + return true; + } + + @override + bool onDragEnd( + Point localPosition, double scale, double pixelsPerSec) { + _isZooming = false; + + return super.onDragEnd(localPosition, scale, pixelsPerSec); + } +} diff --git a/web/charts/common/lib/src/chart/common/behavior/zoom/pan_behavior.dart b/web/charts/common/lib/src/chart/common/behavior/zoom/pan_behavior.dart new file mode 100644 index 000000000..8b3b71ece --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/zoom/pan_behavior.dart @@ -0,0 +1,221 @@ +// 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; +import '../chart_behavior.dart' show ChartBehavior; +import 'panning_tick_provider.dart'; + +/// Adds domain axis panning support to a chart. +/// +/// Panning is supported by clicking and dragging the mouse for web, or tapping +/// and dragging on the chart for mobile devices. +class PanBehavior implements ChartBehavior { + /// Listens for drag gestures. + GestureListener _listener; + + /// Wrapped domain tick provider for pan and zoom behavior. + PanningTickProvider _domainAxisTickProvider; + + @protected + PanningTickProvider get domainAxisTickProvider => _domainAxisTickProvider; + + @override + String get role => 'Pan'; + + /// The chart to which the behavior is attached. + CartesianChart _chart; + + @protected + CartesianChart get chart => _chart; + + /// Flag which is enabled to indicate that the user is "panning" the chart. + bool _isPanning = false; + + @protected + bool get isPanning => _isPanning; + + /// Last position of the mouse/tap that was used to adjust the scale translate + /// factor. + Point _lastPosition; + + @protected + Point get lastPosition => _lastPosition; + + /// Optional callback that is invoked at the end of panning ([onPanEnd]). + PanningCompletedCallback _panningCompletedCallback; + + set panningCompletedCallback(PanningCompletedCallback callback) { + _panningCompletedCallback = callback; + } + + PanBehavior() { + _listener = new GestureListener( + onTapTest: onTapTest, + onDragStart: onDragStart, + onDragUpdate: onDragUpdate, + onDragEnd: onDragEnd); + } + + /// Injects the behavior into a chart. + @override + attachTo(BaseChart chart) { + if (!(chart is CartesianChart)) { + throw new ArgumentError( + 'PanBehavior can only be attached to a CartesianChart'); + } + + _chart = chart; + _chart.addGestureListener(_listener); + + // Disable the autoViewport feature to enable panning. + _chart.domainAxis?.autoViewport = false; + + // Wrap domain axis tick provider with the panning behavior one. + _domainAxisTickProvider = + new PanningTickProvider(_chart.domainAxis.tickProvider); + _chart.domainAxis.tickProvider = _domainAxisTickProvider; + } + + /// Removes the behavior from a chart. + @override + removeFrom(BaseChart chart) { + if (!(chart is CartesianChart)) { + throw new ArgumentError( + 'PanBehavior can only be attached to a CartesianChart'); + } + + _chart = chart; + _chart.removeGestureListener(_listener); + + // Restore the default autoViewport state. + _chart.domainAxis?.autoViewport = true; + + // Restore the original tick providers + _chart.domainAxis.tickProvider = _domainAxisTickProvider.tickProvider; + + _chart = null; + } + + @protected + bool onTapTest(Point localPosition) { + if (_chart == null) { + return false; + } + + return _chart.withinDrawArea(localPosition); + } + + @protected + bool onDragStart(Point localPosition) { + if (_chart == null) { + return false; + } + + onPanStart(); + + _lastPosition = localPosition; + _isPanning = true; + return true; + } + + @protected + bool onDragUpdate(Point localPosition, double scale) { + if (!_isPanning || _lastPosition == null || _chart == null) { + return false; + } + + // Pinch gestures should be handled by the [PanAndZoomBehavior]. + if (scale != 1.0) { + _isPanning = false; + return false; + } + + // Update the domain axis's viewport translate to pan the chart. + final domainAxis = _chart.domainAxis; + + if (domainAxis == null) { + return false; + } + + // This is set during onDragUpdate and NOT onDragStart because we don't yet + // know during onDragStart whether pan/zoom behavior is panning or zooming. + // During panning, domain tick provider set to generate ticks with locked + // steps. + _domainAxisTickProvider.mode = PanningTickProviderMode.stepSizeLocked; + + double domainScalingFactor = domainAxis.viewportScalingFactor; + + double domainChange = + domainAxis.viewportTranslatePx + localPosition.x - _lastPosition.x; + + domainAxis.setViewportSettings(domainScalingFactor, domainChange, + drawAreaWidth: chart.drawAreaBounds.width); + + _lastPosition = localPosition; + + _chart.redraw(skipAnimation: true, skipLayout: true); + return true; + } + + @protected + bool onDragEnd( + Point localPosition, double scale, double pixelsPerSec) { + onPanEnd(); + return true; + } + + @protected + void onPanStart() { + // 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, we get a jittery effect + // as the measure axes location changes ever so slightly during pan/zoom. + _chart.getMeasureAxis().lockAxis = true; + _chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis = true; + } + + @protected + void onPanEnd() { + cancelPanning(); + + // When panning stops, allow tick provider to update ticks, and then + // request redraw. + _domainAxisTickProvider.mode = PanningTickProviderMode.passThrough; + _chart.getMeasureAxis().lockAxis = false; + _chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis = + false; + _chart.redraw(); + + if (_panningCompletedCallback != null) { + _panningCompletedCallback(); + } + } + + /// Cancels the handling of any current panning event. + void cancelPanning() { + _isPanning = false; + } +} + +/// Callback for when panning is completed. +typedef void PanningCompletedCallback(); diff --git a/web/charts/common/lib/src/chart/common/behavior/zoom/panning_tick_provider.dart b/web/charts/common/lib/src/chart/common/behavior/zoom/panning_tick_provider.dart new file mode 100644 index 000000000..c13222270 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/behavior/zoom/panning_tick_provider.dart @@ -0,0 +1,90 @@ +// 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 '../../../cartesian/axis/axis.dart' show AxisOrientation; +import '../../../cartesian/axis/draw_strategy/tick_draw_strategy.dart' + show TickDrawStrategy; +import '../../../cartesian/axis/scale.dart' show MutableScale; +import '../../../cartesian/axis/tick.dart' show Tick; +import '../../../cartesian/axis/tick_formatter.dart' show TickFormatter; +import '../../../cartesian/axis/tick_provider.dart' show TickProvider, TickHint; +import '../../../common/chart_context.dart' show ChartContext; + +enum PanningTickProviderMode { + /// Return cached ticks. + useCachedTicks, + + /// Request ticks with [TickHint] calculated from cached ticks. + stepSizeLocked, + + /// Request ticks directly from tick provider. + passThrough, +} + +/// Wraps an existing tick provider to be able to return cached ticks during +/// zoom in/out, return ticks calculated with locked step size during panning, +/// or just pass through to the existing tick provider. +class PanningTickProvider implements TickProvider { + final TickProvider tickProvider; + + PanningTickProviderMode _mode = PanningTickProviderMode.passThrough; + + List> _ticks; + + PanningTickProvider(this.tickProvider); + + set mode(PanningTickProviderMode mode) { + _mode = mode; + } + + List> getTicks({ + @required ChartContext context, + @required GraphicsFactory graphicsFactory, + @required MutableScale scale, + @required TickFormatter formatter, + @required Map formatterValueCache, + @required TickDrawStrategy tickDrawStrategy, + @required AxisOrientation orientation, + bool viewportExtensionEnabled = false, + TickHint tickHint, + }) { + if (_mode == PanningTickProviderMode.stepSizeLocked) { + tickHint = new TickHint( + _ticks.first.value, + _ticks.last.value, + tickCount: _ticks.length, + ); + } + + if (_mode != PanningTickProviderMode.useCachedTicks) { + _ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: formatterValueCache, + tickDrawStrategy: tickDrawStrategy, + orientation: orientation, + viewportExtensionEnabled: viewportExtensionEnabled, + tickHint: tickHint, + ); + } + + return _ticks; + } +} diff --git a/web/charts/common/lib/src/chart/common/canvas_shapes.dart b/web/charts/common/lib/src/chart/common/canvas_shapes.dart new file mode 100644 index 000000000..0605c2723 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/canvas_shapes.dart @@ -0,0 +1,125 @@ +// 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, Point; + +import '../../common/color.dart' show Color; +import 'chart_canvas.dart' show FillPatternType; + +/// A rectangle to be painted by [ChartCanvas]. +class CanvasRect { + final Rectangle bounds; + final List dashPattern; + final Color fill; + final FillPatternType pattern; + final Color stroke; + final double strokeWidthPx; + + CanvasRect(this.bounds, + {this.dashPattern, + this.fill, + this.pattern, + this.stroke, + this.strokeWidthPx}); +} + +/// A stack of [CanvasRect] to be painted by [ChartCanvas]. +class CanvasBarStack { + final List segments; + final int radius; + final int stackedBarPadding; + final bool roundTopLeft; + final bool roundTopRight; + final bool roundBottomLeft; + final bool roundBottomRight; + final Rectangle fullStackRect; + + factory CanvasBarStack(List segments, + {int radius, + int stackedBarPadding, + bool roundTopLeft, + bool roundTopRight, + bool roundBottomLeft, + bool roundBottomRight}) { + final firstBarBounds = segments.first.bounds; + + // Find the rectangle that would represent the full stack of bars. + var left = firstBarBounds.left; + var top = firstBarBounds.top; + var right = firstBarBounds.right; + var bottom = firstBarBounds.bottom; + + for (var barIndex = 1; barIndex < segments.length; barIndex++) { + final bounds = segments[barIndex].bounds; + + left = min(left, bounds.left); + top = min(top, bounds.top); + right = max(right, bounds.right); + bottom = max(bottom, bounds.bottom); + } + + final width = right - left; + final height = bottom - top; + final fullStackRect = new Rectangle(left, top, width, height); + + return new CanvasBarStack._internal( + segments, + radius: radius, + stackedBarPadding: stackedBarPadding, + roundTopLeft: roundTopLeft, + roundTopRight: roundTopRight, + roundBottomLeft: roundBottomLeft, + roundBottomRight: roundBottomRight, + fullStackRect: fullStackRect, + ); + } + + CanvasBarStack._internal( + this.segments, { + this.radius, + this.stackedBarPadding = 1, + this.roundTopLeft = false, + this.roundTopRight = false, + this.roundBottomLeft = false, + this.roundBottomRight = false, + this.fullStackRect, + }); +} + +/// A list of [CanvasPieSlice]s to be painted by [ChartCanvas]. +class CanvasPie { + final List slices; + Point center; + double radius; + double innerRadius; + + /// Color of separator lines between arcs. + final Color stroke; + + /// Stroke width of separator lines between arcs. + double strokeWidthPx; + + CanvasPie(this.slices, this.center, this.radius, this.innerRadius, + {this.stroke, this.strokeWidthPx = 0.0}); +} + +/// A circle sector to be painted by [ChartCanvas]. +class CanvasPieSlice { + double startAngle; + double endAngle; + Color fill; + + CanvasPieSlice(this.startAngle, this.endAngle, {this.fill}); +} diff --git a/web/charts/common/lib/src/chart/common/chart_canvas.dart b/web/charts/common/lib/src/chart/common/chart_canvas.dart new file mode 100644 index 000000000..c2dadae84 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/chart_canvas.dart @@ -0,0 +1,163 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point, Rectangle; + +import '../../common/color.dart' show Color; +import '../../common/graphics_factory.dart' show GraphicsFactory; +import '../../common/text_element.dart' show TextElement; +import 'canvas_shapes.dart' show CanvasBarStack, CanvasPie; + +abstract class ChartCanvas { + /// Get [GraphicsFactory] for creating native graphics elements. + GraphicsFactory get graphicsFactory; + + /// Set the name of the view doing the rendering for debugging purposes, + /// or null when we believe rendering is complete. + set drawingView(String viewName); + + /// Renders a sector of a circle, with an optional hole in the center. + /// + /// [center] The x, y coordinates of the circle's center. + /// [radius] The radius of the circle. + /// [innerRadius] Optional radius of a hole in the center of the circle that + /// should not be filled in as part of the sector. + /// [startAngle] The angle at which the arc starts, measured clockwise from + /// the positive x axis and expressed in radians + /// [endAngle] The angle at which the arc ends, measured clockwise from the + /// positive x axis and expressed in radians. + /// [fill] Fill color for the sector. + /// [stroke] Stroke color of the arc and radius lines. + /// [strokeWidthPx] Stroke width of the arc and radius lines. + void drawCircleSector(Point center, double radius, double innerRadius, + double startAngle, double endAngle, + {Color fill, Color stroke, double strokeWidthPx}); + + /// Renders a simple line. + /// + /// [dashPattern] controls the pattern of dashes and gaps in a line. It is a + /// list of lengths of alternating dashes and gaps. The rendering is similar + /// to stroke-dasharray in SVG path elements. An odd number of values in the + /// pattern will be repeated to derive an even number of values. "1,2,3" is + /// equivalent to "1,2,3,1,2,3." + void drawLine( + {List points, + Rectangle clipBounds, + Color fill, + Color stroke, + bool roundEndCaps, + double strokeWidthPx, + List dashPattern}); + + /// Renders a pie, with an optional hole in the center. + void drawPie(CanvasPie canvasPie); + + /// Renders a simple point. + /// + /// [point] The x, y coordinates of the point. + /// + /// [radius] The radius of the point. + /// + /// [fill] Fill color for the point. + /// + /// [stroke] and [strokeWidthPx] configure the color and thickness of the + /// outer edge of the point. Both must be provided together for a line to + /// appear. + void drawPoint( + {Point point, + double radius, + Color fill, + Color stroke, + double strokeWidthPx}); + + /// Renders a polygon shape described by a set of points. + /// + /// [points] describes the vertices of the polygon. The last point will always + /// be connected to the first point to close the shape. + /// + /// [fill] configures the color inside the polygon. The shape will be + /// transparent if this is not provided. + /// + /// [stroke] and [strokeWidthPx] configure the color and thickness of the + /// edges of the polygon. Both must be provided together for a line to appear. + void drawPolygon( + {List points, + Rectangle clipBounds, + Color fill, + Color stroke, + double strokeWidthPx}); + + /// Renders a simple rectangle. + /// + /// [drawAreaBounds] if specified and if the bounds of the rectangle exceed + /// the draw area bounds on the top, the first x pixels (decided by the native + /// platform) exceeding the draw area will apply a gradient to transparent + /// with anything exceeding the x pixels to be transparent. + void drawRect(Rectangle bounds, + {Color fill, + Color stroke, + double strokeWidthPx, + Rectangle drawAreaBounds}); + + /// Renders a rounded rectangle. + void drawRRect(Rectangle bounds, + {Color fill, + Color stroke, + num radius, + bool roundTopLeft, + bool roundTopRight, + bool roundBottomLeft, + bool roundBottomRight}); + + /// Renders a stack of bars, rounding the last bar in the stack. + /// + /// The first bar of the stack is expected to be the "base" bar. This would + /// be the bottom most bar for a vertically rendered bar. + /// + /// [drawAreaBounds] if specified and if the bounds of the rectangle exceed + /// the draw area bounds on the top, the first x pixels (decided by the native + /// platform) exceeding the draw area will apply a gradient to transparent + /// with anything exceeding the x pixels to be transparent. + void drawBarStack(CanvasBarStack canvasBarStack, + {Rectangle drawAreaBounds}); + + void drawText(TextElement textElement, int offsetX, int offsetY, + {double rotation = 0.0}); + + /// Request the canvas to clip to [clipBounds]. + /// + /// Applies to all operations until [restClipBounds] is called. + void setClipBounds(Rectangle clipBounds); + + /// Restore + void resetClipBounds(); +} + +Color getAnimatedColor(Color previous, Color target, double animationPercent) { + var r = (((target.r - previous.r) * animationPercent) + previous.r).round(); + var g = (((target.g - previous.g) * animationPercent) + previous.g).round(); + var b = (((target.b - previous.b) * animationPercent) + previous.b).round(); + var a = (((target.a - previous.a) * animationPercent) + previous.a).round(); + + return new Color(a: a, r: r, g: g, b: b); +} + +/// Defines the pattern for a color fill. +/// +/// * [forwardHatch] defines a pattern of white lines angled up and to the right +/// on top of a bar filled with the fill color. +/// * [solid] defines a simple bar filled with the fill color. This is the +/// default pattern for bars. +enum FillPatternType { forwardHatch, solid } diff --git a/web/charts/common/lib/src/chart/common/chart_context.dart b/web/charts/common/lib/src/chart/common/chart_context.dart new file mode 100644 index 000000000..c382ab81e --- /dev/null +++ b/web/charts/common/lib/src/chart/common/chart_context.dart @@ -0,0 +1,61 @@ +// 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 '../../common/rtl_spec.dart' show RTLSpec; +import '../common/behavior/a11y/a11y_node.dart' show A11yNode; + +abstract class ChartContext { + /// Flag indicating whether or not the chart's container was configured in + /// right to left mode. + /// + /// This should be set when the chart is created (or if its container ever + /// gets configured to the other direction setting). + /// + /// Any chart component that needs to know whether the chart axes should be + /// rendered right to left should read [isRtl]. + bool get chartContainerIsRtl; + + /// Configures the behavior of the chart when [chartContainerIsRtl] is true. + RTLSpec get rtlSpec; + + /// Gets whether or not the chart axes should be rendered in right to left + /// mode. + /// + /// This will only be true if the container for the chart component was + /// configured with the rtl direction setting ([chartContainerIsRtl] == true), and the chart's + /// [RTLSpec] is set to reverse the axis direction in rtl mode. + bool get isRtl; + + /// 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; + + double get pixelsPerDp; + + DateTimeFactory get dateTimeFactory; + + void requestRedraw(); + + void requestAnimation(Duration transition); + + void requestPaint(); + + void enableA11yExploreMode(List nodes, {String announcement}); + + void disableA11yExploreMode({String announcement}); +} diff --git a/web/charts/common/lib/src/chart/common/datum_details.dart b/web/charts/common/lib/src/chart/common/datum_details.dart new file mode 100644 index 000000000..722427bda --- /dev/null +++ b/web/charts/common/lib/src/chart/common/datum_details.dart @@ -0,0 +1,222 @@ +// 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 '../../common/color.dart' show Color; +import '../../common/symbol_renderer.dart' show SymbolRenderer; +import 'processed_series.dart' show ImmutableSeries; + +typedef String DomainFormatter(D domain); +typedef String MeasureFormatter(num measure); + +/// Represents processed rendering details for a data point from a series. +class DatumDetails { + final dynamic datum; + + /// The index of the datum in the series. + final int index; + + /// Domain value of [datum]. + final D domain; + + /// Domain lower bound value of [datum]. This may represent an error bound, or + /// a previous domain value. + final D domainLowerBound; + + /// Domain upper bound value of [datum]. This may represent an error bound, or + /// a target domain value. + final D domainUpperBound; + + /// Measure value of [datum]. + final num measure; + + /// Measure lower bound value of [datum]. This may represent an error bound, + /// or a previous value. + final num measureLowerBound; + + /// Measure upper bound value of [datum]. This may represent an error bound, + /// or a target measure value. + final num measureUpperBound; + + /// Measure offset value of [datum]. + final num measureOffset; + + /// Original measure value of [datum]. This may differ from [measure] if a + /// behavior attached to a chart automatically adjusts measure values. + final num rawMeasure; + + /// Original measure lower bound value of [datum]. This may differ from + /// [measureLowerBound] if a behavior attached to a chart automatically + /// adjusts measure values. + final num rawMeasureLowerBound; + + /// Original measure upper bound value of [datum]. This may differ from + /// [measureUpperBound] if a behavior attached to a chart automatically + /// adjusts measure values. + final num rawMeasureUpperBound; + + /// The series the [datum] is from. + final ImmutableSeries series; + + /// The color of this [datum]. + final Color color; + + /// Optional fill color of this [datum]. + /// + /// If this is defined, then [color] will be used as a stroke color. + /// Otherwise, [color] will be used for the fill color. + final Color fillColor; + + /// Optional area color of this [datum]. + /// + /// This color is used for supplemental information on the series, such as + /// confidence intervals or area skirts. If not provided, then some variation + /// of the main [color] will be used (e.g. 10% opacity). + final Color areaColor; + + /// Optional dash pattern of this [datum]. + final List dashPattern; + + /// The chart position of the (domain, measure) for the [datum] from a + /// renderer. + final Point chartPosition; + + /// The chart position of the (domainLowerBound, measureLowerBound) for the + /// [datum] from a renderer. + final Point chartPositionLower; + + /// The chart position of the (domainUpperBound, measureUpperBound) for the + /// [datum] from a renderer. + final Point chartPositionUpper; + + /// Distance of [domain] from a given (x, y) coordinate. + final double domainDistance; + + /// Distance of [measure] from a given (x, y) coordinate. + final double measureDistance; + + /// Relative Cartesian distance of ([domain], [measure]) from a given (x, y) + /// coordinate. + final double relativeDistance; + + /// The radius of this [datum]. + final double radiusPx; + + /// Renderer used to draw the shape of this datum. + /// + /// This is primarily used for point shapes on line and scatter plot charts. + final SymbolRenderer symbolRenderer; + + /// The stroke width of this [datum]. + final double strokeWidthPx; + + /// Optional formatter for [domain]. + DomainFormatter domainFormatter; + + /// Optional formatter for [measure]. + MeasureFormatter measureFormatter; + + DatumDetails( + {this.datum, + this.index, + this.domain, + this.domainLowerBound, + this.domainUpperBound, + this.measure, + this.measureLowerBound, + this.measureUpperBound, + this.measureOffset, + this.rawMeasure, + this.rawMeasureLowerBound, + this.rawMeasureUpperBound, + this.series, + this.color, + this.fillColor, + this.areaColor, + this.dashPattern, + this.chartPosition, + this.chartPositionLower, + this.chartPositionUpper, + this.domainDistance, + this.measureDistance, + this.relativeDistance, + this.radiusPx, + this.symbolRenderer, + this.strokeWidthPx}); + + factory DatumDetails.from(DatumDetails other, + {D datum, + int index, + D domain, + D domainLowerBound, + D domainUpperBound, + num measure, + num measureLowerBound, + num measureUpperBound, + num measureOffset, + num rawMeasure, + num rawMeasureLowerBound, + num rawMeasureUpperBound, + ImmutableSeries series, + Color color, + Color fillColor, + Color areaColor, + List dashPattern, + Point chartPosition, + Point chartPositionLower, + Point chartPositionUpper, + double domainDistance, + double measureDistance, + double radiusPx, + SymbolRenderer symbolRenderer, + double strokeWidthPx}) { + return new DatumDetails( + datum: datum ?? other.datum, + index: index ?? other.index, + domain: domain ?? other.domain, + domainLowerBound: domainLowerBound ?? other.domainLowerBound, + domainUpperBound: domainUpperBound ?? other.domainUpperBound, + measure: measure ?? other.measure, + measureLowerBound: measureLowerBound ?? other.measureLowerBound, + measureUpperBound: measureUpperBound ?? other.measureUpperBound, + measureOffset: measureOffset ?? other.measureOffset, + rawMeasure: rawMeasure ?? other.rawMeasure, + rawMeasureLowerBound: + rawMeasureLowerBound ?? other.rawMeasureLowerBound, + rawMeasureUpperBound: + rawMeasureUpperBound ?? other.rawMeasureUpperBound, + series: series ?? other.series, + color: color ?? other.color, + fillColor: fillColor ?? other.fillColor, + areaColor: areaColor ?? other.areaColor, + dashPattern: dashPattern ?? other.dashPattern, + chartPosition: chartPosition ?? other.chartPosition, + chartPositionLower: chartPositionLower ?? other.chartPositionLower, + chartPositionUpper: chartPositionUpper ?? other.chartPositionUpper, + domainDistance: domainDistance ?? other.domainDistance, + measureDistance: measureDistance ?? other.measureDistance, + radiusPx: radiusPx ?? other.radiusPx, + symbolRenderer: symbolRenderer ?? other.symbolRenderer, + strokeWidthPx: radiusPx ?? other.strokeWidthPx); + } + + String get formattedDomain => + (domainFormatter != null) ? domainFormatter(domain) : domain.toString(); + + String get formattedMeasure => (measureFormatter != null) + ? measureFormatter(measure) + : measure.toString(); +} diff --git a/web/charts/common/lib/src/chart/common/processed_series.dart b/web/charts/common/lib/src/chart/common/processed_series.dart new file mode 100644 index 000000000..56b86bb9b --- /dev/null +++ b/web/charts/common/lib/src/chart/common/processed_series.dart @@ -0,0 +1,232 @@ +// 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 '../../data/series.dart' + show AccessorFn, Series, SeriesAttributes, AttributeKey; +import '../cartesian/axis/axis.dart' show Axis; +import '../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec; +import '../common/chart_canvas.dart' show FillPatternType; + +class MutableSeries extends ImmutableSeries { + final String id; + String displayName; + String seriesCategory; + bool overlaySeries; + int seriesIndex; + + /// Sum of the measure values for the series. + num seriesMeasureTotal; + + List data; + + AccessorFn keyFn; + + AccessorFn domainFn; + AccessorFn domainLowerBoundFn; + AccessorFn domainUpperBoundFn; + AccessorFn measureFn; + AccessorFn measureLowerBoundFn; + AccessorFn measureUpperBoundFn; + AccessorFn measureOffsetFn; + AccessorFn rawMeasureFn; + AccessorFn rawMeasureLowerBoundFn; + AccessorFn rawMeasureUpperBoundFn; + + AccessorFn areaColorFn; + AccessorFn colorFn; + AccessorFn> dashPatternFn; + AccessorFn fillColorFn; + AccessorFn fillPatternFn; + AccessorFn radiusPxFn; + AccessorFn strokeWidthPxFn; + AccessorFn labelAccessorFn; + AccessorFn insideLabelStyleAccessorFn; + AccessorFn outsideLabelStyleAccessorFn; + + final _attrs = new SeriesAttributes(); + + Axis measureAxis; + Axis domainAxis; + + MutableSeries(Series series) : this.id = series.id { + displayName = series.displayName ?? series.id; + seriesCategory = series.seriesCategory; + overlaySeries = series.overlaySeries; + + data = series.data; + keyFn = series.keyFn; + + domainFn = series.domainFn; + domainLowerBoundFn = series.domainLowerBoundFn; + domainUpperBoundFn = series.domainUpperBoundFn; + + measureFn = series.measureFn; + measureLowerBoundFn = series.measureLowerBoundFn; + measureUpperBoundFn = series.measureUpperBoundFn; + measureOffsetFn = series.measureOffsetFn; + + // Save the original measure functions in case they get replaced later. + rawMeasureFn = series.measureFn; + rawMeasureLowerBoundFn = series.measureLowerBoundFn; + rawMeasureUpperBoundFn = series.measureUpperBoundFn; + + // Pre-compute the sum of the measure values to make it available on demand. + seriesMeasureTotal = 0; + for (int i = 0; i < data.length; i++) { + final measure = measureFn(i); + if (measure != null) { + seriesMeasureTotal += measure; + } + } + + areaColorFn = series.areaColorFn; + colorFn = series.colorFn; + dashPatternFn = series.dashPatternFn; + fillColorFn = series.fillColorFn; + fillPatternFn = series.fillPatternFn; + labelAccessorFn = series.labelAccessorFn ?? (i) => domainFn(i).toString(); + insideLabelStyleAccessorFn = series.insideLabelStyleAccessorFn; + outsideLabelStyleAccessorFn = series.outsideLabelStyleAccessorFn; + + radiusPxFn = series.radiusPxFn; + strokeWidthPxFn = series.strokeWidthPxFn; + + _attrs.mergeFrom(series.attributes); + } + + MutableSeries.clone(MutableSeries other) : this.id = other.id { + displayName = other.displayName; + seriesCategory = other.seriesCategory; + overlaySeries = other.overlaySeries; + seriesIndex = other.seriesIndex; + + data = other.data; + keyFn = other.keyFn; + + domainFn = other.domainFn; + domainLowerBoundFn = other.domainLowerBoundFn; + domainUpperBoundFn = other.domainUpperBoundFn; + + measureFn = other.measureFn; + measureLowerBoundFn = other.measureLowerBoundFn; + measureUpperBoundFn = other.measureUpperBoundFn; + measureOffsetFn = other.measureOffsetFn; + + rawMeasureFn = other.rawMeasureFn; + rawMeasureLowerBoundFn = other.rawMeasureLowerBoundFn; + rawMeasureUpperBoundFn = other.rawMeasureUpperBoundFn; + + seriesMeasureTotal = other.seriesMeasureTotal; + + areaColorFn = other.areaColorFn; + colorFn = other.colorFn; + dashPatternFn = other.dashPatternFn; + fillColorFn = other.fillColorFn; + fillPatternFn = other.fillPatternFn; + labelAccessorFn = other.labelAccessorFn; + insideLabelStyleAccessorFn = other.insideLabelStyleAccessorFn; + outsideLabelStyleAccessorFn = other.outsideLabelStyleAccessorFn; + radiusPxFn = other.radiusPxFn; + strokeWidthPxFn = other.strokeWidthPxFn; + + _attrs.mergeFrom(other._attrs); + measureAxis = other.measureAxis; + domainAxis = other.domainAxis; + } + + void setAttr(AttributeKey key, R value) { + this._attrs.setAttr(key, value); + } + + R getAttr(AttributeKey key) { + return this._attrs.getAttr(key); + } + + bool operator ==(Object other) => + other is MutableSeries && data == other.data && id == other.id; + + @override + int get hashCode => data.hashCode * 31 + id.hashCode; +} + +abstract class ImmutableSeries { + String get id; + + String get displayName; + + String get seriesCategory; + + bool get overlaySeries; + + int get seriesIndex; + + /// Sum of the measure values for the series. + num get seriesMeasureTotal; + + List get data; + + /// [keyFn] defines a globally unique identifier for each datum. + /// + /// The key for each datum is used during chart animation to smoothly + /// transition data still in the series to its new state. + /// + /// Note: This is currently an optional function that is not fully used by all + /// series renderers yet. + AccessorFn keyFn; + + AccessorFn get domainFn; + + AccessorFn get domainLowerBoundFn; + + AccessorFn get domainUpperBoundFn; + + AccessorFn get measureFn; + + AccessorFn get measureLowerBoundFn; + + AccessorFn get measureUpperBoundFn; + + AccessorFn get measureOffsetFn; + + AccessorFn get rawMeasureFn; + + AccessorFn get rawMeasureLowerBoundFn; + + AccessorFn get rawMeasureUpperBoundFn; + + AccessorFn get areaColorFn; + + AccessorFn get colorFn; + + AccessorFn> get dashPatternFn; + + AccessorFn get fillColorFn; + + AccessorFn get fillPatternFn; + + AccessorFn get labelAccessorFn; + + AccessorFn insideLabelStyleAccessorFn; + AccessorFn outsideLabelStyleAccessorFn; + + AccessorFn get radiusPxFn; + + AccessorFn get strokeWidthPxFn; + + void setAttr(AttributeKey key, R value); + + R getAttr(AttributeKey key); +} diff --git a/web/charts/common/lib/src/chart/common/selection_model/selection_model.dart b/web/charts/common/lib/src/chart/common/selection_model/selection_model.dart new file mode 100644 index 000000000..bbf7652d0 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/selection_model/selection_model.dart @@ -0,0 +1,233 @@ +// 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 '../processed_series.dart' show ImmutableSeries; +import '../series_datum.dart' show SeriesDatum, SeriesDatumConfig; + +/// Holds the state of interaction or selection for the chart to coordinate +/// between various event sources and things that wish to act upon the selection +/// state (highlight, drill, etc). +/// +/// There is one instance per interaction type (ex: info, action) with each +/// maintaining their own state. Info is typically used to update a hover/touch +/// card while action is used in case of a secondary selection/action. +/// +/// The series selection state is kept separate from datum selection state to +/// allow more complex highlighting. For example: a Hovercard that shows entries +/// for each datum for a given domain/time, but highlights the closest entry to +/// match up with highlighting/bolding of the line and legend. +class SelectionModel { + var _selectedDatum = >[]; + var _selectedSeries = >[]; + + /// Create selection model with the desired selection. + SelectionModel( + {List> selectedData, + List> selectedSeries}) { + if (selectedData != null) { + _selectedDatum = selectedData; + } + if (selectedSeries != null) { + _selectedSeries = selectedSeries; + } + } + + /// Create a deep copy of the selection model. + SelectionModel.fromOther(SelectionModel other) { + _selectedDatum = new List.from(other._selectedDatum); + _selectedSeries = new List.from(other._selectedSeries); + } + + /// Create selection model from configuration. + SelectionModel.fromConfig(List selectedDataConfig, + List selectedSeriesConfig, List> seriesList) { + final selectedDataMap = >{}; + + if (selectedDataConfig != null) { + for (SeriesDatumConfig config in selectedDataConfig) { + selectedDataMap[config.seriesId] ??= []; + selectedDataMap[config.seriesId].add(config.domainValue); + } + + // Add to list of selected series. + _selectedSeries.addAll(seriesList.where((ImmutableSeries series) => + selectedDataMap.keys.contains(series.id))); + + // Add to list of selected data. + for (ImmutableSeries series in seriesList) { + if (selectedDataMap.containsKey(series.id)) { + final domainFn = series.domainFn; + + for (var i = 0; i < series.data.length; i++) { + final datum = series.data[i]; + + if (selectedDataMap[series.id].contains(domainFn(i))) { + _selectedDatum.add(new SeriesDatum(series, datum)); + } + } + } + } + } + + // Add to list of selected series, if it does not already exist. + if (selectedSeriesConfig != null) { + final remainingSeriesToAdd = selectedSeriesConfig + .where((String seriesId) => !selectedSeries.contains(seriesId)) + .toList(); + + _selectedSeries.addAll(seriesList.where((ImmutableSeries series) => + remainingSeriesToAdd.contains(series.id))); + } + } + + /// Returns true if this [SelectionModel] has a selected datum. + bool get hasDatumSelection => _selectedDatum.isNotEmpty; + + bool isDatumSelected(ImmutableSeries series, int index) { + final datum = index == null ? null : series.data[index]; + return _selectedDatum.contains(new SeriesDatum(series, datum)); + } + + /// Returns the selected [SeriesDatum] for this [SelectionModel]. + /// + /// This is empty by default. + List> get selectedDatum => + new List.unmodifiable(_selectedDatum); + + /// Returns true if this [SelectionModel] has a selected series. + bool get hasSeriesSelection => _selectedSeries.isNotEmpty; + + /// Returns the selected [ImmutableSeries] for this [SelectionModel]. + /// + /// This is empty by default. + List> get selectedSeries => + new List.unmodifiable(_selectedSeries); + + /// Returns true if this [SelectionModel] has a selected datum or series. + bool get hasAnySelection => + _selectedDatum.isNotEmpty || selectedSeries.isNotEmpty; + + @override + bool operator ==(Object other) { + return other is SelectionModel && + new ListEquality().equals(_selectedDatum, other.selectedDatum) && + new ListEquality().equals(_selectedSeries, other.selectedSeries); + } + + @override + int get hashCode { + int hashcode = new ListEquality().hash(_selectedDatum); + hashcode = hashcode * 37 + new ListEquality().hash(_selectedSeries); + return hashcode; + } +} + +/// A [SelectionModel] that can be updated. +/// +/// This model will notify listeners subscribed to this model when the selection +/// is modified. +class MutableSelectionModel extends SelectionModel { + final _changedListeners = >[]; + final _updatedListeners = >[]; + + /// When set to true, prevents the model from being updated. + bool locked = false; + + /// Clears the selection state. + bool clearSelection({bool notifyListeners = true}) { + return updateSelection([], [], notifyListeners: notifyListeners); + } + + /// Updates the selection state. If mouse driven, [datumSelection] should be + /// ordered by distance from mouse, closest first. + bool updateSelection( + List> datumSelection, List> seriesList, + {bool notifyListeners = true}) { + if (locked) { + return false; + } + + final origSelectedDatum = _selectedDatum; + final origSelectedSeries = _selectedSeries; + + _selectedDatum = datumSelection; + _selectedSeries = seriesList; + + // Provide a copy, so listeners get an immutable model. + final copyOfSelectionModel = new SelectionModel.fromOther(this); + _updatedListeners.forEach((listener) => listener(copyOfSelectionModel)); + + final changed = + !new ListEquality().equals(origSelectedDatum, _selectedDatum) || + !new ListEquality().equals(origSelectedSeries, _selectedSeries); + if (notifyListeners && changed) { + _changedListeners.forEach((listener) => listener(copyOfSelectionModel)); + } + return changed; + } + + /// Add a listener to be notified when this [SelectionModel] changes. + /// + /// Note: the listener will not be triggered if [updateSelection] is called + /// resulting in the same selection state. + void addSelectionChangedListener(SelectionModelListener listener) { + _changedListeners.add(listener); + } + + /// Remove listener from being notified when this [SelectionModel] changes. + void removeSelectionChangedListener(SelectionModelListener listener) { + _changedListeners.remove(listener); + } + + /// Add a listener to be notified when [updateSelection] is called, even if + /// the selection state is the same. + /// + /// This is necessary in order to support programmatic selections in Flutter. + /// Due to the way widgets are constructed in Flutter, there currently isn't + /// a way for users to programmatically specify the selection. In order to + /// provide this support, the users who subscribe to the selection updated + /// event can keep a copy of the selection model and also decide if it should + /// be overwritten. + void addSelectionUpdatedListener(SelectionModelListener listener) { + _updatedListeners.add(listener); + } + + /// Remove listener from being notified when [updateSelection] is called. + void removeSelectionUpdatedListener(SelectionModelListener listener) { + _updatedListeners.remove(listener); + } + + /// Remove all listeners. + void clearAllListeners() { + _changedListeners.clear(); + _updatedListeners.clear(); + } +} + +/// Callback for SelectionModel. It is triggered when the selection state +/// changes. +typedef SelectionModelListener(SelectionModel model); + +enum SelectionModelType { + /// Typical Hover or Details event for viewing the details of the selected + /// items. + info, + + /// Typical Selection, Drill or Input event likely updating some external + /// content. + action, +} diff --git a/web/charts/common/lib/src/chart/common/series_datum.dart b/web/charts/common/lib/src/chart/common/series_datum.dart new file mode 100644 index 000000000..12185b9ff --- /dev/null +++ b/web/charts/common/lib/src/chart/common/series_datum.dart @@ -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 'processed_series.dart' show ImmutableSeries; + +/// Stores datum and the series the datum originated. +class SeriesDatum { + final ImmutableSeries series; + final dynamic datum; + int _index; + + SeriesDatum(this.series, this.datum) { + _index = datum == null ? null : series.data.indexOf(datum); + } + + int get index => _index; + + @override + bool operator ==(Object other) => + other is SeriesDatum && other.series == series && other.datum == datum; + + @override + int get hashCode => series.hashCode * 31 + datum.hashCode; +} + +/// Represents a series datum based on series id and datum index. +class SeriesDatumConfig { + final String seriesId; + final D domainValue; + + SeriesDatumConfig(this.seriesId, this.domainValue); + + @override + bool operator ==(Object other) { + return other is SeriesDatumConfig && + seriesId == other.seriesId && + domainValue == other.domainValue; + } + + @override + int get hashCode { + int hashcode = seriesId.hashCode; + hashcode = hashcode * 37 + domainValue.hashCode; + return hashcode; + } +} diff --git a/web/charts/common/lib/src/chart/common/series_renderer.dart b/web/charts/common/lib/src/chart/common/series_renderer.dart new file mode 100644 index 000000000..d0d652973 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/series_renderer.dart @@ -0,0 +1,395 @@ +// 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; + +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 SymbolRenderer; +import '../../data/series.dart' show AttributeKey; +import '../layout/layout_view.dart' + show + LayoutPosition, + LayoutView, + LayoutViewConfig, + LayoutViewPositionOrder, + ViewMeasuredSizes; +import 'base_chart.dart' show BaseChart; +import 'chart_canvas.dart' show ChartCanvas; +import 'datum_details.dart' show DatumDetails; +import 'processed_series.dart' show ImmutableSeries, MutableSeries; +import 'series_datum.dart' show SeriesDatum; + +/// Unique identifier used to associate custom series renderers on a chart with +/// one or more series of data. +/// +/// [rendererIdKey] can be added as an attribute to user-defined [Series] +/// objects. +const AttributeKey rendererIdKey = + const AttributeKey('SeriesRenderer.rendererId'); + +const AttributeKey rendererKey = + const AttributeKey('SeriesRenderer.renderer'); + +/// A series renderer draws one or more series of data onto a chart canvas. +abstract class SeriesRenderer extends LayoutView { + static const defaultRendererId = 'default'; + + /// Symbol renderer for this renderer. + /// + /// The default is set natively by the platform. This is because in Flutter, + /// the [SymbolRenderer] has to be a Flutter wrapped version to support + /// building widget based symbols. + SymbolRenderer get symbolRenderer; + + set symbolRenderer(SymbolRenderer symbolRenderer); + + /// Unique identifier for this renderer. Any [Series] on a chart with a + /// matching [rendererIdKey] will be drawn by this renderer. + String get rendererId; + + set rendererId(String rendererId); + + /// Handles any setup of the renderer that needs to be deferred until it is + /// attached to a chart. + void onAttach(BaseChart chart); + + /// Handles any clean-up of the renderer that needs to be performed when it is + /// detached from a chart. + void onDetach(BaseChart chart); + + /// Performs basic configuration for the series, before it is pre-processed. + /// + /// Typically, a series renderer should assign color mapping functions to + /// series that do not have them. + void configureSeries(List> seriesList); + + /// Pre-calculates some details for the series that will be needed later + /// during the drawing phase. + void preprocessSeries(List> seriesList); + + /// Adds the domain values for the given series to the chart's domain axis. + void configureDomainAxes(List> seriesList); + + /// Adds the measure values for the given series to the chart's measure axes. + void configureMeasureAxes(List> seriesList); + + /// Generates rendering data needed to paint the data on the chart. + /// + /// This is called during the post layout phase of the chart draw cycle. + void update(List> seriesList, bool isAnimating); + + /// Renders the series data on the canvas, using the data generated during the + /// [update] call. + void paint(ChartCanvas canvas, double animationPercent); + + /// Gets a list the data from each series that is closest to a given point. + /// + /// [chartPoint] represents a point in the chart, such as a point that was + /// clicked/tapped on by a user. + /// + /// [byDomain] specifies whether the nearest data should be defined by domain + /// distance, or relative Cartesian distance. + /// + /// [boundsOverride] optionally specifies a bounding box for the selection + /// event. If specified, then no data should be returned if [chartPoint] lies + /// outside the box. If not specified, then each series renderer on the chart + /// will use its own component bounds for filtering out selection events + /// (usually the chart draw area). + List> getNearestDatumDetailPerSeries( + Point chartPoint, bool byDomain, Rectangle boundsOverride); + + /// Get an expanded set of processed [DatumDetails] for a given [SeriesDatum]. + /// + /// This is typically called by chart behaviors that need to get full details + /// on selected data. + DatumDetails getDetailsForSeriesDatum(SeriesDatum seriesDatum); + + /// Adds chart position data to [details]. + /// + /// This is a helper function intended to be called from + /// [getDetailsForSeriesDatum]. Every concrete [SeriesRenderer] needs to + /// implement custom logic for setting location data. + DatumDetails addPositionToDetailsForSeriesDatum( + DatumDetails details, SeriesDatum seriesDatum); +} + +/// Concrete base class for [SeriesRenderer]s that implements common +/// functionality. +abstract class BaseSeriesRenderer implements SeriesRenderer { + final LayoutViewConfig layoutConfig; + + String rendererId; + + SymbolRenderer symbolRenderer; + + Rectangle _drawAreaBounds; + + Rectangle get drawBounds => _drawAreaBounds; + + GraphicsFactory _graphicsFactory; + + BaseSeriesRenderer({ + @required this.rendererId, + @required int layoutPaintOrder, + this.symbolRenderer, + }) : this.layoutConfig = new LayoutViewConfig( + paintOrder: layoutPaintOrder, + position: LayoutPosition.DrawArea, + positionOrder: LayoutViewPositionOrder.drawArea); + + @override + GraphicsFactory get graphicsFactory => _graphicsFactory; + + @override + set graphicsFactory(GraphicsFactory value) { + _graphicsFactory = value; + } + + @override + void onAttach(BaseChart chart) {} + + @override + void onDetach(BaseChart chart) {} + + /// Assigns colors to series that are missing their colorFn. + /// + /// [emptyCategoryUsesSinglePalette] Flag indicating whether having all + /// series with no categories will use the same or separate palettes. + /// Setting it to true uses various Blues for each series. + /// Setting it to false used different palettes (ie: s1 uses Blue500, + /// s2 uses Red500), + @protected + assignMissingColors(Iterable> seriesList, + {@required bool emptyCategoryUsesSinglePalette}) { + const defaultCategory = '__default__'; + + // Count up the number of missing series per category, keeping a max across + // categories. + final missingColorCountPerCategory = {}; + int maxMissing = 0; + bool hasSpecifiedCategory = false; + + seriesList.forEach((MutableSeries series) { + if (series.colorFn == null) { + // If there is no category, give it a default category to match logic. + String category = series.seriesCategory; + if (category == null) { + category = defaultCategory; + } else { + hasSpecifiedCategory = true; + } + + // Increment the missing counts for the category. + final missingCnt = (missingColorCountPerCategory[category] ?? 0) + 1; + missingColorCountPerCategory[category] = missingCnt; + maxMissing = max(maxMissing, missingCnt); + } + }); + + if (maxMissing > 0) { + // Special handling of only series with empty categories when we want + // to use different palettes. + if (!emptyCategoryUsesSinglePalette && !hasSpecifiedCategory) { + final palettes = StyleFactory.style.getOrderedPalettes(maxMissing); + int index = 0; + seriesList.forEach((MutableSeries series) { + if (series.colorFn == null) { + final color = palettes[index % palettes.length].shadeDefault; + index++; + series.colorFn = (_) => color; + } + }); + return; + } + + // Get a list of palettes to use given the number of categories we've + // seen. One palette per category (but might need to repeat). + final colorPalettes = StyleFactory.style + .getOrderedPalettes(missingColorCountPerCategory.length); + + // Create a map of Color palettes for each category. Each Palette uses + // the max for any category to ensure that the gradients look appropriate. + final colorsByCategory = >{}; + int index = 0; + missingColorCountPerCategory.keys.forEach((String category) { + colorsByCategory[category] = + colorPalettes[index % colorPalettes.length].makeShades(maxMissing); + index++; + + // Reset the count so we can use it to count as we set the colorFn. + missingColorCountPerCategory[category] = 0; + }); + + seriesList.forEach((MutableSeries series) { + if (series.colorFn == null) { + final category = series.seriesCategory ?? defaultCategory; + + // Get the current index into the color list. + final colorIndex = missingColorCountPerCategory[category]; + missingColorCountPerCategory[category] = colorIndex + 1; + + final color = colorsByCategory[category][colorIndex]; + series.colorFn = (_) => color; + } + + // Fill color defaults to the series color if no accessor is provided. + series.fillColorFn ??= (int index) => series.colorFn(index); + }); + } else { + seriesList.forEach((MutableSeries series) { + // Fill color defaults to the series color if no accessor is provided. + series.fillColorFn ??= (int index) => series.colorFn(index); + }); + } + } + + @override + ViewMeasuredSizes measure(int maxWidth, int maxHeight) { + return null; + } + + @override + void layout(Rectangle componentBounds, Rectangle drawAreaBounds) { + this._drawAreaBounds = drawAreaBounds; + } + + @override + Rectangle get componentBounds => this._drawAreaBounds; + + @override + bool get isSeriesRenderer => true; + + @override + void configureSeries(List> seriesList) {} + + @override + void preprocessSeries(List> seriesList) {} + + @override + void configureDomainAxes(List> seriesList) {} + + @override + void configureMeasureAxes(List> seriesList) {} + + @override + DatumDetails getDetailsForSeriesDatum(SeriesDatum seriesDatum) { + // Generate details relevant to every type of series renderer. Position + // details are left as an exercise for every renderer that extends this + // class. + final series = seriesDatum.series; + final index = seriesDatum.index; + final domainFn = series.domainFn; + final domainLowerBoundFn = series.domainLowerBoundFn; + final domainUpperBoundFn = series.domainUpperBoundFn; + final measureFn = series.measureFn; + final measureLowerBoundFn = series.measureLowerBoundFn; + final measureUpperBoundFn = series.measureUpperBoundFn; + final measureOffsetFn = series.measureOffsetFn; + final rawMeasureFn = series.rawMeasureFn; + final rawMeasureLowerBoundFn = series.rawMeasureLowerBoundFn; + final rawMeasureUpperBoundFn = series.rawMeasureUpperBoundFn; + final colorFn = series.colorFn; + final areaColorFn = series.areaColorFn ?? colorFn; + final fillColorFn = series.fillColorFn ?? colorFn; + final radiusPxFn = series.radiusPxFn; + final strokeWidthPxFn = series.strokeWidthPxFn; + + final domainValue = domainFn(index); + final domainLowerBoundValue = + domainLowerBoundFn != null ? domainLowerBoundFn(index) : null; + final domainUpperBoundValue = + domainUpperBoundFn != null ? domainUpperBoundFn(index) : null; + + final measureValue = measureFn(index); + final measureLowerBoundValue = + measureLowerBoundFn != null ? measureLowerBoundFn(index) : null; + final measureUpperBoundValue = + measureUpperBoundFn != null ? measureUpperBoundFn(index) : null; + final measureOffsetValue = + measureOffsetFn != null ? measureOffsetFn(index) : null; + + final rawMeasureValue = rawMeasureFn(index); + final rawMeasureLowerBoundValue = + rawMeasureLowerBoundFn != null ? rawMeasureLowerBoundFn(index) : null; + final rawMeasureUpperBoundValue = + rawMeasureUpperBoundFn != null ? rawMeasureUpperBoundFn(index) : null; + + final color = colorFn(index); + + // Fill color is an optional override for color. Make sure we get a value if + // the series doesn't define anything specific. + var fillColor = fillColorFn(index); + fillColor ??= color; + + // Area color is entirely optional. + final areaColor = areaColorFn(index); + + var radiusPx = radiusPxFn != null ? radiusPxFn(index) : null; + radiusPx = radiusPx?.toDouble(); + + var strokeWidthPx = strokeWidthPxFn != null ? strokeWidthPxFn(index) : null; + strokeWidthPx = strokeWidthPx?.toDouble(); + + final details = new DatumDetails( + datum: seriesDatum.datum, + index: seriesDatum.index, + domain: domainValue, + domainLowerBound: domainLowerBoundValue, + domainUpperBound: domainUpperBoundValue, + measure: measureValue, + measureLowerBound: measureLowerBoundValue, + measureUpperBound: measureUpperBoundValue, + measureOffset: measureOffsetValue, + rawMeasure: rawMeasureValue, + rawMeasureLowerBound: rawMeasureLowerBoundValue, + rawMeasureUpperBound: rawMeasureUpperBoundValue, + series: series, + color: color, + fillColor: fillColor, + areaColor: areaColor, + radiusPx: radiusPx, + strokeWidthPx: strokeWidthPx); + + // chartPosition depends on the shape of the rendered elements, and must be + // added by concrete [SeriesRenderer] classes. + return addPositionToDetailsForSeriesDatum(details, seriesDatum); + } + + /// Returns true of [chartPoint] is within the component bounds for this + /// renderer. + /// + /// [chartPoint] a point to test. + /// + /// [bounds] optional override for component bounds. If this is passed, then + /// we will check whether the point is within these bounds instead of the + /// component bounds. + bool isPointWithinBounds(Point chartPoint, Rectangle bounds) { + // Was it even in the drawArea? + if (bounds != null) { + if (!bounds.containsPoint(chartPoint)) { + return false; + } + } else if (componentBounds == null || + !componentBounds.containsPoint(chartPoint)) { + return false; + } + + return true; + } +} diff --git a/web/charts/common/lib/src/chart/common/series_renderer_config.dart b/web/charts/common/lib/src/chart/common/series_renderer_config.dart new file mode 100644 index 000000000..87bf19701 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/series_renderer_config.dart @@ -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 '../../common/symbol_renderer.dart'; +import '../../common/typed_registry.dart'; +import 'series_renderer.dart' show SeriesRenderer; + +/// Interface for series renderer configuration. +abstract class SeriesRendererConfig { + /// Stores typed renderer attributes + /// + /// This is useful for storing attributes that is used on the native platform. + /// Such as the SymbolRenderer that is associated with each renderer but is + /// a native builder since legend is built natively. + RendererAttributes get rendererAttributes; + + String get customRendererId; + + SymbolRenderer get symbolRenderer; + + SeriesRenderer build(); +} + +class RendererAttributeKey extends TypedKey { + const RendererAttributeKey(String uniqueKey) : super(uniqueKey); +} + +class RendererAttributes extends TypedRegistry {} diff --git a/web/charts/common/lib/src/chart/common/unitconverter/identity_converter.dart b/web/charts/common/lib/src/chart/common/unitconverter/identity_converter.dart new file mode 100644 index 000000000..599d77237 --- /dev/null +++ b/web/charts/common/lib/src/chart/common/unitconverter/identity_converter.dart @@ -0,0 +1,27 @@ +// 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 'unit_converter.dart' show UnitConverter; + +/// A No op unit converter. +class IdentityConverter implements UnitConverter { + const IdentityConverter(); + + @override + convert(U value) => value; + + @override + invert(U value) => value; +} diff --git a/web/charts/common/lib/src/chart/common/unitconverter/unit_converter.dart b/web/charts/common/lib/src/chart/common/unitconverter/unit_converter.dart new file mode 100644 index 000000000..e1317f20f --- /dev/null +++ b/web/charts/common/lib/src/chart/common/unitconverter/unit_converter.dart @@ -0,0 +1,26 @@ +// 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. + +/// Converts a num value in the 'from' unit to a num value in the 'to' unit. +/// +/// [F] Type of the value in the 'from' units. +/// [T] Type of the value in 'to' units. +abstract class UnitConverter { + /// Converts 'from' unit value to the 'to' unit value. + T convert(F value); + + /// Converts 'to' unit value back to the 'from' unit value. + F invert(T value); +} diff --git a/web/charts/common/lib/src/chart/layout/layout_config.dart b/web/charts/common/lib/src/chart/layout/layout_config.dart new file mode 100644 index 000000000..7c05f73b0 --- /dev/null +++ b/web/charts/common/lib/src/chart/layout/layout_config.dart @@ -0,0 +1,121 @@ +// 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. + +/// Collection of configurations that apply to the [LayoutManager]. +class LayoutConfig { + final MarginSpec leftSpec; + final MarginSpec rightSpec; + final MarginSpec topSpec; + final MarginSpec bottomSpec; + + /// Create a new [LayoutConfig] used by [DynamicLayoutManager]. + LayoutConfig({ + MarginSpec leftSpec, + MarginSpec rightSpec, + MarginSpec topSpec, + MarginSpec bottomSpec, + }) : leftSpec = leftSpec ?? MarginSpec.defaultSpec, + rightSpec = rightSpec ?? MarginSpec.defaultSpec, + topSpec = topSpec ?? MarginSpec.defaultSpec, + bottomSpec = bottomSpec ?? MarginSpec.defaultSpec; +} + +/// Specs that applies to one margin. +class MarginSpec { + /// [MarginSpec] that has max of 50 percent. + static const defaultSpec = const MarginSpec._internal(null, null, null, 50); + + final int _minPixel; + final int _maxPixel; + final int _minPercent; + final int _maxPercent; + + const MarginSpec._internal( + int minPixel, int maxPixel, int minPercent, int maxPercent) + : _minPixel = minPixel, + _maxPixel = maxPixel, + _minPercent = minPercent, + _maxPercent = maxPercent; + + /// Create [MarginSpec] that specifies min/max pixels. + /// + /// [minPixel] if set must be greater than or equal to 0 and less than max if + /// it is also set. + /// [maxPixel] if set must be greater than or equal to 0. + factory MarginSpec.fromPixel({int minPixel, int maxPixel}) { + // Require zero or higher settings if set + assert(minPixel == null || minPixel >= 0); + assert(maxPixel == null || maxPixel >= 0); + // Min must be less than or equal to max. + // Can be equal to enforce strict pixel size. + if (minPixel != null && maxPixel != null) { + assert(minPixel <= maxPixel); + } + + return new MarginSpec._internal(minPixel, maxPixel, null, null); + } + + /// Create [MarginSpec] with a fixed pixel size [pixels]. + /// + /// [pixels] if set must be greater than or equal to 0. + factory MarginSpec.fixedPixel(int pixels) { + // Require require or higher setting if set + assert(pixels == null || pixels >= 0); + + return new MarginSpec._internal(pixels, pixels, null, null); + } + + /// Create [MarginSpec] that specifies min/max percentage. + /// + /// [minPercent] if set must be between 0 and 100 inclusive. If [maxPercent] + /// is also set, then must be less than [maxPercent]. + /// [maxPercent] if set must be between 0 and 100 inclusive. + factory MarginSpec.fromPercent({int minPercent, int maxPercent}) { + // Percent must be within 0 to 100 + assert(minPercent == null || (minPercent >= 0 && minPercent <= 100)); + assert(maxPercent == null || (maxPercent >= 0 && maxPercent <= 100)); + // Min must be less than or equal to max. + // Can be equal to enforce strict percentage. + if (minPercent != null && maxPercent != null) { + assert(minPercent <= maxPercent); + } + + return new MarginSpec._internal(null, null, minPercent, maxPercent); + } + + /// Get the min pixels, given the [totalPixels]. + int getMinPixels(int totalPixels) { + if (_minPixel != null) { + assert(_minPixel < totalPixels); + return _minPixel; + } else if (_minPercent != null) { + return (totalPixels * (_minPercent / 100)).round(); + } else { + return 0; + } + } + + /// Get the max pixels, given the [totalPixels]. + int getMaxPixels(int totalPixels) { + if (_maxPixel != null) { + assert(_maxPixel < totalPixels); + return _maxPixel; + } else if (_maxPercent != null) { + return (totalPixels * (_maxPercent / 100)).round(); + } else { + return totalPixels; + } + } +} diff --git a/web/charts/common/lib/src/chart/layout/layout_manager.dart b/web/charts/common/lib/src/chart/layout/layout_manager.dart new file mode 100644 index 000000000..0ea88cd10 --- /dev/null +++ b/web/charts/common/lib/src/chart/layout/layout_manager.dart @@ -0,0 +1,70 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point, Rectangle; + +import 'layout_view.dart' show LayoutView; + +abstract class LayoutManager { + /// Adds a view to be managed by the LayoutManager. + void addView(LayoutView view); + + /// Removes a view previously added to the LayoutManager. + /// No-op if it wasn't there to begin with. + void removeView(LayoutView view); + + /// Returns true if view is already attached. + bool isAttached(LayoutView view); + + /// Walk through the child views and determine their desired sizes storing + /// off the information for layout. + void measure(int width, int height); + + /// Walk through the child views and set their bounds from the perspective + /// of the canvas origin. + void layout(int width, int height); + + /// Returns the bounds of the drawArea. Must be called after layout(). + Rectangle get drawAreaBounds; + + /// Returns the combined bounds of the drawArea, and all components that + /// function as series draw areas. Must be called after layout(). + Rectangle get drawableLayoutAreaBounds; + + /// Gets the measured size of the bottom margin, available after layout. + int get marginBottom; + + /// Gets the measured size of the left margin, available after layout. + int get marginLeft; + + /// Gets the measured size of the right margin, available after layout. + int get marginRight; + + /// Gets the measured size of the top margin, available after layout. + int get marginTop; + + /// Returns whether or not [point] is within the draw area bounds. + bool withinDrawArea(Point point); + + /// Walk through the child views and apply the function passed in. + void applyToViews(void apply(LayoutView view)); + + /// Return the child views in the order that they should be drawn. + List get paintOrderedViews; + + /// Return the child views in the order that they should be positioned within + /// chart margins. + List get positionOrderedViews; +} diff --git a/web/charts/common/lib/src/chart/layout/layout_manager_impl.dart b/web/charts/common/lib/src/chart/layout/layout_manager_impl.dart new file mode 100644 index 000000000..55dbaaa7b --- /dev/null +++ b/web/charts/common/lib/src/chart/layout/layout_manager_impl.dart @@ -0,0 +1,368 @@ +// 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; + +import 'package:meta/meta.dart' show required; + +import 'layout_config.dart' show LayoutConfig; +import 'layout_manager.dart'; +import 'layout_margin_strategy.dart'; +import 'layout_view.dart' show LayoutView, LayoutPosition; + +/// Default Layout manager for [LayoutView]s. +class LayoutManagerImpl implements LayoutManager { + static const _minDrawWidth = 20; + static const _minDrawHeight = 20; + + // Allow [Layoutconfig] to be mutable so it can be modified without requiring + // a new copy of [DefaultLayoutManager] to be created. + LayoutConfig config; + + /// Unordered list of views in the layout. + final _views = []; + + /// List of views in the order they should be drawn on the canvas. + /// + /// First element is painted first. + List _paintOrderedViews; + + /// List of vies in the order they should be positioned in a chart margin. + /// + /// First element is closest to the draw area. + List _positionOrderedViews; + + _MeasuredSizes _measurements; + + Rectangle _drawAreaBounds; + bool _drawAreaBoundsOutdated = true; + bool _viewsNeedPaintSort = true; + bool _viewsNeedPositionSort = true; + + /// Create a new [LayoutManager]. + LayoutManagerImpl({LayoutConfig config}) + : this.config = config ?? new LayoutConfig(); + + /// Add one [LayoutView]. + void addView(LayoutView view) { + _views.add(view); + _drawAreaBoundsOutdated = true; + _viewsNeedPositionSort = true; + _viewsNeedPaintSort = true; + } + + /// Remove one [LayoutView]. + void removeView(LayoutView view) { + if (_views.remove(view)) { + _drawAreaBoundsOutdated = true; + _viewsNeedPositionSort = true; + _viewsNeedPaintSort = true; + } + } + + /// Returns true if [view] is already attached. + bool isAttached(LayoutView view) => _views.contains(view); + + /// Get all layout components in the order to be drawn. + @override + List get paintOrderedViews { + if (_viewsNeedPaintSort) { + _paintOrderedViews = new List.from(_views); + + _paintOrderedViews.sort((LayoutView v1, LayoutView v2) => + v1.layoutConfig.paintOrder.compareTo(v2.layoutConfig.paintOrder)); + + _viewsNeedPaintSort = false; + } + return _paintOrderedViews; + } + + /// Get all layout components in the order to be visited. + @override + List get positionOrderedViews { + if (_viewsNeedPositionSort) { + _positionOrderedViews = new List.from(_views); + + _positionOrderedViews.sort((LayoutView v1, LayoutView v2) => v1 + .layoutConfig.positionOrder + .compareTo(v2.layoutConfig.positionOrder)); + + _viewsNeedPositionSort = false; + } + return _positionOrderedViews; + } + + @override + Rectangle get drawAreaBounds { + assert(_drawAreaBoundsOutdated == false); + return _drawAreaBounds; + } + + @override + Rectangle get drawableLayoutAreaBounds { + assert(_drawAreaBoundsOutdated == false); + + final drawableViews = + _views.where((LayoutView view) => view.isSeriesRenderer); + + var componentBounds = drawableViews?.first?.componentBounds; + + if (componentBounds != null) { + for (LayoutView view in drawableViews.skip(1)) { + if (view.componentBounds != null) { + componentBounds = componentBounds.boundingBox(view.componentBounds); + } + } + } else { + componentBounds = new Rectangle(0, 0, 0, 0); + } + + return componentBounds; + } + + @override + int get marginBottom { + assert(_drawAreaBoundsOutdated == false); + return _measurements.bottomHeight; + } + + @override + int get marginLeft { + assert(_drawAreaBoundsOutdated == false); + return _measurements.leftWidth; + } + + @override + int get marginRight { + assert(_drawAreaBoundsOutdated == false); + return _measurements.rightWidth; + } + + @override + int get marginTop { + assert(_drawAreaBoundsOutdated == false); + return _measurements.topHeight; + } + + @override + withinDrawArea(Point point) { + return _drawAreaBounds.containsPoint(point); + } + + /// Measure and layout with given [width] and [height]. + @override + void measure(int width, int height) { + var topViews = + _viewsForPositions(LayoutPosition.Top, LayoutPosition.FullTop); + var rightViews = + _viewsForPositions(LayoutPosition.Right, LayoutPosition.FullRight); + var bottomViews = + _viewsForPositions(LayoutPosition.Bottom, LayoutPosition.FullBottom); + var leftViews = + _viewsForPositions(LayoutPosition.Left, LayoutPosition.FullLeft); + + // Assume the full width and height of the chart is available when measuring + // for the first time but adjust the maximum if margin spec is set. + var measurements = _measure(width, height, + topViews: topViews, + rightViews: rightViews, + bottomViews: bottomViews, + leftViews: leftViews, + useMax: true); + + // Measure a second time but pass in the preferred width and height from + // the first measure cycle. + // Allow views to report a different size than the previously measured max. + final secondMeasurements = _measure(width, height, + topViews: topViews, + rightViews: rightViews, + bottomViews: bottomViews, + leftViews: leftViews, + previousMeasurements: measurements, + useMax: true); + + // If views need more space with the 2nd pass, perform a third pass. + if (measurements.leftWidth != secondMeasurements.leftWidth || + measurements.rightWidth != secondMeasurements.rightWidth || + measurements.topHeight != secondMeasurements.topHeight || + measurements.bottomHeight != secondMeasurements.bottomHeight) { + final thirdMeasurements = _measure(width, height, + topViews: topViews, + rightViews: rightViews, + bottomViews: bottomViews, + leftViews: leftViews, + previousMeasurements: secondMeasurements, + useMax: false); + + measurements = thirdMeasurements; + } else { + measurements = secondMeasurements; + } + + _measurements = measurements; + + // Draw area size. + // Set to a minimum size if there is not enough space for the draw area. + // Prevents the app from crashing by rendering overlapping content instead. + final drawAreaWidth = max( + _minDrawWidth, + (width - measurements.leftWidth - measurements.rightWidth), + ); + final drawAreaHeight = max( + _minDrawHeight, + (height - measurements.bottomHeight - measurements.topHeight), + ); + + // Bounds for the draw area. + _drawAreaBounds = new Rectangle(measurements.leftWidth, + measurements.topHeight, drawAreaWidth, drawAreaHeight); + _drawAreaBoundsOutdated = false; + } + + @override + void layout(int width, int height) { + var topViews = + _viewsForPositions(LayoutPosition.Top, LayoutPosition.FullTop); + var rightViews = + _viewsForPositions(LayoutPosition.Right, LayoutPosition.FullRight); + var bottomViews = + _viewsForPositions(LayoutPosition.Bottom, LayoutPosition.FullBottom); + var leftViews = + _viewsForPositions(LayoutPosition.Left, LayoutPosition.FullLeft); + var drawAreaViews = _viewsForPositions(LayoutPosition.DrawArea); + + final fullBounds = new Rectangle(0, 0, width, height); + + // Layout the margins. + new LeftMarginLayoutStrategy() + .layout(leftViews, _measurements.leftSizes, fullBounds, drawAreaBounds); + new RightMarginLayoutStrategy().layout( + rightViews, _measurements.rightSizes, fullBounds, drawAreaBounds); + new BottomMarginLayoutStrategy().layout( + bottomViews, _measurements.bottomSizes, fullBounds, drawAreaBounds); + new TopMarginLayoutStrategy() + .layout(topViews, _measurements.topSizes, fullBounds, drawAreaBounds); + + // Layout the drawArea. + drawAreaViews.forEach( + (LayoutView view) => view.layout(_drawAreaBounds, _drawAreaBounds)); + } + + Iterable _viewsForPositions(LayoutPosition p1, + [LayoutPosition p2]) { + return positionOrderedViews.where((LayoutView view) => + (view.layoutConfig.position == p1 || + (p2 != null && view.layoutConfig.position == p2))); + } + + /// Measure and return size measurements. + /// [width] full width of chart + /// [height] full height of chart + _MeasuredSizes _measure( + int width, + int height, { + Iterable topViews, + Iterable rightViews, + Iterable bottomViews, + Iterable leftViews, + _MeasuredSizes previousMeasurements, + @required bool useMax, + }) { + final maxLeftWidth = config.leftSpec.getMaxPixels(width); + final maxRightWidth = config.rightSpec.getMaxPixels(width); + final maxBottomHeight = config.bottomSpec.getMaxPixels(height); + final maxTopHeight = config.topSpec.getMaxPixels(height); + + // Assume the full width and height of the chart is available when measuring + // for the first time but adjust the maximum if margin spec is set. + var leftWidth = previousMeasurements?.leftWidth ?? maxLeftWidth; + var rightWidth = previousMeasurements?.rightWidth ?? maxRightWidth; + var bottomHeight = previousMeasurements?.bottomHeight ?? maxBottomHeight; + var topHeight = previousMeasurements?.topHeight ?? maxTopHeight; + + // Only adjust the height if we have previous measurements. + final adjustedHeight = (previousMeasurements != null) + ? height - bottomHeight - topHeight + : height; + + var leftSizes = new LeftMarginLayoutStrategy().measure(leftViews, + maxWidth: useMax ? maxLeftWidth : leftWidth, + height: adjustedHeight, + fullHeight: height); + + leftWidth = max(leftSizes.total, config.leftSpec.getMinPixels(width)); + + var rightSizes = new RightMarginLayoutStrategy().measure(rightViews, + maxWidth: useMax ? maxRightWidth : rightWidth, + height: adjustedHeight, + fullHeight: height); + rightWidth = max(rightSizes.total, config.rightSpec.getMinPixels(width)); + + final adjustedWidth = width - leftWidth - rightWidth; + + var bottomSizes = new BottomMarginLayoutStrategy().measure(bottomViews, + maxHeight: useMax ? maxBottomHeight : bottomHeight, + width: adjustedWidth, + fullWidth: width); + bottomHeight = + max(bottomSizes.total, config.bottomSpec.getMinPixels(height)); + + var topSizes = new TopMarginLayoutStrategy().measure(topViews, + maxHeight: useMax ? maxTopHeight : topHeight, + width: adjustedWidth, + fullWidth: width); + topHeight = max(topSizes.total, config.topSpec.getMinPixels(height)); + + return new _MeasuredSizes( + leftWidth: leftWidth, + leftSizes: leftSizes, + rightWidth: rightWidth, + rightSizes: rightSizes, + topHeight: topHeight, + topSizes: topSizes, + bottomHeight: bottomHeight, + bottomSizes: bottomSizes); + } + + @override + void applyToViews(void apply(LayoutView view)) { + _views.forEach((view) => apply(view)); + } +} + +/// Helper class that stores measured width and height during measure cycles. +class _MeasuredSizes { + final int leftWidth; + final SizeList leftSizes; + + final int rightWidth; + final SizeList rightSizes; + + final int topHeight; + final SizeList topSizes; + + final int bottomHeight; + final SizeList bottomSizes; + + _MeasuredSizes( + {this.leftWidth, + this.leftSizes, + this.rightWidth, + this.rightSizes, + this.topHeight, + this.topSizes, + this.bottomHeight, + this.bottomSizes}); +} diff --git a/web/charts/common/lib/src/chart/layout/layout_margin_strategy.dart b/web/charts/common/lib/src/chart/layout/layout_margin_strategy.dart new file mode 100644 index 000000000..9baab1972 --- /dev/null +++ b/web/charts/common/lib/src/chart/layout/layout_margin_strategy.dart @@ -0,0 +1,273 @@ +// 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'; +import 'layout_view.dart'; + +class SizeList { + final _sizes = []; + int _total = 0; + + operator [](i) => _sizes[i]; + + int get total => _total; + + int get length => _sizes.length; + + void add(size) { + _sizes.add(size); + _total += size; + } + + void adjust(int index, int amount) { + _sizes[index] += amount; + _total += amount; + } +} + +class _DesiredViewSizes { + final preferredSizes = new SizeList(); + final minimumSizes = new SizeList(); + + void add(int preferred, int minimum) { + preferredSizes.add(preferred); + minimumSizes.add(minimum); + } + + void adjustedTo(maxSize) { + if (maxSize < preferredSizes.total) { + int delta = preferredSizes.total - maxSize; + for (int i = preferredSizes.length - 1; i >= 0; i--) { + int viewAvailablePx = preferredSizes[i] - minimumSizes[i]; + + if (viewAvailablePx < delta) { + // We need even more than this one view can give up, so assign the + // minimum to the view and adjust totals. + preferredSizes.adjust(i, -viewAvailablePx); + delta -= viewAvailablePx; + } else { + // We can adjust this view to account for the delta. + preferredSizes.adjust(i, -delta); + return; + } + } + } + } +} + +/// A strategy for calculating size of vertical margins (RIGHT & LEFT). +abstract class VerticalMarginStrategy { + SizeList measure(Iterable views, + {@required int maxWidth, + @required int height, + @required int fullHeight}) { + final measuredWidths = new _DesiredViewSizes(); + int remainingWidth = maxWidth; + + views.forEach((LayoutView view) { + final params = view.layoutConfig; + final viewMargin = params.viewMargin; + + final availableHeight = + (params.isFullPosition ? fullHeight : height) - viewMargin.height; + + // Measure with all available space, minus the buffer. + remainingWidth = remainingWidth - viewMargin.width; + maxWidth -= viewMargin.width; + + var size = ViewMeasuredSizes.zero; + // Don't ask component to measure if both measurements are 0. + // + // Measure still needs to be called even when one dimension has a size of + // zero because if the component is an axis, the axis needs to still + // recalculate ticks even if it is not to be shown. + if (remainingWidth > 0 || availableHeight > 0) { + size = view.measure(remainingWidth, availableHeight); + remainingWidth -= size.preferredWidth; + } + + measuredWidths.add(size.preferredWidth, size.minWidth); + }); + + measuredWidths.adjustedTo(maxWidth); + return measuredWidths.preferredSizes; + } + + void layout(List views, SizeList measuredSizes, + Rectangle fullBounds, Rectangle drawAreaBounds); +} + +/// A strategy for calculating size and bounds of left margins. +class LeftMarginLayoutStrategy extends VerticalMarginStrategy { + @override + void layout(Iterable views, SizeList measuredSizes, + Rectangle fullBounds, Rectangle drawAreaBounds) { + var prevBoundsRight = drawAreaBounds.left; + + int i = 0; + views.forEach((LayoutView view) { + final params = view.layoutConfig; + + final width = measuredSizes[i]; + final left = prevBoundsRight - params.viewMargin.rightPx - width; + final height = + (params.isFullPosition ? fullBounds.height : drawAreaBounds.height) - + params.viewMargin.height; + final top = params.viewMargin.topPx + + (params.isFullPosition ? fullBounds.top : drawAreaBounds.top); + + // Update the remaining bounds. + prevBoundsRight = left - params.viewMargin.leftPx; + + // Layout this component. + view.layout(new Rectangle(left, top, width, height), drawAreaBounds); + + i++; + }); + } +} + +/// A strategy for calculating size and bounds of right margins. +class RightMarginLayoutStrategy extends VerticalMarginStrategy { + @override + void layout(Iterable views, SizeList measuredSizes, + Rectangle fullBounds, Rectangle drawAreaBounds) { + var prevBoundsLeft = drawAreaBounds.right; + + int i = 0; + views.forEach((LayoutView view) { + final params = view.layoutConfig; + + final width = measuredSizes[i]; + final left = prevBoundsLeft + params.viewMargin.leftPx; + final height = + (params.isFullPosition ? fullBounds.height : drawAreaBounds.height) - + params.viewMargin.height; + final top = params.viewMargin.topPx + + (params.isFullPosition ? fullBounds.top : drawAreaBounds.top); + + // Update the remaining bounds. + prevBoundsLeft = left + width + params.viewMargin.rightPx; + + // Layout this component. + view.layout(new Rectangle(left, top, width, height), drawAreaBounds); + + i++; + }); + } +} + +/// A strategy for calculating size of horizontal margins (TOP & BOTTOM). +abstract class HorizontalMarginStrategy { + SizeList measure(Iterable views, + {@required int maxHeight, @required int width, @required int fullWidth}) { + final measuredHeights = new _DesiredViewSizes(); + int remainingHeight = maxHeight; + + views.forEach((LayoutView view) { + final params = view.layoutConfig; + final viewMargin = params.viewMargin; + + final availableWidth = + (params.isFullPosition ? fullWidth : width) - viewMargin.width; + + // Measure with all available space, minus the buffer. + remainingHeight = remainingHeight - viewMargin.height; + maxHeight -= viewMargin.height; + + var size = ViewMeasuredSizes.zero; + // Don't ask component to measure if both measurements are 0. + // + // Measure still needs to be called even when one dimension has a size of + // zero because if the component is an axis, the axis needs to still + // recalculate ticks even if it is not to be shown. + if (remainingHeight > 0 || availableWidth > 0) { + size = view.measure(availableWidth, remainingHeight); + remainingHeight -= size.preferredHeight; + } + + measuredHeights.add(size.preferredHeight, size.minHeight); + }); + + measuredHeights.adjustedTo(maxHeight); + return measuredHeights.preferredSizes; + } + + void layout(Iterable views, SizeList measuredSizes, + Rectangle fullBounds, Rectangle drawAreaBounds); +} + +/// A strategy for calculating size and bounds of top margins. +class TopMarginLayoutStrategy extends HorizontalMarginStrategy { + @override + void layout(Iterable views, SizeList measuredSizes, + Rectangle fullBounds, Rectangle drawAreaBounds) { + var prevBoundsBottom = drawAreaBounds.top; + + int i = 0; + views.forEach((LayoutView view) { + final params = view.layoutConfig; + + final height = measuredSizes[i]; + final top = prevBoundsBottom - height - params.viewMargin.bottomPx; + + final width = + (params.isFullPosition ? fullBounds.width : drawAreaBounds.width) - + params.viewMargin.width; + final left = params.viewMargin.leftPx + + (params.isFullPosition ? fullBounds.left : drawAreaBounds.left); + + // Update the remaining bounds. + prevBoundsBottom = top - params.viewMargin.topPx; + + // Layout this component. + view.layout(new Rectangle(left, top, width, height), drawAreaBounds); + + i++; + }); + } +} + +/// A strategy for calculating size and bounds of bottom margins. +class BottomMarginLayoutStrategy extends HorizontalMarginStrategy { + @override + void layout(Iterable views, SizeList measuredSizes, + Rectangle fullBounds, Rectangle drawAreaBounds) { + var prevBoundsTop = drawAreaBounds.bottom; + + int i = 0; + views.forEach((LayoutView view) { + final params = view.layoutConfig; + + final height = measuredSizes[i]; + final top = prevBoundsTop + params.viewMargin.topPx; + + final width = + (params.isFullPosition ? fullBounds.width : drawAreaBounds.width) - + params.viewMargin.width; + final left = params.viewMargin.leftPx + + (params.isFullPosition ? fullBounds.left : drawAreaBounds.left); + + // Update the remaining bounds. + prevBoundsTop = top + height + params.viewMargin.bottomPx; + + // Layout this component. + view.layout(new Rectangle(left, top, width, height), drawAreaBounds); + + i++; + }); + } +} diff --git a/web/charts/common/lib/src/chart/layout/layout_view.dart b/web/charts/common/lib/src/chart/layout/layout_view.dart new file mode 100644 index 000000000..91cb7dc37 --- /dev/null +++ b/web/charts/common/lib/src/chart/layout/layout_view.dart @@ -0,0 +1,208 @@ +// 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'; + +import '../../common/graphics_factory.dart' show GraphicsFactory; +import '../common/chart_canvas.dart' show ChartCanvas; + +/// Position of a [LayoutView]. +enum LayoutPosition { + Bottom, + FullBottom, + + Top, + FullTop, + + Left, + FullLeft, + + Right, + FullRight, + + DrawArea, +} + +/// Standard layout paint orders for all internal components. +/// +/// Custom component layers should define their paintOrder by taking the nearest +/// layer from this list, and adding or subtracting 1. This will help reduce the +/// chance of custom behaviors, renderers, etc. from breaking if we need to +/// re-order these components internally. +class LayoutViewPaintOrder { + // Draw range annotations beneath axis grid lines. + static const rangeAnnotation = -10; + // Axis elements form the "base layer" of all components on the chart. Domain + // axes are drawn on top of measure axes to ensure that the domain axis line + // appears on top of any measure axis grid lines. + static const measureAxis = 0; + static const domainAxis = 5; + // Draw series data on top of axis elements. + static const arc = 10; + static const bar = 10; + static const barTargetLine = 15; + static const line = 20; + static const point = 25; + // Draw most behaviors on top of series data. + static const legend = 100; + static const linePointHighlighter = 110; + static const slider = 150; + static const chartTitle = 160; +} + +/// Standard layout position orders for all internal components. +/// +/// Custom component layers should define their positionOrder by taking the +/// nearest component from this list, and adding or subtracting 1. This will +/// help reduce the chance of custom behaviors, renderers, etc. from breaking if +/// we need to re-order these components internally. +class LayoutViewPositionOrder { + static const drawArea = 0; + static const symbolAnnotation = 10; + static const axis = 20; + static const legend = 30; + static const chartTitle = 40; +} + +/// A configuration for margin (empty space) around a layout child view. +class ViewMargin { + /// A [ViewMargin] with all zero px. + static const empty = + const ViewMargin(topPx: 0, bottomPx: 0, rightPx: 0, leftPx: 0); + + final int topPx; + final int bottomPx; + final int rightPx; + final int leftPx; + + const ViewMargin({int topPx, int bottomPx, int rightPx, int leftPx}) + : topPx = topPx ?? 0, + bottomPx = bottomPx ?? 0, + rightPx = rightPx ?? 0, + leftPx = leftPx ?? 0; + + /// Total width. + int get width => leftPx + rightPx; + + /// Total height. + int get height => topPx + bottomPx; +} + +/// Configuration of a [LayoutView]. +class LayoutViewConfig { + /// Unique identifier for the [LayoutView]. + String id; + + /// The order to paint a [LayoutView] on the canvas. + /// + /// The smaller number is drawn first. + int paintOrder; + + /// The position of a [LayoutView] defining where to place the view. + LayoutPosition position; + + /// The order to place the [LayoutView] within a chart margin. + /// + /// The smaller number is closer to the draw area. Elements positioned closer + /// to the draw area will be given extra layout space first, before those + /// further away. + /// + /// Note that all views positioned in the draw area are given the entire draw + /// area bounds as their component bounds. + int positionOrder; + + /// Defines the space around a layout component. + ViewMargin viewMargin; + + /// Creates new [LayoutParams]. + /// + /// [paintOrder] the order that this component will be drawn. + /// [position] the [ComponentPosition] of this component. + /// [positionOrder] the order of this component in a chart margin. + LayoutViewConfig( + {@required this.paintOrder, + @required this.position, + @required this.positionOrder, + ViewMargin viewMargin}) + : viewMargin = viewMargin ?? ViewMargin.empty; + + /// Returns true if it is a full position. + bool get isFullPosition => + position == LayoutPosition.FullBottom || + position == LayoutPosition.FullTop || + position == LayoutPosition.FullRight || + position == LayoutPosition.FullLeft; +} + +/// Size measurements of one component. +/// +/// The measurement is tight to the component, without adding [ComponentBuffer]. +class ViewMeasuredSizes { + /// All zeroes component size. + static const zero = const ViewMeasuredSizes( + preferredWidth: 0, preferredHeight: 0, minWidth: 0, minHeight: 0); + + final int preferredWidth; + final int preferredHeight; + final int minWidth; + final int minHeight; + + /// Create a new [ViewSizes]. + /// + /// [preferredWidth] the component's preferred width. + /// [preferredHeight] the component's preferred width. + /// [minWidth] the component's minimum width. If not set, default to 0. + /// [minHeight] the component's minimum height. If not set, default to 0. + const ViewMeasuredSizes( + {@required this.preferredWidth, + @required this.preferredHeight, + int minWidth, + int minHeight}) + : minWidth = minWidth ?? 0, + minHeight = minHeight ?? 0; +} + +/// A component that measures its size and accepts bounds to complete layout. +abstract class LayoutView { + GraphicsFactory get graphicsFactory; + + set graphicsFactory(GraphicsFactory value); + + /// Layout params for this component. + LayoutViewConfig get layoutConfig; + + /// Measure and return the size of this component. + /// + /// This measurement is without the [ComponentBuffer], which is added by the + /// layout manager. + ViewMeasuredSizes measure(int maxWidth, int maxHeight); + + /// Layout this component. + void layout(Rectangle componentBounds, Rectangle drawAreaBounds); + + /// Draw this component on the canvas. + void paint(ChartCanvas canvas, double animationPercent); + + /// Bounding box for drawing this component. + Rectangle get componentBounds; + + /// Whether or not this component is a series renderer that draws series + /// data. + /// + /// This component may either render into the chart's draw area, or into a + /// separate area bounded by the component bounds. + bool get isSeriesRenderer; +} diff --git a/web/charts/common/lib/src/chart/line/line_chart.dart b/web/charts/common/lib/src/chart/line/line_chart.dart new file mode 100644 index 000000000..6ea693fd4 --- /dev/null +++ b/web/charts/common/lib/src/chart/line/line_chart.dart @@ -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 '../cartesian/axis/axis.dart' show NumericAxis; +import '../cartesian/cartesian_chart.dart' show NumericCartesianChart; +import '../common/series_renderer.dart' show SeriesRenderer; +import '../layout/layout_config.dart' show LayoutConfig; +import '../line/line_renderer.dart' show LineRenderer; + +class LineChart extends NumericCartesianChart { + LineChart( + {bool vertical, + LayoutConfig layoutConfig, + NumericAxis primaryMeasureAxis, + NumericAxis secondaryMeasureAxis, + LinkedHashMap disjointMeasureAxes}) + : super( + vertical: vertical, + layoutConfig: layoutConfig, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes); + + @override + SeriesRenderer makeDefaultRenderer() { + return new LineRenderer() + ..rendererId = SeriesRenderer.defaultRendererId; + } +} diff --git a/web/charts/common/lib/src/chart/line/line_renderer.dart b/web/charts/common/lib/src/chart/line/line_renderer.dart new file mode 100644 index 000000000..19c7e665f --- /dev/null +++ b/web/charts/common/lib/src/chart/line/line_renderer.dart @@ -0,0 +1,1591 @@ +// 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 Rectangle, Point; + +import 'package:meta/meta.dart' show required, visibleForTesting; + +import '../../common/color.dart' show Color; +import '../../common/math.dart' show clamp; +import '../../data/series.dart' show AttributeKey; +import '../cartesian/axis/axis.dart' + show ImmutableAxis, OrdinalAxis, domainAxisKey, measureAxisKey; +import '../cartesian/cartesian_renderer.dart' show BaseCartesianRenderer; +import '../common/base_chart.dart' show BaseChart; +import '../common/chart_canvas.dart' show ChartCanvas, getAnimatedColor; +import '../common/datum_details.dart' show DatumDetails; +import '../common/processed_series.dart' show ImmutableSeries, MutableSeries; +import '../common/series_datum.dart' show SeriesDatum; +import '../scatter_plot/point_renderer.dart' show PointRenderer; +import '../scatter_plot/point_renderer_config.dart' show PointRendererConfig; +import 'line_renderer_config.dart' show LineRendererConfig; + +const styleSegmentsKey = const AttributeKey>( + 'LineRenderer.styleSegments'); + +const lineStackIndexKey = + const AttributeKey('LineRenderer.lineStackIndex'); + +class LineRenderer extends BaseCartesianRenderer { + // Configuration used to extend the clipping area to extend the draw bounds. + static const drawBoundTopExtensionPx = 5; + static const drawBoundBottomExtensionPx = 5; + + final LineRendererConfig config; + + PointRenderer _pointRenderer; + + BaseChart _chart; + + /// True if any series has a measureUpperBoundFn and measureLowerBoundFn. + /// + /// Used to enable drawing confidence interval areas segments. + bool _hasMeasureBounds; + + /// 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. + final _seriesLineMap = >>{}; + + // Store a list of lines that exist in the series data. + // + // This list will be used to remove any [_AnimatedLine] that were rendered in + // previous draw cycles, but no longer have a corresponding datum in the new + // data. + final _currentKeys = []; + + factory LineRenderer({String rendererId, LineRendererConfig config}) { + return new LineRenderer._internal( + rendererId: rendererId ?? 'line', + config: config ?? new LineRendererConfig()); + } + + LineRenderer._internal({String rendererId, this.config}) + : super( + rendererId: rendererId, + layoutPaintOrder: config.layoutPaintOrder, + symbolRenderer: config.symbolRenderer) { + _pointRenderer = new PointRenderer( + config: new PointRendererConfig(radiusPx: this.config.radiusPx)); + } + + @override + void layout(Rectangle componentBounds, Rectangle drawAreaBounds) { + super.layout(componentBounds, drawAreaBounds); + + if (config.includePoints) { + _pointRenderer.layout(componentBounds, drawAreaBounds); + } + } + + @override + void configureSeries(List> seriesList) { + assignMissingColors(seriesList, emptyCategoryUsesSinglePalette: false); + + seriesList.forEach((MutableSeries series) { + // Add a default area color function which applies the configured + // areaOpacity value to the datum's current color. + series.areaColorFn ??= (int index) { + final color = series.colorFn(index); + + return new Color( + r: color.r, + g: color.g, + b: color.b, + a: (color.a * config.areaOpacity).round()); + }; + }); + + if (config.includePoints) { + _pointRenderer.configureSeries(seriesList); + } + } + + @override + void preprocessSeries(List> seriesList) { + var stackIndex = 0; + + _hasMeasureBounds = seriesList.any((series) => + series.measureUpperBoundFn != null && + series.measureLowerBoundFn != null); + + seriesList.forEach((MutableSeries series) { + final colorFn = series.colorFn; + final areaColorFn = series.areaColorFn; + final domainFn = series.domainFn; + final measureFn = series.measureFn; + final strokeWidthPxFn = series.strokeWidthPxFn; + + series.dashPatternFn ??= (_) => config.dashPattern; + final dashPatternFn = series.dashPatternFn; + + final styleSegments = <_LineRendererElement>[]; + var styleSegmentsIndex = 0; + + final usedKeys = new Set(); + + // Configure style segments for each series. + String previousSegmentKey; + _LineRendererElement currentDetails; + + for (var index = 0; index < series.data.length; index++) { + final domain = domainFn(index); + final measure = measureFn(index); + + if (domain == null || measure == null) { + continue; + } + + final color = colorFn(index); + final areaColor = areaColorFn(index); + final dashPattern = dashPatternFn(index); + final strokeWidthPx = strokeWidthPxFn != null + ? strokeWidthPxFn(index).toDouble() + : config.strokeWidthPx; + + // Create a style key for this datum, and then compare it to the + // previous datum. + // + // Compare strokeWidthPx to 2 decimals of precision. Any less and you + // can't see any difference in the canvas anyways. + final strokeWidthPxRounded = (strokeWidthPx * 100).round() / 100; + var styleKey = '${series.id}__${styleSegmentsIndex}__${color}' + '__${dashPattern}__${strokeWidthPxRounded}'; + + if (styleKey != previousSegmentKey) { + // If we have a repeated style segment, update the repeat index and + // create a new key. + // TODO: Paint repeated styles with multiple clip regions. + if (usedKeys.isNotEmpty && usedKeys.contains(styleKey)) { + styleSegmentsIndex++; + + styleKey = '${series.id}__${styleSegmentsIndex}__${color}' + '__${dashPattern}__${strokeWidthPxRounded}'; + } + + // Make sure that the previous style segment extends to the current + // domain value. This will ensure that the style of the line changes + // right at the point of the datum that changes the style. + if (currentDetails != null) { + currentDetails.domainExtent.includePoint(domain); + } + + // Create a new style segment. + currentDetails = new _LineRendererElement() + ..color = color + ..areaColor = areaColor + ..dashPattern = dashPattern + ..domainExtent = new _Range(domain, domain) + ..strokeWidthPx = strokeWidthPx + ..styleKey = styleKey + ..roundEndCaps = config.roundEndCaps; + + styleSegments.add(currentDetails); + usedKeys.add(styleKey); + + previousSegmentKey = styleKey; + } else { + // Extend the range of the current segment to include the current + // domain value. + currentDetails.domainExtent.includePoint(domain); + } + } + + series.setAttr(styleSegmentsKey, styleSegments); + series.setAttr(lineStackIndexKey, stackIndex); + + if (config.stacked) { + stackIndex++; + } + }); + + if (config.includePoints) { + _pointRenderer.preprocessSeries(seriesList); + } + + // If we are stacking, generate new stacking measure offset functions for + // each series. Each datum should have a measure offset consisting of the + // sum of the measure and measure offsets of each datum with the same domain + // value in series below it in the stack. The first series will be treated + // as the bottom of the stack. + if (config.stacked && seriesList.isNotEmpty) { + var curOffsets = _createInitialOffsetMap(seriesList[0]); + var nextOffsets = {}; + + for (var i = 0; i < seriesList.length; i++) { + final series = seriesList[i]; + final measureOffsetFn = _createStackedMeasureOffsetFunction( + series, curOffsets, nextOffsets); + + if (i > 0) { + series.measureOffsetFn = measureOffsetFn; + } + + curOffsets = nextOffsets; + nextOffsets = {}; + } + } + } + + /// Creates the initial offsets for the series given the measureOffset values. + Map _createInitialOffsetMap(MutableSeries series) { + final domainFn = series.domainFn; + final measureOffsetFn = series.measureOffsetFn; + final initialOffsets = {}; + + for (var index = 0; index < series.data.length; index++) { + initialOffsets[domainFn(index)] = measureOffsetFn(index); + } + + return initialOffsets; + } + + /// Function needed to create a closure preserving the previous series + /// information. y0 for this series is just y + y0 for previous series as long + /// as both y and y0 are not null. If they are null propagate up the + /// missing/null data. + Function _createStackedMeasureOffsetFunction(MutableSeries series, + Map curOffsets, Map nextOffsets) { + final domainFn = series.domainFn; + final measureFn = series.measureFn; + + for (var index = 0; index < series.data.length; index++) { + final domainValue = domainFn(index); + final measure = measureFn(index); + final prevOffset = curOffsets[domainValue]; + + if (measure != null && prevOffset != null) { + nextOffsets[domainValue] = measure + prevOffset; + } + } + + return (int i) => curOffsets[domainFn(i)]; + } + + /// Merge the line map and the new series so that the new elements are mixed + /// with the previous ones. + /// + /// This is to deal with the issue that every new series added after the fact + /// would be be rendered on top of the old ones, no matter the order of the + /// new series list. + void _mergeIntoSeriesMap(List> seriesList) { + List>>> newLineMap = []; + + seriesList.forEach((ImmutableSeries series) { + final key = series.id; + + // First, add all the series from the old map that have been removed from + // the new seriesList in the same order they appear, stopping at the first + // series that is still in the list. We need to maintain them in the same + // order animate them out smoothly. + bool checkNext = true; + while (checkNext && _seriesLineMap.isNotEmpty) { + final firstKey = _seriesLineMap.keys.first; + if (!seriesList.any((s) => s.id == firstKey)) { + newLineMap.add(MapEntry(firstKey, _seriesLineMap.remove(firstKey))); + checkNext = true; + } else { + checkNext = false; + } + } + + // If it's a new key, we add it and move to the next one. If not, we + // remove it from the current list and add it to the new one. + if (!_seriesLineMap.containsKey(key)) { + newLineMap.add(MapEntry(key, [])); + } else { + newLineMap.add(MapEntry(key, _seriesLineMap.remove(key))); + } + }); + + // Now whatever is left is stuff that has been removed. We still add it to + // the end and removed them as the map is modified in place. + newLineMap.addAll(_seriesLineMap.entries); + _seriesLineMap.clear(); + + _seriesLineMap.addEntries(newLineMap); + } + + void update(List> seriesList, bool isAnimatingThisDraw) { + _currentKeys.clear(); + + // List of final points for the previous line in a stack. + List>> previousPointList = []; + + // List of initial points for the previous line in a stack, animated in from + // the measure axis. + List>> previousInitialPointList = []; + + _mergeIntoSeriesMap(seriesList); + + seriesList.forEach((ImmutableSeries series) { + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final lineKey = series.id; + final stackIndex = series.getAttr(lineStackIndexKey); + + previousPointList.add([]); + previousInitialPointList.add([]); + + final elementsList = _seriesLineMap[lineKey]; + + final styleSegments = series.getAttr(styleSegmentsKey); + + // Include the end points of the domain axis range in the first and last + // style segments to avoid clipping everything when the domain range of + // the data is very small. Doing this after [preProcess] handles invalid + // data (e.g. null measure) at the ends of the series data. + // + // TODO: Handle ordinal axes by looking at the next domains. + if (styleSegments.isNotEmpty && !(domainAxis is OrdinalAxis)) { + final startPx = (isRtl ? drawBounds.right : drawBounds.left).toDouble(); + final endPx = (isRtl ? drawBounds.left : drawBounds.right).toDouble(); + + final startDomain = domainAxis.getDomain(startPx); + final endDomain = domainAxis.getDomain(endPx); + + styleSegments.first.domainExtent.includePoint(startDomain); + styleSegments.last.domainExtent.includePoint(endDomain); + } + + // Create a set of animated line and area elements for each style segment. + // + // If the series contains null measure values, then multiple animated line + // and area objects will be created to represent the isolated sections of + // the series. + // + // The full set of line and area elements will be rendered on the canvas + // for each style segment, with a clip region added in the [paint] process + // later to display only the relevant parts of data. This ensures that + // styles that visually depend on the start location, such as dash + // patterns, are not disrupted by other changes in style. + styleSegments.forEach((_LineRendererElement styleSegment) { + final styleKey = styleSegment.styleKey; + + // If we already have an AnimatingPoint for that index, use it. + var animatingElements = elementsList.firstWhere( + (_AnimatedElements elements) => elements.styleKey == styleKey, + orElse: () => null); + + if (animatingElements != null) { + previousInitialPointList[stackIndex] = animatingElements.allPoints; + } else { + // Create a new line and have it animate in from axis. + final lineAndArea = _createLineAndAreaElements( + series, + styleSegment, + stackIndex > 0 ? previousInitialPointList[stackIndex - 1] : null, + true); + final lineElementList = lineAndArea[0]; + final areaElementList = lineAndArea[1]; + final allPointList = lineAndArea[2]; + final boundsElementList = lineAndArea[3]; + + // Create the line elements. + final animatingLines = <_AnimatedLine>[]; + + for (var index = 0; index < lineElementList.length; index++) { + animatingLines.add(new _AnimatedLine( + key: lineElementList[index].styleKey, + overlaySeries: series.overlaySeries) + ..setNewTarget(lineElementList[index])); + } + + // Create the area elements. + List<_AnimatedArea> animatingAreas; + if (config.includeArea) { + animatingAreas = <_AnimatedArea>[]; + + for (var index = 0; index < areaElementList.length; index++) { + animatingAreas.add(new _AnimatedArea( + key: areaElementList[index].styleKey, + overlaySeries: series.overlaySeries) + ..setNewTarget(areaElementList[index])); + } + } + + // Create the bound elements separately from area elements, because + // it needs to be rendered on top of the area elements. + List<_AnimatedArea> animatingBounds; + if (_hasMeasureBounds) { + animatingBounds ??= <_AnimatedArea>[]; + + for (var index = 0; index < boundsElementList.length; index++) { + animatingBounds.add(new _AnimatedArea( + key: boundsElementList[index].styleKey, + overlaySeries: series.overlaySeries) + ..setNewTarget(boundsElementList[index])); + } + } + + animatingElements = new _AnimatedElements() + ..styleKey = styleSegment.styleKey + ..allPoints = allPointList + ..lines = animatingLines + ..areas = animatingAreas + ..bounds = animatingBounds; + + elementsList.add(animatingElements); + + previousInitialPointList[stackIndex] = allPointList; + } + + // Create a new line using the final point locations. + final lineAndArea = _createLineAndAreaElements(series, styleSegment, + stackIndex > 0 ? previousPointList[stackIndex - 1] : null, false); + final lineElementList = lineAndArea[0]; + final areaElementList = lineAndArea[1]; + final allPointList = lineAndArea[2]; + final boundsElementList = lineAndArea[3]; + + for (var index = 0; index < lineElementList.length; index++) { + final lineElement = lineElementList[index]; + + // Add a new animated line if we have more segments in this draw cycle + // than we did in the previous chart draw cycle. + // TODO: Nicer animations for incoming segments. + if (index >= animatingElements.lines.length) { + animatingElements.lines.add(new _AnimatedLine( + key: lineElement.styleKey, + overlaySeries: series.overlaySeries)); + } + animatingElements.lines[index].setNewTarget(lineElement); + } + + if (config.includeArea) { + for (var index = 0; index < areaElementList.length; index++) { + final areaElement = areaElementList[index]; + + // Add a new animated area if we have more segments in this draw + // cycle than we did in the previous chart draw cycle. + // TODO: Nicer animations for incoming segments. + if (index >= animatingElements.areas.length) { + animatingElements.areas.add(new _AnimatedArea( + key: areaElement.styleKey, + overlaySeries: series.overlaySeries)); + } + animatingElements.areas[index].setNewTarget(areaElement); + } + } + + if (_hasMeasureBounds) { + for (var index = 0; index < boundsElementList.length; index++) { + final boundElement = boundsElementList[index]; + + // Add a new animated bound if we have more segments in this draw + // cycle than we did in the previous chart draw cycle. + // TODO: Nicer animations for incoming segments. + if (index >= animatingElements.bounds.length) { + animatingElements.bounds.add(new _AnimatedArea( + key: boundElement.styleKey, + overlaySeries: series.overlaySeries)); + } + animatingElements.bounds[index].setNewTarget(boundElement); + } + } + + animatingElements.allPoints = allPointList; + + // Save the line points for the current series so that we can use them + // in the area skirt for the next stacked series. + previousPointList[stackIndex] = allPointList; + }); + }); + + // Animate out lines that don't exist anymore. + _seriesLineMap.forEach((String key, List<_AnimatedElements> elements) { + for (var element in elements) { + if (element.lines != null) { + for (var line in element.lines) { + if (_currentKeys.contains(line.key) != true) { + line.animateOut(); + } + } + } + if (element.areas != null) { + for (var area in element.areas) { + if (_currentKeys.contains(area.key) != true) { + area.animateOut(); + } + } + } + if (element.bounds != null) { + for (var bound in element.bounds) { + if (_currentKeys.contains(bound.key) != true) { + bound.animateOut(); + } + } + } + } + }); + + if (config.includePoints) { + _pointRenderer.update(seriesList, isAnimatingThisDraw); + } + } + + /// Creates a tuple of lists of [_LineRendererElement]s, + /// [_AreaRendererElement]s, [_DatumPoint]s for a given style segment of a + /// series. + /// + /// The first element in the returned array is a list of line elements, broken + /// apart by null data. + /// + /// The second element in the returned array is a list of area elements, + /// broken apart by null data. + /// + /// The third element in the returned array is a list of all of the points for + /// the entire series. This is intended to be used as the [previousPointList] + /// for the next series. + /// + /// [series] the series that this line represents. + /// + /// [styleSegment] represents the rendering style for a subset of the series + /// data, bounded by its domainExtent. + /// + /// [previousPointList] contains the points for the line below this series in + /// the stack, if stacking is enabled. It forms the bottom edges for the area + /// skirt. + /// + /// [initializeFromZero] controls whether we generate elements with measure + /// values of 0, or using series data. This should be true when calculating + /// point positions to animate in from the measure axis. + List _createLineAndAreaElements( + ImmutableSeries series, + _LineRendererElement styleSegment, + List<_DatumPoint> previousPointList, + bool initializeFromZero) { + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + + final color = styleSegment.color; + final areaColor = styleSegment.areaColor; + final dashPattern = styleSegment.dashPattern; + final domainExtent = styleSegment.domainExtent; + final strokeWidthPx = styleSegment.strokeWidthPx; + final styleKey = styleSegment.styleKey; + final roundEndCaps = styleSegment.roundEndCaps; + + // Get a list of all positioned points for this series. + final pointList = _createPointListForSeries(series, initializeFromZero); + + // Break pointList up into sets of line and area segments, divided by null + // measure values in the series data. + final segmentsList = _createLineAndAreaSegmentsForSeries( + pointList, previousPointList, series, initializeFromZero); + final lineSegments = segmentsList[0]; + final areaSegments = segmentsList[1]; + final boundsSegment = segmentsList[2]; + + _currentKeys.add(styleKey); + + final positionExtent = _createPositionExtent(series, styleSegment); + + // Get the line elements we are going to to set up. + final lineElements = <_LineRendererElement>[]; + for (var index = 0; index < lineSegments.length; index++) { + final linePointList = lineSegments[index]; + + // Update the set of areas that still exist in the series data. + final lineStyleKey = '${styleKey}__line__${index}'; + _currentKeys.add(lineStyleKey); + + lineElements.add(new _LineRendererElement() + ..points = linePointList + ..color = color + ..areaColor = areaColor + ..dashPattern = dashPattern + ..domainExtent = domainExtent + ..measureAxisPosition = measureAxis.getLocation(0.0) + ..positionExtent = positionExtent + ..strokeWidthPx = strokeWidthPx + ..styleKey = lineStyleKey + ..roundEndCaps = roundEndCaps); + } + + // Get the area elements we are going to set up. + final areaElements = <_AreaRendererElement>[]; + if (config.includeArea) { + for (var index = 0; index < areaSegments.length; index++) { + final areaPointList = areaSegments[index]; + + // Update the set of areas that still exist in the series data. + final areaStyleKey = '${styleKey}__area_${index}'; + _currentKeys.add(areaStyleKey); + + areaElements.add(new _AreaRendererElement() + ..points = areaPointList + ..color = color + ..areaColor = areaColor + ..domainExtent = domainExtent + ..measureAxisPosition = measureAxis.getLocation(0.0) + ..positionExtent = positionExtent + ..styleKey = areaStyleKey); + } + } + + // Create the bounds element + final boundsElements = <_AreaRendererElement>[]; + if (_hasMeasureBounds) { + // Update the set of bounds that still exist in the series data. + for (var index = 0; index < boundsSegment.length; index++) { + final boundsPointList = boundsSegment[index]; + + final boundsStyleKey = '${styleKey}__bounds_${index}'; + _currentKeys.add(boundsStyleKey); + + boundsElements.add(new _AreaRendererElement() + ..points = boundsPointList + ..color = color + ..areaColor = areaColor + ..domainExtent = domainExtent + ..measureAxisPosition = measureAxis.getLocation(0.0) + ..positionExtent = positionExtent + ..styleKey = boundsStyleKey); + } + } + + return [lineElements, areaElements, pointList, boundsElements]; + } + + /// Builds a list of data points for the entire series. + /// + /// [series] the series that this line represents. + /// + /// [initializeFromZero] controls whether we generate elements with measure + /// values of 0, or using series data. This should be true when calculating + /// point positions to animate in from the measure axis. + List<_DatumPoint> _createPointListForSeries( + ImmutableSeries series, bool initializeFromZero) { + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final domainFn = series.domainFn; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + final measureFn = series.measureFn; + final measureOffsetFn = series.measureOffsetFn; + + final pointList = <_DatumPoint>[]; + + // Generate [_DatumPoints]s for the series data. + for (var index = 0; index < series.data.length; index++) { + final datum = series.data[index]; + + // TODO: Animate from the nearest lines in the stack. + var measure = measureFn(index); + if (measure != null && initializeFromZero) { + measure = 0.0; + } + + var measureOffset = measureOffsetFn(index); + if (measureOffset != null && initializeFromZero) { + measureOffset = 0.0; + } + + pointList.add(_getPoint(datum, domainFn(index), series, domainAxis, + measure, measureOffset, measureAxis, + index: index)); + } + + return pointList; + } + + /// Builds a list of line and area segments for a series. + /// + /// This method returns a list of two elements. The first is a list of line + /// segments, and the second is a list of area segments. Both sets of segments + /// are broken up by null measure values in the series data. + /// + /// [pointList] list of all points in the line. + /// + /// [previousPointList] list of all points in the line below this one in the + /// stack. + /// + /// [series] the series that this line represents. + List _createLineAndAreaSegmentsForSeries( + List<_DatumPoint> pointList, + List<_DatumPoint> previousPointList, + ImmutableSeries series, + bool initializeFromZero) { + final lineSegments = >>[]; + final areaSegments = >>[]; + final boundsSegments = >>[]; + + int startPointIndex; + int endPointIndex; + + // Only build bound segments for this series if it has bounds functions. + final seriesHasMeasureBounds = series.measureUpperBoundFn != null && + series.measureLowerBoundFn != null; + + for (var index = 0; index < pointList.length; index++) { + final point = pointList[index]; + + if (point.y == null) { + if (startPointIndex == null) { + continue; + } + + lineSegments + .add(_createLineSegment(startPointIndex, endPointIndex, pointList)); + + // Isolated data points are handled by the line painter. Do not add an + // area segment for them. + if (startPointIndex != endPointIndex) { + if (config.includeArea) { + areaSegments.add(_createAreaSegment(startPointIndex, endPointIndex, + pointList, previousPointList, series, initializeFromZero)); + } + if (seriesHasMeasureBounds) { + boundsSegments.add(_createBoundsSegment( + pointList.sublist(startPointIndex, endPointIndex + 1), + series, + initializeFromZero)); + } + } + + startPointIndex = null; + endPointIndex = null; + continue; + } + + startPointIndex ??= index; + endPointIndex = index; + } + + // Create an area point list for the final segment. This will be the only + // segment if no null measure values were found in the series. + if (startPointIndex != null && endPointIndex != null) { + lineSegments + .add(_createLineSegment(startPointIndex, endPointIndex, pointList)); + + // Isolated data points are handled by the line painter. Do not add an + // area segment for them. + if (startPointIndex != endPointIndex) { + if (config.includeArea) { + areaSegments.add(_createAreaSegment(startPointIndex, endPointIndex, + pointList, previousPointList, series, initializeFromZero)); + } + + if (seriesHasMeasureBounds) { + boundsSegments.add(_createBoundsSegment( + pointList.sublist(startPointIndex, endPointIndex + 1), + series, + initializeFromZero)); + } + } + } + + return [lineSegments, areaSegments, boundsSegments]; + } + + /// Builds a list of data points for a line segment. + /// + /// For a line, this is effectively just a sub list of [pointList]. + /// + /// [start] index of the first point in the segment. + /// + /// [end] index of the last point in the segment. + /// + /// [pointList] list of all points in the line. + List<_DatumPoint> _createLineSegment( + int start, int end, List<_DatumPoint> pointList) => + pointList.sublist(start, end + 1); + + /// Builds a list of data points for an area segment. + /// + /// The list of points will include a baseline at the domain axis if there was + /// no previous line in the stack. Otherwise, the bottom of the shape will + /// consist of the points from the previous series that line up with the + /// current series. + /// + /// [start] index of the first point in the segment. + /// + /// [end] index of the last point in the segment. + /// + /// [pointList] list of all points in the line. + /// + /// [previousPointList] list of all points in the line below this one in the + /// stack. + /// + /// [series] the series that this line represents. + List<_DatumPoint> _createAreaSegment( + int start, + int end, + List<_DatumPoint> pointList, + List<_DatumPoint> previousPointList, + ImmutableSeries series, + bool initializeFromZero) { + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final domainFn = series.domainFn; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + + final areaPointList = <_DatumPoint>[]; + + if (!config.stacked || previousPointList == null) { + // Start area segments at the bottom of a stack by adding a bottom line + // segment along the measure axis. + areaPointList.add(_getPoint( + null, domainFn(end), series, domainAxis, 0.0, 0.0, measureAxis)); + + areaPointList.add(_getPoint( + null, domainFn(start), series, domainAxis, 0.0, 0.0, measureAxis)); + } else { + // Start subsequent area segments in a stack by adding the previous + // points in reverse order, so that we can get a properly closed + // polygon. + areaPointList.addAll(previousPointList.sublist(start, end + 1).reversed); + } + + areaPointList.addAll(pointList.sublist(start, end + 1)); + + return areaPointList; + } + + List<_DatumPoint> _createBoundsSegment(List<_DatumPoint> pointList, + ImmutableSeries series, bool initializeFromZero) { + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + final areaPointList = <_DatumPoint>[]; + + // Add all points for upper bounds. + areaPointList.addAll(pointList.map((datumPoint) => new _DatumPoint.from( + datumPoint, + datumPoint.x, + initializeFromZero + ? datumPoint.y + : measureAxis.getLocation( + series.measureUpperBoundFn(datumPoint.index) + + series.measureOffsetFn(datumPoint.index))))); + + // Add all points for lower bounds, in reverse order. + areaPointList.addAll(pointList.reversed.map((datumPoint) => + new _DatumPoint.from( + datumPoint, + datumPoint.x, + initializeFromZero + ? datumPoint.y + : measureAxis.getLocation( + series.measureLowerBoundFn(datumPoint.index) + + series.measureOffsetFn(datumPoint.index))))); + + return areaPointList; + } + + /// Converts the domain value extent for the series into axis positions, + /// clamped to the edges of the draw area. + /// + /// [series] the series that this line represents. + /// + /// [details] represents the element details for a line segment. + _Range _createPositionExtent( + ImmutableSeries series, _LineRendererElement details) { + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + + // Convert the domain extent into axis positions. + // Clamp start position to the beginning of the draw area if it is outside + // the domain viewport range. + final startPosition = domainAxis.getLocation(details.domainExtent.start) ?? + drawBounds.left.toDouble(); + + // Clamp end position to the end of the draw area if it is outside the + // domain viewport range. + final endPosition = domainAxis.getLocation(details.domainExtent.end) ?? + drawBounds.right.toDouble(); + + return new _Range(startPosition, endPosition); + } + + @override + void onAttach(BaseChart 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. + _chart = chart; + } + + void paint(ChartCanvas canvas, double animationPercent) { + // Clean up the lines that no longer exist. + if (animationPercent == 1.0) { + final keysToRemove = []; + + _seriesLineMap.forEach((String key, List<_AnimatedElements> elements) { + elements.removeWhere( + (_AnimatedElements element) => element.animatingOut); + + if (elements.isEmpty) { + keysToRemove.add(key); + } + }); + + keysToRemove.forEach(_seriesLineMap.remove); + } + + _seriesLineMap.forEach((String key, List<_AnimatedElements> elements) { + if (config.includeArea) { + elements + .map>>( + (_AnimatedElements animatingElement) => + animatingElement.areas) + .expand<_AnimatedArea>((List<_AnimatedArea> areas) => areas) + .map<_AreaRendererElement>((_AnimatedArea animatingArea) => + animatingArea?.getCurrentArea(animationPercent)) + .forEach((_AreaRendererElement area) { + if (area != null) { + canvas.drawPolygon( + clipBounds: _getClipBoundsForExtent(area.positionExtent), + fill: area.areaColor != null ? area.areaColor : area.color, + points: area.points); + } + }); + } + + if (_hasMeasureBounds) { + elements + .map>>( + (_AnimatedElements animatingElement) => + animatingElement.bounds) + .expand<_AnimatedArea>((List<_AnimatedArea> bounds) => bounds) + .map<_AreaRendererElement>((_AnimatedArea animatingBounds) => + animatingBounds?.getCurrentArea(animationPercent)) + .forEach((_AreaRendererElement bound) { + if (bound != null) { + canvas.drawPolygon( + clipBounds: _getClipBoundsForExtent(bound.positionExtent), + fill: bound.areaColor != null ? bound.areaColor : bound.color, + points: bound.points); + } + }); + } + + if (config.includeLine) { + elements + .map>>( + (_AnimatedElements animatingElement) => + animatingElement.lines) + .expand<_AnimatedLine>((List<_AnimatedLine> lines) => lines) + .map<_LineRendererElement>((_AnimatedLine animatingLine) => + animatingLine?.getCurrentLine(animationPercent)) + .forEach((_LineRendererElement line) { + if (line != null) { + canvas.drawLine( + clipBounds: _getClipBoundsForExtent(line.positionExtent), + dashPattern: line.dashPattern, + points: line.points, + stroke: line.color, + strokeWidthPx: line.strokeWidthPx, + roundEndCaps: line.roundEndCaps); + } + }); + } + }); + + if (config.includePoints) { + _pointRenderer.paint(canvas, animationPercent); + } + } + + /// Builds a clip region bounding box within the component [drawBounds] for a + /// given domain range [extent]. + Rectangle _getClipBoundsForExtent(_Range extent) { + // In RTL mode, the domain range extent has start on the right side of the + // chart. Adjust the calculated positions to define a regular left-anchored + // [Rectangle]. Clamp both ends to be within the draw area. + final left = isRtl + ? clamp(extent.end, drawBounds.left, drawBounds.right) + : clamp(extent.start, drawBounds.left, drawBounds.right); + + final right = isRtl + ? clamp((extent.start), drawBounds.left, drawBounds.right) + : clamp((extent.end), drawBounds.left, drawBounds.right); + + return new Rectangle( + left, + drawBounds.top - drawBoundTopExtensionPx, + right - left, + drawBounds.height + + drawBoundTopExtensionPx + + drawBoundBottomExtensionPx); + } + + bool get isRtl => _chart?.context?.isRtl ?? false; + + _DatumPoint _getPoint( + dynamic datum, + D domainValue, + ImmutableSeries series, + ImmutableAxis domainAxis, + num measureValue, + num measureOffsetValue, + ImmutableAxis measureAxis, + {int index}) { + final domainPosition = domainAxis.getLocation(domainValue); + + final measurePosition = measureValue != null && measureOffsetValue != null + ? measureAxis.getLocation(measureValue + measureOffsetValue) + : null; + + return new _DatumPoint( + datum: datum, + domain: domainValue, + series: series, + x: domainPosition, + y: measurePosition, + index: index); + } + + @override + List> getNearestDatumDetailPerSeries( + Point chartPoint, bool byDomain, Rectangle boundsOverride) { + final nearest = >[]; + + // Was it even in the component bounds? + if (!isPointWithinBounds(chartPoint, boundsOverride)) { + return nearest; + } + + _seriesLineMap.values.forEach((List<_AnimatedElements> seriesSegments) { + _DatumPoint nearestPoint; + double nearestDomainDistance = 10000.0; + double nearestMeasureDistance = 10000.0; + double nearestRelativeDistance = 10000.0; + + seriesSegments.forEach((_AnimatedElements segment) { + if (segment.overlaySeries) { + return; + } + + segment.allPoints.forEach((Point p) { + // Don't look at points not in the drawArea. + if (p.x < componentBounds.left || p.x > componentBounds.right) { + return; + } + + final domainDistance = (p.x - chartPoint.x).abs(); + + double measureDistance; + double relativeDistance; + + if (p.y != null) { + measureDistance = (p.y - chartPoint.y).abs(); + relativeDistance = chartPoint.distanceTo(p); + } else { + // Null measures have no real position, so make them the farthest + // away by real distance. + measureDistance = double.infinity; + relativeDistance = byDomain ? domainDistance : double.infinity; + } + + if (byDomain) { + if ((domainDistance < nearestDomainDistance) || + ((domainDistance == nearestDomainDistance && + measureDistance < nearestMeasureDistance))) { + nearestPoint = p; + nearestDomainDistance = domainDistance; + nearestMeasureDistance = measureDistance; + nearestRelativeDistance = relativeDistance; + } + } else { + if (relativeDistance < nearestRelativeDistance) { + nearestPoint = p; + nearestDomainDistance = domainDistance; + nearestMeasureDistance = measureDistance; + nearestRelativeDistance = relativeDistance; + } + } + }); + }); + + // Found a point, add it to the list. + if (nearestPoint != null) { + nearest.add(new DatumDetails( + chartPosition: new Point(nearestPoint.x, nearestPoint.y), + datum: nearestPoint.datum, + domain: nearestPoint.domain, + series: nearestPoint.series, + domainDistance: nearestDomainDistance, + measureDistance: nearestMeasureDistance, + relativeDistance: nearestRelativeDistance)); + } + }); + + // Note: the details are already sorted by domain & measure distance in + // base chart. + + return nearest; + } + + DatumDetails addPositionToDetailsForSeriesDatum( + DatumDetails details, SeriesDatum seriesDatum) { + final series = details.series; + + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + + final point = _getPoint(seriesDatum.datum, details.domain, series, + domainAxis, details.measure, details.measureOffset, measureAxis); + final chartPosition = new Point(point.x, point.y); + + return new DatumDetails.from(details, chartPosition: chartPosition); + } +} + +class _DatumPoint extends Point { + final dynamic datum; + final D domain; + final ImmutableSeries series; + final int index; + + _DatumPoint( + {this.datum, this.domain, this.series, this.index, double x, double y}) + : super(x, y); + + factory _DatumPoint.from(_DatumPoint other, [double x, double y]) { + return new _DatumPoint( + datum: other.datum, + domain: other.domain, + series: other.series, + index: other.index, + x: x ?? other.x, + y: y ?? other.y); + } +} + +/// Rendering information for the line portion of a series. +class _LineRendererElement { + List<_DatumPoint> points; + Color color; + Color areaColor; + List dashPattern; + _Range domainExtent; + double measureAxisPosition; + _Range positionExtent; + double strokeWidthPx; + String styleKey; + bool roundEndCaps; + + _LineRendererElement clone() { + return new _LineRendererElement() + ..points = new List<_DatumPoint>.from(points) + ..color = color != null ? new Color.fromOther(color: color) : null + ..areaColor = + areaColor != null ? new Color.fromOther(color: areaColor) : null + ..dashPattern = + dashPattern != null ? new List.from(dashPattern) : null + ..domainExtent = domainExtent + ..measureAxisPosition = measureAxisPosition + ..positionExtent = positionExtent + ..strokeWidthPx = strokeWidthPx + ..styleKey = styleKey + ..roundEndCaps = roundEndCaps; + } + + void updateAnimationPercent(_LineRendererElement previous, + _LineRendererElement target, double animationPercent) { + Point lastPoint; + + int pointIndex; + for (pointIndex = 0; pointIndex < target.points.length; pointIndex++) { + final targetPoint = target.points[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. + // TODO: Can this be done in setNewTarget instead? + _DatumPoint previousPoint; + if (previous.points.length - 1 >= pointIndex) { + previousPoint = previous.points[pointIndex]; + lastPoint = previousPoint; + } else { + previousPoint = + new _DatumPoint.from(targetPoint, targetPoint.x, lastPoint.y); + } + + final x = ((targetPoint.x - previousPoint.x) * animationPercent) + + previousPoint.x; + + double y; + if (targetPoint.y != null && previousPoint.y != null) { + y = ((targetPoint.y - previousPoint.y) * animationPercent) + + previousPoint.y; + } else if (targetPoint.y != null) { + y = targetPoint.y; + } else { + y = null; + } + + if (points.length - 1 >= pointIndex) { + points[pointIndex] = new _DatumPoint.from(targetPoint, x, y); + } else { + points.add(new _DatumPoint.from(targetPoint, x, y)); + } + } + + // Removing extra points that don't exist anymore. + if (pointIndex < points.length) { + points.removeRange(pointIndex, points.length); + } + + color = getAnimatedColor(previous.color, target.color, animationPercent); + + if (areaColor != null) { + areaColor = getAnimatedColor( + previous.areaColor, target.areaColor, animationPercent); + } + + strokeWidthPx = + (((target.strokeWidthPx - previous.strokeWidthPx) * animationPercent) + + previous.strokeWidthPx); + } +} + +/// Animates the line element of a series between different states. +class _AnimatedLine { + final String key; + final bool overlaySeries; + + _LineRendererElement _previousLine; + _LineRendererElement _targetLine; + _LineRendererElement _currentLine; + + // Flag indicating whether this line is being animated out of the chart. + bool animatingOut = false; + + _AnimatedLine({@required this.key, @required this.overlaySeries}); + + /// Animates a line that was removed from the series out of the view. + /// + /// This should be called in place of "setNewTarget" for lines that represent + /// data that has been removed from the series. + /// + /// Animates the height of the line down to the measure axis position + /// (position of 0). + void animateOut() { + var newTarget = _currentLine.clone(); + + // Set the target measure value to the axis position for all points. + // TODO: Animate to the nearest lines in the stack. + var newPoints = <_DatumPoint>[]; + for (var index = 0; index < newTarget.points.length; index++) { + var targetPoint = newTarget.points[index]; + + newPoints.add(new _DatumPoint.from(targetPoint, targetPoint.x, + newTarget.measureAxisPosition.roundToDouble())); + } + + newTarget.points = newPoints; + + // 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(_LineRendererElement newTarget) { + animatingOut = false; + _currentLine ??= newTarget.clone(); + _previousLine = _currentLine.clone(); + _targetLine = newTarget; + } + + _LineRendererElement getCurrentLine(double animationPercent) { + if (animationPercent == 1.0 || _previousLine == null) { + _currentLine = _targetLine; + _previousLine = _targetLine; + return _currentLine; + } + + _currentLine.updateAnimationPercent( + _previousLine, _targetLine, animationPercent); + + return _currentLine; + } + + /// Returns the [points] of the current target element, without updating + /// animation state. + List<_DatumPoint> get currentPoints => _currentLine?.points; +} + +/// Rendering information for the area skirt portion of a series. +class _AreaRendererElement { + List<_DatumPoint> points; + Color color; + Color areaColor; + _Range domainExtent; + double measureAxisPosition; + _Range positionExtent; + String styleKey; + + _AreaRendererElement clone() { + return new _AreaRendererElement() + ..points = new List<_DatumPoint>.from(points) + ..color = color != null ? new Color.fromOther(color: color) : null + ..areaColor = + areaColor != null ? new Color.fromOther(color: areaColor) : null + ..domainExtent = domainExtent + ..measureAxisPosition = measureAxisPosition + ..positionExtent = positionExtent + ..styleKey = styleKey; + } + + void updateAnimationPercent(_AreaRendererElement previous, + _AreaRendererElement target, double animationPercent) { + Point lastPoint; + + int pointIndex; + for (pointIndex = 0; pointIndex < target.points.length; pointIndex++) { + var targetPoint = target.points[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. + // TODO: Can this be done in setNewTarget instead? + _DatumPoint previousPoint; + if (previous.points.length - 1 >= pointIndex) { + previousPoint = previous.points[pointIndex]; + lastPoint = previousPoint; + } else { + previousPoint = + new _DatumPoint.from(targetPoint, targetPoint.x, lastPoint.y); + } + + final x = ((targetPoint.x - previousPoint.x) * animationPercent) + + previousPoint.x; + + double y; + if (targetPoint.y != null && previousPoint.y != null) { + y = ((targetPoint.y - previousPoint.y) * animationPercent) + + previousPoint.y; + } else if (targetPoint.y != null) { + y = targetPoint.y; + } else { + y = null; + } + + if (points.length - 1 >= pointIndex) { + points[pointIndex] = new _DatumPoint.from(targetPoint, x, y); + } else { + points.add(new _DatumPoint.from(targetPoint, x, y)); + } + } + + // Removing extra points that don't exist anymore. + if (pointIndex < points.length) { + points.removeRange(pointIndex, points.length); + } + + color = getAnimatedColor(previous.color, target.color, animationPercent); + + if (areaColor != null) { + areaColor = getAnimatedColor( + previous.areaColor, target.areaColor, animationPercent); + } + } +} + +/// Animates the area element of a series between different states. +class _AnimatedArea { + final String key; + final bool overlaySeries; + + _AreaRendererElement _previousArea; + _AreaRendererElement _targetArea; + _AreaRendererElement _currentArea; + + // Flag indicating whether this line is being animated out of the chart. + bool animatingOut = false; + + _AnimatedArea({@required this.key, @required this.overlaySeries}); + + /// Animates a line that was removed from the series out of the view. + /// + /// This should be called in place of "setNewTarget" for lines that represent + /// data that has been removed from the series. + /// + /// Animates the height of the line down to the measure axis position + /// (position of 0). + void animateOut() { + var newTarget = _currentArea.clone(); + + // Set the target measure value to the axis position for all points. + // TODO: Animate to the nearest areas in the stack. + var newPoints = <_DatumPoint>[]; + for (var index = 0; index < newTarget.points.length; index++) { + var targetPoint = newTarget.points[index]; + + newPoints.add(new _DatumPoint.from(targetPoint, targetPoint.x, + newTarget.measureAxisPosition.roundToDouble())); + } + + newTarget.points = newPoints; + + setNewTarget(newTarget); + animatingOut = true; + } + + void setNewTarget(_AreaRendererElement newTarget) { + animatingOut = false; + _currentArea ??= newTarget.clone(); + _previousArea = _currentArea.clone(); + _targetArea = newTarget; + } + + _AreaRendererElement getCurrentArea(double animationPercent) { + if (animationPercent == 1.0 || _previousArea == null) { + _currentArea = _targetArea; + _previousArea = _targetArea; + return _currentArea; + } + + _currentArea.updateAnimationPercent( + _previousArea, _targetArea, animationPercent); + + return _currentArea; + } +} + +class _AnimatedElements { + List<_DatumPoint> allPoints; + List<_AnimatedArea> areas; + List<_AnimatedLine> lines; + List<_AnimatedArea> bounds; + String styleKey; + + bool get animatingOut { + var areasAnimatingOut = true; + if (areas != null) { + for (_AnimatedArea area in areas) { + areasAnimatingOut = areasAnimatingOut && area.animatingOut; + } + } + + var linesAnimatingOut = true; + if (lines != null) { + for (_AnimatedLine line in lines) { + linesAnimatingOut = linesAnimatingOut && line.animatingOut; + } + } + + var boundsAnimatingOut = true; + if (bounds != null) { + for (_AnimatedArea bound in bounds) { + boundsAnimatingOut = boundsAnimatingOut && bound.animatingOut; + } + } + + return areasAnimatingOut && linesAnimatingOut && boundsAnimatingOut; + } + + bool get overlaySeries { + var areasOverlaySeries = true; + if (areas != null) { + for (_AnimatedArea area in areas) { + areasOverlaySeries = areasOverlaySeries && area.overlaySeries; + } + } + + var linesOverlaySeries = true; + if (lines != null) { + for (_AnimatedLine line in lines) { + linesOverlaySeries = linesOverlaySeries && line.overlaySeries; + } + } + + var boundsOverlaySeries = true; + if (bounds != null) { + for (_AnimatedArea bound in bounds) { + boundsOverlaySeries = boundsOverlaySeries && bound.overlaySeries; + } + } + + return areasOverlaySeries && linesOverlaySeries && boundsOverlaySeries; + } +} + +/// Describes a numeric range with a start and end value. +/// +/// [start] must always be less than [end]. +class _Range { + D _start; + D _end; + + _Range(D start, D end) { + _start = start; + _end = end; + } + + /// Gets the start of the range. + D get start => _start; + + /// Gets the end of the range. + D get end => _end; + + /// Extends the range to include [value]. + void includePoint(D value) { + if (value == null) { + return; + } else if (value is num || value is double || value is int) { + _includePointAsNum(value); + } else if (value is DateTime) { + _includePointAsDateTime(value); + } else if (value is String) { + _includePointAsString(value); + } else { + throw ('Unsupported object type for LineRenderer domain value: ' + '${value.runtimeType}'); + } + } + + /// Extends the range to include value by casting as numbers. + void _includePointAsNum(D value) { + if ((value as num) < (_start as num)) { + _start = value; + } else if ((value as num) > (_end as num)) { + _end = value; + } + } + + /// Extends the range to include value by casting as DateTime objects. + void _includePointAsDateTime(D value) { + if ((value as DateTime).isBefore(_start as DateTime)) { + _start = value; + } else if ((value as DateTime).isAfter(_end as DateTime)) { + _end = value; + } + } + + /// Extends the range to include value by casting as String objects. + /// + /// In this case, we assume that the data is ordered in the same order as the + /// axis. + void _includePointAsString(D value) { + _end = value; + } +} + +@visibleForTesting +class LineRendererTester { + final LineRenderer renderer; + + LineRendererTester(this.renderer); + + Iterable get seriesKeys => renderer._seriesLineMap.keys; + + void setSeriesKeys(List keys) { + renderer._seriesLineMap.addEntries(keys.map((key) => MapEntry(key, []))); + } + + void merge(List> series) { + renderer._mergeIntoSeriesMap(series); + } +} diff --git a/web/charts/common/lib/src/chart/line/line_renderer_config.dart b/web/charts/common/lib/src/chart/line/line_renderer_config.dart new file mode 100644 index 000000000..eee44a658 --- /dev/null +++ b/web/charts/common/lib/src/chart/line/line_renderer_config.dart @@ -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'; +import '../common/series_renderer_config.dart' + show RendererAttributes, SeriesRendererConfig; +import '../layout/layout_view.dart' show LayoutViewConfig, LayoutViewPaintOrder; +import 'line_renderer.dart' show LineRenderer; + +/// Configuration for a line renderer. +class LineRendererConfig extends LayoutViewConfig + implements SeriesRendererConfig { + final String customRendererId; + + final SymbolRenderer symbolRenderer; + + final rendererAttributes = new RendererAttributes(); + + /// Radius of points on the line, if [includePoints] is enabled. + final double radiusPx; + + /// Whether or not series should be rendered in a stack. + /// + /// This is typically enabled when including area skirts. + final bool stacked; + + /// Stroke width of the line. + final double strokeWidthPx; + + /// Dash pattern for the line. + final List dashPattern; + + /// Configures whether a line representing the data will be drawn. + final bool includeLine; + + /// Configures whether points representing the data will be drawn. + final bool includePoints; + + /// Configures whether an area skirt representing the data will be drawn. + /// + /// An area skirt will be drawn from the line for each series, down to the + /// domain axis. It will be layered underneath the primary line on the chart. + /// + /// The area skirt color will be a semi-transparent version of the series + /// color, using [areaOpacity] as the opacity. + /// + /// When stacking is enabled, the bottom of each area skirt will instead be + /// the previous line in the stack. The bottom area will be drawn down to the + /// domain axis. + final bool includeArea; + + /// The order to paint this renderer on the canvas. + final int layoutPaintOrder; + + /// Configures the opacity of the area skirt on the chart. + final double areaOpacity; + + /// Whether lines should have round end caps, or square if false. + final bool roundEndCaps; + + LineRendererConfig( + {this.customRendererId, + this.radiusPx = 3.5, + this.stacked = false, + this.strokeWidthPx = 2.0, + this.dashPattern, + this.includeLine = true, + this.includePoints = false, + this.includeArea = false, + this.layoutPaintOrder = LayoutViewPaintOrder.line, + this.areaOpacity = 0.1, + this.roundEndCaps = false, + SymbolRenderer symbolRenderer}) + : this.symbolRenderer = symbolRenderer ?? new LineSymbolRenderer(); + + @override + LineRenderer build() { + return new LineRenderer(config: this, rendererId: customRendererId); + } +} diff --git a/web/charts/common/lib/src/chart/pie/arc_label_decorator.dart b/web/charts/common/lib/src/chart/pie/arc_label_decorator.dart new file mode 100644 index 000000000..2e65877fd --- /dev/null +++ b/web/charts/common/lib/src/chart/pie/arc_label_decorator.dart @@ -0,0 +1,408 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show cos, min, sin, pi, Point, Rectangle; + +import 'package:meta/meta.dart' show immutable, required; + +import '../../common/color.dart' show Color; +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 '../../data/series.dart' show AccessorFn; +import '../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec; +import '../common/chart_canvas.dart' show ChartCanvas; +import 'arc_renderer.dart' show ArcRendererElementList; +import 'arc_renderer_decorator.dart' show ArcRendererDecorator; + +/// Renders labels for arc renderers. +/// +/// This decorator performs very basic label collision detection. If the y +/// position of a label positioned outside collides with the previously drawn +/// label (on the same side of the chart), then that label will be skipped. +class ArcLabelDecorator extends ArcRendererDecorator { + // Default configuration + static const _defaultLabelPosition = ArcLabelPosition.auto; + static const _defaultLabelPadding = 5; + static final _defaultInsideLabelStyle = + new TextStyleSpec(fontSize: 12, color: Color.white); + static final _defaultOutsideLabelStyle = + new TextStyleSpec(fontSize: 12, color: Color.black); + static final _defaultLeaderLineStyle = new ArcLabelLeaderLineStyleSpec( + length: 20.0, + thickness: 1.0, + color: StyleFactory.style.arcLabelOutsideLeaderLine); + static const _defaultShowLeaderLines = true; + + /// Configures [TextStyleSpec] for labels placed inside the arcs. + final TextStyleSpec insideLabelStyleSpec; + + /// Configures [TextStyleSpec] for labels placed outside the arcs. + final TextStyleSpec outsideLabelStyleSpec; + + /// Configures [ArcLabelLeaderLineStyleSpec] for leader lines for labels + /// placed outside the arcs. + final ArcLabelLeaderLineStyleSpec leaderLineStyleSpec; + + /// Configures where to place the label relative to the arcs. + final ArcLabelPosition labelPosition; + + /// Space before and after the label text. + final int labelPadding; + + /// Whether or not to draw leader lines for labels placed outside the arcs. + final bool showLeaderLines; + + /// Render the labels on top of series data. + final bool renderAbove = true; + + ArcLabelDecorator( + {TextStyleSpec insideLabelStyleSpec, + TextStyleSpec outsideLabelStyleSpec, + ArcLabelLeaderLineStyleSpec leaderLineStyleSpec, + this.labelPosition = _defaultLabelPosition, + this.labelPadding = _defaultLabelPadding, + this.showLeaderLines = _defaultShowLeaderLines, + Color leaderLineColor}) + : insideLabelStyleSpec = insideLabelStyleSpec ?? _defaultInsideLabelStyle, + outsideLabelStyleSpec = + outsideLabelStyleSpec ?? _defaultOutsideLabelStyle, + leaderLineStyleSpec = leaderLineStyleSpec ?? _defaultLeaderLineStyle; + + @override + void decorate(ArcRendererElementList arcElements, ChartCanvas canvas, + GraphicsFactory graphicsFactory, + {@required Rectangle drawBounds, + @required double animationPercent, + bool rtl = false}) { + // Only decorate the arcs 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); + + // Track the Y position of the previous outside label for collision + // detection purposes. + num previousOutsideLabelY; + bool previousLabelLeftOfChart; + + for (var element in arcElements.arcs) { + 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 arcAngle = element.endAngle - element.startAngle; + + final centerAngle = element.startAngle + (arcAngle / 2); + + final centerRadius = arcElements.innerRadius + + ((arcElements.radius - arcElements.innerRadius) / 2); + + final innerPoint = new Point( + arcElements.center.x + arcElements.innerRadius * cos(centerAngle), + arcElements.center.y + arcElements.innerRadius * sin(centerAngle)); + + final outerPoint = new Point( + arcElements.center.x + arcElements.radius * cos(centerAngle), + arcElements.center.y + arcElements.radius * sin(centerAngle)); + + //final bounds = element.bounds; + final bounds = new Rectangle.fromPoints(innerPoint, outerPoint); + + // Get space available inside and outside the arc. + final totalPadding = labelPadding * 2; + final insideArcWidth = (min( + (((arcAngle * 180 / pi) / 360) * (2 * pi * centerRadius)).round(), + (arcElements.radius - arcElements.innerRadius) - labelPadding) + .round()); + + final leaderLineLength = showLeaderLines ? leaderLineStyleSpec.length : 0; + + final outsideArcWidth = ((drawBounds.width / 2) - + bounds.width - + totalPadding - + leaderLineLength) + .round(); + + final labelElement = graphicsFactory.createTextElement(label) + ..maxWidthStrategy = MaxWidthStrategy.ellipsize; + + var calculatedLabelPosition = labelPosition; + if (calculatedLabelPosition == ArcLabelPosition.auto) { + // For auto, first try to fit the text inside the arc. + labelElement.textStyle = datumInsideLabelStyle; + + // A label fits if the space inside the arc is >= outside arc or if the + // length of the text fits and the space. This is because if the arc has + // more space than the outside, it makes more sense to place the label + // inside the arc, even if the entire label does not fit. + calculatedLabelPosition = (insideArcWidth >= outsideArcWidth || + labelElement.measurement.horizontalSliceWidth < insideArcWidth) + ? ArcLabelPosition.inside + : ArcLabelPosition.outside; + } + + // Set the max width and text style. + if (calculatedLabelPosition == ArcLabelPosition.inside) { + labelElement.textStyle = datumInsideLabelStyle; + labelElement.maxWidth = insideArcWidth; + } else { + // calculatedLabelPosition == LabelPosition.outside + labelElement.textStyle = datumOutsideLabelStyle; + labelElement.maxWidth = outsideArcWidth; + } + + // 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]. + if (calculatedLabelPosition == ArcLabelPosition.inside) { + _drawInsideLabel(canvas, arcElements, labelElement, centerAngle); + } else { + final l = _drawOutsideLabel( + canvas, + drawBounds, + arcElements, + labelElement, + centerAngle, + previousOutsideLabelY, + previousLabelLeftOfChart); + + // List destructuring.. + if (l != null) { + previousLabelLeftOfChart = l[0]; + previousOutsideLabelY = l[1]; + } + } + } + } + } + + /// 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 labelFn, int datumIndex, + GraphicsFactory graphicsFactory, + {TextStyle defaultStyle}) { + final styleSpec = (labelFn != null) ? labelFn(datumIndex) : null; + return (styleSpec != null) + ? _getTextStyle(graphicsFactory, styleSpec) + : defaultStyle; + } + + /// Draws a label inside of an arc. + void _drawInsideLabel( + ChartCanvas canvas, + ArcRendererElementList arcElements, + TextElement labelElement, + double centerAngle) { + // Center the label inside the arc. + final labelRadius = arcElements.innerRadius + + (arcElements.radius - arcElements.innerRadius) / 2; + + final labelX = + (arcElements.center.x + labelRadius * cos(centerAngle)).round(); + + final labelY = (arcElements.center.y + + labelRadius * sin(centerAngle) - + insideLabelStyleSpec.fontSize / 2) + .round(); + + labelElement.textDirection = TextDirection.center; + + canvas.drawText(labelElement, labelX, labelY); + } + + /// Draws a label outside of an arc. + List _drawOutsideLabel( + ChartCanvas canvas, + Rectangle drawBounds, + ArcRendererElementList arcElements, + TextElement labelElement, + double centerAngle, + num previousOutsideLabelY, + bool previousLabelLeftOfChart) { + final labelRadius = arcElements.radius + leaderLineStyleSpec.length / 2; + + final labelPoint = new Point( + arcElements.center.x + labelRadius * cos(centerAngle), + arcElements.center.y + labelRadius * sin(centerAngle)); + + // Use the label's chart quandrant to determine whether it's rendered to the + // right or left. + final centerAbs = centerAngle.abs() % (2 * pi); + final labelLeftOfChart = pi / 2 < centerAbs && centerAbs < pi * 3 / 2; + + // Shift the label horizontally away from the center of the chart. + var labelX = labelLeftOfChart + ? (labelPoint.x - labelPadding).round() + : (labelPoint.x + labelPadding).round(); + + // Shift the label up by the size of the font. + final labelY = (labelPoint.y - outsideLabelStyleSpec.fontSize / 2).round(); + + // Outside labels should flow away from the center of the chart + labelElement.textDirection = + labelLeftOfChart ? TextDirection.rtl : TextDirection.ltr; + + // Skip this label if it collides with the previously drawn label. + if (_detectOutsideLabelCollision(labelY, labelLeftOfChart, + previousOutsideLabelY, previousLabelLeftOfChart)) { + return null; + } + + if (showLeaderLines) { + final tailX = _drawLeaderLine(canvas, labelLeftOfChart, labelPoint, + arcElements.radius, arcElements.center, centerAngle); + + // Shift the label horizontally by the length of the leader line. + labelX = (labelX + tailX).round(); + + labelElement.maxWidth = (labelElement.maxWidth - tailX).round(); + } + + canvas.drawText(labelElement, labelX, labelY); + + // Return a structured list of values. + return [labelLeftOfChart, labelY]; + } + + /// Detects whether the current outside label collides with the previous label. + bool _detectOutsideLabelCollision(num labelY, bool labelLeftOfChart, + num previousOutsideLabelY, bool previousLabelLeftOfChart) { + bool collides = false; + + // Given that labels are vertically centered, we can assume they will + // collide if the current label's Y coordinate +/- the font size + // crosses past the Y coordinate of the previous label drawn on the + // same side of the chart. + if (previousOutsideLabelY != null && + labelLeftOfChart == previousLabelLeftOfChart) { + if (labelY > previousOutsideLabelY) { + if (labelY - outsideLabelStyleSpec.fontSize <= previousOutsideLabelY) { + collides = true; + } + } else { + if (labelY + outsideLabelStyleSpec.fontSize >= previousOutsideLabelY) { + collides = true; + } + } + } + + return collides; + } + + /// Draws a leader line for the current arc. + double _drawLeaderLine( + ChartCanvas canvas, + bool labelLeftOfChart, + Point labelPoint, + double radius, + Point arcCenterPoint, + double centerAngle) { + final tailX = (labelLeftOfChart ? -1 : 1) * leaderLineStyleSpec.length; + + final leaderLineTailPoint = + new Point(labelPoint.x + tailX, labelPoint.y); + + final centerRadius = radius - leaderLineStyleSpec.length / 2; + final leaderLineStartPoint = new Point( + arcCenterPoint.x + centerRadius * cos(centerAngle), + arcCenterPoint.y + centerRadius * sin(centerAngle)); + + canvas.drawLine( + points: [ + leaderLineStartPoint, + labelPoint, + leaderLineTailPoint, + ], + stroke: leaderLineStyleSpec.color, + strokeWidthPx: leaderLineStyleSpec.thickness); + + return tailX; + } +} + +/// Configures where to place the label relative to the arcs. +enum ArcLabelPosition { + /// Automatically try to place the label inside the arc first and place it on + /// the outside of the space available outside the arc is greater than space + /// available inside the arc. + auto, + + /// Always place label on the outside. + outside, + + /// Always place label on the inside. + inside, +} + +/// Style configuration for leader lines. +@immutable +class ArcLabelLeaderLineStyleSpec { + final Color color; + final double length; + final double thickness; + + ArcLabelLeaderLineStyleSpec({this.color, this.length, this.thickness}); + + @override + bool operator ==(Object other) { + return other is ArcLabelLeaderLineStyleSpec && + color == other.color && + thickness == other.thickness && + length == other.length; + } + + @override + int get hashCode { + int hashcode = color?.hashCode ?? 0; + hashcode = (hashcode * 37) + thickness?.hashCode ?? 0; + hashcode = (hashcode * 37) + length?.hashCode ?? 0; + return hashcode; + } +} diff --git a/web/charts/common/lib/src/chart/pie/arc_renderer.dart b/web/charts/common/lib/src/chart/pie/arc_renderer.dart new file mode 100644 index 000000000..f33030f55 --- /dev/null +++ b/web/charts/common/lib/src/chart/pie/arc_renderer.dart @@ -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:collection' show LinkedHashMap; +import 'dart:math' show atan2, cos, max, sin, pi, Point, Rectangle; + +import 'package:meta/meta.dart' show required; + +import '../../common/color.dart' show Color; +import '../../common/style/style_factory.dart' show StyleFactory; +import '../../data/series.dart' show AttributeKey; +import '../common/base_chart.dart' show BaseChart; +import '../common/canvas_shapes.dart' show CanvasPieSlice, CanvasPie; +import '../common/chart_canvas.dart' show ChartCanvas, getAnimatedColor; +import '../common/datum_details.dart' show DatumDetails; +import '../common/processed_series.dart' show ImmutableSeries, MutableSeries; +import '../common/series_datum.dart' show SeriesDatum; +import '../common/series_renderer.dart' show BaseSeriesRenderer; +import 'arc_renderer_config.dart' show ArcRendererConfig; +import 'arc_renderer_decorator.dart' show ArcRendererDecorator; + +const arcElementsKey = + const AttributeKey>('ArcRenderer.elements'); + +class ArcRenderer extends BaseSeriesRenderer { + // Constant used in the calculation of [centerContentBounds], calculated once + // to save runtime cost. + static final _cosPIOver4 = cos(pi / 4); + + final ArcRendererConfig config; + + final List arcRendererDecorators; + + BaseChart _chart; + + /// 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. + final _seriesArcMap = new LinkedHashMap>(); + + // Store a list of arcs that exist in the series data. + // + // This list will be used to remove any [_AnimatedArc] that were rendered in + // previous draw cycles, but no longer have a corresponding datum in the new + // data. + final _currentKeys = []; + + factory ArcRenderer({String rendererId, ArcRendererConfig config}) { + return new ArcRenderer._internal( + rendererId: rendererId ?? 'line', + config: config ?? new ArcRendererConfig()); + } + + ArcRenderer._internal({String rendererId, this.config}) + : arcRendererDecorators = config?.arcRendererDecorators ?? [], + super( + rendererId: rendererId, + layoutPaintOrder: config.layoutPaintOrder, + symbolRenderer: config.symbolRenderer); + + @override + void onAttach(BaseChart chart) { + super.onAttach(chart); + _chart = chart; + } + + @override + void configureSeries(List> seriesList) { + assignMissingColors(seriesList, emptyCategoryUsesSinglePalette: false); + } + + @override + void preprocessSeries(List> seriesList) { + seriesList.forEach((MutableSeries series) { + var elements = >[]; + + var domainFn = series.domainFn; + var measureFn = series.measureFn; + + final seriesMeasureTotal = series.seriesMeasureTotal; + + // On the canvas, arc measurements are defined as angles from the positive + // x axis. Start our first slice at the positive y axis instead. + var startAngle = config.startAngle; + var arcLength = config.arcLength; + + var totalAngle = 0.0; + + var measures = []; + + if (series.data.isEmpty) { + // If the series has no data, generate an empty arc element that + // occupies the entire chart. + // + // Use a tiny epsilon difference to ensure that the canvas renders a + // "full" circle, in the correct direction. + var angle = arcLength == 2 * pi ? arcLength * .999999 : arcLength; + var endAngle = startAngle + angle; + + var details = new ArcRendererElement(); + details.startAngle = startAngle; + details.endAngle = endAngle; + details.index = 0; + details.key = 0; + details.series = series; + + elements.add(details); + } else { + // Otherwise, generate an arc element per datum. + for (var arcIndex = 0; arcIndex < series.data.length; arcIndex++) { + var domain = domainFn(arcIndex); + var measure = measureFn(arcIndex); + measures.add(measure); + if (measure == null) { + continue; + } + + final percentOfSeries = (measure / seriesMeasureTotal); + var angle = arcLength * percentOfSeries; + var endAngle = startAngle + angle; + + var details = new ArcRendererElement(); + details.startAngle = startAngle; + details.endAngle = endAngle; + details.index = arcIndex; + details.key = arcIndex; + details.domain = domain; + details.series = series; + + elements.add(details); + + // Update the starting angle for the next datum in the series. + startAngle = endAngle; + + totalAngle = totalAngle + angle; + } + } + + series.setAttr(arcElementsKey, elements); + }); + } + + void update(List> seriesList, bool isAnimatingThisDraw) { + _currentKeys.clear(); + + final bounds = _chart.drawAreaBounds; + + final center = new Point( + (bounds.left + bounds.width / 2).toDouble(), + (bounds.top + bounds.height / 2).toDouble()); + + final radius = bounds.height < bounds.width + ? (bounds.height / 2).toDouble() + : (bounds.width / 2).toDouble(); + + if (config.arcRatio != null) { + if (0 < config.arcRatio || config.arcRatio > 1) { + throw new ArgumentError('arcRatio must be between 0 and 1'); + } + } + + final innerRadius = _calculateInnerRadius(radius); + + seriesList.forEach((ImmutableSeries series) { + var colorFn = series.colorFn; + var arcListKey = series.id; + + var arcList = + _seriesArcMap.putIfAbsent(arcListKey, () => new _AnimatedArcList()); + + var elementsList = series.getAttr(arcElementsKey); + + if (series.data.isEmpty) { + // If the series is empty, set up the "no data" arc element. This should + // occupy the entire chart, and use the chart style's no data color. + final details = elementsList[0]; + + var arcKey = '__no_data__'; + + // If we already have an AnimatingArc for that index, use it. + var animatingArc = arcList.arcs.firstWhere( + (_AnimatedArc arc) => arc.key == arcKey, + orElse: () => null); + + arcList.center = center; + arcList.radius = radius; + arcList.innerRadius = innerRadius; + arcList.series = series; + arcList.stroke = config.noDataColor; + arcList.strokeWidthPx = 0.0; + + // If we don't have any existing arc element, create a new arc. Unlike + // real arcs, we should not animate the no data state in from 0. + if (animatingArc == null) { + animatingArc = new _AnimatedArc(arcKey, null, null); + arcList.arcs.add(animatingArc); + } else { + animatingArc.datum = null; + animatingArc.domain = null; + } + + // Update the set of arcs that still exist in the series data. + _currentKeys.add(arcKey); + + // Get the arcElement we are going to setup. + // Optimization to prevent allocation in non-animating case. + final arcElement = new ArcRendererElement() + ..color = config.noDataColor + ..startAngle = details.startAngle + ..endAngle = details.endAngle + ..series = series; + + animatingArc.setNewTarget(arcElement); + } else { + var previousEndAngle = config.startAngle; + + for (var arcIndex = 0; arcIndex < series.data.length; arcIndex++) { + final datum = series.data[arcIndex]; + final details = elementsList[arcIndex]; + D domainValue = details.domain; + + var arcKey = domainValue.toString(); + + // If we already have an AnimatingArc for that index, use it. + var animatingArc = arcList.arcs.firstWhere( + (_AnimatedArc arc) => arc.key == arcKey, + orElse: () => null); + + arcList.center = center; + arcList.radius = radius; + arcList.innerRadius = innerRadius; + arcList.series = series; + arcList.stroke = config.stroke; + arcList.strokeWidthPx = config.strokeWidthPx; + + // If we don't have any existing arc element, create a new arc and + // have it animate in from the position of the previous arc's end + // angle. If there were no previous arcs, then animate everything in + // from 0. + if (animatingArc == null) { + animatingArc = new _AnimatedArc(arcKey, datum, domainValue) + ..setNewTarget(new ArcRendererElement() + ..color = colorFn(arcIndex) + ..startAngle = previousEndAngle + ..endAngle = previousEndAngle + ..index = arcIndex + ..series = series); + + arcList.arcs.add(animatingArc); + } else { + animatingArc.datum = datum; + + previousEndAngle = animatingArc.previousArcEndAngle ?? 0.0; + } + + animatingArc.domain = domainValue; + + // Update the set of arcs that still exist in the series data. + _currentKeys.add(arcKey); + + // Get the arcElement we are going to setup. + // Optimization to prevent allocation in non-animating case. + final arcElement = new ArcRendererElement() + ..color = colorFn(arcIndex) + ..startAngle = details.startAngle + ..endAngle = details.endAngle + ..index = arcIndex + ..series = series; + + animatingArc.setNewTarget(arcElement); + } + } + }); + + // Animate out arcs that don't exist anymore. + _seriesArcMap.forEach((String key, _AnimatedArcList arcList) { + for (var arcIndex = 0; arcIndex < arcList.arcs.length; arcIndex++) { + final arc = arcList.arcs[arcIndex]; + final arcStartAngle = arc.previousArcStartAngle; + + if (_currentKeys.contains(arc.key) != true) { + // Default to animating out to the top of the chart, clockwise, if + // there are no arcs that start past this arc. + var targetArcAngle = (2 * pi) + config.startAngle; + + // Find the nearest start angle of the next arc that still exists in + // the data. + for (_AnimatedArc nextArc + in arcList.arcs.where((arc) => _currentKeys.contains(arc.key))) { + final nextArcStartAngle = nextArc.newTargetArcStartAngle; + + if (arcStartAngle < nextArcStartAngle && + nextArcStartAngle < targetArcAngle) { + targetArcAngle = nextArcStartAngle; + } + } + + arc.animateOut(targetArcAngle); + } + } + }); + } + + void paint(ChartCanvas canvas, double animationPercent) { + // Clean up the arcs that no longer exist. + if (animationPercent == 1.0) { + final keysToRemove = []; + + _seriesArcMap.forEach((String key, _AnimatedArcList arcList) { + arcList.arcs.removeWhere((_AnimatedArc arc) => arc.animatingOut); + + if (arcList.arcs.isEmpty) { + keysToRemove.add(key); + } + }); + + keysToRemove.forEach(_seriesArcMap.remove); + } + + _seriesArcMap.forEach((String key, _AnimatedArcList arcList) { + final circleSectors = []; + final arcElementsList = new ArcRendererElementList() + ..arcs = >[] + ..center = arcList.center + ..innerRadius = arcList.innerRadius + ..radius = arcList.radius + ..startAngle = config.startAngle + ..stroke = arcList.stroke + ..strokeWidthPx = arcList.strokeWidthPx; + + arcList.arcs + .map>((_AnimatedArc animatingArc) => + animatingArc.getCurrentArc(animationPercent)) + .forEach((ArcRendererElement arc) { + circleSectors.add( + new CanvasPieSlice(arc.startAngle, arc.endAngle, fill: arc.color)); + + arcElementsList.arcs.add(arc); + }); + + // Decorate the arcs with decorators that should appear below the main + // series data. + arcRendererDecorators + .where((ArcRendererDecorator decorator) => !decorator.renderAbove) + .forEach((ArcRendererDecorator decorator) { + decorator.decorate(arcElementsList, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: animationPercent, + rtl: isRtl); + }); + + // Draw the arcs. + canvas.drawPie(new CanvasPie( + circleSectors, arcList.center, arcList.radius, arcList.innerRadius, + stroke: arcList.stroke, strokeWidthPx: arcList.strokeWidthPx)); + + // Decorate the arcs with decorators that should appear above the main + // series data. This is the typical place for labels. + arcRendererDecorators + .where((ArcRendererDecorator decorator) => decorator.renderAbove) + .forEach((ArcRendererDecorator decorator) { + decorator.decorate(arcElementsList, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: animationPercent, + rtl: isRtl); + }); + }); + } + + bool get isRtl => _chart?.context?.isRtl ?? false; + + /// Gets a bounding box for the largest center content card that can fit + /// inside the hole of the chart. + /// + /// If the inner radius of the arcs is smaller than + /// [ArcRendererConfig.minHoleWidthForCenterContent], this will return a + /// rectangle of 0 width and height to indicate that no card can fit inside + /// the chart. + Rectangle get centerContentBounds { + // Grab the first arcList from the animated set. + var arcList = _seriesArcMap.isNotEmpty ? _seriesArcMap.values.first : null; + + // No card should be visible if the hole in the chart is too small. + if (arcList == null || + arcList.innerRadius < config.minHoleWidthForCenterContent) { + // Return default bounds of 0 size. + final bounds = _chart.drawAreaBounds; + return new Rectangle((bounds.left + bounds.width / 2).round(), + (bounds.top + bounds.height / 2).round(), 0, 0); + } + + // Fix the height and width of the center content div to the maximum box + // size that will fit within the pie's inner radius. + final width = (_cosPIOver4 * arcList.innerRadius).floor(); + + return new Rectangle((arcList.center.x - width).round(), + (arcList.center.y - width).round(), width * 2, width * 2); + } + + /// Returns an expanded [DatumDetails] object that contains location data. + DatumDetails getExpandedDatumDetails(SeriesDatum seriesDatum) { + final series = seriesDatum.series; + final datum = seriesDatum.datum; + final datumIndex = seriesDatum.index; + + final domain = series.domainFn(datumIndex); + final measure = series.measureFn(datumIndex); + final color = series.colorFn(datumIndex); + + final chartPosition = _getChartPosition(series.id, domain.toString()); + + return new DatumDetails( + datum: datum, + domain: domain, + measure: measure, + series: series, + color: color, + chartPosition: chartPosition); + } + + /// Returns the chart position for a given datum by series ID and domain + /// value. + /// + /// [seriesId] the series ID. + /// + /// [key] the key in the current animated arc list. + Point _getChartPosition(String seriesId, String key) { + Point chartPosition; + + final arcList = _seriesArcMap[seriesId]; + + if (arcList == null) { + return chartPosition; + } + + for (_AnimatedArc arc in arcList.arcs) { + if (arc.key == key) { + // Now that we have found the matching arc, calculate the center point + // halfway between the inner and outer radius, and the start and end + // angles. + final centerAngle = arc.currentArcStartAngle + + (arc.currentArcEndAngle - arc.currentArcStartAngle) / 2; + + final centerPointRadius = + arcList.innerRadius + (arcList.radius - arcList.innerRadius) / 2; + + chartPosition = new Point( + centerPointRadius * cos(centerAngle) + arcList.center.x, + centerPointRadius * sin(centerAngle) + arcList.center.y); + + break; + } + } + + return chartPosition; + } + + @override + List> getNearestDatumDetailPerSeries( + Point chartPoint, bool byDomain, Rectangle boundsOverride) { + final nearest = >[]; + + // Was it even in the component bounds? + if (!isPointWithinBounds(chartPoint, boundsOverride)) { + return nearest; + } + + _seriesArcMap.forEach((String key, _AnimatedArcList arcList) { + if (arcList.series.overlaySeries) { + return; + } + + final center = arcList.center; + final innerRadius = arcList.innerRadius; + final radius = arcList.radius; + + final distance = center.distanceTo(chartPoint); + + // Calculate the angle of [chartPoint] from the center of the arcs. + var chartPointAngle = + atan2(chartPoint.y - center.y, chartPoint.x - center.x); + + // atan2 returns NaN if we are at the exact center of the circle. + if (chartPointAngle.isNaN) { + chartPointAngle = config.startAngle; + } + + // atan2 returns an angle in the range -PI..PI, from the positive x-axis. + // Our arcs start at the positive y-axis, in the range -PI/2..3PI/2. Thus, + // if angle is in the -x, +y section of the circle, we need to adjust the + // angle into our range. + if (chartPointAngle < config.startAngle && chartPointAngle < 0) { + chartPointAngle = 2 * pi + chartPointAngle; + } + + arcList.arcs.forEach((_AnimatedArc arc) { + if (innerRadius <= distance && distance <= radius) { + if (arc.currentArcStartAngle <= chartPointAngle && + chartPointAngle <= arc.currentArcEndAngle) { + nearest.add(new DatumDetails( + series: arcList.series, + datum: arc.datum, + domain: arc.domain, + domainDistance: 0.0, + measureDistance: 0.0, + )); + } + } + }); + }); + + return nearest; + } + + @override + DatumDetails addPositionToDetailsForSeriesDatum( + DatumDetails details, SeriesDatum seriesDatum) { + final chartPosition = + _getChartPosition(details.series.id, details.domain.toString()); + + return new DatumDetails.from(details, chartPosition: chartPosition); + } + + /// Assigns colors to series that are missing their colorFn. + @override + assignMissingColors(Iterable seriesList, + {@required bool emptyCategoryUsesSinglePalette}) { + int maxMissing = 0; + + seriesList.forEach((MutableSeries series) { + if (series.colorFn == null) { + maxMissing = max(maxMissing, series.data.length); + } + }); + + if (maxMissing > 0) { + final colorPalettes = StyleFactory.style.getOrderedPalettes(1); + final colorPalette = colorPalettes[0].makeShades(maxMissing); + + seriesList.forEach((MutableSeries series) { + series.colorFn ??= (index) => colorPalette[index]; + }); + } + } + + /// Calculates the size of the inner pie radius given the outer radius. + double _calculateInnerRadius(double radius) { + // arcRatio trumps arcWidth. If neither is defined, then inner radius is 0. + if (config.arcRatio != null) { + return max(radius - radius * config.arcRatio, 0.0).toDouble(); + } else if (config.arcWidth != null) { + return max(radius - config.arcWidth, 0.0).toDouble(); + } else { + return 0.0; + } + } +} + +class ArcRendererElementList { + List> arcs; + Point center; + double innerRadius; + double radius; + double startAngle; + + /// Color of separator lines between arcs. + Color stroke; + + /// Stroke width of separator lines between arcs. + double strokeWidthPx; +} + +class ArcRendererElement { + double startAngle; + double endAngle; + Color color; + int index; + num key; + D domain; + ImmutableSeries series; + + ArcRendererElement clone() { + return new ArcRendererElement() + ..startAngle = startAngle + ..endAngle = endAngle + ..color = new Color.fromOther(color: color) + ..index = index + ..key = key + ..series = series; + } + + void updateAnimationPercent(ArcRendererElement previous, + ArcRendererElement target, double animationPercent) { + startAngle = + ((target.startAngle - previous.startAngle) * animationPercent) + + previous.startAngle; + + endAngle = ((target.endAngle - previous.endAngle) * animationPercent) + + previous.endAngle; + + color = getAnimatedColor(previous.color, target.color, animationPercent); + } +} + +class _AnimatedArcList { + final arcs = <_AnimatedArc>[]; + Point center; + double innerRadius; + double radius; + ImmutableSeries series; + + /// Color of separator lines between arcs. + Color stroke; + + /// Stroke width of separator lines between arcs. + double strokeWidthPx; +} + +class _AnimatedArc { + final String key; + dynamic datum; + D domain; + + ArcRendererElement _previousArc; + ArcRendererElement _targetArc; + ArcRendererElement _currentArc; + + // Flag indicating whether this arc is being animated out of the chart. + bool animatingOut = false; + + _AnimatedArc(this.key, this.datum, this.domain); + + /// Animates a arc that was removed from the series out of the view. + /// + /// This should be called in place of "setNewTarget" for arcs that represent + /// data that has been removed from the series. + /// + /// Animates the angle of the arc to [endAngle], in radians. + void animateOut(endAngle) { + var newTarget = _currentArc.clone(); + + // Animate the arc out by setting the angles to 0. + newTarget.startAngle = endAngle; + newTarget.endAngle = endAngle; + + setNewTarget(newTarget); + animatingOut = true; + } + + void setNewTarget(ArcRendererElement newTarget) { + animatingOut = false; + _currentArc ??= newTarget.clone(); + _previousArc = _currentArc.clone(); + _targetArc = newTarget; + } + + ArcRendererElement getCurrentArc(double animationPercent) { + if (animationPercent == 1.0 || _previousArc == null) { + _currentArc = _targetArc; + _previousArc = _targetArc; + return _currentArc; + } + + _currentArc.updateAnimationPercent( + _previousArc, _targetArc, animationPercent); + + return _currentArc; + } + + /// Returns the [startAngle] of the new target element, without updating + /// animation state. + double get newTargetArcStartAngle { + return _targetArc != null ? _targetArc.startAngle : null; + } + + /// Returns the [endAngle] of the new target element, without updating + /// animation state. + double get currentArcEndAngle { + return _currentArc != null ? _currentArc.endAngle : null; + } + + /// Returns the [startAngle] of the currently rendered element, without + /// updating animation state. + double get currentArcStartAngle { + return _currentArc != null ? _currentArc.startAngle : null; + } + + /// Returns the [endAngle] of the new target element, without updating + /// animation state. + double get previousArcEndAngle { + return _previousArc != null ? _previousArc.endAngle : null; + } + + /// Returns the [startAngle] of the previously rendered element, without + /// updating animation state. + double get previousArcStartAngle { + return _previousArc != null ? _previousArc.startAngle : null; + } +} diff --git a/web/charts/common/lib/src/chart/pie/arc_renderer_config.dart b/web/charts/common/lib/src/chart/pie/arc_renderer_config.dart new file mode 100644 index 000000000..ac98034b3 --- /dev/null +++ b/web/charts/common/lib/src/chart/pie/arc_renderer_config.dart @@ -0,0 +1,94 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show pi; + +import '../../common/color.dart' show Color; +import '../../common/style/style_factory.dart' show StyleFactory; +import '../../common/symbol_renderer.dart'; +import '../common/series_renderer_config.dart' + show RendererAttributes, SeriesRendererConfig; +import '../layout/layout_view.dart' show LayoutViewConfig, LayoutViewPaintOrder; +import 'arc_renderer.dart' show ArcRenderer; +import 'arc_renderer_decorator.dart' show ArcRendererDecorator; + +/// Configuration for an [ArcRenderer]. +class ArcRendererConfig extends LayoutViewConfig + implements SeriesRendererConfig { + final String customRendererId; + + /// List of decorators applied to rendered arcs. + final List arcRendererDecorators; + + final SymbolRenderer symbolRenderer; + + final rendererAttributes = new RendererAttributes(); + + /// Total arc length, in radians. + /// + /// The default arcLength is 2π. + final double arcLength; + + /// If set, configures the arcWidth to be a percentage of the radius. + final double arcRatio; + + /// Fixed width of the arc within the radius. + /// + /// If arcRatio is set, this value will be ignored. + final int arcWidth; + + /// The order to paint this renderer on the canvas. + final int layoutPaintOrder; + + /// Minimum radius in pixels of the hole in a donut chart for center content + /// to appear. + final int minHoleWidthForCenterContent; + + /// Start angle for pie slices, in radians. + /// + /// Angles are defined from the positive x axis in Cartesian space. The + /// default startAngle is -π/2. + final double startAngle; + + /// Stroke width of the border of the arcs. + final double strokeWidthPx; + + /// Stroke color of the border of the arcs. + final Color stroke; + + /// Color of the "no data" state for the chart, used when an empty series is + /// drawn. + final Color noDataColor; + + ArcRendererConfig( + {this.customRendererId, + this.arcLength = 2 * pi, + this.arcRendererDecorators = const [], + this.arcRatio, + this.arcWidth, + this.layoutPaintOrder = LayoutViewPaintOrder.arc, + this.minHoleWidthForCenterContent = 30, + this.startAngle = -pi / 2, + this.strokeWidthPx = 2.0, + SymbolRenderer symbolRenderer}) + : this.noDataColor = StyleFactory.style.noDataColor, + this.stroke = StyleFactory.style.white, + this.symbolRenderer = symbolRenderer ?? new CircleSymbolRenderer(); + + @override + ArcRenderer build() { + return new ArcRenderer(config: this, rendererId: customRendererId); + } +} diff --git a/web/charts/common/lib/src/chart/pie/arc_renderer_decorator.dart b/web/charts/common/lib/src/chart/pie/arc_renderer_decorator.dart new file mode 100644 index 000000000..4d2286d3d --- /dev/null +++ b/web/charts/common/lib/src/chart/pie/arc_renderer_decorator.dart @@ -0,0 +1,37 @@ +// 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 'arc_renderer.dart' show ArcRendererElementList; + +/// Decorates arcs after the arcs have already been painted. +abstract class ArcRendererDecorator { + const ArcRendererDecorator(); + + /// Configures whether the decorator should be rendered on top of or below + /// series data elements. + bool get renderAbove; + + void decorate(ArcRendererElementList arcElements, ChartCanvas canvas, + GraphicsFactory graphicsFactory, + {@required Rectangle drawBounds, + @required double animationPercent, + bool rtl = false}); +} diff --git a/web/charts/common/lib/src/chart/pie/pie_chart.dart b/web/charts/common/lib/src/chart/pie/pie_chart.dart new file mode 100644 index 000000000..665d92a7f --- /dev/null +++ b/web/charts/common/lib/src/chart/pie/pie_chart.dart @@ -0,0 +1,84 @@ +// 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 '../common/base_chart.dart' show BaseChart; +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 rendererIdKey, SeriesRenderer; +import '../layout/layout_config.dart' show LayoutConfig, MarginSpec; +import 'arc_renderer.dart' show ArcRenderer; + +class PieChart extends BaseChart { + 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), + ); + + PieChart({LayoutConfig layoutConfig}) + : super(layoutConfig: layoutConfig ?? _defaultLayoutConfig); + + @override + void drawInternal(List> seriesList, + {bool skipAnimation, bool skipLayout}) { + if (seriesList.length > 1) { + throw new ArgumentError('PieChart can only render a single series'); + } + super.drawInternal(seriesList, + skipAnimation: skipAnimation, skipLayout: skipLayout); + } + + @override + SeriesRenderer makeDefaultRenderer() { + return new ArcRenderer()..rendererId = SeriesRenderer.defaultRendererId; + } + + /// Returns a list of datum details from selection model of [type]. + @override + List> getDatumDetails(SelectionModelType type) { + final entries = >[]; + + getSelectionModel(type).selectedDatum.forEach((seriesDatum) { + final rendererId = seriesDatum.series.getAttr(rendererIdKey); + final renderer = getSeriesRenderer(rendererId); + + // This should never happen. + if (!(renderer is ArcRenderer)) { + return; + } + + final details = + (renderer as ArcRenderer).getExpandedDatumDetails(seriesDatum); + + if (details != null) { + entries.add(details); + } + }); + + return entries; + } + + Rectangle get centerContentBounds { + if (defaultRenderer is ArcRenderer) { + return (defaultRenderer as ArcRenderer).centerContentBounds; + } else { + return null; + } + } +} diff --git a/web/charts/common/lib/src/chart/scatter_plot/comparison_points_decorator.dart b/web/charts/common/lib/src/chart/scatter_plot/comparison_points_decorator.dart new file mode 100644 index 000000000..848fa53d1 --- /dev/null +++ b/web/charts/common/lib/src/chart/scatter_plot/comparison_points_decorator.dart @@ -0,0 +1,241 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point, Rectangle; + +import 'package:meta/meta.dart' show protected, required; + +import '../../common/graphics_factory.dart' show GraphicsFactory; +import '../../common/symbol_renderer.dart'; +import '../common/chart_canvas.dart' show ChartCanvas; +import 'point_renderer.dart' show PointRendererElement; +import 'point_renderer_decorator.dart' show PointRendererDecorator; + +/// Decorates a point chart by drawing a shape connecting the domain and measure +/// data bounds. +/// +/// The line will connect the point (domainLowerBound, measureLowerBound) to the +/// point (domainUpperBound, measureUpperBound). +class ComparisonPointsDecorator extends PointRendererDecorator { + /// Renderer used to draw the points. Defaults to a line with circular end + /// caps. + final PointSymbolRenderer symbolRenderer; + + /// Render the bounds shape underneath series data. + final bool renderAbove = false; + + ComparisonPointsDecorator({PointSymbolRenderer symbolRenderer}) + : this.symbolRenderer = symbolRenderer ?? new CylinderSymbolRenderer(); + + @override + void decorate(PointRendererElement pointElement, ChartCanvas canvas, + GraphicsFactory graphicsFactory, + {@required Rectangle drawBounds, + @required double animationPercent, + bool rtl = false}) { + final points = computeBoundedPointsForElement(pointElement, drawBounds); + + if (points == null) { + return; + } + + final color = pointElement.color.lighter; + + symbolRenderer.paint(canvas, points[0], pointElement.boundsLineRadiusPx, + fillColor: color, strokeColor: color, p2: points[1]); + } + + /// Computes end points for the [pointElement]'s lower and upper data bounds. + /// + /// This will compute two points representing the end points of the symbol, + /// from (xLower, yLower) to (xUpper, yUpper). The end points will be clamped + /// along the line so that it is fully contained within [drawBounds]. + /// + /// Returns null if [pointElement] is missing any of the data bounds, or if + /// the line connecting them is located entirely outside of [drawBounds]. + @protected + List> computeBoundedPointsForElement( + PointRendererElement pointElement, Rectangle drawBounds) { + // All bounds points must be defined for a valid comparison point to be + // drawn. + if (pointElement.point.xLower == null || + pointElement.point.xUpper == null || + pointElement.point.yLower == null || + pointElement.point.yUpper == null) { + return null; + } + + // Construct the points that describe our line p1p2. + var p1 = + new Point(pointElement.point.xLower, pointElement.point.yLower); + var p2 = + new Point(pointElement.point.xUpper, pointElement.point.yUpper); + + // First check to see if there is no intersection at all between the line + // p1p2 and [drawBounds]. + final dataBoundsRect = new Rectangle.fromPoints(p1, p2); + if (!drawBounds.intersects(dataBoundsRect)) { + return null; + } + + // Line with end points [p1] and [p2]. + final p1p2 = new _Line.fromPoints(p1, p2); + + // Next, slide p1 along the line p1p2 towards the edge of the draw area if + // the point is located outside of it. + if (!drawBounds.containsPoint(p1)) { + final p = _clampPointAlongLineToBoundingBox(p1, p1p2, drawBounds); + if (p != null) { + p1 = p; + } + } + + // Next, slide p2 along the line p1p2 towards the edge of the draw area if + // the point is located outside of it. + if (!drawBounds.containsPoint(p2)) { + final p = _clampPointAlongLineToBoundingBox(p2, p1p2, drawBounds); + if (p != null) { + p2 = p; + } + } + + return [p1, p2]; + } + + /// Slide the given point [p1] along the line [line], such that it intersects + /// the nearest edge of [bounds]. + /// + /// This method assumes that we have already verified that the [line] + /// intercepts the [bounds] somewhere. + Point _clampPointAlongLineToBoundingBox( + Point p1, _Line line, Rectangle bounds) { + // The top and bottom edges of the bounds box describe two horizontal lines, + // with equations y = bounds.top and y = bounds.bottom. We can pass these + // into a standard line interception method to find our point. + if (p1.y < bounds.top) { + final p = line.intersection(new _Line(0.0, bounds.top.toDouble())); + if (p != null && bounds.containsPoint(p)) { + return p; + } + } + + if (p1.y > bounds.bottom) { + final p = line.intersection(new _Line(0.0, bounds.bottom.toDouble())); + if (p != null && bounds.containsPoint(p)) { + return p; + } + } + + // The left and right edges of the bounds box describe two vertical lines, + // with equations x = bounds.right and x = bounds.left. To find the + // intersection, we just need to solve for y in our line described by + // [slope] and [yIntercept]: + // + // y = slope * x + yIntercept + if (p1.x < bounds.left) { + final p = + line.intersection(new _Line.fromVertical(bounds.left.toDouble())); + if (p != null && bounds.containsPoint(p)) { + return p; + } + } + + if (p1.x > bounds.right) { + final p = + line.intersection(new _Line.fromVertical(bounds.right.toDouble())); + if (p != null && bounds.containsPoint(p)) { + return p; + } + } + + return null; + } +} + +/// Describes a simple line with the equation y = slope * x + yIntercept. +class _Line { + /// Slope of the line. + double slope; + + /// y-intercept of the line (i.e. the y value of the point where the line + /// intercepts the y axis). + double yIntercept; + + /// x-intercept of the line (i.e. the x value of the point where the line + /// intercepts the x axis). This is normally only needed for vertical lines, + /// which have no slope. + double xIntercept; + + /// True if this line is a vertical line, of the form x = [xIntercept]. + bool get vertical => slope == null && xIntercept != null; + + _Line(this.slope, this.yIntercept, [this.xIntercept]); + + /// Creates a line with end points [p1] and [p2]. + factory _Line.fromPoints(Point p1, Point p2) { + // Handle vertical lines. + if (p1.x == p2.x) { + return new _Line.fromVertical(p1.x); + } + + // Slope of the line p1p2. + double m = ((p2.y - p1.y) / (p2.x - p1.x)).toDouble(); + + // y-intercept of the line p1p2. + double b = (p1.y - (m * p1.x)).toDouble(); + + return new _Line(m, b); + } + + /// Creates a vertical line, with the question x = [xIntercept]. + factory _Line.fromVertical(num xIntercept) { + return new _Line(null, null, xIntercept.toDouble()); + } + + /// Computes the intersection of `this` and [other]. + /// + /// Returns the intersection of this and `other`, or `null` if they don't + /// intersect. + Point intersection(_Line other) { + // Parallel lines have no intersection. + if (slope == other.slope || (vertical && other.vertical)) { + return null; + } + + // If the other line is a vertical line (has undefined slope), then we can + // just plug its xIntercept value into the line equation as x and solve for + // y. + if (other.vertical) { + return new Point( + other.xIntercept, slope * other.xIntercept + yIntercept); + } + + // If this line is a vertical line (has undefined slope), then we can just + // plug its xIntercept value into the line equation as x and solve for y. + if (vertical) { + return new Point( + xIntercept, other.slope * xIntercept + other.yIntercept); + } + + // Now that we know that we have intersecting, non-vertical lines, compute + // the intersection. + final x = (other.yIntercept - yIntercept) / (slope - other.slope); + + final y = slope * (other.yIntercept - yIntercept) / (slope - other.slope) + + yIntercept; + + return new Point(x, y); + } +} diff --git a/web/charts/common/lib/src/chart/scatter_plot/point_renderer.dart b/web/charts/common/lib/src/chart/scatter_plot/point_renderer.dart new file mode 100644 index 000000000..a25c4e8fd --- /dev/null +++ b/web/charts/common/lib/src/chart/scatter_plot/point_renderer.dart @@ -0,0 +1,866 @@ +// 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 min, Point, Rectangle; + +import 'package:meta/meta.dart' show protected, required; +import 'package:vector_math/vector_math.dart' show Vector2; + +import '../../common/color.dart' show Color; +import '../../common/math.dart' show distanceBetweenPointAndLineSegment; +import '../../common/symbol_renderer.dart' + show CircleSymbolRenderer, SymbolRenderer; +import '../../data/series.dart' show AccessorFn, AttributeKey, TypedAccessorFn; +import '../cartesian/axis/axis.dart' + show ImmutableAxis, domainAxisKey, measureAxisKey; +import '../cartesian/cartesian_renderer.dart' show BaseCartesianRenderer; +import '../common/base_chart.dart' show BaseChart; +import '../common/chart_canvas.dart' show ChartCanvas, getAnimatedColor; +import '../common/datum_details.dart' show DatumDetails; +import '../common/processed_series.dart' show ImmutableSeries, MutableSeries; +import '../common/series_datum.dart' show SeriesDatum; +import '../layout/layout_view.dart' show LayoutViewPaintOrder; +import 'comparison_points_decorator.dart' show ComparisonPointsDecorator; +import 'point_renderer_config.dart' show PointRendererConfig; +import 'point_renderer_decorator.dart' show PointRendererDecorator; + +const pointElementsKey = + const AttributeKey>('PointRenderer.elements'); + +const pointSymbolRendererFnKey = + const AttributeKey>('PointRenderer.symbolRendererFn'); + +const pointSymbolRendererIdKey = + const AttributeKey('PointRenderer.symbolRendererId'); + +/// Defines a fixed radius for data bounds lines (typically drawn by attaching a +/// [ComparisonPointsDecorator] to the renderer. +const boundsLineRadiusPxKey = + const AttributeKey('SymbolAnnotationRenderer.boundsLineRadiusPx'); + +/// Defines an [AccessorFn] for the radius for data bounds lines (typically +/// drawn by attaching a [ComparisonPointsDecorator] to the renderer. +const boundsLineRadiusPxFnKey = const AttributeKey>( + 'SymbolAnnotationRenderer.boundsLineRadiusPxFn'); + +const defaultSymbolRendererId = '__default__'; + +/// Large number used as a starting sentinel for data distance comparisons. +/// +/// This is generally larger than the distance from any datum to the mouse. +const _maxInitialDistance = 10000.0; + +class PointRenderer extends BaseCartesianRenderer { + final PointRendererConfig config; + + final List pointRendererDecorators; + + BaseChart _chart; + + /// 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. + @protected + var seriesPointMap = new LinkedHashMap>>(); + + // Store a list of lines 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 = []; + + PointRenderer({String rendererId, PointRendererConfig config}) + : this.config = config ?? new PointRendererConfig(), + pointRendererDecorators = config?.pointRendererDecorators ?? [], + super( + rendererId: rendererId ?? 'point', + layoutPaintOrder: + config?.layoutPaintOrder ?? LayoutViewPaintOrder.point, + symbolRenderer: + config?.symbolRenderer ?? new CircleSymbolRenderer()); + + @override + void configureSeries(List> seriesList) { + assignMissingColors(seriesList, emptyCategoryUsesSinglePalette: false); + } + + @override + void preprocessSeries(List> seriesList) { + seriesList.forEach((MutableSeries series) { + final elements = >[]; + + // Default to the configured radius if none was defined by the series. + series.radiusPxFn ??= (_) => config.radiusPx; + + // Create an accessor function for the bounds line radius, if needed. If + // the series doesn't define an accessor function, then each datum's + // boundsLineRadiusPx value will be filled in by using the following + // values, in order of what is defined: + // + // 1) boundsLineRadiusPx defined on the series. + // 2) boundsLineRadiusPx defined on the renderer config. + // 3) Final fallback is to use the point radiusPx for this datum. + var boundsLineRadiusPxFn = series.getAttr(boundsLineRadiusPxFnKey); + + if (boundsLineRadiusPxFn == null) { + var boundsLineRadiusPx = series.getAttr(boundsLineRadiusPxKey); + boundsLineRadiusPx ??= config.boundsLineRadiusPx; + if (boundsLineRadiusPx != null) { + boundsLineRadiusPxFn = (_) => boundsLineRadiusPx.toDouble(); + series.setAttr(boundsLineRadiusPxFnKey, boundsLineRadiusPxFn); + } + } + + final symbolRendererFn = series.getAttr(pointSymbolRendererFnKey); + + // Add a key function to help animate points moved in position in the + // series data between chart draw cycles. Ideally we should require the + // user to provide a key function, but this at least provides some + // smoothing when adding/removing data. + series.keyFn ??= + (int index) => '${series.id}__${series.domainFn(index)}__' + '${series.measureFn(index)}'; + + for (var index = 0; index < series.data.length; index++) { + // Default to the configured radius if none was returned by the + // accessor function. + var radiusPx = series.radiusPxFn(index); + radiusPx ??= config.radiusPx; + + num boundsLineRadiusPx; + if (boundsLineRadiusPxFn != null) { + boundsLineRadiusPx = (boundsLineRadiusPxFn is TypedAccessorFn) + ? (boundsLineRadiusPxFn as TypedAccessorFn)( + series.data[index], index) + : boundsLineRadiusPxFn(index); + } + boundsLineRadiusPx ??= config.boundsLineRadiusPx; + boundsLineRadiusPx ??= radiusPx; + + // Default to the configured stroke width if none was returned by the + // accessor function. + var strokeWidthPx = series.strokeWidthPxFn != null + ? series.strokeWidthPxFn(index) + : null; + strokeWidthPx ??= config.strokeWidthPx; + + // Get the ID of the [SymbolRenderer] for this point. An ID may be + // specified on the datum, or on the series. If neither is specified, + // fall back to the default. + String symbolRendererId; + if (symbolRendererFn != null) { + symbolRendererId = symbolRendererFn(index); + } + symbolRendererId ??= series.getAttr(pointSymbolRendererIdKey); + symbolRendererId ??= defaultSymbolRendererId; + + // Get the colors. If no fill color is provided, default it to the + // primary data color. + final colorFn = series.colorFn; + final fillColorFn = series.fillColorFn ?? colorFn; + + final color = colorFn(index); + + // Fill color is an optional override for color. Make sure we get a + // value if the series doesn't define anything specific. + var fillColor = fillColorFn(index); + fillColor ??= color; + + final details = new PointRendererElement() + ..color = color + ..fillColor = fillColor + ..radiusPx = radiusPx.toDouble() + ..boundsLineRadiusPx = boundsLineRadiusPx.toDouble() + ..strokeWidthPx = strokeWidthPx.toDouble() + ..symbolRendererId = symbolRendererId; + + elements.add(details); + } + + series.setAttr(pointElementsKey, elements); + }); + } + + void update(List> seriesList, bool isAnimatingThisDraw) { + _currentKeys.clear(); + + // Build a list of sorted series IDs as we iterate through the list, used + // later for sorting. + final sortedSeriesIds = []; + + seriesList.forEach((ImmutableSeries series) { + sortedSeriesIds.add(series.id); + + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final domainFn = series.domainFn; + final domainLowerBoundFn = series.domainLowerBoundFn; + final domainUpperBoundFn = series.domainUpperBoundFn; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + final measureFn = series.measureFn; + final measureLowerBoundFn = series.measureLowerBoundFn; + final measureUpperBoundFn = series.measureUpperBoundFn; + final measureOffsetFn = series.measureOffsetFn; + final seriesKey = series.id; + final keyFn = series.keyFn; + + var pointList = seriesPointMap.putIfAbsent(seriesKey, () => []); + + var elementsList = series.getAttr(pointElementsKey); + + for (var index = 0; index < series.data.length; index++) { + final datum = series.data[index]; + final details = elementsList[index]; + + D domainValue = domainFn(index); + D domainLowerBoundValue = + domainLowerBoundFn != null ? domainLowerBoundFn(index) : null; + D domainUpperBoundValue = + domainUpperBoundFn != null ? domainUpperBoundFn(index) : null; + + num measureValue = measureFn(index); + num measureLowerBoundValue = + measureLowerBoundFn != null ? measureLowerBoundFn(index) : null; + num measureUpperBoundValue = + measureUpperBoundFn != null ? measureUpperBoundFn(index) : null; + num measureOffsetValue = measureOffsetFn(index); + + // Create a new point using the final location. + final point = getPoint( + datum, + domainValue, + domainLowerBoundValue, + domainUpperBoundValue, + series, + domainAxis, + measureValue, + measureLowerBoundValue, + measureUpperBoundValue, + measureOffsetValue, + measureAxis); + + final pointKey = keyFn(index); + + // If we already have an AnimatingPoint for that index, use it. + var animatingPoint = pointList.firstWhere( + (AnimatedPoint point) => point.key == pointKey, + orElse: () => null); + + // If we don't have any existing arc element, create a new arc and + // have it animate in from the position of the previous arc's end + // angle. If there were no previous arcs, then animate everything in + // from 0. + if (animatingPoint == null) { + // Create a new point and have it animate in from axis. + final point = getPoint( + datum, + domainValue, + domainLowerBoundValue, + domainUpperBoundValue, + series, + domainAxis, + 0.0, + 0.0, + 0.0, + 0.0, + measureAxis); + + animatingPoint = new AnimatedPoint( + key: pointKey, overlaySeries: series.overlaySeries) + ..setNewTarget(new PointRendererElement() + ..color = details.color + ..fillColor = details.fillColor + ..measureAxisPosition = measureAxis.getLocation(0.0) + ..point = point + ..radiusPx = details.radiusPx + ..boundsLineRadiusPx = details.boundsLineRadiusPx + ..strokeWidthPx = details.strokeWidthPx + ..symbolRendererId = details.symbolRendererId); + + pointList.add(animatingPoint); + } + + // Update the set of arcs that still exist in the series data. + _currentKeys.add(pointKey); + + // Get the pointElement we are going to setup. + final pointElement = new PointRendererElement() + ..color = details.color + ..fillColor = details.fillColor + ..measureAxisPosition = measureAxis.getLocation(0.0) + ..point = point + ..radiusPx = details.radiusPx + ..boundsLineRadiusPx = details.boundsLineRadiusPx + ..strokeWidthPx = details.strokeWidthPx + ..symbolRendererId = details.symbolRendererId; + + animatingPoint.setNewTarget(pointElement); + } + }); + + // Sort the renderer elements to be in the same order as the series list. + // They may get disordered between chart draw cycles if a behavior adds or + // removes series from the list (e.g. click to hide on legends). + seriesPointMap = new LinkedHashMap.fromIterable(sortedSeriesIds, + key: (k) => k, value: (k) => seriesPointMap[k]); + + // Animate out points that don't exist anymore. + seriesPointMap.forEach((String key, List> points) { + for (var point in points) { + if (_currentKeys.contains(point.key) != true) { + point.animateOut(); + } + } + }); + } + + @override + void onAttach(BaseChart 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. + _chart = chart; + } + + void paint(ChartCanvas canvas, double animationPercent) { + // Clean up the points that no longer exist. + if (animationPercent == 1.0) { + final keysToRemove = []; + + seriesPointMap.forEach((String key, List> points) { + points.removeWhere((AnimatedPoint point) => point.animatingOut); + + if (points.isEmpty) { + keysToRemove.add(key); + } + }); + + keysToRemove.forEach((String key) => seriesPointMap.remove(key)); + } + + seriesPointMap.forEach((String key, List> points) { + points + .map>((AnimatedPoint animatingPoint) => + animatingPoint.getCurrentPoint(animationPercent)) + .forEach((PointRendererElement point) { + // Decorate the points with decorators that should appear below the main + // series data. + pointRendererDecorators + .where((PointRendererDecorator decorator) => !decorator.renderAbove) + .forEach((PointRendererDecorator decorator) { + decorator.decorate(point, canvas, graphicsFactory, + drawBounds: componentBounds, + animationPercent: animationPercent, + rtl: isRtl); + }); + + // Skip points whose center lies outside the draw bounds. Those that lie + // near the edge will be allowed to render partially outside. This + // prevents harshly clipping off half of the shape. + if (point.point.y != null && + componentBounds.containsPoint(point.point)) { + final bounds = new Rectangle( + point.point.x - point.radiusPx, + point.point.y - point.radiusPx, + point.radiusPx * 2, + point.radiusPx * 2); + + if (point.symbolRendererId == defaultSymbolRendererId) { + symbolRenderer.paint(canvas, bounds, + fillColor: point.fillColor, + strokeColor: point.color, + strokeWidthPx: point.strokeWidthPx); + } else { + final id = point.symbolRendererId; + if (!config.customSymbolRenderers.containsKey(id)) { + throw new ArgumentError( + 'Invalid custom symbol renderer id "${id}"'); + } + + final customRenderer = config.customSymbolRenderers[id]; + customRenderer.paint(canvas, bounds, + fillColor: point.fillColor, + strokeColor: point.color, + strokeWidthPx: point.strokeWidthPx); + } + } + + // Decorate the points with decorators that should appear above the main + // series data. This is the typical place for labels. + pointRendererDecorators + .where((PointRendererDecorator decorator) => decorator.renderAbove) + .forEach((PointRendererDecorator decorator) { + decorator.decorate(point, canvas, graphicsFactory, + drawBounds: componentBounds, + animationPercent: animationPercent, + rtl: isRtl); + }); + }); + }); + } + + bool get isRtl => _chart?.context?.isRtl ?? false; + + @protected + DatumPoint getPoint( + final datum, + D domainValue, + D domainLowerBoundValue, + D domainUpperBoundValue, + ImmutableSeries series, + ImmutableAxis domainAxis, + num measureValue, + num measureLowerBoundValue, + num measureUpperBoundValue, + num measureOffsetValue, + ImmutableAxis measureAxis) { + final domainPosition = domainAxis.getLocation(domainValue); + + final domainLowerBoundPosition = domainLowerBoundValue != null + ? domainAxis.getLocation(domainLowerBoundValue) + : null; + + final domainUpperBoundPosition = domainUpperBoundValue != null + ? domainAxis.getLocation(domainUpperBoundValue) + : null; + + final measurePosition = + measureAxis.getLocation(measureValue + measureOffsetValue); + + final measureLowerBoundPosition = measureLowerBoundValue != null + ? measureAxis.getLocation(measureLowerBoundValue + measureOffsetValue) + : null; + + final measureUpperBoundPosition = measureUpperBoundValue != null + ? measureAxis.getLocation(measureUpperBoundValue + measureOffsetValue) + : null; + + return new DatumPoint( + datum: datum, + domain: domainValue, + series: series, + x: domainPosition, + xLower: domainLowerBoundPosition, + xUpper: domainUpperBoundPosition, + y: measurePosition, + yLower: measureLowerBoundPosition, + yUpper: measureUpperBoundPosition); + } + + @override + List> getNearestDatumDetailPerSeries( + Point chartPoint, bool byDomain, Rectangle boundsOverride) { + final nearest = >[]; + + // Was it even in the component bounds? + if (!isPointWithinBounds(chartPoint, boundsOverride)) { + return nearest; + } + + seriesPointMap.values.forEach((List> points) { + PointRendererElement nearestPoint; + double nearestDomainDistance = _maxInitialDistance; + double nearestMeasureDistance = _maxInitialDistance; + double nearestRelativeDistance = _maxInitialDistance; + + points.forEach((AnimatedPoint point) { + if (point.overlaySeries) { + return; + } + + Point p = point._currentPoint.point; + + // Don't look at points not in the drawArea. + if (p.x < componentBounds.left || p.x > componentBounds.right) { + return; + } + + final distances = _getDatumDistance(point, chartPoint); + + if (byDomain) { + if ((distances.domainDistance < nearestDomainDistance) || + ((distances.domainDistance == nearestDomainDistance && + distances.measureDistance < nearestMeasureDistance))) { + nearestPoint = point._currentPoint; + nearestDomainDistance = distances.domainDistance; + nearestMeasureDistance = distances.measureDistance; + nearestRelativeDistance = distances.relativeDistance; + } + } else { + if (distances.relativeDistance < nearestRelativeDistance) { + nearestPoint = point._currentPoint; + nearestDomainDistance = distances.domainDistance; + nearestMeasureDistance = distances.measureDistance; + nearestRelativeDistance = distances.relativeDistance; + } + } + }); + + // Found a point, add it to the list. + if (nearestPoint != null) { + SymbolRenderer nearestSymbolRenderer; + if (nearestPoint.symbolRendererId == defaultSymbolRendererId) { + nearestSymbolRenderer = symbolRenderer; + } else { + final id = nearestPoint.symbolRendererId; + if (!config.customSymbolRenderers.containsKey(id)) { + throw new ArgumentError( + 'Invalid custom symbol renderer id "${id}"'); + } + + nearestSymbolRenderer = config.customSymbolRenderers[id]; + } + + nearest.add(new DatumDetails( + datum: nearestPoint.point.datum, + domain: nearestPoint.point.domain, + series: nearestPoint.point.series, + domainDistance: nearestDomainDistance, + measureDistance: nearestMeasureDistance, + relativeDistance: nearestRelativeDistance, + symbolRenderer: nearestSymbolRenderer)); + } + }); + + // Note: the details are already sorted by domain & measure distance in + // base chart. + + return nearest; + } + + /// Returns a struct containing domain, measure, and relative distance between + /// a datum and a point within the chart. + _Distances _getDatumDistance( + AnimatedPoint point, Point chartPoint) { + final datumPoint = point._currentPoint.point; + final radiusPx = point._currentPoint.radiusPx; + final boundsLineRadiusPx = point._currentPoint.boundsLineRadiusPx; + + // Compute distances from [chartPoint] to the primary point of the datum. + final domainDistance = (chartPoint.x - datumPoint.x).abs(); + + final measureDistance = datumPoint.y != null + ? (chartPoint.y - datumPoint.y).abs() + : _maxInitialDistance; + + var relativeDistance = datumPoint.y != null + ? chartPoint.distanceTo(datumPoint) + : _maxInitialDistance; + + var insidePoint = false; + + if (datumPoint.xLower != null && + datumPoint.xUpper != null && + datumPoint.yLower != null && + datumPoint.yUpper != null) { + // If we have data bounds, compute the relative distance between + // [chartPoint] and the nearest point of the data bounds element. We will + // use the smaller of this distance and the distance from the primary + // point as the relativeDistance from this datum. + final num relativeDistanceBounds = distanceBetweenPointAndLineSegment( + new Vector2(chartPoint.x, chartPoint.y), + new Vector2(datumPoint.xLower, datumPoint.yLower), + new Vector2(datumPoint.xUpper, datumPoint.yUpper)); + + insidePoint = (relativeDistance < radiusPx) || + (boundsLineRadiusPx != null && + // This may be inaccurate if the symbol is drawn without end caps. + relativeDistanceBounds < boundsLineRadiusPx); + + // Keep the smaller relative distance after we have determined whether + // [chartPoint] is located inside the datum. + relativeDistance = min(relativeDistance, relativeDistanceBounds); + } else { + insidePoint = (relativeDistance < radiusPx); + } + + return new _Distances( + domainDistance: domainDistance, + measureDistance: measureDistance, + relativeDistance: relativeDistance, + insidePoint: insidePoint, + ); + } + + DatumDetails addPositionToDetailsForSeriesDatum( + DatumDetails details, SeriesDatum seriesDatum) { + final series = details.series; + + final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis; + final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis; + + final point = getPoint( + seriesDatum.datum, + details.domain, + details.domainLowerBound, + details.domainUpperBound, + series, + domainAxis, + details.measure, + details.measureLowerBound, + details.measureUpperBound, + details.measureOffset, + measureAxis); + + final symbolRendererFn = series.getAttr(pointSymbolRendererFnKey); + + // Get the ID of the [SymbolRenderer] for this point. An ID may be + // specified on the datum, or on the series. If neither is specified, + // fall back to the default. + String symbolRendererId; + if (symbolRendererFn != null) { + symbolRendererId = symbolRendererFn(details.index); + } + symbolRendererId ??= series.getAttr(pointSymbolRendererIdKey); + symbolRendererId ??= defaultSymbolRendererId; + + // Now that we have the ID, get the configured [SymbolRenderer]. + SymbolRenderer nearestSymbolRenderer; + if (symbolRendererId == defaultSymbolRendererId) { + nearestSymbolRenderer = symbolRenderer; + } else { + final id = symbolRendererId; + if (!config.customSymbolRenderers.containsKey(id)) { + throw new ArgumentError('Invalid custom symbol renderer id "${id}"'); + } + + nearestSymbolRenderer = config.customSymbolRenderers[id]; + } + + return new DatumDetails.from(details, + chartPosition: new Point(point.x, point.y), + chartPositionLower: new Point(point.xLower, point.yLower), + chartPositionUpper: new Point(point.xUpper, point.yUpper), + symbolRenderer: nearestSymbolRenderer); + } +} + +class DatumPoint extends Point { + final Object datum; + final D domain; + final ImmutableSeries series; + + // Coordinates for domain bounds. + final double xLower; + final double xUpper; + + // Coordinates for measure bounds. + final double yLower; + final double yUpper; + + DatumPoint( + {this.datum, + this.domain, + this.series, + double x, + this.xLower, + this.xUpper, + double y, + this.yLower, + this.yUpper}) + : super(x, y); + + factory DatumPoint.from(DatumPoint other, + {double x, + double xLower, + double xUpper, + double y, + double yLower, + double yUpper}) { + return new DatumPoint( + datum: other.datum, + domain: other.domain, + series: other.series, + x: x ?? other.x, + xLower: xLower ?? other.xLower, + xUpper: xUpper ?? other.xUpper, + y: y ?? other.y, + yLower: yLower ?? other.yLower, + yUpper: yUpper ?? other.yUpper); + } +} + +class PointRendererElement { + DatumPoint point; + Color color; + Color fillColor; + double measureAxisPosition; + double radiusPx; + double boundsLineRadiusPx; + double strokeWidthPx; + String symbolRendererId; + + PointRendererElement clone() { + return new PointRendererElement() + ..point = new DatumPoint.from(point) + ..color = color != null ? new Color.fromOther(color: color) : null + ..fillColor = + fillColor != null ? new Color.fromOther(color: fillColor) : null + ..measureAxisPosition = measureAxisPosition + ..radiusPx = radiusPx + ..boundsLineRadiusPx = boundsLineRadiusPx + ..strokeWidthPx = strokeWidthPx + ..symbolRendererId = symbolRendererId; + } + + void updateAnimationPercent(PointRendererElement previous, + PointRendererElement target, double animationPercent) { + final targetPoint = target.point; + final previousPoint = previous.point; + + final x = ((targetPoint.x - previousPoint.x) * animationPercent) + + previousPoint.x; + + final xLower = targetPoint.xLower != null && previousPoint.xLower != null + ? ((targetPoint.xLower - previousPoint.xLower) * animationPercent) + + previousPoint.xLower + : null; + + final xUpper = targetPoint.xUpper != null && previousPoint.xUpper != null + ? ((targetPoint.xUpper - previousPoint.xUpper) * animationPercent) + + previousPoint.xUpper + : null; + + double y; + if (targetPoint.y != null && previousPoint.y != null) { + y = ((targetPoint.y - previousPoint.y) * animationPercent) + + previousPoint.y; + } else if (targetPoint.y != null) { + y = targetPoint.y; + } else { + y = null; + } + + final yLower = targetPoint.yLower != null && previousPoint.yLower != null + ? ((targetPoint.yLower - previousPoint.yLower) * animationPercent) + + previousPoint.yLower + : null; + + final yUpper = targetPoint.yUpper != null && previousPoint.yUpper != null + ? ((targetPoint.yUpper - previousPoint.yUpper) * animationPercent) + + previousPoint.yUpper + : null; + + point = new DatumPoint.from(targetPoint, + x: x, + xLower: xLower, + xUpper: xUpper, + y: y, + yLower: yLower, + yUpper: yUpper); + + color = getAnimatedColor(previous.color, target.color, animationPercent); + + fillColor = getAnimatedColor( + previous.fillColor, target.fillColor, animationPercent); + + radiusPx = (((target.radiusPx - previous.radiusPx) * animationPercent) + + previous.radiusPx); + + boundsLineRadiusPx = + (((target.boundsLineRadiusPx - previous.boundsLineRadiusPx) * + animationPercent) + + previous.boundsLineRadiusPx); + + strokeWidthPx = + (((target.strokeWidthPx - previous.strokeWidthPx) * animationPercent) + + previous.strokeWidthPx); + } +} + +class AnimatedPoint { + final String key; + final bool overlaySeries; + + PointRendererElement _previousPoint; + PointRendererElement _targetPoint; + PointRendererElement _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() { + var newTarget = _currentPoint.clone(); + + // Set the target measure value to the axis position. + var targetPoint = newTarget.point; + newTarget.point = new DatumPoint.from(targetPoint, + x: targetPoint.x, + y: newTarget.measureAxisPosition.roundToDouble(), + yLower: newTarget.measureAxisPosition.roundToDouble(), + yUpper: newTarget.measureAxisPosition.roundToDouble()); + + // Animate the radius and stroke width to 0 so that we don't get a lingering + // point after animation is done. + newTarget.radiusPx = 0.0; + newTarget.strokeWidthPx = 0.0; + + setNewTarget(newTarget); + animatingOut = true; + } + + void setNewTarget(PointRendererElement newTarget) { + animatingOut = false; + _currentPoint ??= newTarget.clone(); + _previousPoint = _currentPoint.clone(); + _targetPoint = newTarget; + } + + PointRendererElement getCurrentPoint(double animationPercent) { + if (animationPercent == 1.0 || _previousPoint == null) { + _currentPoint = _targetPoint; + _previousPoint = _targetPoint; + return _currentPoint; + } + + _currentPoint.updateAnimationPercent( + _previousPoint, _targetPoint, animationPercent); + + return _currentPoint; + } +} + +/// Struct of distances between a datum and a point in the chart. +class _Distances { + /// Distance between two points along the domain axis. + final double domainDistance; + + /// Distance between two points along the measure axis. + final double measureDistance; + + /// Cartesian distance between the two points. + final double relativeDistance; + + /// Whether or not the point was located inside the datum. + final bool insidePoint; + + _Distances( + {this.domainDistance, + this.measureDistance, + this.relativeDistance, + this.insidePoint}); +} diff --git a/web/charts/common/lib/src/chart/scatter_plot/point_renderer_config.dart b/web/charts/common/lib/src/chart/scatter_plot/point_renderer_config.dart new file mode 100644 index 000000000..9729306a0 --- /dev/null +++ b/web/charts/common/lib/src/chart/scatter_plot/point_renderer_config.dart @@ -0,0 +1,80 @@ +// 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/series_renderer_config.dart' + show RendererAttributes, SeriesRendererConfig; +import '../layout/layout_view.dart' show LayoutViewConfig, LayoutViewPaintOrder; +import 'point_renderer.dart' show PointRenderer, pointSymbolRendererIdKey; +import 'point_renderer_decorator.dart' show PointRendererDecorator; + +/// Configuration for a line renderer. +class PointRendererConfig extends LayoutViewConfig + implements SeriesRendererConfig { + final String customRendererId; + + /// The order to paint this renderer on the canvas. + final int layoutPaintOrder; + + /// List of decorators applied to rendered points. + final List pointRendererDecorators; + + /// Renderer used to draw the points. Defaults to a circle. + final SymbolRenderer symbolRenderer; + + /// Map of custom symbol renderers used to draw points. + /// + /// Each series or point can be associated with a custom renderer by + /// specifying a [pointSymbolRendererIdKey] matching a key in the map. Any + /// point that doesn't define one will fall back to the default + /// [symbolRenderer]. + final Map customSymbolRenderers; + + final rendererAttributes = new RendererAttributes(); + + /// Default radius of the points, used if a series does not define a radiusPx + /// accessor function. + final double radiusPx; + + /// Stroke width of the target line. + final double strokeWidthPx; + + /// Optional default radius of data bounds lines, used if a series does not + /// define a boundsLineRadiusPx accessor function. + /// + /// If the series does not define a boundsLineRadiusPx accessor function, then + /// each datum's boundsLineRadiusPx value will be filled in by using the + /// following values, in order of what is defined: + /// + /// 1) boundsLineRadiusPx property defined on the series. + /// 2) boundsLineRadiusPx property defined on this renderer config. + /// 3) Final fallback is to use the point radiusPx for the datum. + final double boundsLineRadiusPx; + + PointRendererConfig( + {this.customRendererId, + this.layoutPaintOrder = LayoutViewPaintOrder.point, + this.pointRendererDecorators = const [], + this.radiusPx = 3.5, + this.boundsLineRadiusPx, + this.strokeWidthPx = 0.0, + this.symbolRenderer, + this.customSymbolRenderers}); + + @override + PointRenderer build() { + return new PointRenderer(config: this, rendererId: customRendererId); + } +} diff --git a/web/charts/common/lib/src/chart/scatter_plot/point_renderer_decorator.dart b/web/charts/common/lib/src/chart/scatter_plot/point_renderer_decorator.dart new file mode 100644 index 000000000..b9a4e415c --- /dev/null +++ b/web/charts/common/lib/src/chart/scatter_plot/point_renderer_decorator.dart @@ -0,0 +1,37 @@ +// 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 'point_renderer.dart' show PointRendererElement; + +/// Decorates points after the points have already been painted. +abstract class PointRendererDecorator { + const PointRendererDecorator(); + + /// Configures whether the decorator should be rendered on top of or below + /// series data elements. + bool get renderAbove; + + void decorate(PointRendererElement pointElement, ChartCanvas canvas, + GraphicsFactory graphicsFactory, + {@required Rectangle drawBounds, + @required double animationPercent, + bool rtl = false}); +} diff --git a/web/charts/common/lib/src/chart/scatter_plot/scatter_plot_chart.dart b/web/charts/common/lib/src/chart/scatter_plot/scatter_plot_chart.dart new file mode 100644 index 000000000..d0e0aec23 --- /dev/null +++ b/web/charts/common/lib/src/chart/scatter_plot/scatter_plot_chart.dart @@ -0,0 +1,67 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection' show LinkedHashMap; + +import '../cartesian/axis/axis.dart' show NumericAxis; +import '../cartesian/axis/draw_strategy/gridline_draw_strategy.dart' + show GridlineRendererSpec; +import '../cartesian/cartesian_chart.dart' show NumericCartesianChart; +import '../common/series_renderer.dart' show SeriesRenderer; +import '../layout/layout_config.dart' show LayoutConfig; +import 'point_renderer.dart' show PointRenderer; + +/// A scatter plot draws series data as a collection of points in a two +/// dimensional Cartesian space, plotting two variables from each datum at a +/// point represented by (domain, measure). +/// +/// A third and fourth metric can be represented by configuring the color and +/// radius of each datum. +/// +/// Scatter plots render grid lines along both the domain and measure axes by +/// default. +class ScatterPlotChart extends NumericCartesianChart { + /// Select data by relative Cartesian distance. Scatter plots draw potentially + /// overlapping data in an arbitrary (x, y) space, and do not consider the + /// domain axis to be more or less important for data selection than the + /// measure axis. + @override + bool get selectNearestByDomain => false; + + ScatterPlotChart( + {bool vertical, + LayoutConfig layoutConfig, + NumericAxis primaryMeasureAxis, + NumericAxis secondaryMeasureAxis, + LinkedHashMap disjointMeasureAxes}) + : super( + vertical: vertical, + layoutConfig: layoutConfig, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes); + + @override + SeriesRenderer makeDefaultRenderer() { + return new PointRenderer() + ..rendererId = SeriesRenderer.defaultRendererId; + } + + @override + void initDomainAxis() { + domainAxis.tickDrawStrategy = new GridlineRendererSpec() + .createDrawStrategy(context, graphicsFactory); + } +} diff --git a/web/charts/common/lib/src/chart/scatter_plot/symbol_annotation_renderer.dart b/web/charts/common/lib/src/chart/scatter_plot/symbol_annotation_renderer.dart new file mode 100644 index 000000000..aeae87b3d --- /dev/null +++ b/web/charts/common/lib/src/chart/scatter_plot/symbol_annotation_renderer.dart @@ -0,0 +1,271 @@ +// 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, Rectangle; + +import 'package:meta/meta.dart' show required; + +import '../../common/graphics_factory.dart' show GraphicsFactory; +import '../cartesian/axis/axis.dart' show ImmutableAxis; +import '../cartesian/cartesian_chart.dart' show CartesianChart; +import '../common/base_chart.dart' show BaseChart; +import '../common/chart_canvas.dart' show ChartCanvas; +import '../common/processed_series.dart' show ImmutableSeries, MutableSeries; +import '../layout/layout_view.dart' + show + LayoutPosition, + LayoutView, + LayoutViewConfig, + LayoutViewPaintOrder, + LayoutViewPositionOrder, + ViewMeasuredSizes; +import 'point_renderer.dart' show AnimatedPoint, DatumPoint, PointRenderer; +import 'symbol_annotation_renderer_config.dart' + show SymbolAnnotationRendererConfig; + +/// Series renderer which draws a row of symbols for each series below the +/// drawArea but above the bottom axis. +/// +/// This renderer can draw point annotations and range annotations. Point +/// annotations are drawn at the location of the domain along the chart's domain +/// axis, in the row for its series. Range annotations are drawn as a range +/// shape between the domainLowerBound and domainUpperBound positions along the +/// chart's domain axis. Point annotations are drawn on top of range +/// annotations. +/// +/// Limitations: +/// Does not handle horizontal bars. +class SymbolAnnotationRenderer extends PointRenderer + implements LayoutView { + Rectangle _componentBounds; + GraphicsFactory _graphicsFactory; + + CartesianChart _chart; + + var _currentHeight = 0; + + final _seriesInfo = new LinkedHashMap>(); + + SymbolAnnotationRenderer( + {String rendererId, SymbolAnnotationRendererConfig config}) + : super(rendererId: rendererId ?? 'symbolAnnotation', config: config); + + // + // Renderer methods + // + /// Symbol annotations do not use any measure axes, or draw anything in the + /// main draw area associated with them. + @override + void configureMeasureAxes(List> seriesList) {} + + @override + void preprocessSeries(List> seriesList) { + var localConfig = (config as SymbolAnnotationRendererConfig); + + _seriesInfo.clear(); + + double offset = 0.0; + + seriesList.forEach((MutableSeries series) { + final seriesKey = series.id; + + // Default to the configured radius if none was defined by the series. + series.radiusPxFn ??= (_) => config.radiusPx; + + var maxRadius = 0.0; + for (var index = 0; index < series.data.length; index++) { + // Default to the configured radius if none was returned by the + // accessor function. + var radiusPx = series.radiusPxFn(index); + radiusPx ??= config.radiusPx; + + maxRadius = max(maxRadius, radiusPx); + } + + final rowInnerHeight = maxRadius * 2; + + final rowHeight = localConfig.verticalSymbolBottomPaddingPx + + localConfig.verticalSymbolTopPaddingPx + + rowInnerHeight; + + final symbolCenter = offset + + localConfig.verticalSymbolTopPaddingPx + + (rowInnerHeight / 2); + + series.measureFn = (int index) => 0; + series.measureOffsetFn = (int index) => 0; + + // Override the key function to allow for range annotations that start at + // the same point. This is a necessary hack because every annotation has a + // measure value of 0, so the key generated in [PointRenderer] is not + // unique enough. + series.keyFn ??= + (int index) => '${series.id}__${series.domainFn(index)}__' + '${series.domainLowerBoundFn(index)}__' + '${series.domainUpperBoundFn(index)}'; + + _seriesInfo[seriesKey] = new _SeriesInfo( + rowHeight: rowHeight, + rowStart: offset, + symbolCenter: symbolCenter, + ); + + offset += rowHeight; + }); + + _currentHeight = offset.ceil(); + + super.preprocessSeries(seriesList); + } + + @override + DatumPoint getPoint( + final datum, + D domainValue, + D domainLowerBoundValue, + D domainUpperBoundValue, + ImmutableSeries series, + ImmutableAxis domainAxis, + num measureValue, + num measureLowerBoundValue, + num measureUpperBoundValue, + num measureOffsetValue, + ImmutableAxis measureAxis) { + final domainPosition = domainAxis.getLocation(domainValue); + + final domainLowerBoundPosition = domainLowerBoundValue != null + ? domainAxis.getLocation(domainLowerBoundValue) + : null; + + final domainUpperBoundPosition = domainUpperBoundValue != null + ? domainAxis.getLocation(domainUpperBoundValue) + : null; + + final seriesKey = series.id; + final seriesInfo = _seriesInfo[seriesKey]; + + final measurePosition = _componentBounds.top + seriesInfo.symbolCenter; + + final measureLowerBoundPosition = + domainLowerBoundPosition != null ? measurePosition : null; + + final measureUpperBoundPosition = + domainUpperBoundPosition != null ? measurePosition : null; + + return new DatumPoint( + datum: datum, + domain: domainValue, + series: series, + x: domainPosition, + xLower: domainLowerBoundPosition, + xUpper: domainUpperBoundPosition, + y: measurePosition, + yLower: measureLowerBoundPosition, + yUpper: measureUpperBoundPosition); + } + + @override + void onAttach(BaseChart chart) { + if (!(chart is CartesianChart)) { + throw new ArgumentError( + 'SymbolAnnotationRenderer can only be attached to a CartesianChart'); + } + + _chart = chart as CartesianChart; + + // Only vertical rendering is supported by this behavior. + assert(_chart.vertical); + + super.onAttach(chart); + _chart.addView(this); + } + + @override + void onDetach(BaseChart chart) { + chart.removeView(this); + } + + @override + void paint(ChartCanvas canvas, double animationPercent) { + super.paint(canvas, animationPercent); + + // Use the domain axis of the attached chart to render the separator lines + // to keep the same overall style. + if ((config as SymbolAnnotationRendererConfig).showSeparatorLines) { + seriesPointMap.forEach((String key, List> points) { + final seriesInfo = _seriesInfo[key]; + + final y = componentBounds.top + seriesInfo.rowStart; + + final domainAxis = _chart.domainAxis; + final bounds = new Rectangle( + componentBounds.left, y.round(), componentBounds.width, 0); + domainAxis.tickDrawStrategy + .drawAxisLine(canvas, domainAxis.axisOrientation, bounds); + }); + } + } + + @override + GraphicsFactory get graphicsFactory => _graphicsFactory; + + @override + set graphicsFactory(GraphicsFactory value) { + _graphicsFactory = value; + } + + // + // Layout methods + // + + @override + LayoutViewConfig get layoutConfig { + return new LayoutViewConfig( + paintOrder: LayoutViewPaintOrder.point, + position: LayoutPosition.Bottom, + positionOrder: LayoutViewPositionOrder.symbolAnnotation); + } + + @override + ViewMeasuredSizes measure(int maxWidth, int maxHeight) { + // The sizing of component is not flexible. It's height is always a multiple + // of the number of series rendered, even if that ends up taking all of the + // available margin space. + return new ViewMeasuredSizes( + preferredWidth: maxWidth, preferredHeight: _currentHeight); + } + + @override + void layout(Rectangle componentBounds, Rectangle drawAreaBounds) { + _componentBounds = componentBounds; + + super.layout(componentBounds, drawAreaBounds); + } + + @override + Rectangle get componentBounds => _componentBounds; +} + +class _SeriesInfo { + double rowHeight; + double rowStart; + double symbolCenter; + + _SeriesInfo( + {@required this.rowHeight, + @required this.rowStart, + @required this.symbolCenter}); +} diff --git a/web/charts/common/lib/src/chart/scatter_plot/symbol_annotation_renderer_config.dart b/web/charts/common/lib/src/chart/scatter_plot/symbol_annotation_renderer_config.dart new file mode 100644 index 000000000..bb3489aa5 --- /dev/null +++ b/web/charts/common/lib/src/chart/scatter_plot/symbol_annotation_renderer_config.dart @@ -0,0 +1,72 @@ +// 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 'comparison_points_decorator.dart' show ComparisonPointsDecorator; +import 'point_renderer_config.dart' show PointRendererConfig; +import 'point_renderer_decorator.dart' show PointRendererDecorator; +import 'symbol_annotation_renderer.dart' show SymbolAnnotationRenderer; + +/// Configuration for [SymbolAnnotationRenderer]. +/// +/// This renderer is configured with a [ComparisonPointsDecorator] by default, +/// used to draw domain ranges. This decorator will draw a rectangular shape +/// between the points (domainLowerBound, measureLowerBound) and +/// (domainUpperBound, measureUpperBound), beneath the primary point for each +/// series. +class SymbolAnnotationRendererConfig extends PointRendererConfig { + /// Whether a separator line should be drawn between the bottom row of + /// rendered symbols and the axis ticks/labels. + final bool showBottomSeparatorLine; + + /// Whether or not separator lines will be rendered between rows of rendered + /// symbols. + final bool showSeparatorLines; + + /// Space reserved at the bottom of each row where the symbol should not + /// render into. + final double verticalSymbolBottomPaddingPx; + + /// Space reserved at the top of each row where the symbol should not render + /// into. + final double verticalSymbolTopPaddingPx; + + SymbolAnnotationRendererConfig( + {String customRendererId, + List pointRendererDecorators, + double radiusPx = 5.0, + SymbolRenderer symbolRenderer, + Map customSymbolRenderers, + this.showBottomSeparatorLine = false, + this.showSeparatorLines = true, + this.verticalSymbolBottomPaddingPx = 5.0, + this.verticalSymbolTopPaddingPx = 5.0}) + : super( + customRendererId: customRendererId, + pointRendererDecorators: pointRendererDecorators ?? + [ + new ComparisonPointsDecorator( + symbolRenderer: new RectangleRangeSymbolRenderer()) + ], + radiusPx: radiusPx, + symbolRenderer: symbolRenderer, + customSymbolRenderers: customSymbolRenderers); + + @override + SymbolAnnotationRenderer build() { + return new SymbolAnnotationRenderer( + config: this, rendererId: customRendererId); + } +} diff --git a/web/charts/common/lib/src/chart/time_series/time_series_chart.dart b/web/charts/common/lib/src/chart/time_series/time_series_chart.dart new file mode 100644 index 000000000..3242da069 --- /dev/null +++ b/web/charts/common/lib/src/chart/time_series/time_series_chart.dart @@ -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 'dart:collection' show LinkedHashMap; + +import '../../common/date_time_factory.dart' + show DateTimeFactory, LocalDateTimeFactory; +import '../cartesian/axis/axis.dart' show Axis, NumericAxis; +import '../cartesian/axis/draw_strategy/small_tick_draw_strategy.dart' + show SmallTickRendererSpec; +import '../cartesian/axis/spec/axis_spec.dart' show AxisSpec; +import '../cartesian/axis/spec/date_time_axis_spec.dart' show DateTimeAxisSpec; +import '../cartesian/axis/time/date_time_axis.dart' show DateTimeAxis; +import '../cartesian/cartesian_chart.dart' show CartesianChart; +import '../common/series_renderer.dart' show SeriesRenderer; +import '../layout/layout_config.dart' show LayoutConfig; +import '../line/line_renderer.dart' show LineRenderer; + +class TimeSeriesChart extends CartesianChart { + final DateTimeFactory dateTimeFactory; + + TimeSeriesChart( + {bool vertical, + LayoutConfig layoutConfig, + NumericAxis primaryMeasureAxis, + NumericAxis secondaryMeasureAxis, + LinkedHashMap disjointMeasureAxes, + this.dateTimeFactory = const LocalDateTimeFactory()}) + : super( + vertical: vertical, + layoutConfig: layoutConfig, + domainAxis: new DateTimeAxis(dateTimeFactory), + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes); + + @override + void initDomainAxis() { + domainAxis.tickDrawStrategy = new SmallTickRendererSpec() + .createDrawStrategy(context, graphicsFactory); + } + + @override + SeriesRenderer makeDefaultRenderer() { + return new LineRenderer() + ..rendererId = SeriesRenderer.defaultRendererId; + } + + @override + Axis createDomainAxisFromSpec(AxisSpec axisSpec) { + return (axisSpec as DateTimeAxisSpec).createDateTimeAxis(dateTimeFactory); + } +} diff --git a/web/charts/common/lib/src/common/color.dart b/web/charts/common/lib/src/common/color.dart new file mode 100644 index 000000000..b100ca866 --- /dev/null +++ b/web/charts/common/lib/src/common/color.dart @@ -0,0 +1,113 @@ +// 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; + +@immutable +class Color { + static const black = const Color(r: 0, g: 0, b: 0); + static const white = const Color(r: 255, g: 255, b: 255); + static const transparent = const Color(r: 0, g: 0, b: 0, a: 0); + + static const _darkerPercentOfOrig = 0.7; + static const _lighterPercentOfOrig = 0.1; + + final int r; + final int g; + final int b; + final int a; + + final Color _darker; + final Color _lighter; + + const Color( + {this.r, this.g, this.b, this.a = 255, Color darker, Color lighter}) + : _darker = darker, + _lighter = lighter; + + Color.fromOther({Color color, Color darker, Color lighter}) + : r = color.r, + g = color.g, + b = color.b, + a = color.a, + _darker = darker ?? color._darker, + _lighter = lighter ?? color._lighter; + + /// Construct the color from a hex code string, of the format #RRGGBB. + factory Color.fromHex({String code}) { + var str = code.substring(1, 7); + var bigint = int.parse(str, radix: 16); + final r = (bigint >> 16) & 255; + final g = (bigint >> 8) & 255; + final b = bigint & 255; + final a = 255; + return new Color(r: r, g: g, b: b, a: a); + } + + Color get darker => + _darker ?? + new Color( + r: (r * _darkerPercentOfOrig).round(), + g: (g * _darkerPercentOfOrig).round(), + b: (b * _darkerPercentOfOrig).round(), + a: a); + + Color get lighter => + _lighter ?? + new Color( + r: r + ((255 - r) * _lighterPercentOfOrig).round(), + g: g + ((255 - g) * _lighterPercentOfOrig).round(), + b: b + ((255 - b) * _lighterPercentOfOrig).round(), + a: a); + + @override + bool operator ==(Object other) => + other is Color && + other.r == r && + other.g == g && + other.b == b && + other.a == a; + + @override + int get hashCode { + var hashcode = r.hashCode; + hashcode = hashcode * 37 + g.hashCode; + hashcode = hashcode * 37 + b.hashCode; + hashcode = hashcode * 37 + a.hashCode; + return hashcode; + } + + @override + String toString() => rgbaHexString; + + /// Converts the character into a #RGBA hex string. + String get rgbaHexString => '#${_get2CharHex(r)}${_get2CharHex(g)}' + '${_get2CharHex(b)}${_get2CharHex(a)}'; + + /// Converts the character into a #RGB hex string. + String get hexString { + // Alpha is not included in the hex string. + assert(a == 255); + return '#${_get2CharHex(r)}${_get2CharHex(g)}${_get2CharHex(b)}'; + } + + String _get2CharHex(int num) { + var str = num.toRadixString(16); + while (str.length < 2) { + str = '0' + str; + } + return str; + } +} diff --git a/web/charts/common/lib/src/common/date_time_factory.dart b/web/charts/common/lib/src/common/date_time_factory.dart new file mode 100644 index 000000000..0fdc52d59 --- /dev/null +++ b/web/charts/common/lib/src/common/date_time_factory.dart @@ -0,0 +1,98 @@ +// 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; + +/// Interface for factory that creates [DateTime] and [DateFormat]. +/// +/// This allows for creating of locale specific date time and date format. +abstract class DateTimeFactory { + // TODO: Per cbraun@, we need to allow setting the timezone that + // is used globally (along with other settings like which day the week starts + // on. Use DateTimeFactory - either return a local DateTime or a UTC date time + // based on the setting. + + // TODO: We need to incorporate the time zoned calendar here + // because Dart DateTime doesn't do this. TZDateTime implements DateTime, so + // we can use DateTime as the interface. + DateTime createDateTimeFromMilliSecondsSinceEpoch(int millisecondsSinceEpoch); + + DateTime createDateTime(int year, + [int month = 1, + int day = 1, + int hour = 0, + int minute = 0, + int second = 0, + int millisecond = 0, + int microsecond = 0]); + + /// Returns a [DateFormat]. + DateFormat createDateFormat(String pattern); +} + +/// A local time [DateTimeFactory]. +class LocalDateTimeFactory implements DateTimeFactory { + const LocalDateTimeFactory(); + + DateTime createDateTimeFromMilliSecondsSinceEpoch( + int millisecondsSinceEpoch) { + return new DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch); + } + + DateTime createDateTime(int year, + [int month = 1, + int day = 1, + int hour = 0, + int minute = 0, + int second = 0, + int millisecond = 0, + int microsecond = 0]) { + return new DateTime( + year, month, day, hour, minute, second, millisecond, microsecond); + } + + /// Returns a [DateFormat]. + DateFormat createDateFormat(String pattern) { + return new DateFormat(pattern); + } +} + +/// An UTC time [DateTimeFactory]. +class UTCDateTimeFactory implements DateTimeFactory { + const UTCDateTimeFactory(); + + DateTime createDateTimeFromMilliSecondsSinceEpoch( + int millisecondsSinceEpoch) { + return new DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch, + isUtc: true); + } + + DateTime createDateTime(int year, + [int month = 1, + int day = 1, + int hour = 0, + int minute = 0, + int second = 0, + int millisecond = 0, + int microsecond = 0]) { + return new DateTime.utc( + year, month, day, hour, minute, second, millisecond, microsecond); + } + + /// Returns a [DateFormat]. + DateFormat createDateFormat(String pattern) { + return new DateFormat(pattern); + } +} diff --git a/web/charts/common/lib/src/common/gesture_listener.dart b/web/charts/common/lib/src/common/gesture_listener.dart new file mode 100644 index 000000000..049205b32 --- /dev/null +++ b/web/charts/common/lib/src/common/gesture_listener.dart @@ -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 'dart:math' show Point; + +/// Listener to touch gestures. +/// +/// [GestureListeners] can override only the gestures it is interested in. +/// +/// Each gesture returns true if the event is consumed or false if it should +/// continue to alert other listeners. +class GestureListener { + static final GestureCancelCallback defaultTapCancel = () {}; + static final GestureSinglePointCallback defaultTapTest = (_) => false; + + /// Called before all gestures (except onHover) as a preliminary test to + /// see who is interested in an event. + /// + /// All listeners that return true will get the next gesture event. + /// + /// Any listener that returns false will only get the next gesture event if + /// no one returned true. + /// + /// This is useful for figuring out who is claiming a gesture event. + /// Example: SelectNearest returns true for onTapTest if the point is within + /// the drawArea. SeriesLegend returns true for onTapTest if the point is + /// within the legend. If the tap occurs in either of those places the + /// corresponding listener. If the tap occurs outside of both targets, then + /// both will be given the event so they can deselect everything in the + /// selection model. + /// + /// Defaults to function that returns false allowing other listeners to preempt. + final GestureSinglePointCallback onTapTest; + + /// Called if onTapTest was previously called, but listener is being preempted. + final GestureCancelCallback onTapCancel; + + /// Called after the tap event has been going on for a period of time (500ms) + /// without moving much (20px). + /// The onTap or onDragStart gestures can still trigger after this gesture. + final GestureSinglePointCallback onLongPress; + + /// Called on tap up if not dragging. + final GestureSinglePointCallback onTap; + + /// Called when a mouse hovers over the chart. (No tap event). + final GestureSinglePointCallback onHover; + + /// Called when the tap event has moved beyond a threshold indicating that + /// the user is dragging. + /// + /// This will only be called once per drag gesture independent of how many + /// touches are going on until the last touch is complete. onDragUpdate is + /// called as touches move updating the scale as determined by the first + /// two points. onDragEnd is called when the last touch event lifts and the + /// velocity is calculated from the final movement. + /// + /// onDragStart, onDragUpdate, and onDragEnd are also called for mouse wheel + /// with the scale and point updated given the WheelEvent (deltaY updates the + /// scale, deltaX updates the event point/pans). + /// + /// TODO: Add a "discrete" flag that tells drag listeners whether + /// they should be expecting a series of continuous updates, or one large + /// update. This will mostly be used to control whether we animate the chart + /// between onDragUpdate calls. + /// + /// TODO: Investigate low performance of chart rendering from + /// flutter when animation is enabled and we pinch to zoom on the chart. + final GestureDragStartCallback onDragStart; + final GestureDragUpdateCallback onDragUpdate; + final GestureDragEndCallback onDragEnd; + + GestureListener( + {GestureSinglePointCallback onTapTest, + GestureCancelCallback onTapCancel, + this.onLongPress, + this.onTap, + this.onHover, + this.onDragStart, + this.onDragUpdate, + this.onDragEnd}) + : this.onTapTest = onTapTest ?? defaultTapTest, + this.onTapCancel = onTapCancel ?? defaultTapCancel; +} + +typedef GestureCancelCallback(); +typedef bool GestureSinglePointCallback(Point localPosition); + +typedef bool GestureDragStartCallback(Point localPosition); +typedef GestureDragUpdateCallback(Point localPosition, double scale); +typedef GestureDragEndCallback( + Point localPosition, double scale, double pixelsPerSec); diff --git a/web/charts/common/lib/src/common/graphics_factory.dart b/web/charts/common/lib/src/common/graphics_factory.dart new file mode 100644 index 000000000..7bce54a56 --- /dev/null +++ b/web/charts/common/lib/src/common/graphics_factory.dart @@ -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 'line_style.dart' show LineStyle; +import 'text_element.dart' show TextElement; +import 'text_style.dart' show TextStyle; + +/// Interface to native platform graphics functions. +abstract class GraphicsFactory { + LineStyle createLinePaint(); + + /// Returns a [TextStyle] object. + TextStyle createTextPaint(); + + /// Returns a text element from [text] and [style]. + TextElement createTextElement(String text); +} diff --git a/web/charts/common/lib/src/common/line_style.dart b/web/charts/common/lib/src/common/line_style.dart new file mode 100644 index 000000000..38d8e8572 --- /dev/null +++ b/web/charts/common/lib/src/common/line_style.dart @@ -0,0 +1,24 @@ +// 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 'paint_style.dart' show PaintStyle; + +abstract class LineStyle extends PaintStyle { + List get dashPattern; + set dashPattern(List dashPattern); + + int get strokeWidth; + set strokeWidth(int strokeWidth); +} diff --git a/web/charts/common/lib/src/common/material_palette.dart b/web/charts/common/lib/src/common/material_palette.dart new file mode 100644 index 000000000..770a21bbe --- /dev/null +++ b/web/charts/common/lib/src/common/material_palette.dart @@ -0,0 +1,232 @@ +// 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 'color.dart' show Color; +import 'palette.dart' show Palette; + +/// A canonical palette of colors from material.io. +/// +/// @link https://material.io/guidelines/style/color.html#color-color-palette +class MaterialPalette { + static const black = const Color(r: 0, g: 0, b: 0); + static const transparent = const Color(r: 0, g: 0, b: 0, a: 0); + static const white = const Color(r: 255, g: 255, b: 255); + + static Palette get blue => const MaterialBlue(); + static Palette get red => const MaterialRed(); + static Palette get yellow => const MaterialYellow(); + static Palette get green => const MaterialGreen(); + static Palette get purple => const MaterialPurple(); + static Palette get cyan => const MaterialCyan(); + static Palette get deepOrange => const MaterialDeepOrange(); + static Palette get lime => const MaterialLime(); + static Palette get indigo => const MaterialIndigo(); + static Palette get pink => const MaterialPink(); + static Palette get teal => const MaterialTeal(); + static MaterialGray get gray => const MaterialGray(); + + static List getOrderedPalettes(int count) { + final orderedPalettes = []; + if (orderedPalettes.length < count) { + orderedPalettes.add(blue); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(red); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(yellow); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(green); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(purple); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(cyan); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(deepOrange); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(lime); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(indigo); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(pink); + } + if (orderedPalettes.length < count) { + orderedPalettes.add(teal); + } + return orderedPalettes; + } +} + +class MaterialBlue extends Palette { + static const _shade200 = const Color(r: 0x90, g: 0xCA, b: 0xF9); //#90CAF9 + static const _shade500 = const Color( + r: 0x21, g: 0x96, b: 0xF3, darker: _shade700, lighter: _shade200); + static const _shade700 = const Color(r: 0x19, g: 0x76, b: 0xD2); //#1976D2 + + const MaterialBlue(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialRed extends Palette { + static const _shade200 = const Color(r: 0xEF, g: 0x9A, b: 0x9A); //#EF9A9A + static const _shade700 = const Color(r: 0xD3, g: 0x2F, b: 0x2F); //#D32F2F + static const _shade500 = const Color( + r: 0xF4, g: 0x43, b: 0x36, darker: _shade700, lighter: _shade200); + + const MaterialRed(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialYellow extends Palette { + static const _shade200 = const Color(r: 0xFF, g: 0xF5, b: 0x9D); //#FFF59D + static const _shade700 = const Color(r: 0xFB, g: 0xC0, b: 0x2D); //#FBC02D + static const _shade500 = const Color( + r: 0xFF, g: 0xEB, b: 0x3B, darker: _shade700, lighter: _shade200); + + const MaterialYellow(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialGreen extends Palette { + static const _shade200 = const Color(r: 0xA5, g: 0xD6, b: 0xA7); //#A5D6A7 + static const _shade700 = const Color(r: 0x38, g: 0x8E, b: 0x3C); //#388E3C; + static const _shade500 = const Color( + r: 0x4C, g: 0xAF, b: 0x50, darker: _shade700, lighter: _shade200); + + const MaterialGreen(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialPurple extends Palette { + static const _shade200 = const Color(r: 0xCE, g: 0x93, b: 0xD8); //#CE93D8 + static const _shade700 = const Color(r: 0x7B, g: 0x1F, b: 0xA2); //#7B1FA2 + static const _shade500 = const Color( + r: 0x9C, g: 0x27, b: 0xB0, darker: _shade700, lighter: _shade200); + + const MaterialPurple(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialCyan extends Palette { + static const _shade200 = const Color(r: 0x80, g: 0xDE, b: 0xEA); //#80DEEA + static const _shade700 = const Color(r: 0x00, g: 0x97, b: 0xA7); //#0097A7 + static const _shade500 = const Color( + r: 0x00, g: 0xBC, b: 0xD4, darker: _shade700, lighter: _shade200); + + const MaterialCyan(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialDeepOrange extends Palette { + static const _shade200 = const Color(r: 0xFF, g: 0xAB, b: 0x91); //#FFAB91 + static const _shade700 = const Color(r: 0xE6, g: 0x4A, b: 0x19); //#E64A19 + static const _shade500 = const Color( + r: 0xFF, g: 0x57, b: 0x22, darker: _shade700, lighter: _shade200); + + const MaterialDeepOrange(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialLime extends Palette { + static const _shade200 = const Color(r: 0xE6, g: 0xEE, b: 0x9C); //#E6EE9C + static const _shade700 = const Color(r: 0xAF, g: 0xB4, b: 0x2B); //#AFB42B + static const _shade500 = const Color( + r: 0xCD, g: 0xDC, b: 0x39, darker: _shade700, lighter: _shade200); + + const MaterialLime(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialIndigo extends Palette { + static const _shade200 = const Color(r: 0x9F, g: 0xA8, b: 0xDA); //#9FA8DA + static const _shade700 = const Color(r: 0x30, g: 0x3F, b: 0x9F); //#303F9F + static const _shade500 = const Color( + r: 0x3F, g: 0x51, b: 0xB5, darker: _shade700, lighter: _shade200); + + const MaterialIndigo(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialPink extends Palette { + static const _shade200 = const Color(r: 0xF4, g: 0x8F, b: 0xB1); //#F48FB1 + static const _shade700 = const Color(r: 0xC2, g: 0x18, b: 0x5B); //#C2185B + static const _shade500 = const Color( + r: 0xE9, g: 0x1E, b: 0x63, darker: _shade700, lighter: _shade200); + + const MaterialPink(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialTeal extends Palette { + static const _shade200 = const Color(r: 0x80, g: 0xCB, b: 0xC4); //#80CBC4 + static const _shade700 = const Color(r: 0x00, g: 0x79, b: 0x6B); //#00796B + static const _shade500 = const Color( + r: 0x00, g: 0x96, b: 0x88, darker: _shade700, lighter: _shade200); + + const MaterialTeal(); + + @override + Color get shadeDefault => _shade500; +} + +class MaterialGray extends Palette { + static const _shade200 = const Color(r: 0xEE, g: 0xEE, b: 0xEE); //#EEEEEE + static const _shade700 = const Color(r: 0x61, g: 0x61, b: 0x61); //#616161 + static const _shade500 = const Color( + r: 0x9E, g: 0x9E, b: 0x9E, darker: _shade700, lighter: _shade200); + + const MaterialGray(); + + @override + Color get shadeDefault => _shade500; + + Color get shade50 => const Color(r: 0xFA, g: 0xFA, b: 0xFA); //#FAFAFA + Color get shade100 => const Color(r: 0xF5, g: 0xF5, b: 0xF5); //#F5F5F5 + Color get shade200 => _shade200; + Color get shade300 => const Color(r: 0xE0, g: 0xE0, b: 0xE0); //#E0E0E0 + Color get shade400 => const Color(r: 0xBD, g: 0xBD, b: 0xBD); //#BDBDBD + Color get shade500 => _shade500; + Color get shade600 => const Color(r: 0x75, g: 0x75, b: 0x75); //#757575 + Color get shade700 => _shade700; + Color get shade800 => const Color(r: 0x42, g: 0x42, b: 0x42); //#424242 + Color get shade900 => const Color(r: 0x21, g: 0x21, b: 0xA1); //#212121 +} diff --git a/web/charts/common/lib/src/common/math.dart b/web/charts/common/lib/src/common/math.dart new file mode 100644 index 000000000..0e6c4b7e1 --- /dev/null +++ b/web/charts/common/lib/src/common/math.dart @@ -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 'dart:math' show max, min, sqrt; +import 'package:vector_math/vector_math.dart' show Vector2; + +/// Takes a number and clamps it to within the provided bounds. +/// +/// Returns the input number if it is within bounds, or the nearest number +/// within the bounds. +/// +/// [value] The input number. +/// [minValue] The minimum value to return. +/// [maxValue] The maximum value to return. +num clamp(num value, num minValue, num maxValue) { + return min(max(value, minValue), maxValue); +} + +/// Returns the minimum distance between point p and the line segment vw. +/// +/// [p] The point. +/// [v] Start point for the line segment. +/// [w] End point for the line segment. +double distanceBetweenPointAndLineSegment(Vector2 p, Vector2 v, Vector2 w) { + return sqrt(distanceBetweenPointAndLineSegmentSquared(p, v, w)); +} + +/// Returns the squared minimum distance between point p and the line segment +/// vw. +/// +/// [p] The point. +/// [v] Start point for the line segment. +/// [w] End point for the line segment. +double distanceBetweenPointAndLineSegmentSquared( + Vector2 p, Vector2 v, Vector2 w) { + final lineLength = v.distanceToSquared(w); + + if (lineLength == 0) { + return p.distanceToSquared(v); + } + + var t0 = (p - v).dot(w - v) / lineLength; + t0 = max(0.0, min(1.0, t0)); + + final projection = v + ((w - v) * t0); + + return p.distanceToSquared(projection); +} diff --git a/web/charts/common/lib/src/common/paint_style.dart b/web/charts/common/lib/src/common/paint_style.dart new file mode 100644 index 000000000..047f3e929 --- /dev/null +++ b/web/charts/common/lib/src/common/paint_style.dart @@ -0,0 +1,23 @@ +// 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 'color.dart'; + +/// Style properties of a paintable object. +abstract class PaintStyle { + Color get color; + + set color(Color value); +} diff --git a/web/charts/common/lib/src/common/palette.dart b/web/charts/common/lib/src/common/palette.dart new file mode 100644 index 000000000..a85e6eb06 --- /dev/null +++ b/web/charts/common/lib/src/common/palette.dart @@ -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 'color.dart' show Color; + +/// A color palette. +abstract class Palette { + const Palette(); + + /// The default shade. + Color get shadeDefault; + + /// Returns a list of colors for this color palette. + List makeShades(int colorCnt) { + final colors = [shadeDefault]; + + // If we need more than 2 colors, then [unselected] collides with one of the + // generated colors. Otherwise divide the space between the top color + // and white in half. + final lighterColor = colorCnt < 3 + ? shadeDefault.lighter + : _getSteppedColor(shadeDefault, (colorCnt * 2) - 1, colorCnt * 2); + + // Divide the space between 255 and c500 evenly according to the colorCnt. + for (int i = 1; i < colorCnt; i++) { + colors.add(_getSteppedColor(shadeDefault, i, colorCnt, + darker: shadeDefault.darker, lighter: lighterColor)); + } + + colors.add(new Color.fromOther(color: shadeDefault, lighter: lighterColor)); + return colors; + } + + Color _getSteppedColor(Color color, int index, int steps, + {Color darker, Color lighter}) { + final fraction = index / steps; + return new Color( + r: color.r + ((255 - color.r) * fraction).round(), + g: color.g + ((255 - color.g) * fraction).round(), + b: color.b + ((255 - color.b) * fraction).round(), + a: color.a + ((255 - color.a) * fraction).round(), + darker: darker, + lighter: lighter, + ); + } +} diff --git a/web/charts/common/lib/src/common/performance.dart b/web/charts/common/lib/src/common/performance.dart new file mode 100644 index 000000000..3706ce1c6 --- /dev/null +++ b/web/charts/common/lib/src/common/performance.dart @@ -0,0 +1,21 @@ +// 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. + +typedef PerformanceCallback(String tag); + +class Performance { + static PerformanceCallback time = (_) {}; + static PerformanceCallback timeEnd = (_) {}; +} diff --git a/web/charts/common/lib/src/common/proxy_gesture_listener.dart b/web/charts/common/lib/src/common/proxy_gesture_listener.dart new file mode 100644 index 000000000..a78d4fda0 --- /dev/null +++ b/web/charts/common/lib/src/common/proxy_gesture_listener.dart @@ -0,0 +1,144 @@ +// 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 'gesture_listener.dart' show GestureListener; + +/// Listens to all gestures and proxies to child listeners. +class ProxyGestureListener { + final _listeners = []; + var _activeListeners = []; + + void add(GestureListener listener) { + _listeners.add(listener); + _activeListeners.clear(); + } + + void remove(GestureListener listener) { + _listeners.remove(listener); + _activeListeners.clear(); + } + + bool onTapTest(Point localPosition) { + _activeListeners.clear(); + return _populateActiveListeners(localPosition); + } + + bool onLongPress(Point localPosition) { + // Walk through listeners stopping at the first handled listener. + final claimingListener = _activeListeners.firstWhere( + (GestureListener listener) => + listener.onLongPress != null && listener.onLongPress(localPosition), + orElse: () => null); + + // If someone claims the long press, then cancel everyone else. + if (claimingListener != null) { + _activeListeners = + _cancel(all: _activeListeners, keep: [claimingListener]); + return true; + } + return false; + } + + bool onTap(Point localPosition) { + // Walk through listeners stopping at the first handled listener. + final claimingListener = _activeListeners.firstWhere( + (GestureListener listener) => + listener.onTap != null && listener.onTap(localPosition), + orElse: () => null); + + // If someone claims the tap, then cancel everyone else. + // This should hopefully be rare, like for drilling. + if (claimingListener != null) { + _activeListeners = + _cancel(all: _activeListeners, keep: [claimingListener]); + return true; + } + return false; + } + + bool onHover(Point localPosition) { + // Cancel any previously active long lived gestures. + _activeListeners = []; + + // Walk through listeners stopping at the first handled listener. + return _listeners.any((GestureListener listener) => + listener.onHover != null && listener.onHover(localPosition)); + } + + bool onDragStart(Point localPosition) { + // In Flutter, a tap test may not be triggered because a tap down event + // may not be registered if the the drag gesture happens without any pause. + if (_activeListeners.isEmpty) { + _populateActiveListeners(localPosition); + } + + // Walk through listeners stopping at the first handled listener. + final claimingListener = _activeListeners.firstWhere( + (GestureListener listener) => + listener.onDragStart != null && listener.onDragStart(localPosition), + orElse: () => null); + + if (claimingListener != null) { + _activeListeners = + _cancel(all: _activeListeners, keep: [claimingListener]); + return true; + } + return false; + } + + bool onDragUpdate(Point localPosition, double scale) { + return _activeListeners.any((GestureListener listener) => + listener.onDragUpdate != null && + listener.onDragUpdate(localPosition, scale)); + } + + bool onDragEnd( + Point localPosition, double scale, double pixelsPerSecond) { + return _activeListeners.any((GestureListener listener) => + listener.onDragEnd != null && + listener.onDragEnd(localPosition, scale, pixelsPerSecond)); + } + + List _cancel( + {List all, List keep}) { + all.forEach((GestureListener listener) { + if (!keep.contains(listener)) { + listener.onTapCancel(); + } + }); + return keep; + } + + bool _populateActiveListeners(Point localPosition) { + var localListeners = new List.from(_listeners); + + var previouslyClaimed = false; + localListeners.forEach((GestureListener listener) { + var claimed = listener.onTapTest(localPosition); + if (claimed && !previouslyClaimed) { + // Cancel any already added non-claiming listeners now that someone is + // claiming it. + _activeListeners = _cancel(all: _activeListeners, keep: [listener]); + previouslyClaimed = true; + } else if (claimed || !previouslyClaimed) { + _activeListeners.add(listener); + } + }); + + return previouslyClaimed; + } +} diff --git a/web/charts/common/lib/src/common/rtl_spec.dart b/web/charts/common/lib/src/common/rtl_spec.dart new file mode 100644 index 000000000..fbdc845c6 --- /dev/null +++ b/web/charts/common/lib/src/common/rtl_spec.dart @@ -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. + +/// Defines the behavior of the chart if it is RTL. +class RTLSpec { + /// Creates [RTLSpec]. If no parameters are specified, the defaults are used. + const RTLSpec({ + this.axisDirection = AxisDirection.reversed, + }); + + /// Direction of the domain axis when the chart container is configured for + /// RTL mode. + final AxisDirection axisDirection; +} + +/// Direction of the domain axis when the chart container is configured for +/// RTL mode. +/// +/// [normal] Vertically rendered charts will have the primary measure axis on +/// the left and secondary measure axis on the right. Domain axis is on the left +/// and the domain output range starts from the left and grows to the right. +/// Horizontally rendered charts will have the primary measure axis on the +/// bottom and secondary measure axis on the right. Measure output range starts +/// from the left and grows to the right. +/// +/// [reversed] Vertically rendered charts will have the primary measure axis on +/// the right and secondary measure axis on the left. Domain axis is on the +/// right and domain values grows from the right to the left. Horizontally +/// rendered charts will have the primary measure axis on the top and secondary +/// measure axis on the left. Measure output range is flipped and grows from the +/// right to the left. +enum AxisDirection { + normal, + reversed, +} diff --git a/web/charts/common/lib/src/common/style/material_style.dart b/web/charts/common/lib/src/common/style/material_style.dart new file mode 100644 index 000000000..23615acf9 --- /dev/null +++ b/web/charts/common/lib/src/common/style/material_style.dart @@ -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 '../../chart/cartesian/axis/spec/axis_spec.dart' show LineStyleSpec; +import '../color.dart' show Color; +import '../graphics_factory.dart' show GraphicsFactory; +import '../line_style.dart' show LineStyle; +import '../material_palette.dart' show MaterialPalette; +import '../palette.dart' show Palette; +import 'style.dart' show Style; + +class MaterialStyle implements Style { + const MaterialStyle(); + + @override + Color get black => MaterialPalette.black; + + @override + Color get transparent => MaterialPalette.transparent; + + @override + Color get white => MaterialPalette.white; + + @override + List getOrderedPalettes(int count) => + MaterialPalette.getOrderedPalettes(count); + + @override + LineStyle createAxisLineStyle( + GraphicsFactory graphicsFactory, LineStyleSpec spec) { + return graphicsFactory.createLinePaint() + ..color = spec?.color ?? MaterialPalette.gray.shadeDefault + ..dashPattern = spec?.dashPattern + ..strokeWidth = spec?.thickness ?? 1; + } + + @override + LineStyle createTickLineStyle( + GraphicsFactory graphicsFactory, LineStyleSpec spec) { + return graphicsFactory.createLinePaint() + ..color = spec?.color ?? MaterialPalette.gray.shadeDefault + ..dashPattern = spec?.dashPattern + ..strokeWidth = spec?.thickness ?? 1; + } + + @override + int get tickLength => 3; + + @override + Color get tickColor => MaterialPalette.gray.shade800; + + @override + LineStyle createGridlineStyle( + GraphicsFactory graphicsFactory, LineStyleSpec spec) { + return graphicsFactory.createLinePaint() + ..color = spec?.color ?? MaterialPalette.gray.shade300 + ..dashPattern = spec?.dashPattern + ..strokeWidth = spec?.thickness ?? 1; + } + + @override + Color get arcLabelOutsideLeaderLine => MaterialPalette.gray.shade600; + + @override + Color get legendEntryTextColor => MaterialPalette.gray.shade800; + + @override + Color get legendTitleTextColor => MaterialPalette.gray.shade800; + + @override + Color get linePointHighlighterColor => MaterialPalette.gray.shade600; + + @override + Color get noDataColor => MaterialPalette.gray.shade200; + + @override + Color get rangeAnnotationColor => MaterialPalette.gray.shade100; + + @override + Color get sliderFillColor => MaterialPalette.white; + + @override + Color get sliderStrokeColor => MaterialPalette.gray.shade600; + + @override + Color get chartBackgroundColor => MaterialPalette.white; +} diff --git a/web/charts/common/lib/src/common/style/style.dart b/web/charts/common/lib/src/common/style/style.dart new file mode 100644 index 000000000..d055e4a83 --- /dev/null +++ b/web/charts/common/lib/src/common/style/style.dart @@ -0,0 +1,90 @@ +// 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 '../../chart/cartesian/axis/spec/axis_spec.dart' show LineStyleSpec; +import '../color.dart' show Color; +import '../graphics_factory.dart' show GraphicsFactory; +import '../line_style.dart' show LineStyle; +import '../palette.dart'; + +// TODO: Implementation of style will change drastically, see bug +// for more details. This is an intermediate step in order to allow overriding +// the default style using style factory. + +/// A set of styling rules that determines the default look and feel of charts. +/// +/// Get or set the [Style] that is used for the app using [StyleFactory.style]. +abstract class Style { + Color get black; + + Color get transparent; + + Color get white; + + /// Gets list with [count] of palettes. + List getOrderedPalettes(int count); + + /// Creates [LineStyleSpec] for axis line from spec. + /// + /// Fill missing value(s) with default. + LineStyle createAxisLineStyle( + GraphicsFactory graphicsFactory, LineStyleSpec spec); + + /// Creates [LineStyleSpec] for tick lines from spec. + /// + /// Fill missing value(s) with default. + LineStyle createTickLineStyle( + GraphicsFactory graphicsFactory, LineStyleSpec spec); + + /// Default tick length. + int get tickLength; + + /// Default tick color. + Color get tickColor; + + /// + /// Creates [LineStyle] for axis gridlines from spec. + /// + /// Fill missing value(s) with default. + LineStyle createGridlineStyle( + GraphicsFactory graphicsFactory, LineStyleSpec spec); + + /// Default color for outside label leader lines for [ArcLabelDecorator]. + Color get arcLabelOutsideLeaderLine; + + /// Default color for entry text for [Legend]. + Color get legendEntryTextColor; + + /// Default color for title text for [Legend]. + Color get legendTitleTextColor; + + /// Default color for [LinePointHighlighter]. + Color get linePointHighlighterColor; + + /// Default color for "no data" states on charts. + Color get noDataColor; + + /// Default color for [RangeAnnotation]. + Color get rangeAnnotationColor; + + /// Default fill color for [Slider]. + Color get sliderFillColor; + + /// Default stroke color for [Slider]. + Color get sliderStrokeColor; + + /// Default background color for the chart. + Color get chartBackgroundColor; +} diff --git a/web/charts/common/lib/src/common/style/style_factory.dart b/web/charts/common/lib/src/common/style/style_factory.dart new file mode 100644 index 000000000..37dfe1d90 --- /dev/null +++ b/web/charts/common/lib/src/common/style/style_factory.dart @@ -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 'material_style.dart' show MaterialStyle; +import 'style.dart' show Style; + +class StyleFactory { + static final StyleFactory _styleFactory = new StyleFactory._internal(); + + Style _style = const MaterialStyle(); + + /// The [Style] that is used for all the charts in this application. + static Style get style => _styleFactory._style; + + static set style(Style value) { + _styleFactory._style = value; + } + + StyleFactory._internal(); +} diff --git a/web/charts/common/lib/src/common/symbol_renderer.dart b/web/charts/common/lib/src/common/symbol_renderer.dart new file mode 100644 index 000000000..5933f7319 --- /dev/null +++ b/web/charts/common/lib/src/common/symbol_renderer.dart @@ -0,0 +1,348 @@ +// 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, min; + +import 'package:meta/meta.dart' show protected; + +import '../chart/common/chart_canvas.dart' show ChartCanvas; +import 'color.dart' show Color; +import 'style/style_factory.dart' show StyleFactory; + +/// Strategy for rendering a symbol. +abstract class BaseSymbolRenderer { + bool shouldRepaint(covariant BaseSymbolRenderer oldRenderer); +} + +/// Strategy for rendering a symbol bounded within a box. +abstract class SymbolRenderer extends BaseSymbolRenderer { + /// Whether the symbol should be rendered as a solid shape, or a hollow shape. + /// + /// If this is true, then fillColor and strokeColor will be used to fill in + /// the shape, and draw a border, respectively. The stroke (border) will only + /// be visible if a non-zero strokeWidthPx is configured. + /// + /// If this is false, then the shape will be filled in with a white color + /// (overriding fillColor). strokeWidthPx will default to 2 if none was + /// configured. + final bool isSolid; + + SymbolRenderer({this.isSolid}); + + void paint(ChartCanvas canvas, Rectangle bounds, + {List dashPattern, + Color fillColor, + Color strokeColor, + double strokeWidthPx}); + + @protected + double getSolidStrokeWidthPx(double strokeWidthPx) { + return isSolid ? strokeWidthPx : strokeWidthPx ?? 2.0; + } + + @protected + Color getSolidFillColor(Color fillColor) { + return isSolid ? fillColor : StyleFactory.style.white; + } + + @override + bool operator ==(Object other) { + return other is SymbolRenderer && other.isSolid == isSolid; + } + + @override + int get hashCode => isSolid.hashCode; +} + +/// Strategy for rendering a symbol centered around a point. +/// +/// An optional second point can describe an extended symbol. +abstract class PointSymbolRenderer extends BaseSymbolRenderer { + void paint(ChartCanvas canvas, Point p1, double radius, + {Point p2, Color fillColor, strokeColor}); +} + +/// Rounded rectangular symbol with corners having [radius]. +class RoundedRectSymbolRenderer extends SymbolRenderer { + final double radius; + + RoundedRectSymbolRenderer({bool isSolid = true, double radius}) + : radius = radius ?? 1.0, + super(isSolid: isSolid); + + @override + void paint(ChartCanvas canvas, Rectangle bounds, + {List dashPattern, + Color fillColor, + Color strokeColor, + double strokeWidthPx}) { + canvas.drawRRect(bounds, + fill: getSolidFillColor(fillColor), + stroke: strokeColor, + radius: radius, + roundTopLeft: true, + roundTopRight: true, + roundBottomRight: true, + roundBottomLeft: true); + } + + @override + bool shouldRepaint(RoundedRectSymbolRenderer oldRenderer) { + return this != oldRenderer; + } + + @override + bool operator ==(Object other) { + return other is RoundedRectSymbolRenderer && + other.radius == radius && + super == (other); + } + + @override + int get hashCode { + int hashcode = super.hashCode; + hashcode = (hashcode * 37) + radius.hashCode; + return hashcode; + } +} + +/// Line symbol renderer. +class LineSymbolRenderer extends SymbolRenderer { + static const roundEndCapsPixels = 2; + static const minLengthToRoundCaps = (roundEndCapsPixels * 2) + 1; + static const strokeWidthForRoundEndCaps = 4.0; + static const strokeWidthForNonRoundedEndCaps = 2.0; + + /// Thickness of the line stroke. + final double strokeWidth; + + /// Dash pattern for the line. + final List _dashPattern; + + LineSymbolRenderer( + {List dashPattern, bool isSolid = true, double strokeWidth}) + : strokeWidth = strokeWidth ?? strokeWidthForRoundEndCaps, + _dashPattern = dashPattern, + super(isSolid: isSolid); + + @override + void paint(ChartCanvas canvas, Rectangle bounds, + {List dashPattern, + Color fillColor, + Color strokeColor, + double strokeWidthPx}) { + final centerHeight = (bounds.bottom - bounds.top) / 2; + + // If we have a dash pattern, do not round the end caps, and set + // strokeWidthPx to a smaller value. Using round end caps makes smaller + // patterns blurry. + final localDashPattern = dashPattern ?? _dashPattern; + final roundEndCaps = localDashPattern == null; + + // If we have a dash pattern, the normal stroke width makes them look + // strangely tall. + final localStrokeWidthPx = localDashPattern == null + ? getSolidStrokeWidthPx(strokeWidthPx ?? strokeWidth) + : strokeWidthForNonRoundedEndCaps; + + // Adjust the length so the total width includes the rounded pixels. + // Otherwise the cap is drawn past the bounds and appears to be cut off. + // If bounds is not long enough to accommodate the line, do not adjust. + var left = bounds.left; + var right = bounds.right; + + if (roundEndCaps && bounds.width >= minLengthToRoundCaps) { + left += roundEndCapsPixels; + right -= roundEndCapsPixels; + } + + // TODO: Pass in strokeWidth, roundEndCaps, and dashPattern from + // line renderer config. + canvas.drawLine( + points: [new Point(left, centerHeight), new Point(right, centerHeight)], + dashPattern: localDashPattern, + fill: getSolidFillColor(fillColor), + roundEndCaps: roundEndCaps, + stroke: strokeColor, + strokeWidthPx: localStrokeWidthPx, + ); + } + + @override + bool shouldRepaint(LineSymbolRenderer oldRenderer) { + return this != oldRenderer; + } + + @override + bool operator ==(Object other) { + return other is LineSymbolRenderer && + other.strokeWidth == strokeWidth && + super == (other); + } + + @override + int get hashCode { + int hashcode = super.hashCode; + hashcode = (hashcode * 37) + strokeWidth.hashCode; + return hashcode; + } +} + +/// Circle symbol renderer. +class CircleSymbolRenderer extends SymbolRenderer { + CircleSymbolRenderer({bool isSolid = true}) : super(isSolid: isSolid); + + @override + void paint(ChartCanvas canvas, Rectangle bounds, + {List dashPattern, + Color fillColor, + Color strokeColor, + double strokeWidthPx}) { + final center = new Point( + bounds.left + (bounds.width / 2), + bounds.top + (bounds.height / 2), + ); + final radius = min(bounds.width, bounds.height) / 2; + canvas.drawPoint( + point: center, + radius: radius, + fill: getSolidFillColor(fillColor), + stroke: strokeColor, + strokeWidthPx: getSolidStrokeWidthPx(strokeWidthPx)); + } + + @override + bool shouldRepaint(CircleSymbolRenderer oldRenderer) { + return this != oldRenderer; + } + + @override + bool operator ==(Object other) => + other is CircleSymbolRenderer && super == (other); + + @override + int get hashCode { + int hashcode = super.hashCode; + hashcode = (hashcode * 37) + runtimeType.hashCode; + return hashcode; + } +} + +/// Rectangle symbol renderer. +class RectSymbolRenderer extends SymbolRenderer { + RectSymbolRenderer({bool isSolid = true}) : super(isSolid: isSolid); + + @override + void paint(ChartCanvas canvas, Rectangle bounds, + {List dashPattern, + Color fillColor, + Color strokeColor, + double strokeWidthPx}) { + canvas.drawRect(bounds, + fill: getSolidFillColor(fillColor), + stroke: strokeColor, + strokeWidthPx: getSolidStrokeWidthPx(strokeWidthPx)); + } + + @override + bool shouldRepaint(RectSymbolRenderer oldRenderer) { + return this != oldRenderer; + } + + @override + bool operator ==(Object other) => + other is RectSymbolRenderer && super == (other); + + @override + int get hashCode { + int hashcode = super.hashCode; + hashcode = (hashcode * 37) + runtimeType.hashCode; + return hashcode; + } +} + +/// Draws a cylindrical shape connecting two points. +class CylinderSymbolRenderer extends PointSymbolRenderer { + CylinderSymbolRenderer(); + + @override + void paint(ChartCanvas canvas, Point p1, double radius, + {Point p2, Color fillColor, strokeColor, double strokeWidthPx}) { + if (p1 == null) { + throw new ArgumentError('Invalid point p1 "${p1}"'); + } + + if (p2 == null) { + throw new ArgumentError('Invalid point p2 "${p2}"'); + } + + final adjustedP1 = new Point(p1.x, p1.y); + final adjustedP2 = new Point(p2.x, p2.y); + + canvas.drawLine( + points: [adjustedP1, adjustedP2], + stroke: strokeColor, + roundEndCaps: true, + strokeWidthPx: radius * 2); + } + + @override + bool shouldRepaint(CylinderSymbolRenderer oldRenderer) { + return this != oldRenderer; + } + + @override + bool operator ==(Object other) => other is CylinderSymbolRenderer; + + @override + int get hashCode => runtimeType.hashCode; +} + +/// Draws a rectangular shape connecting two points. +class RectangleRangeSymbolRenderer extends PointSymbolRenderer { + RectangleRangeSymbolRenderer(); + + @override + void paint(ChartCanvas canvas, Point p1, double radius, + {Point p2, Color fillColor, strokeColor, double strokeWidthPx}) { + if (p1 == null) { + throw new ArgumentError('Invalid point p1 "${p1}"'); + } + + if (p2 == null) { + throw new ArgumentError('Invalid point p2 "${p2}"'); + } + + final adjustedP1 = new Point(p1.x, p1.y); + final adjustedP2 = new Point(p2.x, p2.y); + + canvas.drawLine( + points: [adjustedP1, adjustedP2], + stroke: strokeColor, + roundEndCaps: false, + strokeWidthPx: radius * 2); + } + + @override + bool shouldRepaint(RectangleRangeSymbolRenderer oldRenderer) { + return this != oldRenderer; + } + + @override + bool operator ==(Object other) => other is RectangleRangeSymbolRenderer; + + @override + int get hashCode => runtimeType.hashCode; +} diff --git a/web/charts/common/lib/src/common/text_element.dart b/web/charts/common/lib/src/common/text_element.dart new file mode 100644 index 000000000..a25714563 --- /dev/null +++ b/web/charts/common/lib/src/common/text_element.dart @@ -0,0 +1,80 @@ +// 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 'text_measurement.dart' show TextMeasurement; +import 'text_style.dart' show TextStyle; + +/// Interface for accessing text measurement and painter. +abstract class TextElement { + /// The [TextStyle] of this [TextElement]. + TextStyle get textStyle; + + set textStyle(TextStyle value); + + /// The max width of this [TextElement] during measure and layout. + /// + /// If the text exceeds maxWidth, the [maxWidthStrategy] is used. + int get maxWidth; + + set maxWidth(int value); + + /// The strategy to use if this [TextElement] exceeds the [maxWidth]. + MaxWidthStrategy get maxWidthStrategy; + + set maxWidthStrategy(MaxWidthStrategy maxWidthStrategy); + + /// The opacity of this element, in addition to the alpha set on the color + /// of this element. + set opacity(double opacity); + + // The text of this [TextElement]. + String get text; + + /// The [TextMeasurement] of this [TextElement] as an approximate of what + /// is actually printed. + /// + /// Will return the [maxWidth] if set and the actual text width is larger. + TextMeasurement get measurement; + + /// The direction to render the text relative to the coordinate. + TextDirection get textDirection; + set textDirection(TextDirection direction); + + /// Return true if settings are all the same. + /// + /// Purposely excludes measurement because the measurement will request the + /// native [TextElement] to layout, which is expensive. We want to avoid the + /// layout by comparing with another [TextElement] to see if they have the + /// same settings. + static bool elementSettingsSame(TextElement a, TextElement b) { + return a.textStyle == b.textStyle && + a.maxWidth == b.maxWidth && + a.maxWidthStrategy == b.maxWidthStrategy && + a.text == b.text && + a.textDirection == b.textDirection; + } +} + +enum TextDirection { + ltr, + rtl, + center, +} + +/// The strategy to use if a [TextElement] exceeds the [maxWidth]. +enum MaxWidthStrategy { + truncate, + ellipsize, +} diff --git a/web/charts/common/lib/src/common/text_measurement.dart b/web/charts/common/lib/src/common/text_measurement.dart new file mode 100644 index 000000000..fb419a05b --- /dev/null +++ b/web/charts/common/lib/src/common/text_measurement.dart @@ -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. + +/// A measurement result for rendering text. +class TextMeasurement { + /// Rendered width of the text. + final double horizontalSliceWidth; + + /// Vertical slice is likely based off the rendered text. + /// + /// This means that 'mo' and 'My' will have different heights so do not use + /// this for centering vertical text. + final double verticalSliceWidth; + + /// Baseline of the text for text vertical alignment. + final double baseline; + + TextMeasurement( + {this.horizontalSliceWidth, this.verticalSliceWidth, this.baseline}); +} diff --git a/web/charts/common/lib/src/common/text_style.dart b/web/charts/common/lib/src/common/text_style.dart new file mode 100644 index 000000000..b88dbd1ac --- /dev/null +++ b/web/charts/common/lib/src/common/text_style.dart @@ -0,0 +1,25 @@ +// 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 'paint_style.dart' show PaintStyle; + +/// Paint properties of a text. +abstract class TextStyle extends PaintStyle { + int get fontSize; + set fontSize(int value); + + String get fontFamily; + set fontFamily(String fontFamily); +} diff --git a/web/charts/common/lib/src/common/typed_registry.dart b/web/charts/common/lib/src/common/typed_registry.dart new file mode 100644 index 000000000..0fc7051c8 --- /dev/null +++ b/web/charts/common/lib/src/common/typed_registry.dart @@ -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. + +class TypedRegistry { + final Map _registry = {}; + + R getAttr(TypedKey key) { + return _registry[key] as R; + } + + void setAttr(TypedKey key, R value) { + _registry[key] = value; + } + + void mergeFrom(TypedRegistry other) { + _registry.addAll(other._registry); + } +} + +class TypedKey { + final String uniqueKey; + const TypedKey(this.uniqueKey); + + @override + int get hashCode => uniqueKey.hashCode; + + @override + bool operator ==(other) => other is TypedKey && uniqueKey == other.uniqueKey; +} diff --git a/web/charts/common/lib/src/data/series.dart b/web/charts/common/lib/src/data/series.dart new file mode 100644 index 000000000..1e7b06b40 --- /dev/null +++ b/web/charts/common/lib/src/data/series.dart @@ -0,0 +1,225 @@ +// 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 '../chart/cartesian/axis/spec/axis_spec.dart' show TextStyleSpec; +import '../chart/common/chart_canvas.dart' show FillPatternType; +import '../common/color.dart' show Color; +import '../common/typed_registry.dart' show TypedRegistry, TypedKey; + +class Series { + final String id; + final String displayName; + final String seriesCategory; + final bool overlaySeries; + + final List data; + + /// [keyFn] defines a globally unique identifier for each datum. + /// + /// The key for each datum is used during chart animation to smoothly + /// transition data still in the series to its new state. + /// + /// Note: This is currently an optional function that is not fully used by all + /// series renderers yet. + final AccessorFn keyFn; + + final AccessorFn domainFn; + final AccessorFn domainLowerBoundFn; + final AccessorFn domainUpperBoundFn; + final AccessorFn measureFn; + final AccessorFn measureLowerBoundFn; + final AccessorFn measureUpperBoundFn; + final AccessorFn measureOffsetFn; + + /// [areaColorFn] returns the area color for a given data value. If not + /// provided, then some variation of the main [colorFn] will be used (e.g. + /// 10% opacity). + /// + /// This color is used for supplemental information on the series, such as + /// confidence intervals or area skirts. + final AccessorFn areaColorFn; + + /// [colorFn] returns the rendered stroke color for a given data value. + final AccessorFn colorFn; + + /// [dashPatternFn] returns the dash pattern for a given data value. + final AccessorFn> dashPatternFn; + + /// [fillColorFn] returns the rendered fill color for a given data value. If + /// not provided, then [colorFn] will be used as a fallback. + final AccessorFn fillColorFn; + + final AccessorFn fillPatternFn; + final AccessorFn radiusPxFn; + final AccessorFn strokeWidthPxFn; + final AccessorFn labelAccessorFn; + final AccessorFn insideLabelStyleAccessorFn; + final AccessorFn outsideLabelStyleAccessorFn; + + // TODO: should this be immutable as well? If not, should any of + // the non-required ones be final? + final SeriesAttributes attributes = new SeriesAttributes(); + + factory Series( + {@required String id, + @required List data, + @required TypedAccessorFn domainFn, + @required TypedAccessorFn measureFn, + String displayName, + TypedAccessorFn areaColorFn, + TypedAccessorFn colorFn, + TypedAccessorFn> dashPatternFn, + TypedAccessorFn domainLowerBoundFn, + TypedAccessorFn domainUpperBoundFn, + TypedAccessorFn fillColorFn, + TypedAccessorFn fillPatternFn, + TypedAccessorFn keyFn, + TypedAccessorFn labelAccessorFn, + TypedAccessorFn insideLabelStyleAccessorFn, + TypedAccessorFn outsideLabelStyleAccessorFn, + TypedAccessorFn measureLowerBoundFn, + TypedAccessorFn measureUpperBoundFn, + TypedAccessorFn measureOffsetFn, + bool overlaySeries = false, + TypedAccessorFn radiusPxFn, + String seriesCategory, + TypedAccessorFn strokeWidthPxFn}) { + // Wrap typed accessors. + final _domainFn = (int index) => domainFn(data[index], index); + final _measureFn = (int index) => measureFn(data[index], index); + final _areaColorFn = areaColorFn == null + ? null + : (int index) => areaColorFn(data[index], index); + final _colorFn = + colorFn == null ? null : (int index) => colorFn(data[index], index); + final _dashPatternFn = dashPatternFn == null + ? null + : (int index) => dashPatternFn(data[index], index); + final _domainLowerBoundFn = domainLowerBoundFn == null + ? null + : (int index) => domainLowerBoundFn(data[index], index); + final _domainUpperBoundFn = domainUpperBoundFn == null + ? null + : (int index) => domainUpperBoundFn(data[index], index); + final _fillColorFn = fillColorFn == null + ? null + : (int index) => fillColorFn(data[index], index); + final _fillPatternFn = fillPatternFn == null + ? null + : (int index) => fillPatternFn(data[index], index); + final _labelAccessorFn = labelAccessorFn == null + ? null + : (int index) => labelAccessorFn(data[index], index); + final _insideLabelStyleAccessorFn = insideLabelStyleAccessorFn == null + ? null + : (int index) => insideLabelStyleAccessorFn(data[index], index); + final _outsideLabelStyleAccessorFn = outsideLabelStyleAccessorFn == null + ? null + : (int index) => outsideLabelStyleAccessorFn(data[index], index); + final _measureLowerBoundFn = measureLowerBoundFn == null + ? null + : (int index) => measureLowerBoundFn(data[index], index); + final _measureUpperBoundFn = measureUpperBoundFn == null + ? null + : (int index) => measureUpperBoundFn(data[index], index); + final _measureOffsetFn = measureOffsetFn == null + ? null + : (int index) => measureOffsetFn(data[index], index); + final _radiusPxFn = radiusPxFn == null + ? null + : (int index) => radiusPxFn(data[index], index); + final _strokeWidthPxFn = strokeWidthPxFn == null + ? null + : (int index) => strokeWidthPxFn(data[index], index); + + return new Series._internal( + id: id, + data: data, + domainFn: _domainFn, + measureFn: _measureFn, + displayName: displayName, + areaColorFn: _areaColorFn, + colorFn: _colorFn, + dashPatternFn: _dashPatternFn, + domainLowerBoundFn: _domainLowerBoundFn, + domainUpperBoundFn: _domainUpperBoundFn, + fillColorFn: _fillColorFn, + fillPatternFn: _fillPatternFn, + labelAccessorFn: _labelAccessorFn, + insideLabelStyleAccessorFn: _insideLabelStyleAccessorFn, + outsideLabelStyleAccessorFn: _outsideLabelStyleAccessorFn, + measureLowerBoundFn: _measureLowerBoundFn, + measureUpperBoundFn: _measureUpperBoundFn, + measureOffsetFn: _measureOffsetFn, + overlaySeries: overlaySeries, + radiusPxFn: _radiusPxFn, + seriesCategory: seriesCategory, + strokeWidthPxFn: _strokeWidthPxFn, + ); + } + + Series._internal({ + @required this.id, + @required this.data, + @required this.domainFn, + @required this.measureFn, + this.displayName, + this.areaColorFn, + this.colorFn, + this.dashPatternFn, + this.domainLowerBoundFn, + this.domainUpperBoundFn, + this.fillColorFn, + this.fillPatternFn, + this.keyFn, + this.labelAccessorFn, + this.insideLabelStyleAccessorFn, + this.outsideLabelStyleAccessorFn, + this.measureLowerBoundFn, + this.measureUpperBoundFn, + this.measureOffsetFn, + this.overlaySeries = false, + this.radiusPxFn, + this.seriesCategory, + this.strokeWidthPxFn, + }); + + void setAttribute(AttributeKey key, R value) { + this.attributes.setAttr(key, value); + } + + R getAttribute(AttributeKey key) { + return this.attributes.getAttr(key); + } +} + +/// Computed property on series. +/// +/// If the [index] argument is `null`, the accessor is asked to provide a +/// property of [series] as a whole. Accessors are not required to support +/// such usage. +/// +/// Otherwise, [index] must be a valid subscript into a list of `series.length`. +typedef R AccessorFn(int index); + +typedef R TypedAccessorFn(T datum, int index); + +class AttributeKey extends TypedKey { + const AttributeKey(String uniqueKey) : super(uniqueKey); +} + +class SeriesAttributes extends TypedRegistry {} diff --git a/web/charts/common/pubspec.lock b/web/charts/common/pubspec.lock new file mode 100644 index 000000000..319bd45a1 --- /dev/null +++ b/web/charts/common/pubspec.lock @@ -0,0 +1,376 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + collection: + dependency: "direct main" + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: "direct main" + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: "direct main" + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + multi_server_socket: + dependency: transitive + description: + name: multi_server_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.5" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.3" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: "direct main" + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + vm_service_client: + dependency: transitive + description: + name: vm_service_client + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6+2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.2.0 <3.0.0" diff --git a/web/charts/common/pubspec.yaml b/web/charts/common/pubspec.yaml new file mode 100644 index 000000000..41a6f1c55 --- /dev/null +++ b/web/charts/common/pubspec.yaml @@ -0,0 +1,19 @@ +name: charts_common +version: 0.6.0 +description: A common library for charting packages. +author: Charts Team +homepage: https://github.com/google/charts + +environment: + sdk: '>=2.1.0 <3.0.0' + +dependencies: + collection: ^1.14.5 + intl: ^0.15.2 + logging: any + meta: ^1.1.1 + vector_math: ^2.0.8 + +dev_dependencies: + mockito: ^4.0.0 + test: ^1.5.3 diff --git a/web/charts/common/test/chart/bar/bar_label_decorator_test.dart b/web/charts/common/test/chart/bar/bar_label_decorator_test.dart new file mode 100644 index 000000000..5e5090cba --- /dev/null +++ b/web/charts/common/test/chart/bar/bar_label_decorator_test.dart @@ -0,0 +1,404 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Rectangle; +import 'package:charts_common/src/chart/common/processed_series.dart' + show ImmutableSeries; +import 'package:charts_common/src/common/color.dart' show Color; +import 'package:charts_common/src/common/graphics_factory.dart' + show GraphicsFactory; +import 'package:charts_common/src/common/line_style.dart' show LineStyle; +import 'package:charts_common/src/common/text_element.dart' + show TextDirection, TextElement, MaxWidthStrategy; +import 'package:charts_common/src/common/text_measurement.dart' + show TextMeasurement; +import 'package:charts_common/src/common/text_style.dart' show TextStyle; +import 'package:charts_common/src/chart/bar/bar_renderer.dart' + show ImmutableBarRendererElement; +import 'package:charts_common/src/chart/cartesian/axis/spec/axis_spec.dart' + show TextStyleSpec; +import 'package:charts_common/src/chart/common/chart_canvas.dart' + show ChartCanvas; +import 'package:charts_common/src/chart/bar/bar_label_decorator.dart' + show BarLabelDecorator, BarLabelAnchor, BarLabelPosition; +import 'package:charts_common/src/data/series.dart' show AccessorFn; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockCanvas extends Mock implements ChartCanvas {} + +/// A fake [GraphicsFactory] that returns [FakeTextStyle] and [FakeTextElement]. +class FakeGraphicsFactory extends GraphicsFactory { + @override + TextStyle createTextPaint() => new FakeTextStyle(); + + @override + TextElement createTextElement(String text) => new FakeTextElement(text); + + @override + LineStyle createLinePaint() => new MockLinePaint(); +} + +/// Stores [TextStyle] properties for test to verify. +class FakeTextStyle implements TextStyle { + Color color; + int fontSize; + String fontFamily; +} + +/// Fake [TextElement] which returns text length as [horizontalSliceWidth]. +/// +/// Font size is returned for [verticalSliceWidth] and [baseline]. +class FakeTextElement implements TextElement { + final String text; + TextStyle textStyle; + int maxWidth; + MaxWidthStrategy maxWidthStrategy; + TextDirection textDirection; + double opacity; + + FakeTextElement(this.text); + + TextMeasurement get measurement => new TextMeasurement( + horizontalSliceWidth: text.length.toDouble(), + verticalSliceWidth: textStyle.fontSize.toDouble(), + baseline: textStyle.fontSize.toDouble()); +} + +class MockLinePaint extends Mock implements LineStyle {} + +class FakeBarRendererElement implements ImmutableBarRendererElement { + final _series = new MockImmutableSeries(); + final AccessorFn labelAccessor; + final String datum; + final Rectangle bounds; + final List data; + int index; + + FakeBarRendererElement( + this.datum, this.bounds, this.labelAccessor, this.data) { + index = data.indexOf(datum); + when(_series.labelAccessorFn).thenReturn(labelAccessor); + when(_series.data).thenReturn(data); + } + + ImmutableSeries get series => _series; +} + +class MockImmutableSeries extends Mock implements ImmutableSeries {} + +void main() { + ChartCanvas canvas; + GraphicsFactory graphicsFactory; + Rectangle drawBounds; + + setUpAll(() { + canvas = new MockCanvas(); + graphicsFactory = new FakeGraphicsFactory(); + drawBounds = new Rectangle(0, 0, 200, 100); + }); + + group('horizontal bar chart', () { + test('Paint labels with default settings', () { + final data = ['A', 'B']; + final barElements = [ + // 'LabelA' and 'LabelB' both have lengths of 6. + // 'LabelB' would not fit inside the bar in auto setting because it has + // width of 5. + new FakeBarRendererElement( + 'A', new Rectangle(0, 20, 50, 20), (_) => 'LabelA', data), + new FakeBarRendererElement( + 'B', new Rectangle(0, 70, 5, 20), (_) => 'LabelB', data) + ]; + final decorator = new BarLabelDecorator(); + + decorator.decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + // Draw text is called twice (once for each bar) and all 3 parameters were + // captured. Total parameters captured expected to be 6. + expect(captured, hasLength(6)); + // For bar 'A'. + expect(captured[0].maxWidth, equals(50 - decorator.labelPadding * 2)); + expect(captured[0].textDirection, equals(TextDirection.ltr)); + expect(captured[1], equals(decorator.labelPadding)); + expect(captured[2], + equals(30 - decorator.insideLabelStyleSpec.fontSize ~/ 2)); + // For bar 'B'. + expect( + captured[3].maxWidth, equals(200 - 5 - decorator.labelPadding * 2)); + expect(captured[3].textDirection, equals(TextDirection.ltr)); + expect(captured[4], equals(5 + decorator.labelPadding)); + expect(captured[5], + equals(80 - decorator.outsideLabelStyleSpec.fontSize ~/ 2)); + }); + + test('LabelPosition.auto paints inside bar if outside bar has less width', + () { + final barElements = [ + // 'LabelABC' would not fit inside the bar in auto setting because it + // has a width of 8. + new FakeBarRendererElement( + 'A', new Rectangle(0, 0, 6, 20), (_) => 'LabelABC', ['A']), + ]; + // Draw bounds with width of 10 means that space inside the bar is larger. + final smallDrawBounds = new Rectangle(0, 0, 10, 20); + + new BarLabelDecorator( + labelPadding: 0, // Turn off label padding for testing. + insideLabelStyleSpec: new TextStyleSpec(fontSize: 10)) + .decorate(barElements, canvas, graphicsFactory, + drawBounds: smallDrawBounds, + animationPercent: 1.0, + renderingVertically: false); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + expect(captured, hasLength(3)); + expect(captured[0].maxWidth, equals(6)); + expect(captured[0].textDirection, equals(TextDirection.ltr)); + expect(captured[1], equals(0)); + expect(captured[2], equals(5)); + }); + + test('LabelPosition.inside always paints inside the bar', () { + final barElements = [ + // 'LabelABC' would not fit inside the bar in auto setting because it + // has a width of 8. + new FakeBarRendererElement( + 'A', new Rectangle(0, 0, 6, 20), (_) => 'LabelABC', ['A']), + ]; + + new BarLabelDecorator( + labelPosition: BarLabelPosition.inside, + labelPadding: 0, // Turn off label padding for testing. + insideLabelStyleSpec: new TextStyleSpec(fontSize: 10)) + .decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + expect(captured, hasLength(3)); + expect(captured[0].maxWidth, equals(6)); + expect(captured[0].textDirection, equals(TextDirection.ltr)); + expect(captured[1], equals(0)); + expect(captured[2], equals(5)); + }); + + test('LabelPosition.outside always paints outside the bar', () { + final barElements = [ + new FakeBarRendererElement( + 'A', new Rectangle(0, 0, 10, 20), (_) => 'Label', ['A']), + ]; + + new BarLabelDecorator( + labelPosition: BarLabelPosition.outside, + labelPadding: 0, // Turn off label padding for testing. + outsideLabelStyleSpec: new TextStyleSpec(fontSize: 10)) + .decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + expect(captured, hasLength(3)); + expect(captured[0].maxWidth, equals(190)); + expect(captured[0].textDirection, equals(TextDirection.ltr)); + expect(captured[1], equals(10)); + expect(captured[2], equals(5)); + }); + + test('Inside and outside label styles are applied', () { + final data = ['A', 'B']; + final barElements = [ + // 'LabelA' and 'LabelB' both have lengths of 6. + // 'LabelB' would not fit inside the bar in auto setting because it has + // width of 5. + new FakeBarRendererElement( + 'A', new Rectangle(0, 20, 50, 20), (_) => 'LabelA', data), + new FakeBarRendererElement( + 'B', new Rectangle(0, 70, 5, 20), (_) => 'LabelB', data) + ]; + final insideColor = new Color(r: 0, g: 0, b: 0); + final outsideColor = new Color(r: 255, g: 255, b: 255); + final decorator = new BarLabelDecorator( + labelPadding: 0, + insideLabelStyleSpec: new TextStyleSpec( + fontSize: 10, fontFamily: 'insideFont', color: insideColor), + outsideLabelStyleSpec: new TextStyleSpec( + fontSize: 8, fontFamily: 'outsideFont', color: outsideColor)); + + decorator.decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + // Draw text is called twice (once for each bar) and all 3 parameters were + // captured. Total parameters captured expected to be 6. + expect(captured, hasLength(6)); + // For bar 'A'. + expect(captured[0].maxWidth, equals(50)); + expect(captured[0].textDirection, equals(TextDirection.ltr)); + expect(captured[0].textStyle.fontFamily, equals('insideFont')); + expect(captured[0].textStyle.color, equals(insideColor)); + expect(captured[1], equals(0)); + expect(captured[2], equals(30 - 5)); + // For bar 'B'. + expect(captured[3].maxWidth, equals(200 - 5)); + expect(captured[3].textDirection, equals(TextDirection.ltr)); + expect(captured[3].textStyle.fontFamily, equals('outsideFont')); + expect(captured[3].textStyle.color, equals(outsideColor)); + expect(captured[4], equals(5)); + expect(captured[5], equals(80 - 4)); + }); + + test('TextAnchor.end starts on the right most of bar', () { + final barElements = [ + new FakeBarRendererElement( + 'A', new Rectangle(0, 0, 10, 20), (_) => 'LabelA', ['A']) + ]; + + new BarLabelDecorator( + labelAnchor: BarLabelAnchor.end, + labelPosition: BarLabelPosition.inside, + labelPadding: 0, // Turn off label padding for testing. + insideLabelStyleSpec: new TextStyleSpec(fontSize: 10)) + .decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + expect(captured, hasLength(3)); + expect(captured[0].maxWidth, equals(10)); + expect(captured[0].textDirection, equals(TextDirection.rtl)); + expect(captured[1], equals(10)); + expect(captured[2], equals(5)); + }); + + test('RTL TextAnchor.start starts on the right', () { + final barElements = [ + new FakeBarRendererElement( + 'A', new Rectangle(0, 0, 10, 20), (_) => 'LabelA', ['A']) + ]; + + new BarLabelDecorator( + labelAnchor: BarLabelAnchor.start, + labelPosition: BarLabelPosition.inside, + labelPadding: 0, // Turn off label padding for testing. + insideLabelStyleSpec: new TextStyleSpec(fontSize: 10)) + .decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false, + rtl: true); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + expect(captured, hasLength(3)); + expect(captured[0].maxWidth, equals(10)); + expect(captured[0].textDirection, equals(TextDirection.rtl)); + expect(captured[1], equals(10)); + expect(captured[2], equals(5)); + }); + + test('RTL TextAnchor.end starts on the left', () { + final barElements = [ + new FakeBarRendererElement( + 'A', new Rectangle(0, 0, 10, 20), (_) => 'LabelA', ['A']) + ]; + + new BarLabelDecorator( + labelAnchor: BarLabelAnchor.end, + labelPosition: BarLabelPosition.inside, + labelPadding: 0, // Turn off label padding for testing. + insideLabelStyleSpec: new TextStyleSpec(fontSize: 10)) + .decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false, + rtl: true); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + expect(captured, hasLength(3)); + expect(captured[0].maxWidth, equals(10)); + expect(captured[0].textDirection, equals(TextDirection.ltr)); + expect(captured[1], equals(0)); + expect(captured[2], equals(5)); + }); + }); + + group('Null and empty label scenarios', () { + test('Skip label if label accessor does not exist', () { + final barElements = [ + new FakeBarRendererElement( + 'A', new Rectangle(0, 0, 10, 20), null, ['A']) + ]; + + new BarLabelDecorator().decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false); + + verifyNever(canvas.drawText(any, any, any)); + }); + + test('Skip label if label is null or empty', () { + final data = ['A', 'B']; + final barElements = [ + new FakeBarRendererElement( + 'A', new Rectangle(0, 0, 10, 20), null, data), + new FakeBarRendererElement( + 'B', new Rectangle(0, 50, 10, 20), (_) => '', data), + ]; + + new BarLabelDecorator().decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false); + + verifyNever(canvas.drawText(any, any, any)); + }); + + test('Skip label if no width available', () { + final barElements = [ + new FakeBarRendererElement( + 'A', new Rectangle(0, 0, 200, 20), (_) => 'a', ['A']) + ]; + + new BarLabelDecorator( + labelPadding: 0, + labelPosition: BarLabelPosition.outside, + ).decorate(barElements, canvas, graphicsFactory, + drawBounds: drawBounds, + animationPercent: 1.0, + renderingVertically: false); + + verifyNever(canvas.drawText(any, any, any)); + }); + }); +} diff --git a/web/charts/common/test/chart/bar/bar_renderer_test.dart b/web/charts/common/test/chart/bar/bar_renderer_test.dart new file mode 100644 index 000000000..c0c31705c --- /dev/null +++ b/web/charts/common/test/chart/bar/bar_renderer_test.dart @@ -0,0 +1,882 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/bar/bar_renderer.dart'; +import 'package:charts_common/src/chart/bar/bar_renderer_config.dart'; +import 'package:charts_common/src/chart/bar/base_bar_renderer.dart'; +import 'package:charts_common/src/chart/bar/base_bar_renderer_config.dart'; +import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart'; +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart' + show MutableSeries; +import 'package:charts_common/src/common/material_palette.dart' + show MaterialPalette; +import 'package:charts_common/src/common/color.dart'; +import 'package:charts_common/src/data/series.dart' show Series; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +/// Datum/Row for the chart. +class MyRow { + final String campaign; + final int clickCount; + MyRow(this.campaign, this.clickCount); +} + +class MockAxis extends Mock implements Axis {} + +class MockCanvas extends Mock implements ChartCanvas {} + +class MockContext extends Mock implements ChartContext {} + +class MockChart extends Mock implements CartesianChart {} + +class FakeBarRenderer extends BarRenderer { + int paintBarCallCount = 0; + + factory FakeBarRenderer({BarRendererConfig config, String rendererId}) { + return new FakeBarRenderer._internal( + config: config, rendererId: rendererId); + } + + FakeBarRenderer._internal({BarRendererConfig config, String rendererId}) + : super.internal(config: config, rendererId: rendererId); + + @override + void paintBar(ChartCanvas canvas, double animationPercent, + Iterable> barElements) { + paintBarCallCount += 1; + } +} + +void main() { + BarRenderer renderer; + List> seriesList; + List> groupedStackedSeriesList; + + ///////////////////////////////////////// + // Convenience methods for creating mocks. + ///////////////////////////////////////// + _configureBaseRenderer(BaseBarRenderer renderer, bool vertical) { + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(false); + when(context.isRtl).thenReturn(false); + final verticalChart = new MockChart(); + when(verticalChart.vertical).thenReturn(vertical); + when(verticalChart.context).thenReturn(context); + renderer.onAttach(verticalChart); + + return renderer; + } + + BarRenderer makeRenderer({BarRendererConfig config}) { + final renderer = new BarRenderer(config: config); + _configureBaseRenderer(renderer, true); + return renderer; + } + + FakeBarRenderer makeFakeRenderer({BarRendererConfig config}) { + final renderer = new FakeBarRenderer(config: config); + _configureBaseRenderer(renderer, true); + return renderer; + } + + setUp(() { + var myFakeDesktopAData = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 25), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + + var myFakeTabletAData = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 25), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + + var myFakeMobileAData = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 25), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + + var myFakeDesktopBData = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 25), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + + var myFakeTabletBData = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 25), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + + var myFakeMobileBData = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 25), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + + seriesList = [ + new MutableSeries(new Series( + id: 'Desktop', + colorFn: (_, __) => MaterialPalette.blue.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeDesktopAData)), + new MutableSeries(new Series( + id: 'Tablet', + colorFn: (_, __) => MaterialPalette.red.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeTabletAData)), + new MutableSeries(new Series( + id: 'Mobile', + colorFn: (_, __) => MaterialPalette.green.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeMobileAData)) + ]; + + groupedStackedSeriesList = [ + new MutableSeries(new Series( + id: 'Desktop A', + seriesCategory: 'A', + colorFn: (_, __) => MaterialPalette.blue.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeDesktopAData)), + new MutableSeries(new Series( + id: 'Tablet A', + seriesCategory: 'A', + colorFn: (_, __) => MaterialPalette.red.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeTabletAData)), + new MutableSeries(new Series( + id: 'Mobile A', + seriesCategory: 'A', + colorFn: (_, __) => MaterialPalette.green.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeMobileAData)), + new MutableSeries(new Series( + id: 'Desktop B', + seriesCategory: 'B', + colorFn: (_, __) => MaterialPalette.blue.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeDesktopBData)), + new MutableSeries(new Series( + id: 'Tablet B', + seriesCategory: 'B', + colorFn: (_, __) => MaterialPalette.red.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeTabletBData)), + new MutableSeries(new Series( + id: 'Mobile B', + seriesCategory: 'B', + colorFn: (_, __) => MaterialPalette.green.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeMobileBData)) + ]; + }); + + group('preprocess', () { + test('with grouped bars', () { + renderer = makeRenderer( + config: new BarRendererConfig(groupingType: BarGroupingType.grouped)); + + renderer.preprocessSeries(seriesList); + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + + // Validate Tablet series. + series = seriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + + // Validate Mobile series. + series = seriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(2)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + }); + + test('with grouped stacked bars', () { + renderer = makeRenderer( + config: new BarRendererConfig( + groupingType: BarGroupingType.groupedStacked)); + + renderer.preprocessSeries(groupedStackedSeriesList); + + expect(groupedStackedSeriesList.length, equals(6)); + + // Validate Desktop A series. + var series = groupedStackedSeriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(0.5)); + expect(series.getAttr(stackKeyKey), equals('A')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(10)); + expect(element.measureOffsetPlusMeasure, equals(15)); + expect(series.measureOffsetFn(0), equals(10)); + + // Validate Tablet A series. + series = groupedStackedSeriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(0.5)); + expect(series.getAttr(stackKeyKey), equals('A')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + + // Validate Mobile A series. + series = groupedStackedSeriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(0.5)); + expect(series.getAttr(stackKeyKey), equals('A')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + + // Validate Desktop B series. + series = groupedStackedSeriesList[3]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.5)); + expect(series.getAttr(barGroupWeightKey), equals(0.5)); + expect(series.getAttr(stackKeyKey), equals('B')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(10)); + expect(element.measureOffsetPlusMeasure, equals(15)); + expect(series.measureOffsetFn(0), equals(10)); + + // Validate Tablet B series. + series = groupedStackedSeriesList[4]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.5)); + expect(series.getAttr(barGroupWeightKey), equals(0.5)); + expect(series.getAttr(stackKeyKey), equals('B')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + + // Validate Mobile B series. + series = groupedStackedSeriesList[5]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.5)); + expect(series.getAttr(barGroupWeightKey), equals(0.5)); + expect(series.getAttr(stackKeyKey), equals('B')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + }); + + test('with stacked bars', () { + renderer = makeRenderer( + config: new BarRendererConfig(groupingType: BarGroupingType.stacked)); + + renderer.preprocessSeries(seriesList); + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(10)); + expect(element.measureOffsetPlusMeasure, equals(15)); + expect(series.measureOffsetFn(0), equals(10)); + + // Validate Tablet series. + series = seriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + + // Validate Mobile series. + series = seriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + }); + + test('with stacked bars containing zero and null', () { + // Set up some nulls and zeros in the data. + seriesList[2].data[0] = new MyRow('MyCampaign1', null); + seriesList[2].data[2] = new MyRow('MyCampaign3', 0); + + seriesList[1].data[1] = new MyRow('MyCampaign2', null); + seriesList[1].data[3] = new MyRow('MyOtherCampaign', 0); + + seriesList[0].data[2] = new MyRow('MyCampaign3', 0); + + renderer = makeRenderer( + config: new BarRendererConfig(groupingType: BarGroupingType.stacked)); + + renderer.preprocessSeries(seriesList); + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + var elementsList = series.getAttr(barElementsKey); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + + element = elementsList[1]; + expect(element.measureOffset, equals(25)); + expect(element.measureOffsetPlusMeasure, equals(50)); + expect(series.measureOffsetFn(1), equals(25)); + + element = elementsList[2]; + expect(element.measureOffset, equals(100)); + expect(element.measureOffsetPlusMeasure, equals(100)); + expect(series.measureOffsetFn(2), equals(100)); + + element = elementsList[3]; + expect(element.measureOffset, equals(75)); + expect(element.measureOffsetPlusMeasure, equals(150)); + expect(series.measureOffsetFn(3), equals(75)); + + // Validate Tablet series. + series = seriesList[1]; + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + + element = elementsList[1]; + expect(element.measureOffset, equals(25)); + expect(element.measureOffsetPlusMeasure, equals(25)); + expect(series.measureOffsetFn(1), equals(25)); + + element = elementsList[2]; + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(100)); + expect(series.measureOffsetFn(2), equals(0)); + + element = elementsList[3]; + expect(element.measureOffset, equals(75)); + expect(element.measureOffsetPlusMeasure, equals(75)); + expect(series.measureOffsetFn(3), equals(75)); + + // Validate Mobile series. + series = seriesList[2]; + elementsList = series.getAttr(barElementsKey); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(0)); + expect(series.measureOffsetFn(0), equals(0)); + + element = elementsList[1]; + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(25)); + expect(series.measureOffsetFn(1), equals(0)); + + element = elementsList[2]; + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(0)); + expect(series.measureOffsetFn(2), equals(0)); + + element = elementsList[3]; + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(75)); + expect(series.measureOffsetFn(3), equals(0)); + }); + }); + + group('preprocess weight pattern', () { + test('with grouped bars', () { + renderer = makeRenderer( + config: new BarRendererConfig( + groupingType: BarGroupingType.grouped, weightPattern: [3, 2, 1])); + + renderer.preprocessSeries(seriesList); + + // Verify that bar group weights are proportional to the sum of the used + // segments of weightPattern. The weightPattern should be distributed + // amongst bars that share the same domain value. + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(0.5)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + + // Validate Tablet series. + series = seriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.5)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + + // Validate Mobile series. + series = seriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(2)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.5 + 1 / 3)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 6)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + }); + + test('with grouped stacked bars', () { + renderer = makeRenderer( + config: new BarRendererConfig( + groupingType: BarGroupingType.groupedStacked, + weightPattern: [2, 1])); + + renderer.preprocessSeries(groupedStackedSeriesList); + + // Verify that bar group weights are proportional to the sum of the used + // segments of weightPattern. The weightPattern should be distributed + // amongst bars that share the same domain and series category values. + + expect(groupedStackedSeriesList.length, equals(6)); + + // Validate Desktop A series. + var series = groupedStackedSeriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(2 / 3)); + expect(series.getAttr(stackKeyKey), equals('A')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(10)); + expect(element.measureOffsetPlusMeasure, equals(15)); + expect(series.measureOffsetFn(0), equals(10)); + + // Validate Tablet A series. + series = groupedStackedSeriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(2 / 3)); + expect(series.getAttr(stackKeyKey), equals('A')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + + // Validate Mobile A series. + series = groupedStackedSeriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(2 / 3)); + expect(series.getAttr(stackKeyKey), equals('A')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + + // Validate Desktop B series. + series = groupedStackedSeriesList[3]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('B')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(10)); + expect(element.measureOffsetPlusMeasure, equals(15)); + expect(series.measureOffsetFn(0), equals(10)); + + // Validate Tablet B series. + series = groupedStackedSeriesList[4]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('B')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + + // Validate Mobile B series. + series = groupedStackedSeriesList[5]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(2)); + expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('B')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + }); + + test('with stacked bars - weightPattern not used', () { + renderer = makeRenderer( + config: new BarRendererConfig( + groupingType: BarGroupingType.stacked, weightPattern: [2, 1])); + + renderer.preprocessSeries(seriesList); + + // Verify that weightPattern is not used, since stacked bars have only a + // single group per domain value. + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(10)); + expect(element.measureOffsetPlusMeasure, equals(15)); + expect(series.measureOffsetFn(0), equals(10)); + + // Validate Tablet series. + series = seriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + + // Validate Mobile series. + series = seriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + }); + }); + + group('null measure', () { + test('only include null in draw if animating from a non null measure', () { + // Helper to create series list for this test only. + List> _createSeriesList(List data) { + final domainAxis = new MockAxis(); + when(domainAxis.rangeBand).thenReturn(100.0); + when(domainAxis.getLocation('MyCampaign1')).thenReturn(20.0); + when(domainAxis.getLocation('MyCampaign2')).thenReturn(40.0); + when(domainAxis.getLocation('MyCampaign3')).thenReturn(60.0); + when(domainAxis.getLocation('MyOtherCampaign')).thenReturn(80.0); + final measureAxis = new MockAxis(); + when(measureAxis.getLocation(0)).thenReturn(0.0); + when(measureAxis.getLocation(5)).thenReturn(5.0); + when(measureAxis.getLocation(75)).thenReturn(75.0); + when(measureAxis.getLocation(100)).thenReturn(100.0); + + final color = new Color.fromHex(code: '#000000'); + + final series = new MutableSeries(new Series( + id: 'Desktop', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + colorFn: (_, __) => color, + fillColorFn: (_, __) => color, + dashPatternFn: (_, __) => [1], + data: data)) + ..setAttr(domainAxisKey, domainAxis) + ..setAttr(measureAxisKey, measureAxis); + + return [series]; + } + + final canvas = new MockCanvas(); + + final myDataWithNull = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', null), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + final seriesListWithNull = _createSeriesList(myDataWithNull); + + final myDataWithMeasures = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 0), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + final seriesListWithMeasures = _createSeriesList(myDataWithMeasures); + + final renderer = makeFakeRenderer( + config: new BarRendererConfig(groupingType: BarGroupingType.grouped)); + + // Verify that only 3 bars are drawn for an initial draw with null data. + renderer.preprocessSeries(seriesListWithNull); + renderer.update(seriesListWithNull, true); + renderer.paintBarCallCount = 0; + renderer.paint(canvas, 0.5); + expect(renderer.paintBarCallCount, equals(3)); + + // On animation complete, verify that only 3 bars are drawn. + renderer.paintBarCallCount = 0; + renderer.paint(canvas, 1.0); + expect(renderer.paintBarCallCount, equals(3)); + + // Change series list where there are measures on all values, verify all + // 4 bars were drawn + renderer.preprocessSeries(seriesListWithMeasures); + renderer.update(seriesListWithMeasures, true); + renderer.paintBarCallCount = 0; + renderer.paint(canvas, 0.5); + expect(renderer.paintBarCallCount, equals(4)); + + // Change series to one with null measures, verifies all 4 bars drawn + renderer.preprocessSeries(seriesListWithNull); + renderer.update(seriesListWithNull, true); + renderer.paintBarCallCount = 0; + renderer.paint(canvas, 0.5); + expect(renderer.paintBarCallCount, equals(4)); + + // On animation complete, verify that only 3 bars are drawn. + renderer.paintBarCallCount = 0; + renderer.paint(canvas, 1.0); + expect(renderer.paintBarCallCount, equals(3)); + }); + }); +} diff --git a/web/charts/common/test/chart/bar/bar_target_line_renderer_test.dart b/web/charts/common/test/chart/bar/bar_target_line_renderer_test.dart new file mode 100644 index 000000000..863e11e49 --- /dev/null +++ b/web/charts/common/test/chart/bar/bar_target_line_renderer_test.dart @@ -0,0 +1,653 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point, Rectangle; +import 'package:charts_common/src/chart/bar/bar_target_line_renderer.dart'; +import 'package:charts_common/src/chart/bar/bar_target_line_renderer_config.dart'; +import 'package:charts_common/src/chart/bar/base_bar_renderer.dart'; +import 'package:charts_common/src/chart/bar/base_bar_renderer_config.dart'; +import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart'; +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart' + show MutableSeries; +import 'package:charts_common/src/common/color.dart'; +import 'package:charts_common/src/data/series.dart' show Series; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +/// Datum/Row for the chart. +class MyRow { + final String campaign; + final int clickCount; + MyRow(this.campaign, this.clickCount); +} + +class MockAxis extends Mock implements Axis {} + +class MockCanvas extends Mock implements ChartCanvas { + final drawLinePointsList = >[]; + + void drawLine( + {List points, + Rectangle clipBounds, + Color fill, + Color stroke, + bool roundEndCaps, + double strokeWidthPx, + List dashPattern}) { + drawLinePointsList.add(points); + } +} + +class MockContext extends Mock implements ChartContext {} + +class MockChart extends Mock implements CartesianChart {} + +void main() { + BarTargetLineRenderer renderer; + List> seriesList; + + ///////////////////////////////////////// + // Convenience methods for creating mocks. + ///////////////////////////////////////// + _configureBaseRenderer(BaseBarRenderer renderer, bool vertical) { + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(false); + when(context.isRtl).thenReturn(false); + final verticalChart = new MockChart(); + when(verticalChart.vertical).thenReturn(vertical); + when(verticalChart.context).thenReturn(context); + renderer.onAttach(verticalChart); + + return renderer; + } + + BarTargetLineRenderer makeRenderer({BarTargetLineRendererConfig config}) { + final renderer = new BarTargetLineRenderer(config: config); + _configureBaseRenderer(renderer, true); + return renderer; + } + + setUp(() { + var myFakeDesktopData = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 25), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + + var myFakeTabletData = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 25), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + + var myFakeMobileData = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 25), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + + seriesList = [ + new MutableSeries(new Series( + id: 'Desktop', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeDesktopData)), + new MutableSeries(new Series( + id: 'Tablet', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeTabletData)), + new MutableSeries(new Series( + id: 'Mobile', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeMobileData)) + ]; + }); + + group('preprocess', () { + test('with grouped bar target lines', () { + renderer = makeRenderer( + config: new BarTargetLineRendererConfig( + groupingType: BarGroupingType.grouped)); + + renderer.preprocessSeries(seriesList); + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Tablet series. + series = seriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Mobile series. + series = seriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(2)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(2 / 3)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + }); + + test('with stacked bar target lines', () { + renderer = makeRenderer( + config: new BarTargetLineRendererConfig( + groupingType: BarGroupingType.stacked)); + + renderer.preprocessSeries(seriesList); + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(10)); + expect(element.measureOffsetPlusMeasure, equals(15)); + expect(series.measureOffsetFn(0), equals(10)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Tablet series. + series = seriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Mobile series. + series = seriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + }); + + test('with stacked bar target lines containing zero and null', () { + // Set up some nulls and zeros in the data. + seriesList[2].data[0] = new MyRow('MyCampaign1', null); + seriesList[2].data[2] = new MyRow('MyCampaign3', 0); + + seriesList[1].data[1] = new MyRow('MyCampaign2', null); + seriesList[1].data[3] = new MyRow('MyOtherCampaign', 0); + + seriesList[0].data[2] = new MyRow('MyCampaign3', 0); + + renderer = makeRenderer( + config: new BarTargetLineRendererConfig( + groupingType: BarGroupingType.stacked)); + + renderer.preprocessSeries(seriesList); + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + var elementsList = series.getAttr(barElementsKey); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + expect(element.strokeWidthPx, equals(3)); + + element = elementsList[1]; + expect(element.measureOffset, equals(25)); + expect(element.measureOffsetPlusMeasure, equals(50)); + expect(series.measureOffsetFn(1), equals(25)); + expect(element.strokeWidthPx, equals(3)); + + element = elementsList[2]; + expect(element.measureOffset, equals(100)); + expect(element.measureOffsetPlusMeasure, equals(100)); + expect(series.measureOffsetFn(2), equals(100)); + expect(element.strokeWidthPx, equals(3)); + + element = elementsList[3]; + expect(element.measureOffset, equals(75)); + expect(element.measureOffsetPlusMeasure, equals(150)); + expect(series.measureOffsetFn(3), equals(75)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Tablet series. + series = seriesList[1]; + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + + element = elementsList[1]; + expect(element.measureOffset, equals(25)); + expect(element.measureOffsetPlusMeasure, equals(25)); + expect(series.measureOffsetFn(1), equals(25)); + expect(element.strokeWidthPx, equals(3)); + + element = elementsList[2]; + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(100)); + expect(series.measureOffsetFn(2), equals(0)); + expect(element.strokeWidthPx, equals(3)); + + element = elementsList[3]; + expect(element.measureOffset, equals(75)); + expect(element.measureOffsetPlusMeasure, equals(75)); + expect(series.measureOffsetFn(3), equals(75)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Mobile series. + series = seriesList[2]; + elementsList = series.getAttr(barElementsKey); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(0)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + + element = elementsList[1]; + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(25)); + expect(series.measureOffsetFn(1), equals(0)); + expect(element.strokeWidthPx, equals(3)); + + element = elementsList[2]; + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(0)); + expect(series.measureOffsetFn(2), equals(0)); + expect(element.strokeWidthPx, equals(3)); + + element = elementsList[3]; + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(75)); + expect(series.measureOffsetFn(3), equals(0)); + expect(element.strokeWidthPx, equals(3)); + }); + }); + + test('with stroke width target lines', () { + renderer = makeRenderer( + config: new BarTargetLineRendererConfig( + groupingType: BarGroupingType.grouped, strokeWidthPx: 5.0)); + + renderer.preprocessSeries(seriesList); + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + var elementsList = series.getAttr(barElementsKey); + + var element = elementsList[0]; + expect(element.strokeWidthPx, equals(5)); + + element = elementsList[1]; + expect(element.strokeWidthPx, equals(5)); + + element = elementsList[2]; + expect(element.strokeWidthPx, equals(5)); + + element = elementsList[3]; + expect(element.strokeWidthPx, equals(5)); + + // Validate Tablet series. + series = seriesList[1]; + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.strokeWidthPx, equals(5)); + + element = elementsList[1]; + expect(element.strokeWidthPx, equals(5)); + + element = elementsList[2]; + expect(element.strokeWidthPx, equals(5)); + + element = elementsList[3]; + expect(element.strokeWidthPx, equals(5)); + + // Validate Mobile series. + series = seriesList[2]; + elementsList = series.getAttr(barElementsKey); + + element = elementsList[0]; + expect(element.strokeWidthPx, equals(5)); + + element = elementsList[1]; + expect(element.strokeWidthPx, equals(5)); + + element = elementsList[2]; + expect(element.strokeWidthPx, equals(5)); + + element = elementsList[3]; + expect(element.strokeWidthPx, equals(5)); + }); + + group('preprocess with weight pattern', () { + test('with grouped bar target lines', () { + renderer = makeRenderer( + config: new BarTargetLineRendererConfig( + groupingType: BarGroupingType.grouped, weightPattern: [3, 2, 1])); + + renderer.preprocessSeries(seriesList); + + // Verify that bar group weights are proportional to the sum of the used + // segments of weightPattern. The weightPattern should be distributed + // amongst bars that share the same domain value. + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(0.5)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Tablet series. + series = seriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(1)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.5)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 3)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Mobile series. + series = seriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(2)); + expect(series.getAttr(barGroupCountKey), equals(3)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.5 + 1 / 3)); + expect(series.getAttr(barGroupWeightKey), equals(1 / 6)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(null)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + }); + + test('with stacked bar target lines - weightPattern not used', () { + renderer = makeRenderer( + config: new BarTargetLineRendererConfig( + groupingType: BarGroupingType.stacked, weightPattern: [2, 1])); + + renderer.preprocessSeries(seriesList); + + // Verify that weightPattern is not used, since stacked bars have only a + // single group per domain value. + + expect(seriesList.length, equals(3)); + + // Validate Desktop series. + var series = seriesList[0]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + var elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + var element = elementsList[0]; + expect(element.barStackIndex, equals(2)); + expect(element.measureOffset, equals(10)); + expect(element.measureOffsetPlusMeasure, equals(15)); + expect(series.measureOffsetFn(0), equals(10)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Tablet series. + series = seriesList[1]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(1)); + expect(element.measureOffset, equals(5)); + expect(element.measureOffsetPlusMeasure, equals(10)); + expect(series.measureOffsetFn(0), equals(5)); + expect(element.strokeWidthPx, equals(3)); + + // Validate Mobile series. + series = seriesList[2]; + expect(series.getAttr(barGroupIndexKey), equals(0)); + expect(series.getAttr(barGroupCountKey), equals(1)); + expect(series.getAttr(previousBarGroupWeightKey), equals(0.0)); + expect(series.getAttr(barGroupWeightKey), equals(1)); + expect(series.getAttr(stackKeyKey), equals('__defaultKey__')); + + elementsList = series.getAttr(barElementsKey); + expect(elementsList.length, equals(4)); + + element = elementsList[0]; + expect(element.barStackIndex, equals(0)); + expect(element.measureOffset, equals(0)); + expect(element.measureOffsetPlusMeasure, equals(5)); + expect(series.measureOffsetFn(0), equals(0)); + expect(element.strokeWidthPx, equals(3)); + }); + }); + + group('null measure', () { + test('only include null in draw if animating from a non null measure', () { + // Helper to create series list for this test only. + List> _createSeriesList(List data) { + final domainAxis = new MockAxis(); + when(domainAxis.rangeBand).thenReturn(100.0); + when(domainAxis.getLocation('MyCampaign1')).thenReturn(20.0); + when(domainAxis.getLocation('MyCampaign2')).thenReturn(40.0); + when(domainAxis.getLocation('MyCampaign3')).thenReturn(60.0); + when(domainAxis.getLocation('MyOtherCampaign')).thenReturn(80.0); + final measureAxis = new MockAxis(); + when(measureAxis.getLocation(0)).thenReturn(0.0); + when(measureAxis.getLocation(5)).thenReturn(5.0); + when(measureAxis.getLocation(75)).thenReturn(75.0); + when(measureAxis.getLocation(100)).thenReturn(100.0); + + final color = new Color.fromHex(code: '#000000'); + + final series = new MutableSeries(new Series( + id: 'Desktop', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + colorFn: (_, __) => color, + fillColorFn: (_, __) => color, + dashPatternFn: (_, __) => [1], + data: data)) + ..setAttr(domainAxisKey, domainAxis) + ..setAttr(measureAxisKey, measureAxis); + + return [series]; + } + + final canvas = new MockCanvas(); + + final myDataWithNull = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', null), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + final seriesListWithNull = _createSeriesList(myDataWithNull); + + final myDataWithMeasures = [ + new MyRow('MyCampaign1', 5), + new MyRow('MyCampaign2', 0), + new MyRow('MyCampaign3', 100), + new MyRow('MyOtherCampaign', 75), + ]; + final seriesListWithMeasures = _createSeriesList(myDataWithMeasures); + + renderer = makeRenderer( + config: new BarTargetLineRendererConfig( + groupingType: BarGroupingType.grouped)); + + // Verify that only 3 lines are drawn for an initial draw with null data. + renderer.preprocessSeries(seriesListWithNull); + renderer.update(seriesListWithNull, true); + canvas.drawLinePointsList.clear(); + renderer.paint(canvas, 0.5); + expect(canvas.drawLinePointsList, hasLength(3)); + + // On animation complete, verify that only 3 lines are drawn. + canvas.drawLinePointsList.clear(); + renderer.paint(canvas, 1.0); + expect(canvas.drawLinePointsList, hasLength(3)); + + // Change series list where there are measures on all values, verify all + // 4 lines were drawn + renderer.preprocessSeries(seriesListWithMeasures); + renderer.update(seriesListWithMeasures, true); + canvas.drawLinePointsList.clear(); + renderer.paint(canvas, 0.5); + expect(canvas.drawLinePointsList, hasLength(4)); + + // Change series to one with null measures, verifies all 4 lines drawn + renderer.preprocessSeries(seriesListWithNull); + renderer.update(seriesListWithNull, true); + canvas.drawLinePointsList.clear(); + renderer.paint(canvas, 0.5); + expect(canvas.drawLinePointsList, hasLength(4)); + + // On animation complete, verify that only 3 lines are drawn. + canvas.drawLinePointsList.clear(); + renderer.paint(canvas, 1.0); + expect(canvas.drawLinePointsList, hasLength(3)); + }); + }); +} diff --git a/web/charts/common/test/chart/bar/renderer_nearest_detail_test.dart b/web/charts/common/test/chart/bar/renderer_nearest_detail_test.dart new file mode 100644 index 000000000..48302dc18 --- /dev/null +++ b/web/charts/common/test/chart/bar/renderer_nearest_detail_test.dart @@ -0,0 +1,1428 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/bar/bar_renderer.dart'; +import 'package:charts_common/src/chart/bar/bar_renderer_config.dart'; +import 'package:charts_common/src/chart/bar/bar_target_line_renderer.dart'; +import 'package:charts_common/src/chart/bar/bar_target_line_renderer_config.dart'; +import 'package:charts_common/src/chart/bar/base_bar_renderer.dart'; +import 'package:charts_common/src/chart/bar/base_bar_renderer_config.dart'; +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/common/color.dart'; +import 'package:charts_common/src/data/series.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +/// Datum/Row for the chart. +class MyRow { + final String campaign; + final int clickCount; + MyRow(this.campaign, this.clickCount); +} + +/// Datum for the time series chart +class MyDateTimeRow { + final DateTime time; + final int clickCount; + MyDateTimeRow(this.time, this.clickCount); +} + +// TODO: Test in RTL context as well. + +class MockContext extends Mock implements ChartContext {} + +class MockChart extends Mock implements CartesianChart {} + +class MockOrdinalAxis extends Mock implements OrdinalAxis {} + +class MockNumericAxis extends Mock implements Axis {} + +class MockDateTimeAxis extends Mock implements Axis {} + +class MockCanvas extends Mock implements ChartCanvas {} + +void main() { + final date0 = new DateTime(2018, 2, 1); + final date1 = new DateTime(2018, 2, 7); + final dateOutsideViewport = new DateTime(2018, 1, 1); + + ///////////////////////////////////////// + // Convenience methods for creating mocks. + ///////////////////////////////////////// + _configureBaseRenderer(BaseBarRenderer renderer, bool vertical) { + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(false); + when(context.isRtl).thenReturn(false); + final verticalChart = new MockChart(); + when(verticalChart.vertical).thenReturn(vertical); + when(verticalChart.context).thenReturn(context); + renderer.onAttach(verticalChart); + + final layoutBounds = vertical + ? new Rectangle(70, 20, 230, 100) + : new Rectangle(70, 20, 100, 230); + renderer.layout(layoutBounds, layoutBounds); + return renderer; + } + + BaseBarRenderer _makeBarRenderer({bool vertical, BarGroupingType groupType}) { + final renderer = + new BarRenderer(config: new BarRendererConfig(groupingType: groupType)); + _configureBaseRenderer(renderer, vertical); + return renderer; + } + + BaseBarRenderer _makeBarTargetRenderer( + {bool vertical, BarGroupingType groupType}) { + final renderer = new BarTargetLineRenderer( + config: new BarTargetLineRendererConfig(groupingType: groupType)); + _configureBaseRenderer(renderer, vertical); + return renderer; + } + + MutableSeries _makeSeries( + {String id, String seriesCategory, bool vertical = true}) { + final data = [ + new MyRow('camp0', 10), + new MyRow('camp1', 10), + ]; + + final series = new MutableSeries(new Series( + id: id, + data: data, + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.clickCount, + seriesCategory: seriesCategory, + )); + + series.measureOffsetFn = (_) => 0.0; + series.colorFn = (_) => new Color.fromHex(code: '#000000'); + + // Mock the Domain axis results. + final domainAxis = new MockOrdinalAxis(); + when(domainAxis.rangeBand).thenReturn(100.0); + final domainOffset = vertical ? 70.0 : 20.0; + when(domainAxis.getLocation('camp0')) + .thenReturn(domainOffset + 10.0 + 50.0); + when(domainAxis.getLocation('camp1')) + .thenReturn(domainOffset + 10.0 + 100.0 + 10.0 + 50.0); + when(domainAxis.getLocation('outsideViewport')).thenReturn(-51.0); + + if (vertical) { + when(domainAxis.getDomain(100.0)).thenReturn('camp0'); + when(domainAxis.getDomain(93.0)).thenReturn('camp0'); + when(domainAxis.getDomain(130.0)).thenReturn('camp0'); + when(domainAxis.getDomain(65.0)).thenReturn('outsideViewport'); + } else { + when(domainAxis.getDomain(50.0)).thenReturn('camp0'); + when(domainAxis.getDomain(43.0)).thenReturn('camp0'); + when(domainAxis.getDomain(80.0)).thenReturn('camp0'); + } + series.setAttr(domainAxisKey, domainAxis); + + // Mock the Measure axis results. + final measureAxis = new MockNumericAxis(); + if (vertical) { + when(measureAxis.getLocation(0.0)).thenReturn(20.0 + 100.0); + when(measureAxis.getLocation(10.0)).thenReturn(20.0 + 100.0 - 10.0); + when(measureAxis.getLocation(20.0)).thenReturn(20.0 + 100.0 - 20.0); + } else { + when(measureAxis.getLocation(0.0)).thenReturn(70.0); + when(measureAxis.getLocation(10.0)).thenReturn(70.0 + 10.0); + when(measureAxis.getLocation(20.0)).thenReturn(70.0 + 20.0); + } + series.setAttr(measureAxisKey, measureAxis); + + return series; + } + + MutableSeries _makeDateTimeSeries( + {String id, String seriesCategory, bool vertical = true}) { + final data = [ + new MyDateTimeRow(date0, 10), + new MyDateTimeRow(date1, 10), + ]; + + final series = new MutableSeries(new Series( + id: id, + data: data, + domainFn: (dynamic row, _) => row.time, + measureFn: (dynamic row, _) => row.clickCount, + seriesCategory: seriesCategory, + )); + + series.measureOffsetFn = (_) => 0.0; + series.colorFn = (_) => new Color.fromHex(code: '#000000'); + + // Mock the Domain axis results. + final domainAxis = new MockDateTimeAxis(); + when(domainAxis.rangeBand).thenReturn(100.0); + final domainOffset = vertical ? 70.0 : 20.0; + when(domainAxis.getLocation(date0)).thenReturn(domainOffset + 10.0 + 50.0); + when(domainAxis.getLocation(date1)) + .thenReturn(domainOffset + 10.0 + 100.0 + 10.0 + 50.0); + when(domainAxis.getLocation(dateOutsideViewport)).thenReturn(-51.0); + + series.setAttr(domainAxisKey, domainAxis); + + // Mock the Measure axis results. + final measureAxis = new MockNumericAxis(); + if (vertical) { + when(measureAxis.getLocation(0.0)).thenReturn(20.0 + 100.0); + when(measureAxis.getLocation(10.0)).thenReturn(20.0 + 100.0 - 10.0); + when(measureAxis.getLocation(20.0)).thenReturn(20.0 + 100.0 - 20.0); + } else { + when(measureAxis.getLocation(0.0)).thenReturn(70.0); + when(measureAxis.getLocation(10.0)).thenReturn(70.0 + 10.0); + when(measureAxis.getLocation(20.0)).thenReturn(70.0 + 20.0); + } + series.setAttr(measureAxisKey, measureAxis); + + return series; + } + + bool selectNearestByDomain; + + setUp(() { + selectNearestByDomain = true; + }); + + ///////////////////////////////////////// + // Additional edge test cases + ///////////////////////////////////////// + group('edge cases', () { + test('hit target on missing data in group should highlight group', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo')..data.clear(), + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('bar')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(31)); // 2 + 49 - 20 + expect(closest.measureDistance, equals(0)); + }); + + test('all series without data is skipped', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo')..data.clear(), + _makeSeries(id: 'bar')..data.clear(), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(0)); + }); + + test('single overlay series is skipped', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo')..overlaySeries = true, + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('bar')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(31)); // 2 + 49 - 20 + expect(closest.measureDistance, equals(0)); + }); + + test('all overlay series is skipped', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo')..overlaySeries = true, + _makeSeries(id: 'bar')..overlaySeries = true, + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(0)); + }); + }); + + ///////////////////////////////////////// + // Vertical BarRenderer + ///////////////////////////////////////// + group('Vertical BarRenderer', () { + test('hit test works on bar', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.stacked); + final seriesList = [_makeSeries(id: 'foo')]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 13.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series, equals(seriesList[0])); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + }); + + test('hit test expands to grouped bars', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo'), + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(31)); // 2 + 49 - 20 + expect(next.measureDistance, equals(0)); + }); + + test('hit test expands to stacked bars', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.stacked); + final seriesList = [ + _makeSeries(id: 'foo'), + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 13.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + // For vertical stacked bars, the first series is at the top of the stack. + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('bar')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('foo')); + expect(next.datum, equals(seriesList[0].data[0])); + expect(next.domainDistance, equals(0)); + expect(next.measureDistance, equals(5.0)); + }); + + test('hit test expands to grouped stacked', () { + // Setup + final renderer = _makeBarRenderer( + vertical: true, groupType: BarGroupingType.groupedStacked); + final seriesList = [ + _makeSeries(id: 'foo0', seriesCategory: 'c0'), + _makeSeries(id: 'bar0', seriesCategory: 'c0'), + _makeSeries(id: 'foo1', seriesCategory: 'c1'), + _makeSeries(id: 'bar1', seriesCategory: 'c1'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(4)); + + // For vertical stacked bars, the first series is at the top of the stack. + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('bar0')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + + final other1 = details[1]; + expect(other1.domain, equals('camp0')); + expect(other1.series.id, equals('foo0')); + expect(other1.datum, equals(seriesList[0].data[0])); + expect(other1.domainDistance, equals(0)); + expect(other1.measureDistance, equals(5)); + + var other2 = details[2]; + expect(other2.domain, equals('camp0')); + expect(other2.series.id, equals('bar1')); + expect(other2.datum, equals(seriesList[3].data[0])); + expect(other2.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other2.measureDistance, equals(0)); + + var other3 = details[3]; + expect(other3.domain, equals('camp0')); + expect(other3.series.id, equals('foo1')); + expect(other3.datum, equals(seriesList[2].data[0])); + expect(other3.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other3.measureDistance, equals(5)); + }); + + test('hit test works above bar', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.stacked); + final seriesList = [_makeSeries(id: 'foo')]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 13.0, 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series, equals(seriesList[0])); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(90)); + }); + + test('hit test works between bars in a group', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo'), + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 50.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(1)); + expect(closest.measureDistance, equals(0)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(1)); + expect(next.measureDistance, equals(0)); + }); + + test('no selection for bars outside of viewport', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo')..data.add(new MyRow('outsideViewport', 20)) + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + // Note: point is in the axis, over a bar outside of the viewport. + final details = renderer.getNearestDatumDetailPerSeries( + new Point(65.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(0)); + }); + }); + + ///////////////////////////////////////// + // Horizontal BarRenderer + ///////////////////////////////////////// + group('Horizontal BarRenderer', () { + test('hit test works on bar', () { + // Setup + final renderer = + _makeBarRenderer(vertical: false, groupType: BarGroupingType.stacked); + final seriesList = [ + _makeSeries(id: 'foo', vertical: false) + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 5.0, 20.0 + 10.0 + 13.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series, equals(seriesList[0])); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + }); + + test('hit test expands to grouped bars', () { + // Setup + final renderer = + _makeBarRenderer(vertical: false, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo', vertical: false), + _makeSeries(id: 'bar', vertical: false), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 5.0, 20.0 + 10.0 + 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(31)); // 2 + 49 - 20 + expect(next.measureDistance, equals(0)); + }); + + test('hit test expands to stacked bars', () { + // Setup + final renderer = + _makeBarRenderer(vertical: false, groupType: BarGroupingType.stacked); + final seriesList = [ + _makeSeries(id: 'foo', vertical: false), + _makeSeries(id: 'bar', vertical: false), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 5.0, 20.0 + 10.0 + 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(0)); + expect(next.measureDistance, equals(5.0)); + }); + + test('hit test expands to grouped stacked', () { + // Setup + final renderer = _makeBarRenderer( + vertical: false, groupType: BarGroupingType.groupedStacked); + final seriesList = [ + _makeSeries(id: 'foo0', seriesCategory: 'c0', vertical: false), + _makeSeries(id: 'bar0', seriesCategory: 'c0', vertical: false), + _makeSeries(id: 'foo1', seriesCategory: 'c1', vertical: false), + _makeSeries(id: 'bar1', seriesCategory: 'c1', vertical: false), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 5.0, 20.0 + 10.0 + 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(4)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo0')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + + final other1 = details[1]; + expect(other1.domain, equals('camp0')); + expect(other1.series.id, equals('bar0')); + expect(other1.datum, equals(seriesList[1].data[0])); + expect(other1.domainDistance, equals(0)); + expect(other1.measureDistance, equals(5)); + + var other2 = details[2]; + expect(other2.domain, equals('camp0')); + expect(other2.series.id, equals('foo1')); + expect(other2.datum, equals(seriesList[2].data[0])); + expect(other2.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other2.measureDistance, equals(0)); + + var other3 = details[3]; + expect(other3.domain, equals('camp0')); + expect(other3.series.id, equals('bar1')); + expect(other3.datum, equals(seriesList[3].data[0])); + expect(other3.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other3.measureDistance, equals(5)); + }); + + test('hit test works above bar', () { + // Setup + final renderer = + _makeBarRenderer(vertical: false, groupType: BarGroupingType.stacked); + final seriesList = [ + _makeSeries(id: 'foo', vertical: false) + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 100.0, 20.0 + 10.0 + 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series, equals(seriesList[0])); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(90)); + }); + + test('hit test works between bars in a group', () { + // Setup + final renderer = + _makeBarRenderer(vertical: false, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo', vertical: false), + _makeSeries(id: 'bar', vertical: false), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 5.0, 20.0 + 10.0 + 50.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(1)); + expect(closest.measureDistance, equals(0)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(1)); + expect(next.measureDistance, equals(0)); + }); + }); + + ///////////////////////////////////////// + // Vertical BarTargetRenderer + ///////////////////////////////////////// + group('Vertical BarTargetRenderer', () { + test('hit test works above target', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.stacked); + final seriesList = [_makeSeries(id: 'foo')]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 13.0, 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series, equals(seriesList[0])); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(90)); + }); + + test('hit test expands to grouped bar targets', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo'), + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(5)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(31)); // 2 + 49 - 20 + expect(next.measureDistance, equals(5)); + }); + + test('hit test expands to stacked bar targets', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.stacked); + final seriesList = [ + _makeSeries(id: 'foo'), + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 13.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + // For vertical stacked bars, the first series is at the top of the stack. + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('bar')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(5)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('foo')); + expect(next.datum, equals(seriesList[0].data[0])); + expect(next.domainDistance, equals(0)); + expect(next.measureDistance, equals(15.0)); + }); + + test('hit test expands to grouped stacked', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.groupedStacked); + final seriesList = [ + _makeSeries(id: 'foo0', seriesCategory: 'c0'), + _makeSeries(id: 'bar0', seriesCategory: 'c0'), + _makeSeries(id: 'foo1', seriesCategory: 'c1'), + _makeSeries(id: 'bar1', seriesCategory: 'c1'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(4)); + + // For vertical stacked bars, the first series is at the top of the stack. + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('bar0')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(5)); + + final other1 = details[1]; + expect(other1.domain, equals('camp0')); + expect(other1.series.id, equals('foo0')); + expect(other1.datum, equals(seriesList[0].data[0])); + expect(other1.domainDistance, equals(0)); + expect(other1.measureDistance, equals(15)); + + var other2 = details[2]; + expect(other2.domain, equals('camp0')); + expect(other2.series.id, equals('bar1')); + expect(other2.datum, equals(seriesList[3].data[0])); + expect(other2.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other2.measureDistance, equals(5)); + + var other3 = details[3]; + expect(other3.domain, equals('camp0')); + expect(other3.series.id, equals('foo1')); + expect(other3.datum, equals(seriesList[2].data[0])); + expect(other3.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other3.measureDistance, equals(15)); + }); + + test('hit test works between targets in a group', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo'), + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 50.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(1)); + expect(closest.measureDistance, equals(5)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(1)); + expect(next.measureDistance, equals(5)); + }); + + test('no selection for targets outside of viewport', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo')..data.add(new MyRow('outsideViewport', 20)) + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + // Note: point is in the axis, over a bar outside of the viewport. + final details = renderer.getNearestDatumDetailPerSeries( + new Point(65.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(0)); + }); + }); + + ///////////////////////////////////////// + // Horizontal BarTargetRenderer + ///////////////////////////////////////// + group('Horizontal BarTargetRenderer', () { + test('hit test works above target', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: false, groupType: BarGroupingType.stacked); + final seriesList = [ + _makeSeries(id: 'foo', vertical: false) + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 100.0, 20.0 + 10.0 + 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series, equals(seriesList[0])); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(90)); + }); + + test('hit test expands to grouped bar targets', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: false, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo', vertical: false), + _makeSeries(id: 'bar', vertical: false), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 5.0, 20.0 + 10.0 + 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(5)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(31)); // 2 + 49 - 20 + expect(next.measureDistance, equals(5)); + }); + + test('hit test expands to stacked bar targets', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: false, groupType: BarGroupingType.stacked); + final seriesList = [ + _makeSeries(id: 'foo', vertical: false), + _makeSeries(id: 'bar', vertical: false), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 5.0, 20.0 + 10.0 + 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(5)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(0)); + expect(next.measureDistance, equals(15)); + }); + + test('hit test expands to grouped stacked', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: false, groupType: BarGroupingType.groupedStacked); + final seriesList = [ + _makeSeries(id: 'foo0', seriesCategory: 'c0', vertical: false), + _makeSeries(id: 'bar0', seriesCategory: 'c0', vertical: false), + _makeSeries(id: 'foo1', seriesCategory: 'c1', vertical: false), + _makeSeries(id: 'bar1', seriesCategory: 'c1', vertical: false), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 5.0, 20.0 + 10.0 + 20.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(4)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo0')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(5)); + + final other1 = details[1]; + expect(other1.domain, equals('camp0')); + expect(other1.series.id, equals('bar0')); + expect(other1.datum, equals(seriesList[1].data[0])); + expect(other1.domainDistance, equals(0)); + expect(other1.measureDistance, equals(15)); + + var other2 = details[2]; + expect(other2.domain, equals('camp0')); + expect(other2.series.id, equals('foo1')); + expect(other2.datum, equals(seriesList[2].data[0])); + expect(other2.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other2.measureDistance, equals(5)); + + var other3 = details[3]; + expect(other3.domain, equals('camp0')); + expect(other3.series.id, equals('bar1')); + expect(other3.datum, equals(seriesList[3].data[0])); + expect(other3.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other3.measureDistance, equals(15)); + }); + + test('hit test works between bars in a group', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: false, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeSeries(id: 'foo', vertical: false), + _makeSeries(id: 'bar', vertical: false), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 5.0, 20.0 + 10.0 + 50.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals('camp0')); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(1)); + expect(closest.measureDistance, equals(5)); + + final next = details[1]; + expect(next.domain, equals('camp0')); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(1)); + expect(next.measureDistance, equals(5)); + }); + }); + + ///////////////////////////////////////// + // Bar renderer with datetime axis + ///////////////////////////////////////// + group('with date time axis and vertical bar', () { + test('hit test works on bar', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.stacked); + final seriesList = [_makeDateTimeSeries(id: 'foo')]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 13.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + final closest = details[0]; + expect(closest.domain, equals(date0)); + expect(closest.series, equals(seriesList[0])); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + }); + + test('hit test expands to grouped bars', () { + // Setup + final renderer = + _makeBarRenderer(vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeDateTimeSeries(id: 'foo'), + _makeDateTimeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals(date0)); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(0)); + + final next = details[1]; + expect(next.domain, equals(date0)); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(31)); // 2 + 49 - 20 + expect(next.measureDistance, equals(0)); + }); + + test('hit test expands to stacked bar targets', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.stacked); + final seriesList = [ + _makeDateTimeSeries(id: 'foo'), + _makeDateTimeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 13.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + // For vertical stacked bars, the first series is at the top of the stack. + final closest = details[0]; + expect(closest.domain, equals(date0)); + expect(closest.series.id, equals('bar')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(5)); + + final next = details[1]; + expect(next.domain, equals(date0)); + expect(next.series.id, equals('foo')); + expect(next.datum, equals(seriesList[0].data[0])); + expect(next.domainDistance, equals(0)); + expect(next.measureDistance, equals(15.0)); + }); + + test('hit test expands to grouped stacked', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.groupedStacked); + final seriesList = [ + _makeDateTimeSeries(id: 'foo0', seriesCategory: 'c0'), + _makeDateTimeSeries(id: 'bar0', seriesCategory: 'c0'), + _makeDateTimeSeries(id: 'foo1', seriesCategory: 'c1'), + _makeDateTimeSeries(id: 'bar1', seriesCategory: 'c1'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 20.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(4)); + + // For vertical stacked bars, the first series is at the top of the stack. + final closest = details[0]; + expect(closest.domain, equals(date0)); + expect(closest.series.id, equals('bar0')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(0)); + expect(closest.measureDistance, equals(5)); + + final other1 = details[1]; + expect(other1.domain, equals(date0)); + expect(other1.series.id, equals('foo0')); + expect(other1.datum, equals(seriesList[0].data[0])); + expect(other1.domainDistance, equals(0)); + expect(other1.measureDistance, equals(15)); + + var other2 = details[2]; + expect(other2.domain, equals(date0)); + expect(other2.series.id, equals('bar1')); + expect(other2.datum, equals(seriesList[3].data[0])); + expect(other2.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other2.measureDistance, equals(5)); + + var other3 = details[3]; + expect(other3.domain, equals(date0)); + expect(other3.series.id, equals('foo1')); + expect(other3.datum, equals(seriesList[2].data[0])); + expect(other3.domainDistance, equals(31)); // 2 + 49 - 20 + expect(other3.measureDistance, equals(15)); + }); + + test('hit test works between targets in a group', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeDateTimeSeries(id: 'foo'), + _makeDateTimeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0 + 50.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals(date0)); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(1)); + expect(closest.measureDistance, equals(5)); + + final next = details[1]; + expect(next.domain, equals(date0)); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(1)); + expect(next.measureDistance, equals(5)); + }); + + test('no selection for targets outside of viewport', () { + // Setup + final renderer = _makeBarTargetRenderer( + vertical: true, groupType: BarGroupingType.grouped); + final seriesList = [ + _makeDateTimeSeries(id: 'foo') + ..data.add(new MyDateTimeRow(dateOutsideViewport, 20)) + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + // Note: point is in the axis, over a bar outside of the viewport. + final details = renderer.getNearestDatumDetailPerSeries( + new Point(65.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(0)); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/axis_test.dart b/web/charts/common/test/chart/cartesian/axis/axis_test.dart new file mode 100644 index 000000000..0d2ec4a05 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/axis_test.dart @@ -0,0 +1,60 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/tick_draw_strategy.dart'; +import 'package:charts_common/src/chart/cartesian/axis/scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/spec/tick_spec.dart'; +import 'package:charts_common/src/chart/cartesian/axis/static_tick_provider.dart'; +import 'package:charts_common/src/common/graphics_factory.dart'; +import 'package:charts_common/src/common/text_element.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockTickDrawStrategy extends Mock implements TickDrawStrategy {} + +class MockGraphicsFactory extends Mock implements GraphicsFactory { + TextElement createTextElement(String _) { + return MockTextElement(); + } +} + +class MockTextElement extends Mock implements TextElement {} + +StaticTickProvider _createProvider(List values) => + StaticTickProvider(values.map((v) => TickSpec(v)).toList()); + +void main() { + test('changing first tick only', () { + var axis = NumericAxis( + tickProvider: _createProvider([1, 10]), + ); + + var tester = AxisTester(axis); + axis.tickDrawStrategy = MockTickDrawStrategy(); + axis.graphicsFactory = MockGraphicsFactory(); + tester.scale.range = new ScaleOutputExtent(0, 300); + + axis.updateTicks(); + + axis.tickProvider = _createProvider([5, 10]); + axis.updateTicks(); + + // The old value should still be there as it gets animated out, but the + // values should be sorted by their position. + expect(tester.axisValues, equals([1, 5, 10])); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/axis_tick_test.dart b/web/charts/common/test/chart/cartesian/axis/axis_tick_test.dart new file mode 100644 index 000000000..dd283c9a0 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/axis_tick_test.dart @@ -0,0 +1,186 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/common/text_element.dart'; +import 'package:charts_common/src/common/text_measurement.dart'; +import 'package:charts_common/src/common/text_style.dart'; +import 'package:charts_common/src/chart/cartesian/axis/axis_tick.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick.dart'; +import 'package:test/test.dart'; + +/// Fake [TextElement] for testing. +class FakeTextElement implements TextElement { + final String text; + double opacity; + + TextMeasurement measurement; + TextStyle textStyle; + int maxWidth; + MaxWidthStrategy maxWidthStrategy; + TextDirection textDirection; + + FakeTextElement(this.text); +} + +/// Helper to create a tick for testing. +Tick _createTestTick(String value, double locationPx) { + return new Tick( + value: value, + textElement: new FakeTextElement(value), + locationPx: locationPx); +} + +void _verify(Tick tick, {double location, double opacity}) { + expect(tick.locationPx, equals(location)); + expect((tick.textElement as FakeTextElement).opacity, equals(opacity)); +} + +void main() { + // Tick first render. + test('tick created for the first time', () { + final tick = new AxisTicks(_createTestTick('a', 100.0)); + + // Animate in the tick, there was no previous position to animated in from + // so the tick appears in the target immediately. + tick.setCurrentTick(0.0); + _verify(tick, location: 100.0, opacity: 1.0); + + tick.setCurrentTick(0.25); + _verify(tick, location: 100.0, opacity: 1.0); + + tick.setCurrentTick(0.75); + _verify(tick, location: 100.0, opacity: 1.0); + + tick.setCurrentTick(1.0); + _verify(tick, location: 100.0, opacity: 1.0); + }); + + // Tick that is animated in. + test('tick created with a previous location', () { + final tick = new AxisTicks(_createTestTick('a', 200.0)) + ..animateInFrom(100.0); + + tick.setCurrentTick(0.0); + _verify(tick, location: 100.0, opacity: 0.0); + + tick.setCurrentTick(0.25); + _verify(tick, location: 125.0, opacity: 0.25); + + tick.setCurrentTick(0.75); + _verify(tick, location: 175.0, opacity: 0.75); + + tick.setCurrentTick(1.0); + _verify(tick, location: 200.0, opacity: 1.0); + }); + + // Tick that is being animated out. + test('tick is animated in and then out', () { + final tick = new AxisTicks(_createTestTick('a', 100.0)); + + // Animate in the tick, there was no previous position to animated in from + // so the tick appears in the target immediately. + tick.setCurrentTick(0.25); + _verify(tick, location: 100.0, opacity: 1.0); + + // Change to animate the tick out. + tick.animateOut(0.0); + + expect(tick.markedForRemoval, isTrue); + + tick.setCurrentTick(0.0); + _verify(tick, location: 100.0, opacity: 1.0); + + tick.setCurrentTick(0.25); + _verify(tick, location: 75.0, opacity: 0.75); + + tick.setCurrentTick(0.75); + _verify(tick, location: 25.0, opacity: 0.25); + + tick.setCurrentTick(1.0); + _verify(tick, location: 0.0, opacity: 0.0); + }); + + test('tick target change after reaching target', () { + final tick = new AxisTicks(_createTestTick('a', 100.0)); + + // Animate in the tick. + tick.setCurrentTick(1.0); + _verify(tick, location: 100.0, opacity: 1.0); + + tick.setNewTarget(200.0); + + expect(tick.markedForRemoval, isFalse); + + tick.setCurrentTick(0.0); + _verify(tick, location: 100.0, opacity: 1.0); + + tick.setCurrentTick(0.25); + _verify(tick, location: 125.0, opacity: 1.0); + + tick.setCurrentTick(0.75); + _verify(tick, location: 175.0, opacity: 1.0); + + tick.setCurrentTick(1.0); + _verify(tick, location: 200.0, opacity: 1.0); + }); + + test('tick target change before reaching initial target', () { + final tick = new AxisTicks(_createTestTick('a', 400.0))..animateInFrom(0.0); + + // Animate in the tick. + tick.setCurrentTick(0.25); + _verify(tick, location: 100.0, opacity: 0.25); + + tick.setNewTarget(200.0); + + expect(tick.markedForRemoval, isFalse); + + tick.setCurrentTick(0.0); + _verify(tick, location: 100.0, opacity: 0.25); + + tick.setCurrentTick(0.25); + _verify(tick, location: 125.0, opacity: 0.4375); + + tick.setCurrentTick(0.75); + _verify(tick, location: 175.0, opacity: 0.8125); + + tick.setCurrentTick(1.0); + _verify(tick, location: 200.0, opacity: 1.0); + }); + + test('tick target animate out before reaching initial target', () { + final tick = new AxisTicks(_createTestTick('a', 400.0))..animateInFrom(0.0); + + // Animate in the tick. + tick.setCurrentTick(0.25); + _verify(tick, location: 100.0, opacity: 0.25); + + tick.animateOut(200.0); + + expect(tick.markedForRemoval, isTrue); + + tick.setCurrentTick(0.0); + _verify(tick, location: 100.0, opacity: 0.25); + + tick.setCurrentTick(0.25); + _verify(tick, location: 125.0, opacity: 0.1875); + + tick.setCurrentTick(0.75); + _verify(tick, location: 175.0, opacity: 0.0625); + + tick.setCurrentTick(1.0); + _verify(tick, location: 200.0, opacity: 0.0); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/bucketing_numeric_tick_provider_test.dart b/web/charts/common/test/chart/cartesian/axis/bucketing_numeric_tick_provider_test.dart new file mode 100644 index 000000000..2031785aa --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/bucketing_numeric_tick_provider_test.dart @@ -0,0 +1,180 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart'; +import 'package:charts_common/src/chart/cartesian/axis/collision_report.dart'; +import 'package:charts_common/src/chart/cartesian/axis/numeric_scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart'; +import 'package:charts_common/src/chart/cartesian/axis/numeric_extents.dart'; +import 'package:charts_common/src/chart/cartesian/axis/linear/bucketing_numeric_tick_provider.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/common/unitconverter/unit_converter.dart'; +import 'package:charts_common/src/common/graphics_factory.dart'; +import 'package:charts_common/src/common/line_style.dart'; +import 'package:charts_common/src/common/text_style.dart'; +import 'package:charts_common/src/common/text_element.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockNumericScale extends Mock implements NumericScale {} + +/// A fake draw strategy that reports collision and alternate ticks +/// +/// Reports collision when the tick count is greater than or equal to +/// [collidesAfterTickCount]. +/// +/// Reports alternate rendering after tick count is greater than or equal to +/// [alternateRenderingAfterTickCount]. +class FakeDrawStrategy extends BaseTickDrawStrategy { + final int collidesAfterTickCount; + final int alternateRenderingAfterTickCount; + + FakeDrawStrategy( + this.collidesAfterTickCount, this.alternateRenderingAfterTickCount) + : super(null, new FakeGraphicsFactory()); + + @override + CollisionReport collides(List> ticks, _) { + final ticksCollide = ticks.length >= collidesAfterTickCount; + final alternateTicksUsed = ticks.length >= alternateRenderingAfterTickCount; + + return new CollisionReport( + ticksCollide: ticksCollide, + ticks: ticks, + alternateTicksUsed: alternateTicksUsed); + } + + @override + void draw(ChartCanvas canvas, Tick tick, + {AxisOrientation orientation, + Rectangle axisBounds, + Rectangle drawAreaBounds, + bool isFirst, + bool isLast}) {} +} + +/// A fake [GraphicsFactory] that returns [MockTextStyle] and [MockTextElement]. +class FakeGraphicsFactory extends GraphicsFactory { + @override + TextStyle createTextPaint() => new MockTextStyle(); + + @override + TextElement createTextElement(String text) => new MockTextElement(text); + + @override + LineStyle createLinePaint() => new MockLinePaint(); +} + +class MockTextStyle extends Mock implements TextStyle {} + +class MockTextElement extends Mock implements TextElement { + String text; + + MockTextElement(this.text); +} + +class MockLinePaint extends Mock implements LineStyle {} + +class MockChartContext extends Mock implements ChartContext {} + +/// A celsius to fahrenheit converter for testing axis with unit converter. +class CelsiusToFahrenheitConverter implements UnitConverter { + const CelsiusToFahrenheitConverter(); + + @override + num convert(num value) => (value * 1.8) + 32.0; + + @override + num invert(num value) => (value - 32.0) / 1.8; +} + +void main() { + FakeGraphicsFactory graphicsFactory; + MockNumericScale scale; + BucketingNumericTickProvider tickProvider; + TickFormatter formatter; + ChartContext context; + + setUp(() { + graphicsFactory = new FakeGraphicsFactory(); + scale = new MockNumericScale(); + tickProvider = new BucketingNumericTickProvider(); + formatter = new NumericTickFormatter(); + context = new MockChartContext(); + }); + + group('threshold', () { + test('tick generated correctly with no ticks between it and zero', () { + tickProvider + ..dataIsInWholeNumbers = false + ..threshold = 0.1 + ..showBucket = true + ..setFixedTickCount(21) + ..allowedSteps = [1.0, 2.5, 5.0]; + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(0.1, 0.7)); + when(scale.rangeWidth).thenReturn(1000); + when(scale[0.1]).thenReturn(90.0); + when(scale[0]).thenReturn(100.0); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + // Verify. + // We expect to have 20 ticks, because the expected tick at 0.05 should be + // removed from the list. + expect(ticks, hasLength(20)); + + // Verify that we still have a 0 tick with an empty label. + expect(ticks[0].labelOffsetPx, isNull); + expect(ticks[0].locationPx, equals(100.0)); + expect(ticks[0].value, equals(0.0)); + expect(ticks[0].textElement.text, equals('')); + + // Verify that we have a threshold tick. + expect(ticks[1].labelOffsetPx, equals(5.0)); + expect(ticks[1].locationPx, equals(90.0)); + expect(ticks[1].value, equals(0.10)); + expect(ticks[1].textElement.text, equals('< 0.1')); + + // Verify that the rest of the ticks are all above the threshold in value + // and have normal labels. + var aboveThresholdTicks = ticks.sublist(2); + aboveThresholdTicks.retainWhere((Tick tick) => tick.value > 0.1); + expect(aboveThresholdTicks, hasLength(18)); + + aboveThresholdTicks = ticks.sublist(2); + aboveThresholdTicks.retainWhere((Tick tick) => + tick.textElement.text != '' && !tick.textElement.text.contains('<')); + expect(aboveThresholdTicks, hasLength(18)); + + aboveThresholdTicks = ticks.sublist(2); + aboveThresholdTicks + .retainWhere((Tick tick) => tick.labelOffsetPx == null); + expect(aboveThresholdTicks, hasLength(18)); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/draw_strategy/tick_draw_strategy_test.dart b/web/charts/common/test/chart/cartesian/axis/draw_strategy/tick_draw_strategy_test.dart new file mode 100644 index 000000000..9edb281c2 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/draw_strategy/tick_draw_strategy_test.dart @@ -0,0 +1,408 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; +import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart'; +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/cartesian/axis/spec/axis_spec.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/common/graphics_factory.dart'; +import 'package:charts_common/src/common/line_style.dart'; +import 'package:charts_common/src/common/text_element.dart'; +import 'package:charts_common/src/common/text_measurement.dart'; +import 'package:charts_common/src/common/text_style.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockContext extends Mock implements ChartContext {} + +/// Implementation of [BaseTickDrawStrategy] that does nothing on draw. +class BaseTickDrawStrategyImpl extends BaseTickDrawStrategy { + BaseTickDrawStrategyImpl( + ChartContext chartContext, GraphicsFactory graphicsFactory, + {TextStyleSpec labelStyleSpec, + LineStyleSpec axisLineStyleSpec, + TickLabelAnchor labelAnchor, + TickLabelJustification labelJustification, + int labelOffsetFromAxisPx, + int labelOffsetFromTickPx, + int minimumPaddingBetweenLabelsPx}) + : super(chartContext, graphicsFactory, + labelStyleSpec: labelStyleSpec, + axisLineStyleSpec: axisLineStyleSpec, + labelAnchor: labelAnchor, + labelJustification: labelJustification, + labelOffsetFromAxisPx: labelOffsetFromAxisPx, + labelOffsetFromTickPx: labelOffsetFromTickPx, + minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx); + + void draw(ChartCanvas canvas, Tick tick, + {AxisOrientation orientation, + Rectangle axisBounds, + Rectangle drawAreaBounds, + bool isFirst, + bool isLast}) {} +} + +/// Fake [TextElement] for testing. +/// +/// [baseline] returns the same value as the [verticalSliceWidth] specified. +class FakeTextElement implements TextElement { + final String text; + final TextMeasurement measurement; + TextStyle textStyle; + int maxWidth; + MaxWidthStrategy maxWidthStrategy; + TextDirection textDirection; + double opacity; + + FakeTextElement( + this.text, + this.textDirection, + double horizontalSliceWidth, + double verticalSliceWidth, + ) : measurement = new TextMeasurement( + horizontalSliceWidth: horizontalSliceWidth, + verticalSliceWidth: verticalSliceWidth); +} + +class MockGraphicsFactory extends Mock implements GraphicsFactory {} + +class MockLineStyle extends Mock implements LineStyle {} + +class MockTextStyle extends Mock implements TextStyle {} + +/// Helper function to create [Tick] for testing. +Tick createTick(String value, double locationPx, + {double horizontalWidth, + double verticalWidth, + TextDirection textDirection}) { + return new Tick( + value: value, + locationPx: locationPx, + textElement: new FakeTextElement( + value, textDirection, horizontalWidth, verticalWidth)); +} + +void main() { + GraphicsFactory graphicsFactory; + ChartContext chartContext; + + setUpAll(() { + graphicsFactory = new MockGraphicsFactory(); + when(graphicsFactory.createLinePaint()).thenReturn(new MockLineStyle()); + when(graphicsFactory.createTextPaint()).thenReturn(new MockTextStyle()); + + chartContext = new MockContext(); + when(chartContext.chartContainerIsRtl).thenReturn(false); + when(chartContext.isRtl).thenReturn(false); + }); + + group('collision detection - vertically drawn axis', () { + test('ticks do not collide', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 2); + + final ticks = [ + createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2) + createTick('B', 20.0, verticalWidth: 8.0), // 20.0 - 30.0 (28.0 + 2) + createTick('C', 30.0, verticalWidth: 8.0), // 30.0 - 40.0 (38.0 + 2) + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.left); + + expect(report.ticksCollide, isFalse); + }); + + test('ticks collide because it does not have minimum padding', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 2); + + final ticks = [ + createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2) + createTick('B', 20.0, verticalWidth: 9.0), // 20.0 - 31.0 (28.0 + 3) + createTick('C', 30.0, verticalWidth: 8.0), // 30.0 - 40.0 (38.0 + 2) + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.left); + + expect(report.ticksCollide, isTrue); + }); + + test('first tick causes collision', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 0); + + final ticks = [ + createTick('A', 10.0, verticalWidth: 11.0), // 10.0 - 21.0 + createTick('B', 20.0, verticalWidth: 10.0), // 20.0 - 30.0 + createTick('C', 30.0, verticalWidth: 10.0), // 30.0 - 40.0 + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.left); + + expect(report.ticksCollide, isTrue); + }); + + test('last tick causes collision', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 0); + + final ticks = [ + createTick('A', 10.0, verticalWidth: 10.0), // 10.0 - 20.0 + createTick('B', 20.0, verticalWidth: 10.0), // 20.0 - 30.0 + createTick('C', 29.0, verticalWidth: 10.0), // 29.0 - 40.0 + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.left); + + expect(report.ticksCollide, isTrue); + }); + + test('ticks do not collide for inside tick label anchor', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 2, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2) + createTick('B', 25.0, verticalWidth: 8.0), // 20.0 - 30.0 (25 + 2 + 1) + createTick('C', 40.0, verticalWidth: 8.0), // 30.0 - 40.0 (40-8-2) + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.left); + + expect(report.ticksCollide, isFalse); + }); + + test('ticks collide for inside anchor - first tick too large', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 2, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('A', 10.0, verticalWidth: 9.0), // 10.0 - 21.0 (19.0 + 2) + createTick('B', 25.0, verticalWidth: 8.0), // 20.0 - 30.0 (25 + 2 + 1) + createTick('C', 40.0, verticalWidth: 8.0), // 30.0 - 40.0 (40-8-2) + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.left); + + expect(report.ticksCollide, isTrue); + }); + + test('ticks collide for inside anchor - center tick too large', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 2, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2) + createTick('B', 25.0, verticalWidth: 9.0), // 19.5 - 30.5 (25 + 2.5 + 1) + createTick('C', 40.0, verticalWidth: 8.0), // 30.0 - 40.0 (40-8-2) + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.left); + + expect(report.ticksCollide, isTrue); + }); + + test('ticks collide for inside anchor - last tick too large', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 2, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('A', 10.0, verticalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2) + createTick('B', 25.0, verticalWidth: 8.0), // 20.0 - 30.0 (25 + 2 + 1) + createTick('C', 40.0, verticalWidth: 9.0), // 29.0 - 40.0 (40-9-2) + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.left); + + expect(report.ticksCollide, isTrue); + }); + }); + + group('collision detection - horizontally drawn axis', () { + test('ticks do not collide for TickLabelAnchor.before', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 2, + labelAnchor: TickLabelAnchor.before); + + final ticks = [ + createTick('A', 10.0, horizontalWidth: 8.0), // 10.0 - 20.0 (18.0 + 2) + createTick('B', 20.0, horizontalWidth: 8.0), // 20.0 - 30.0 (28.0 + 2) + createTick('C', 30.0, horizontalWidth: 8.0), // 30.0 - 40.0 (38.0 + 2) + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.bottom); + + expect(report.ticksCollide, isFalse); + }); + + test('ticks do not collide for TickLabelAnchor.inside', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 0, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('A', 10.0, + horizontalWidth: 10.0, + textDirection: TextDirection.ltr), // 10.0 - 20.0 + createTick('B', 25.0, + horizontalWidth: 10.0, + textDirection: TextDirection.center), // 20.0 - 30.0 + createTick('C', 40.0, + horizontalWidth: 10.0, + textDirection: TextDirection.rtl), // 30.0 - 40.0 + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.bottom); + + expect(report.ticksCollide, isFalse); + }); + + test('ticks collide - first tick too large', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 0, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('A', 10.0, horizontalWidth: 11.0), // 10.0 - 21.0 + createTick('B', 25.0, horizontalWidth: 10.0), // 20.0 - 30.0 + createTick('C', 40.0, horizontalWidth: 10.0), // 30.0 - 40.0 + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.bottom); + + expect(report.ticksCollide, isTrue); + }); + + test('ticks collide - middle tick too large', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 0, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('A', 10.0, horizontalWidth: 10.0), // 10.0 - 20.0 + createTick('B', 25.0, horizontalWidth: 11.0), // 19.5 - 30.5 + createTick('C', 40.0, horizontalWidth: 10.0), // 30.0 - 40.0 + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.bottom); + + expect(report.ticksCollide, isTrue); + }); + + test('ticks collide - last tick too large', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 0, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('A', 10.0, horizontalWidth: 10.0), // 10.0 - 20.0 + createTick('B', 25.0, horizontalWidth: 10.0), // 20.0 - 30.0 + createTick('C', 40.0, horizontalWidth: 11.0), // 29.0 - 40.0 + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.bottom); + + expect(report.ticksCollide, isTrue); + }); + }); + + group('collision detection - unsorted ticks', () { + test('ticks do not collide', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 0, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('C', 40.0, horizontalWidth: 10.0), // 30.0 - 40.0 + createTick('B', 25.0, horizontalWidth: 10.0), // 20.0 - 30.0 + createTick('A', 10.0, horizontalWidth: 10.0), // 10.0 - 20.0 + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.bottom); + + expect(report.ticksCollide, isFalse); + }); + + test('ticks collide - tick B is too large', () { + final drawStrategy = new BaseTickDrawStrategyImpl( + chartContext, graphicsFactory, + minimumPaddingBetweenLabelsPx: 0, + labelAnchor: TickLabelAnchor.inside); + + final ticks = [ + createTick('A', 10.0, horizontalWidth: 10.0), // 10.0 - 20.0 + createTick('C', 40.0, horizontalWidth: 10.0), // 30.0 - 40.0 + createTick('B', 25.0, horizontalWidth: 11.0), // 19.5 - 30.5 + ]; + + final report = drawStrategy.collides(ticks, AxisOrientation.bottom); + + expect(report.ticksCollide, isTrue); + }); + }); + + group('collision detection - corner cases', () { + test('null ticks do not collide', () { + final drawStrategy = + new BaseTickDrawStrategyImpl(chartContext, graphicsFactory); + + final report = drawStrategy.collides(null, AxisOrientation.left); + + expect(report.ticksCollide, isFalse); + }); + + test('empty tick list do not collide', () { + final drawStrategy = + new BaseTickDrawStrategyImpl(chartContext, graphicsFactory); + + final report = drawStrategy.collides([], AxisOrientation.left); + + expect(report.ticksCollide, isFalse); + }); + + test('single tick does not collide', () { + final drawStrategy = + new BaseTickDrawStrategyImpl(chartContext, graphicsFactory); + + final report = drawStrategy.collides( + [createTick('A', 10.0, horizontalWidth: 10.0)], + AxisOrientation.bottom); + + expect(report.ticksCollide, isFalse); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/end_points_tick_provider_test.dart b/web/charts/common/test/chart/cartesian/axis/end_points_tick_provider_test.dart new file mode 100644 index 000000000..dbf923ad6 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/end_points_tick_provider_test.dart @@ -0,0 +1,237 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart'; +import 'package:charts_common/src/common/graphics_factory.dart'; +import 'package:charts_common/src/common/line_style.dart'; +import 'package:charts_common/src/common/text_style.dart'; +import 'package:charts_common/src/common/text_element.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/cartesian/axis/collision_report.dart'; +import 'package:charts_common/src/chart/cartesian/axis/end_points_tick_provider.dart'; +import 'package:charts_common/src/chart/cartesian/axis/numeric_scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/simple_ordinal_scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart'; +import 'package:charts_common/src/chart/cartesian/axis/numeric_extents.dart'; +import 'package:charts_common/src/chart/cartesian/axis/time/date_time_extents.dart'; +import 'package:charts_common/src/chart/cartesian/axis/time/date_time_scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/time/date_time_tick_formatter.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'time/simple_date_time_factory.dart' show SimpleDateTimeFactory; + +class MockDateTimeScale extends Mock implements DateTimeScale {} + +class MockNumericScale extends Mock implements NumericScale {} + +class MockOrdinalScale extends Mock implements SimpleOrdinalScale {} + +/// A fake draw strategy that reports collision and alternate ticks +/// +/// Reports collision when the tick count is greater than or equal to +/// [collidesAfterTickCount]. +/// +/// Reports alternate rendering after tick count is greater than or equal to +/// [alternateRenderingAfterTickCount]. +class FakeDrawStrategy extends BaseTickDrawStrategy { + final int collidesAfterTickCount; + final int alternateRenderingAfterTickCount; + + FakeDrawStrategy( + this.collidesAfterTickCount, this.alternateRenderingAfterTickCount) + : super(null, new FakeGraphicsFactory()); + + @override + CollisionReport collides(List> ticks, _) { + final ticksCollide = ticks.length >= collidesAfterTickCount; + final alternateTicksUsed = ticks.length >= alternateRenderingAfterTickCount; + + return new CollisionReport( + ticksCollide: ticksCollide, + ticks: ticks, + alternateTicksUsed: alternateTicksUsed); + } + + @override + void draw(ChartCanvas canvas, Tick tick, + {AxisOrientation orientation, + Rectangle axisBounds, + Rectangle drawAreaBounds, + bool isFirst, + bool isLast}) {} +} + +/// A fake [GraphicsFactory] that returns [MockTextStyle] and [MockTextElement]. +class FakeGraphicsFactory extends GraphicsFactory { + @override + TextStyle createTextPaint() => new MockTextStyle(); + + @override + TextElement createTextElement(String text) => new MockTextElement(); + + @override + LineStyle createLinePaint() => new MockLinePaint(); +} + +class MockTextStyle extends Mock implements TextStyle {} + +class MockTextElement extends Mock implements TextElement {} + +class MockLinePaint extends Mock implements LineStyle {} + +class MockChartContext extends Mock implements ChartContext {} + +void main() { + const dateTimeFactory = const SimpleDateTimeFactory(); + FakeGraphicsFactory graphicsFactory; + EndPointsTickProvider tickProvider; + ChartContext context; + + setUp(() { + graphicsFactory = new FakeGraphicsFactory(); + context = new MockChartContext(); + }); + + test('dateTime_choosesEndPointTicks', () { + final formatter = new DateTimeTickFormatter(dateTimeFactory); + final scale = new MockDateTimeScale(); + tickProvider = new EndPointsTickProvider(); + + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new DateTimeExtents( + start: new DateTime(2018, 8, 1), end: new DateTime(2018, 8, 11))); + when(scale.rangeWidth).thenReturn(1000); + when(scale.domainStepSize).thenReturn(1000.0); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks, hasLength(2)); + expect(ticks[0].value, equals(new DateTime(2018, 8, 1))); + expect(ticks[1].value, equals(new DateTime(2018, 8, 11))); + }); + + test('numeric_choosesEndPointTicks', () { + final formatter = new NumericTickFormatter(); + final scale = new MockNumericScale(); + tickProvider = new EndPointsTickProvider(); + + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 70.0)); + when(scale.rangeWidth).thenReturn(1000); + when(scale.domainStepSize).thenReturn(1000.0); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks, hasLength(2)); + expect(ticks[0].value, equals(10)); + expect(ticks[1].value, equals(70)); + }); + + test('ordinal_choosesEndPointTicks', () { + final formatter = new OrdinalTickFormatter(); + final scale = new SimpleOrdinalScale(); + scale.addDomain('A'); + scale.addDomain('B'); + scale.addDomain('C'); + scale.addDomain('D'); + tickProvider = new EndPointsTickProvider(); + + final drawStrategy = new FakeDrawStrategy(10, 10); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks, hasLength(2)); + expect(ticks[0].value, equals('A')); + expect(ticks[1].value, equals('D')); + }); + + test('dateTime_emptySeriesChoosesNoTicks', () { + final formatter = new DateTimeTickFormatter(dateTimeFactory); + final scale = new MockDateTimeScale(); + tickProvider = new EndPointsTickProvider(); + + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new DateTimeExtents( + start: new DateTime(2018, 8, 1), end: new DateTime(2018, 8, 11))); + when(scale.rangeWidth).thenReturn(1000); + + // An un-configured axis has no domain step size, and its scale defaults to + // infinity. + when(scale.domainStepSize).thenReturn(double.infinity); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks, hasLength(0)); + }); + + test('numeric_emptySeriesChoosesNoTicks', () { + final formatter = new NumericTickFormatter(); + final scale = new MockNumericScale(); + tickProvider = new EndPointsTickProvider(); + + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 70.0)); + when(scale.rangeWidth).thenReturn(1000); + + // An un-configured axis has no domain step size, and its scale defaults to + // infinity. + when(scale.domainStepSize).thenReturn(double.infinity); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks, hasLength(0)); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/linear/linear_scale_test.dart b/web/charts/common/test/chart/cartesian/axis/linear/linear_scale_test.dart new file mode 100644 index 000000000..0ed704155 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/linear/linear_scale_test.dart @@ -0,0 +1,307 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/axis/numeric_extents.dart' + show NumericExtents; +import 'package:charts_common/src/chart/cartesian/axis/linear/linear_scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/scale.dart' + show RangeBandConfig, ScaleOutputExtent, StepSizeConfig; + +import 'package:test/test.dart'; + +const EPSILON = 0.001; + +void main() { + group('Stacking bars', () { + test('basic apply survives copy and reset', () { + LinearScale scale = new LinearScale(); + scale.addDomain(100.0); + scale.addDomain(130.0); + scale.addDomain(200.0); + scale.addDomain(170.0); + scale.range = new ScaleOutputExtent(2000, 1000); + + expect(scale.range.start, equals(2000)); + expect(scale.range.end, equals(1000)); + expect(scale.range.diff, equals(-1000)); + + expect(scale.dataExtent.min, equals(100.0)); + expect(scale.dataExtent.max, equals(200.0)); + + expect(scale[100.0], closeTo(2000, EPSILON)); + expect(scale[200.0], closeTo(1000, EPSILON)); + expect(scale[166.0], closeTo(1340, EPSILON)); + expect(scale[0.0], closeTo(3000, EPSILON)); + expect(scale[300.0], closeTo(0, EPSILON)); + + // test copy + LinearScale other = scale.copy(); + expect(other[166.0], closeTo(1340, EPSILON)); + expect(other.range.start, equals(2000)); + expect(other.range.end, equals(1000)); + + // test reset + other.resetDomain(); + other.resetViewportSettings(); + other.addDomain(10.0); + other.addDomain(20.0); + expect(other.dataExtent.min, equals(10.0)); + expect(other.dataExtent.max, equals(20.0)); + expect(other.viewportDomain.min, equals(10.0)); + expect(other.viewportDomain.max, equals(20.0)); + + expect(other[15.0], closeTo(1500, EPSILON)); + // original scale shouldn't have been touched. + expect(scale[166.0], closeTo(1340, EPSILON)); + + // should always return true. + expect(scale.canTranslate(3.14), isTrue); + }); + + test('viewport assigned domain extent applies to scale', () { + LinearScale scale = new LinearScale()..keepViewportWithinData = false; + scale.addDomain(50.0); + scale.addDomain(70.0); + scale.viewportDomain = new NumericExtents(100.0, 200.0); + scale.range = new ScaleOutputExtent(0, 200); + + expect(scale[200.0], closeTo(200, EPSILON)); + expect(scale[100.0], closeTo(0, EPSILON)); + expect(scale[50.0], closeTo(-100, EPSILON)); + expect(scale[150.0], closeTo(100, EPSILON)); + + scale.resetDomain(); + scale.resetViewportSettings(); + scale.addDomain(50.0); + scale.addDomain(100.0); + scale.viewportDomain = new NumericExtents(0.0, 100.0); + scale.range = new ScaleOutputExtent(0, 200); + + expect(scale[0.0], closeTo(0, EPSILON)); + expect(scale[100.0], closeTo(200, EPSILON)); + expect(scale[50.0], closeTo(100, EPSILON)); + expect(scale[200.0], closeTo(400, EPSILON)); + }); + + test('comparing domain and range to viewport handles extent edges', () { + LinearScale scale = new LinearScale(); + scale.range = new ScaleOutputExtent(1000, 1400); + scale.domainOverride = new NumericExtents(100.0, 300.0); + scale.viewportDomain = new NumericExtents(200.0, 300.0); + + expect(scale.viewportDomain, equals(new NumericExtents(200.0, 300.0))); + + expect(scale[210.0], closeTo(1040, EPSILON)); + expect(scale[400.0], closeTo(1800, EPSILON)); + expect(scale[100.0], closeTo(600, EPSILON)); + + expect(scale.compareDomainValueToViewport(199.0), equals(-1)); + expect(scale.compareDomainValueToViewport(200.0), equals(0)); + expect(scale.compareDomainValueToViewport(201.0), equals(0)); + expect(scale.compareDomainValueToViewport(299.0), equals(0)); + expect(scale.compareDomainValueToViewport(300.0), equals(0)); + expect(scale.compareDomainValueToViewport(301.0), equals(1)); + + expect(scale.isRangeValueWithinViewport(999.0), isFalse); + expect(scale.isRangeValueWithinViewport(1100.0), isTrue); + expect(scale.isRangeValueWithinViewport(1401.0), isFalse); + }); + + test('scale applies in reverse', () { + LinearScale scale = new LinearScale(); + scale.range = new ScaleOutputExtent(1000, 1400); + scale.domainOverride = new NumericExtents(100.0, 300.0); + scale.viewportDomain = new NumericExtents(200.0, 300.0); + + expect(scale.reverse(1040.0), closeTo(210.0, EPSILON)); + expect(scale.reverse(1800.0), closeTo(400.0, EPSILON)); + expect(scale.reverse(600.0), closeTo(100.0, EPSILON)); + }); + + test('scale works with a range from larger to smaller', () { + LinearScale scale = new LinearScale(); + scale.range = new ScaleOutputExtent(1400, 1000); + scale.domainOverride = new NumericExtents(100.0, 300.0); + scale.viewportDomain = new NumericExtents(200.0, 300.0); + + expect(scale[200.0], closeTo(1400.0, EPSILON)); + expect(scale[250.0], closeTo(1200.0, EPSILON)); + expect(scale[300.0], closeTo(1000.0, EPSILON)); + }); + + test('scaleFactor and translate applies to scale', () { + LinearScale scale = new LinearScale(); + scale.range = new ScaleOutputExtent(1000, 1200); + scale.domainOverride = new NumericExtents(100.0, 200.0); + scale.setViewportSettings(4.0, -50.0); + + expect(scale[100.0], closeTo(950.0, EPSILON)); + expect(scale[200.0], closeTo(1750.0, EPSILON)); + expect(scale[150.0], closeTo(1350.0, EPSILON)); + expect(scale[106.25], closeTo(1000.0, EPSILON)); + expect(scale[131.25], closeTo(1200.0, EPSILON)); + + expect(scale.compareDomainValueToViewport(106.0), equals(-1)); + expect(scale.compareDomainValueToViewport(106.25), equals(0)); + expect(scale.compareDomainValueToViewport(107.0), equals(0)); + + expect(scale.compareDomainValueToViewport(131.0), equals(0)); + expect(scale.compareDomainValueToViewport(131.25), equals(0)); + expect(scale.compareDomainValueToViewport(132.0), equals(1)); + + expect(scale.isRangeValueWithinViewport(999.0), isFalse); + expect(scale.isRangeValueWithinViewport(1100.0), isTrue); + expect(scale.isRangeValueWithinViewport(1201.0), isFalse); + }); + + test('scale handles single point', () { + LinearScale domainScale = new LinearScale(); + domainScale.range = new ScaleOutputExtent(1000, 1200); + domainScale.addDomain(50.0); + + // A single point should render in the middle of the scale. + expect(domainScale[50.0], closeTo(1100.0, EPSILON)); + }); + + test('testAllZeros', () { + LinearScale measureScale = new LinearScale(); + measureScale.range = new ScaleOutputExtent(1000, 1200); + measureScale.addDomain(0.0); + + expect(measureScale[0.0], closeTo(1100.0, EPSILON)); + }); + + test('scale calculates step size', () { + LinearScale scale = new LinearScale(); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0); + scale.addDomain(1.0); + scale.addDomain(3.0); + scale.addDomain(11.0); + scale.range = new ScaleOutputExtent(100, 200); + + // 1 - 11 has 6 steps of size 2, 0 - 12 + expect(scale.rangeBand, closeTo(100.0 / 6.0, EPSILON)); + }); + + test('scale applies rangeBand to detected step size', () { + LinearScale scale = new LinearScale(); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(0.5); + scale.addDomain(1.0); + scale.addDomain(2.0); + scale.addDomain(10.0); + scale.range = new ScaleOutputExtent(100, 200); + + // 100 range / 10 steps * 0.5percentStep = 5 + expect(scale.rangeBand, closeTo(5.0, EPSILON)); + }); + + test('scale stepSize calculation survives copy', () { + LinearScale scale = new LinearScale(); + scale.stepSizeConfig = new StepSizeConfig.fixedDomain(1.0); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0); + scale.addDomain(1.0); + scale.addDomain(3.0); + scale.range = new ScaleOutputExtent(100, 200); + expect(scale.copy().rangeBand, closeTo(100.0 / 3.0, EPSILON)); + }); + + test('scale rangeBand calculation survives copy', () { + LinearScale scale = new LinearScale(); + scale.rangeBandConfig = new RangeBandConfig.fixedPixel(123.0); + scale.addDomain(1.0); + scale.addDomain(3.0); + scale.range = new ScaleOutputExtent(100, 200); + + expect(scale.copy().rangeBand, closeTo(123, EPSILON)); + }); + + test('scale rangeBand works for single domain value', () { + LinearScale scale = new LinearScale(); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0); + scale.addDomain(1.0); + scale.range = new ScaleOutputExtent(100, 200); + + expect(scale.rangeBand, closeTo(100, EPSILON)); + }); + + test('scale rangeBand works for multiple domains of the same value', () { + LinearScale scale = new LinearScale(); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0); + scale.addDomain(1.0); + scale.addDomain(1.0); + scale.range = new ScaleOutputExtent(100, 200); + + expect(scale.rangeBand, closeTo(100.0, EPSILON)); + }); + + test('scale rangeBand is zero when no domains are added', () { + LinearScale scale = new LinearScale(); + scale.range = new ScaleOutputExtent(100, 200); + + expect(scale.rangeBand, closeTo(0.0, EPSILON)); + }); + + test('scale domain info reset on resetDomain', () { + LinearScale scale = new LinearScale(); + scale.addDomain(1.0); + scale.addDomain(3.0); + scale.range = new ScaleOutputExtent(100, 200); + scale.setViewportSettings(1000.0, 2000.0); + + scale.resetDomain(); + scale.resetViewportSettings(); + expect(scale.viewportScalingFactor, closeTo(1.0, EPSILON)); + expect(scale.viewportTranslatePx, closeTo(0, EPSILON)); + expect(scale.range, equals(new ScaleOutputExtent(100, 200))); + }); + + test('scale handles null domain values', () { + LinearScale scale = new LinearScale(); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0); + scale.addDomain(1.0); + scale.addDomain(null); + scale.addDomain(3.0); + scale.addDomain(11.0); + scale.range = new ScaleOutputExtent(100, 200); + + expect(scale.rangeBand, closeTo(100.0 / 6.0, EPSILON)); + }); + + test('scale domainOverride survives copy', () { + LinearScale scale = new LinearScale()..keepViewportWithinData = false; + scale.addDomain(1.0); + scale.addDomain(3.0); + scale.range = new ScaleOutputExtent(100, 200); + scale.setViewportSettings(2.0, 10.0); + scale.domainOverride = new NumericExtents(0.0, 100.0); + + LinearScale other = scale.copy(); + + expect(other.domainOverride, equals(new NumericExtents(0.0, 100.0))); + expect(other[5.0], closeTo(120.0, EPSILON)); + }); + + test('scale calculates a scaleFactor given a domain window', () { + LinearScale scale = new LinearScale(); + scale.addDomain(100.0); + scale.addDomain(130.0); + scale.addDomain(200.0); + scale.addDomain(170.0); + + expect(scale.computeViewportScaleFactor(10.0), closeTo(10, EPSILON)); + expect(scale.computeViewportScaleFactor(100.0), closeTo(1, EPSILON)); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/numeric_tick_provider_test.dart b/web/charts/common/test/chart/cartesian/axis/numeric_tick_provider_test.dart new file mode 100644 index 000000000..ae0c9173a --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/numeric_tick_provider_test.dart @@ -0,0 +1,498 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart'; +import 'package:charts_common/src/common/graphics_factory.dart'; +import 'package:charts_common/src/common/line_style.dart'; +import 'package:charts_common/src/common/text_style.dart'; +import 'package:charts_common/src/common/text_element.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/common/unitconverter/unit_converter.dart'; +import 'package:charts_common/src/chart/cartesian/axis/collision_report.dart'; +import 'package:charts_common/src/chart/cartesian/axis/numeric_scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick_provider.dart'; +import 'package:charts_common/src/chart/cartesian/axis/numeric_extents.dart'; +import 'package:charts_common/src/chart/cartesian/axis/numeric_tick_provider.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockNumericScale extends Mock implements NumericScale {} + +/// A fake draw strategy that reports collision and alternate ticks +/// +/// Reports collision when the tick count is greater than or equal to +/// [collidesAfterTickCount]. +/// +/// Reports alternate rendering after tick count is greater than or equal to +/// [alternateRenderingAfterTickCount]. +class FakeDrawStrategy extends BaseTickDrawStrategy { + final int collidesAfterTickCount; + final int alternateRenderingAfterTickCount; + + FakeDrawStrategy( + this.collidesAfterTickCount, this.alternateRenderingAfterTickCount) + : super(null, new FakeGraphicsFactory()); + + @override + CollisionReport collides(List> ticks, _) { + final ticksCollide = ticks.length >= collidesAfterTickCount; + final alternateTicksUsed = ticks.length >= alternateRenderingAfterTickCount; + + return new CollisionReport( + ticksCollide: ticksCollide, + ticks: ticks, + alternateTicksUsed: alternateTicksUsed); + } + + @override + void draw(ChartCanvas canvas, Tick tick, + {AxisOrientation orientation, + Rectangle axisBounds, + Rectangle drawAreaBounds, + bool isFirst, + bool isLast}) {} +} + +/// A fake [GraphicsFactory] that returns [MockTextStyle] and [MockTextElement]. +class FakeGraphicsFactory extends GraphicsFactory { + @override + TextStyle createTextPaint() => new MockTextStyle(); + + @override + TextElement createTextElement(String text) => new MockTextElement(); + + @override + LineStyle createLinePaint() => new MockLinePaint(); +} + +class MockTextStyle extends Mock implements TextStyle {} + +class MockTextElement extends Mock implements TextElement {} + +class MockLinePaint extends Mock implements LineStyle {} + +class MockChartContext extends Mock implements ChartContext {} + +/// A celsius to fahrenheit converter for testing axis with unit converter. +class CelsiusToFahrenheitConverter implements UnitConverter { + const CelsiusToFahrenheitConverter(); + + @override + num convert(num value) => (value * 1.8) + 32.0; + + @override + num invert(num value) => (value - 32.0) / 1.8; +} + +void main() { + FakeGraphicsFactory graphicsFactory; + MockNumericScale scale; + NumericTickProvider tickProvider; + TickFormatter formatter; + ChartContext context; + + setUp(() { + graphicsFactory = new FakeGraphicsFactory(); + scale = new MockNumericScale(); + tickProvider = new NumericTickProvider(); + formatter = new NumericTickFormatter(); + context = new MockChartContext(); + }); + + test('singleTickCount_choosesTicksWithSmallestStepCoveringDomain', () { + tickProvider + ..zeroBound = false + ..dataIsInWholeNumbers = false + ..setFixedTickCount(4) + ..allowedSteps = [1.0, 2.5, 5.0]; + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 70.0)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks, hasLength(4)); + expect(ticks[0].value, equals(0)); + expect(ticks[1].value, equals(25)); + expect(ticks[2].value, equals(50)); + expect(ticks[3].value, equals(75)); + }); + + test( + 'tickCountRangeChoosesTicksWithMostTicksAndSmallestIntervalCoveringDomain', + () { + tickProvider + ..zeroBound = false + ..dataIsInWholeNumbers = false + ..setTickCount(5, 3) + ..allowedSteps = [1.0, 2.5, 5.0]; + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 80.0)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks, hasLength(5)); + expect(ticks[0].value, equals(0)); + expect(ticks[1].value, equals(25)); + expect(ticks[2].value, equals(50)); + expect(ticks[3].value, equals(75)); + expect(ticks[4].value, equals(100)); + }); + + test('choosesNonAlternateRenderingTicksEvenIfIntervalIsLarger', () { + tickProvider + ..zeroBound = false + ..dataIsInWholeNumbers = false + ..setTickCount(5, 3) + ..allowedSteps = [1.0, 2.5, 6.0]; + final drawStrategy = new FakeDrawStrategy(10, 5); + when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 80.0)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks, hasLength(3)); + expect(ticks[0].value, equals(0)); + expect(ticks[1].value, equals(60)); + expect(ticks[2].value, equals(120)); + }); + + test('choosesNonCollidingTicksEvenIfIntervalIsLarger', () { + tickProvider + ..zeroBound = false + ..dataIsInWholeNumbers = false + ..setTickCount(5, 3) + ..allowedSteps = [1.0, 2.5, 6.0]; + final drawStrategy = new FakeDrawStrategy(5, 5); + when(scale.viewportDomain).thenReturn(new NumericExtents(10.0, 80.0)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks, hasLength(3)); + expect(ticks[0].value, equals(0)); + expect(ticks[1].value, equals(60)); + expect(ticks[2].value, equals(120)); + }); + + test('zeroBound_alwaysReturnsZeroTick', () { + tickProvider + ..zeroBound = true + ..dataIsInWholeNumbers = false + ..setFixedTickCount(3) + ..allowedSteps = [1.0, 2.5, 5.0]; + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(55.0, 135.0)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + final tickValues = ticks.map((tick) => tick.value).toList(); + + expect(tickValues, contains(0.0)); + }); + + test('boundsCrossOrigin_alwaysReturnsZeroTick', () { + tickProvider + ..zeroBound = false + ..dataIsInWholeNumbers = false + ..setFixedTickCount(3) + ..allowedSteps = [1.0, 2.5, 5.0]; + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(-55.0, 135.0)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + final tickValues = ticks.map((tick) => tick.value).toList(); + + expect(tickValues, contains(0.0)); + }); + + test('boundsCrossOrigin_returnsValidTickRange', () { + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(-55.0, 135.0)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + final tickValues = ticks.map((tick) => tick.value).toList(); + + // We expect to see a range of ticks that crosses zero. + expect(tickValues, + equals([-60.0, -30.0, 0.0, 30.0, 60.0, 90.0, 120.0, 150.0])); + }); + + test('dataIsWholeNumbers_returnsWholeNumberTicks', () { + tickProvider + ..zeroBound = false + ..dataIsInWholeNumbers = true + ..setFixedTickCount(3) + ..allowedSteps = [1.0, 2.5, 5.0]; + final drawStrategy = new FakeDrawStrategy(10, 10); + + when(scale.viewportDomain).thenReturn(new NumericExtents(0.25, 0.75)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks[0].value, equals(0)); + expect(ticks[1].value, equals(1)); + expect(ticks[2].value, equals(2)); + }); + + test('choosesTicksBasedOnPreferredAxisUnits', () { + tickProvider + ..zeroBound = true + ..dataIsInWholeNumbers = false + ..setFixedTickCount(3) + ..allowedSteps = [5.0] + ..dataToAxisUnitConverter = const CelsiusToFahrenheitConverter(); + + final drawStrategy = new FakeDrawStrategy(10, 10); + + when(scale.viewportDomain).thenReturn(new NumericExtents(0.0, 20.0)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks[0].value, closeTo(-17.8, 0.1)); // 0 in axis units + expect(ticks[1].value, closeTo(10, 0.1)); // 50 in axis units + expect(ticks[2].value, closeTo(37.8, 0.1)); // 100 in axis units + }); + + test('handlesVerySmallMeasures', () { + tickProvider + ..zeroBound = true + ..dataIsInWholeNumbers = false + ..setFixedTickCount(5); + + final drawStrategy = new FakeDrawStrategy(10, 10); + + when(scale.viewportDomain) + .thenReturn(new NumericExtents(0.000001, 0.000002)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks.length, equals(5)); + expect(ticks[0].value, equals(0)); + expect(ticks[1].value, equals(0.0000005)); + expect(ticks[2].value, equals(0.0000010)); + expect(ticks[3].value, equals(0.0000015)); + expect(ticks[4].value, equals(0.000002)); + }); + + test('handlesVerySmallMeasuresForWholeNumbers', () { + tickProvider + ..zeroBound = true + ..dataIsInWholeNumbers = true + ..setFixedTickCount(5); + + final drawStrategy = new FakeDrawStrategy(10, 10); + + when(scale.viewportDomain) + .thenReturn(new NumericExtents(0.000001, 0.000002)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks.length, equals(5)); + expect(ticks[0].value, equals(0)); + expect(ticks[1].value, equals(1)); + expect(ticks[2].value, equals(2)); + expect(ticks[3].value, equals(3)); + expect(ticks[4].value, equals(4)); + }); + + test('handlesVerySmallMeasuresForWholeNumbersWithoutZero', () { + tickProvider + ..zeroBound = false + ..dataIsInWholeNumbers = true + ..setFixedTickCount(5); + + final drawStrategy = new FakeDrawStrategy(10, 10); + + when(scale.viewportDomain) + .thenReturn(new NumericExtents(101.000001, 101.000002)); + when(scale.rangeWidth).thenReturn(1000); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(ticks.length, equals(5)); + expect(ticks[0].value, equals(101)); + expect(ticks[1].value, equals(102)); + expect(ticks[2].value, equals(103)); + expect(ticks[3].value, equals(104)); + expect(ticks[4].value, equals(105)); + }); + + test('handles tick hint for non zero ticks', () { + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(20.0, 35.0)); + when(scale.rangeWidth).thenReturn(1000); + + // Step Size: 3, + // Previous start tick: 10 + // Previous window: 10 - 25 + // Previous ticks: 10, 13, 16, 19, 22, 25 + final tickHint = new TickHint(10, 25, tickCount: 6); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null, + tickHint: tickHint, + ); + + // New adjusted ticks for window 20 - 35 + // Should have ticks 22, 25, 28, 31, 34, 37 + expect(ticks, hasLength(6)); + expect(ticks[0].value, equals(22)); + expect(ticks[1].value, equals(25)); + expect(ticks[2].value, equals(28)); + expect(ticks[3].value, equals(31)); + expect(ticks[4].value, equals(34)); + expect(ticks[5].value, equals(37)); + }); + + test('handles tick hint for negative starting ticks', () { + final drawStrategy = new FakeDrawStrategy(10, 10); + when(scale.viewportDomain).thenReturn(new NumericExtents(-35.0, -20.0)); + when(scale.rangeWidth).thenReturn(1000); + + // Step Size: 3, + // Previous start tick: -25 + // Previous window: -25 to -10 + // Previous ticks: -25, -22, -19, -16, -13, -10 + final tickHint = new TickHint(-25, -10, tickCount: 6); + + final ticks = tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null, + tickHint: tickHint, + ); + + // New adjusted ticks for window -35 to -20 + // Should have ticks -34, -31, -28, -25, -22, -19 + expect(ticks, hasLength(6)); + expect(ticks[0].value, equals(-34)); + expect(ticks[1].value, equals(-31)); + expect(ticks[2].value, equals(-28)); + expect(ticks[3].value, equals(-25)); + expect(ticks[4].value, equals(-22)); + expect(ticks[5].value, equals(-19)); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/ordinal_scale_test.dart b/web/charts/common/test/chart/cartesian/axis/ordinal_scale_test.dart new file mode 100644 index 000000000..de22eb8f2 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/ordinal_scale_test.dart @@ -0,0 +1,250 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/axis/scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/simple_ordinal_scale.dart'; + +import 'package:test/test.dart'; + +const EPSILON = 0.001; + +void main() { + SimpleOrdinalScale scale; + + setUp(() { + scale = new SimpleOrdinalScale(); + scale.addDomain('a'); + scale.addDomain('b'); + scale.addDomain('c'); + scale.addDomain('d'); + + scale.range = new ScaleOutputExtent(2000, 1000); + }); + + group('conversion', () { + test('with duplicate keys', () { + scale.addDomain('c'); + scale.addDomain('a'); + + // Current RangeBandConfig.styleAssignedPercent sets size to 0.65 percent. + expect(scale.rangeBand, closeTo(250 * 0.65, EPSILON)); + expect(scale['a'], closeTo(2000 - 125, EPSILON)); + expect(scale['b'], closeTo(2000 - 375, EPSILON)); + expect(scale['c'], closeTo(2000 - 625, EPSILON)); + }); + + test('invalid domain does not throw exception', () { + expect(scale['e'], 0); + }); + + test('invalid domain can translate is false', () { + expect(scale.canTranslate('e'), isFalse); + }); + }); + + group('copy', () { + test('can convert domain', () { + final copied = scale.copy(); + expect(copied['c'], closeTo(2000 - 625, EPSILON)); + }); + + test('does not affect original', () { + final copied = scale.copy(); + copied.addDomain('bar'); + + expect(copied.canTranslate('bar'), isTrue); + expect(scale.canTranslate('bar'), isFalse); + }); + }); + + group('reset', () { + test('clears domains', () { + scale.resetDomain(); + scale.addDomain('foo'); + scale.addDomain('bar'); + + expect(scale['foo'], closeTo(2000 - 250, EPSILON)); + }); + }); + + group('set RangeBandConfig', () { + test('fixed pixel range band changes range band', () { + scale.rangeBandConfig = new RangeBandConfig.fixedPixel(123.0); + + expect(scale.rangeBand, closeTo(123.0, EPSILON)); + + // Adding another domain to ensure it still doesn't change. + scale.addDomain('foo'); + expect(scale.rangeBand, closeTo(123.0, EPSILON)); + }); + + test('percent range band changes range band', () { + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(0.5); + // 125 = 0.5f * 1000pixels / 4domains + expect(scale.rangeBand, closeTo(125.0, EPSILON)); + }); + + test('space from step changes range band', () { + scale.rangeBandConfig = + new RangeBandConfig.fixedPixelSpaceBetweenStep(50.0); + // 200 = 1000pixels / 4domains) - 50 + expect(scale.rangeBand, closeTo(200.0, EPSILON)); + }); + + test('fixed domain throws argument exception', () { + expect(() => scale.rangeBandConfig = new RangeBandConfig.fixedDomain(5.0), + throwsArgumentError); + }); + + test('type of none throws argument exception', () { + expect(() => scale.rangeBandConfig = new RangeBandConfig.none(), + throwsArgumentError); + }); + + test('set to null throws argument exception', () { + expect(() => scale.rangeBandConfig = null, throwsArgumentError); + }); + }); + + group('set step size config', () { + test('to null does not throw', () { + scale.stepSizeConfig = null; + }); + + test('to auto does not throw', () { + scale.stepSizeConfig = new StepSizeConfig.auto(); + }); + + test('to fixed domain throw arugment exception', () { + expect(() => scale.stepSizeConfig = new StepSizeConfig.fixedDomain(1.0), + throwsArgumentError); + }); + + test('to fixed pixel throw arugment exception', () { + expect(() => scale.stepSizeConfig = new StepSizeConfig.fixedPixels(1.0), + throwsArgumentError); + }); + }); + + group('set range persists', () { + test('', () { + expect(scale.range.start, equals(2000)); + expect(scale.range.end, equals(1000)); + expect(scale.range.min, equals(1000)); + expect(scale.range.max, equals(2000)); + expect(scale.rangeWidth, equals(1000)); + + expect(scale.isRangeValueWithinViewport(1500.0), isTrue); + expect(scale.isRangeValueWithinViewport(1000.0), isTrue); + expect(scale.isRangeValueWithinViewport(2000.0), isTrue); + + expect(scale.isRangeValueWithinViewport(500.0), isFalse); + expect(scale.isRangeValueWithinViewport(2500.0), isFalse); + }); + }); + + group('scale factor', () { + test('sets', () { + scale.setViewportSettings(2.0, -700.0); + + expect(scale.viewportScalingFactor, closeTo(2.0, EPSILON)); + expect(scale.viewportTranslatePx, closeTo(-700.0, EPSILON)); + }); + + test('rangeband is scaled', () { + scale.setViewportSettings(2.0, -700.0); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0); + + expect(scale.rangeBand, closeTo(500.0, EPSILON)); + }); + + test('translate to pixels is scaled', () { + scale.setViewportSettings(2.0, -700.0); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0); + scale.range = new ScaleOutputExtent(1000, 2000); + + final scaledStepWidth = 500.0; + final scaledInitialShift = 250.0; + + expect(scale['a'], closeTo(1000 + scaledInitialShift - 700, EPSILON)); + + expect(scale['b'], + closeTo(1000 + scaledInitialShift - 700 + scaledStepWidth, EPSILON)); + }); + + test('only b and c should be within the viewport', () { + scale.setViewportSettings(2.0, -700.0); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(1.0); + scale.range = new ScaleOutputExtent(1000, 2000); + + expect(scale.compareDomainValueToViewport('a'), equals(-1)); + expect(scale.compareDomainValueToViewport('c'), equals(0)); + expect(scale.compareDomainValueToViewport('d'), equals(1)); + expect(scale.compareDomainValueToViewport('f'), isNot(0)); + }); + }); + + group('viewport', () { + test('set adjust scale to show viewport', () { + scale.range = new ScaleOutputExtent(1000, 2000); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(0.5); + scale.setViewport(2, 'b'); + + expect(scale['a'], closeTo(750, EPSILON)); + expect(scale['b'], closeTo(1250, EPSILON)); + expect(scale['c'], closeTo(1750, EPSILON)); + expect(scale['d'], closeTo(2250, EPSILON)); + expect(scale.compareDomainValueToViewport('a'), equals(-1)); + expect(scale.compareDomainValueToViewport('b'), equals(0)); + expect(scale.compareDomainValueToViewport('c'), equals(0)); + expect(scale.compareDomainValueToViewport('d'), equals(1)); + }); + + test('illegal to set window size less than one', () { + expect(() => scale.setViewport(0, 'b'), throwsArgumentError); + }); + + test('set starting value if starting domain is not in domain list', () { + scale.range = new ScaleOutputExtent(1000, 2000); + scale.rangeBandConfig = new RangeBandConfig.percentOfStep(0.5); + scale.setViewport(2, 'f'); + + expect(scale['a'], closeTo(1250, EPSILON)); + expect(scale['b'], closeTo(1750, EPSILON)); + expect(scale['c'], closeTo(2250, EPSILON)); + expect(scale['d'], closeTo(2750, EPSILON)); + }); + + test('get size returns number of full steps that fit scale range', () { + scale.range = new ScaleOutputExtent(1000, 2000); + + scale.setViewportSettings(2.0, 0.0); + expect(scale.viewportDataSize, equals(2)); + + scale.setViewportSettings(5.0, 0.0); + expect(scale.viewportDataSize, equals(0)); + }); + + test('get starting viewport gets first fully visible domain', () { + scale.range = new ScaleOutputExtent(1000, 2000); + + scale.setViewportSettings(2.0, -500.0); + expect(scale.viewportStartingDomain, equals('b')); + + scale.setViewportSettings(2.0, -100.0); + expect(scale.viewportStartingDomain, equals('b')); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/static_tick_provider_test.dart b/web/charts/common/test/chart/cartesian/axis/static_tick_provider_test.dart new file mode 100644 index 000000000..a5a5b67f7 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/static_tick_provider_test.dart @@ -0,0 +1,180 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/axis/static_tick_provider.dart'; +import 'package:charts_common/src/chart/cartesian/axis/linear/linear_scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/draw_strategy/base_tick_draw_strategy.dart'; +import 'package:charts_common/src/common/graphics_factory.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/cartesian/axis/scale.dart'; +import 'package:charts_common/src/chart/cartesian/axis/spec/tick_spec.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockChartContext extends Mock implements ChartContext {} + +class MockGraphicsFactory extends Mock implements GraphicsFactory {} + +class MockNumericTickFormatter extends Mock implements TickFormatter {} + +class FakeNumericTickFormatter implements TickFormatter { + int calledTimes = 0; + + @override + List format(List tickValues, Map cache, + {num stepSize}) { + calledTimes += 1; + + return tickValues.map((value) => value.toString()).toList(); + } +} + +class MockDrawStrategy extends Mock implements BaseTickDrawStrategy {} + +void main() { + ChartContext context; + GraphicsFactory graphicsFactory; + TickFormatter formatter; + BaseTickDrawStrategy drawStrategy; + LinearScale scale; + + setUp(() { + context = new MockChartContext(); + graphicsFactory = new MockGraphicsFactory(); + formatter = new MockNumericTickFormatter(); + drawStrategy = new MockDrawStrategy(); + scale = new LinearScale()..range = new ScaleOutputExtent(0, 300); + }); + + group('scale is extended with static tick values', () { + test('values extend existing domain values', () { + final tickProvider = new StaticTickProvider([ + new TickSpec(50, label: '50'), + new TickSpec(75, label: '75'), + new TickSpec(100, label: '100'), + ]); + + scale.addDomain(60); + scale.addDomain(80); + + expect(scale.dataExtent.min, equals(60)); + expect(scale.dataExtent.max, equals(80)); + + tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(scale.dataExtent.min, equals(50)); + expect(scale.dataExtent.max, equals(100)); + }); + + test('values within data extent', () { + final tickProvider = new StaticTickProvider([ + new TickSpec(50, label: '50'), + new TickSpec(75, label: '75'), + new TickSpec(100, label: '100'), + ]); + + scale.addDomain(0); + scale.addDomain(150); + + expect(scale.dataExtent.min, equals(0)); + expect(scale.dataExtent.max, equals(150)); + + tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: formatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(scale.dataExtent.min, equals(0)); + expect(scale.dataExtent.max, equals(150)); + }); + }); + + group('formatter', () { + test('is not called when all ticks have labels', () { + final tickProvider = new StaticTickProvider([ + new TickSpec(50, label: '50'), + new TickSpec(75, label: '75'), + new TickSpec(100, label: '100'), + ]); + + final fakeFormatter = new FakeNumericTickFormatter(); + + tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: fakeFormatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(fakeFormatter.calledTimes, equals(0)); + }); + + test('is called when one ticks does not have label', () { + final tickProvider = new StaticTickProvider([ + new TickSpec(50, label: '50'), + new TickSpec(75), + new TickSpec(100, label: '100'), + ]); + + final fakeFormatter = new FakeNumericTickFormatter(); + + tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: fakeFormatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(fakeFormatter.calledTimes, equals(1)); + }); + + test('is called when all ticks do not have labels', () { + final tickProvider = new StaticTickProvider([ + new TickSpec(50), + new TickSpec(75), + new TickSpec(100), + ]); + + final fakeFormatter = new FakeNumericTickFormatter(); + + tickProvider.getTicks( + context: context, + graphicsFactory: graphicsFactory, + scale: scale, + formatter: fakeFormatter, + formatterValueCache: {}, + tickDrawStrategy: drawStrategy, + orientation: null); + + expect(fakeFormatter.calledTimes, equals(1)); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/time/date_time_tick_formatter_test.dart b/web/charts/common/test/chart/cartesian/axis/time/date_time_tick_formatter_test.dart new file mode 100644 index 000000000..5042c6be9 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/time/date_time_tick_formatter_test.dart @@ -0,0 +1,253 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/axis/time/time_tick_formatter.dart'; +import 'package:charts_common/src/chart/cartesian/axis/time/date_time_tick_formatter.dart'; +import 'package:test/test.dart'; + +const EPSILON = 0.001; + +typedef bool IsTransitionFunction(DateTime tickValue, DateTime prevTickValue); + +class FakeTimeTickFormatter implements TimeTickFormatter { + static const firstTick = '-firstTick-'; + static const simpleTick = '-simpleTick-'; + static const transitionTick = '-transitionTick-'; + static final transitionAlwaysFalse = (_, __) => false; + + final String id; + final IsTransitionFunction isTransitionFunction; + + FakeTimeTickFormatter(this.id, {IsTransitionFunction isTransitionFunction}) + : isTransitionFunction = isTransitionFunction ?? transitionAlwaysFalse; + + @override + String formatFirstTick(DateTime date) => + id + firstTick + date.millisecondsSinceEpoch.toString(); + + @override + String formatSimpleTick(DateTime date) => + id + simpleTick + date.millisecondsSinceEpoch.toString(); + + @override + String formatTransitionTick(DateTime date) => + id + transitionTick + date.millisecondsSinceEpoch.toString(); + + @override + bool isTransition(DateTime tickValue, DateTime prevTickValue) => + isTransitionFunction(tickValue, prevTickValue); +} + +void main() { + TimeTickFormatter timeFormatter1; + TimeTickFormatter timeFormatter2; + TimeTickFormatter timeFormatter3; + + setUp(() { + timeFormatter1 = new FakeTimeTickFormatter('fake1'); + timeFormatter2 = new FakeTimeTickFormatter('fake2'); + timeFormatter3 = new FakeTimeTickFormatter('fake3'); + }); + + group('Uses formatter', () { + test('with largest interval less than diff between tickValues', () { + final formatter = new DateTimeTickFormatter.withFormatters( + {10: timeFormatter1, 100: timeFormatter2, 1000: timeFormatter3}); + final formatterCache = {}; + + final ticksWith10Diff = [ + new DateTime.fromMillisecondsSinceEpoch(0), + new DateTime.fromMillisecondsSinceEpoch(10), + new DateTime.fromMillisecondsSinceEpoch(20) + ]; + final ticksWith20Diff = [ + new DateTime.fromMillisecondsSinceEpoch(0), + new DateTime.fromMillisecondsSinceEpoch(20), + new DateTime.fromMillisecondsSinceEpoch(40) + ]; + final ticksWith100Diff = [ + new DateTime.fromMillisecondsSinceEpoch(0), + new DateTime.fromMillisecondsSinceEpoch(100), + new DateTime.fromMillisecondsSinceEpoch(200) + ]; + final ticksWith200Diff = [ + new DateTime.fromMillisecondsSinceEpoch(0), + new DateTime.fromMillisecondsSinceEpoch(200), + new DateTime.fromMillisecondsSinceEpoch(400) + ]; + final ticksWith1000Diff = [ + new DateTime.fromMillisecondsSinceEpoch(0), + new DateTime.fromMillisecondsSinceEpoch(1000), + new DateTime.fromMillisecondsSinceEpoch(2000) + ]; + + final expectedLabels10Diff = [ + 'fake1-firstTick-0', + 'fake1-simpleTick-10', + 'fake1-simpleTick-20' + ]; + final expectedLabels20Diff = [ + 'fake1-firstTick-0', + 'fake1-simpleTick-20', + 'fake1-simpleTick-40' + ]; + final expectedLabels100Diff = [ + 'fake2-firstTick-0', + 'fake2-simpleTick-100', + 'fake2-simpleTick-200' + ]; + final expectedLabels200Diff = [ + 'fake2-firstTick-0', + 'fake2-simpleTick-200', + 'fake2-simpleTick-400' + ]; + final expectedLabels1000Diff = [ + 'fake3-firstTick-0', + 'fake3-simpleTick-1000', + 'fake3-simpleTick-2000' + ]; + + final actualLabelsWith10Diff = + formatter.format(ticksWith10Diff, formatterCache, stepSize: 10); + final actualLabelsWith20Diff = + formatter.format(ticksWith20Diff, formatterCache, stepSize: 20); + final actualLabelsWith100Diff = + formatter.format(ticksWith100Diff, formatterCache, stepSize: 100); + final actualLabelsWith200Diff = + formatter.format(ticksWith200Diff, formatterCache, stepSize: 200); + final actualLabelsWith1000Diff = + formatter.format(ticksWith1000Diff, formatterCache, stepSize: 1000); + + expect(actualLabelsWith10Diff, equals(expectedLabels10Diff)); + expect(actualLabelsWith20Diff, equals(expectedLabels20Diff)); + + expect(actualLabelsWith100Diff, equals(expectedLabels100Diff)); + expect(actualLabelsWith200Diff, equals(expectedLabels200Diff)); + expect(actualLabelsWith1000Diff, equals(expectedLabels1000Diff)); + }); + + test('with smallest interval when no smaller one exists', () { + final formatter = new DateTimeTickFormatter.withFormatters( + {10: timeFormatter1, 100: timeFormatter2}); + final formatterCache = {}; + + final ticks = [ + new DateTime.fromMillisecondsSinceEpoch(0), + new DateTime.fromMillisecondsSinceEpoch(1), + new DateTime.fromMillisecondsSinceEpoch(2) + ]; + final expectedLabels = [ + 'fake1-firstTick-0', + 'fake1-simpleTick-1', + 'fake1-simpleTick-2' + ]; + final actualLabels = formatter.format(ticks, formatterCache, stepSize: 1); + + expect(actualLabels, equals(expectedLabels)); + }); + + test('with smallest interval for single tick input', () { + final formatter = new DateTimeTickFormatter.withFormatters( + {10: timeFormatter1, 100: timeFormatter2}); + final formatterCache = {}; + + final ticks = [new DateTime.fromMillisecondsSinceEpoch(5000)]; + final expectedLabels = ['fake1-firstTick-5000']; + final actualLabels = formatter.format(ticks, formatterCache, stepSize: 0); + expect(actualLabels, equals(expectedLabels)); + }); + + test('on empty input doesnt break', () { + final formatter = + new DateTimeTickFormatter.withFormatters({10: timeFormatter1}); + final formatterCache = {}; + + final actualLabels = + formatter.format([], formatterCache, stepSize: 10); + expect(actualLabels, isEmpty); + }); + + test('that formats transition tick with transition format', () { + final timeFormatter = new FakeTimeTickFormatter('fake', + isTransitionFunction: (DateTime tickValue, _) => + tickValue.millisecondsSinceEpoch == 20); + final formatterCache = {}; + + final formatter = + new DateTimeTickFormatter.withFormatters({10: timeFormatter}); + + final ticks = [ + new DateTime.fromMillisecondsSinceEpoch(0), + new DateTime.fromMillisecondsSinceEpoch(10), + new DateTime.fromMillisecondsSinceEpoch(20), + new DateTime.fromMillisecondsSinceEpoch(30) + ]; + + final expectedLabels = [ + 'fake-firstTick-0', + 'fake-simpleTick-10', + 'fake-transitionTick-20', + 'fake-simpleTick-30' + ]; + final actualLabels = + formatter.format(ticks, formatterCache, stepSize: 10); + + expect(actualLabels, equals(expectedLabels)); + }); + }); + + group('check custom time tick formatters', () { + test('throws arugment error if time resolution key is not positive', () { + // -1 is reserved for any, if there is only one formatter, -1 is allowed. + expect( + () => new DateTimeTickFormatter.withFormatters( + {-1: timeFormatter1, 2: timeFormatter2}), + throwsArgumentError); + }); + + test('throws argument error if formatters is null or empty', () { + expect(() => new DateTimeTickFormatter.withFormatters(null), + throwsArgumentError); + expect(() => new DateTimeTickFormatter.withFormatters({}), + throwsArgumentError); + }); + + test('throws arugment error if formatters are not sorted', () { + expect( + () => new DateTimeTickFormatter.withFormatters({ + 3: timeFormatter1, + 1: timeFormatter2, + 2: timeFormatter3, + }), + throwsArgumentError); + + expect( + () => new DateTimeTickFormatter.withFormatters({ + 1: timeFormatter1, + 3: timeFormatter2, + 2: timeFormatter3, + }), + throwsArgumentError); + + expect( + () => new DateTimeTickFormatter.withFormatters({ + 2: timeFormatter1, + 3: timeFormatter2, + 1: timeFormatter3, + }), + throwsArgumentError); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/time/simple_date_time_factory.dart b/web/charts/common/test/chart/cartesian/axis/time/simple_date_time_factory.dart new file mode 100644 index 000000000..340ad685d --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/time/simple_date_time_factory.dart @@ -0,0 +1,42 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/common/date_time_factory.dart'; +import 'package:intl/intl.dart' show DateFormat; + +/// Returns DateTime for testing. +class SimpleDateTimeFactory implements DateTimeFactory { + const SimpleDateTimeFactory(); + + @override + DateTime createDateTimeFromMilliSecondsSinceEpoch( + int millisecondsSinceEpoch) => + new DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch); + + @override + DateTime createDateTime(int year, + [int month = 1, + int day = 1, + int hour = 0, + int minute = 0, + int second = 0, + int millisecond = 0, + int microsecond = 0]) => + new DateTime( + year, month, day, hour, minute, second, millisecond, microsecond); + + @override + DateFormat createDateFormat(String pattern) => new DateFormat(pattern); +} diff --git a/web/charts/common/test/chart/cartesian/axis/time/time_stepper_test.dart b/web/charts/common/test/chart/cartesian/axis/time/time_stepper_test.dart new file mode 100644 index 000000000..d17e30425 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/time/time_stepper_test.dart @@ -0,0 +1,484 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/axis/time/date_time_extents.dart'; +import 'package:charts_common/src/chart/cartesian/axis/time/day_time_stepper.dart'; +import 'package:charts_common/src/chart/cartesian/axis/time/hour_time_stepper.dart'; +import 'package:charts_common/src/chart/cartesian/axis/time/minute_time_stepper.dart'; +import 'package:charts_common/src/chart/cartesian/axis/time/month_time_stepper.dart'; +import 'package:charts_common/src/chart/cartesian/axis/time/year_time_stepper.dart'; +import 'package:test/test.dart'; +import 'simple_date_time_factory.dart' show SimpleDateTimeFactory; + +const EPSILON = 0.001; + +void main() { + const dateTimeFactory = const SimpleDateTimeFactory(); + const millisecondsInHour = 3600 * 1000; + + setUp(() {}); + + group('Day time stepper', () { + test('get steps with 1 day increments', () { + final stepper = new DayTimeStepper(dateTimeFactory); + final extent = new DateTimeExtents( + start: new DateTime(2017, 8, 20), end: new DateTime(2017, 8, 25)); + final stepIterable = stepper.getSteps(extent)..iterator.reset(1); + final steps = stepIterable.toList(); + + expect(steps.length, equals(6)); + expect( + steps, + equals([ + new DateTime(2017, 8, 20), + new DateTime(2017, 8, 21), + new DateTime(2017, 8, 22), + new DateTime(2017, 8, 23), + new DateTime(2017, 8, 24), + new DateTime(2017, 8, 25), + ])); + }); + + test('get steps with 5 day increments', () { + final stepper = new DayTimeStepper(dateTimeFactory); + final extent = new DateTimeExtents( + start: new DateTime(2017, 8, 10), + end: new DateTime(2017, 8, 26), + ); + + final stepIterable = stepper.getSteps(extent)..iterator.reset(5); + final steps = stepIterable.toList(); + + expect(steps.length, equals(4)); + // Note, this is because 5 day increments in a month is 1,6,11,16,21,26,31 + expect( + steps, + equals([ + new DateTime(2017, 8, 11), + new DateTime(2017, 8, 16), + new DateTime(2017, 8, 21), + new DateTime(2017, 8, 26), + ])); + }); + + test('step through daylight saving forward change', () { + final stepper = new DayTimeStepper(dateTimeFactory); + // DST for PST 2017 begin on March 12 + final extent = new DateTimeExtents( + start: new DateTime(2017, 3, 11), + end: new DateTime(2017, 3, 13), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(1); + final steps = stepIterable.toList(); + + expect(steps.length, equals(3)); + expect( + steps, + equals([ + new DateTime(2017, 3, 11), + new DateTime(2017, 3, 12), + new DateTime(2017, 3, 13), + ])); + }); + + test('step through daylight saving backward change', () { + final stepper = new DayTimeStepper(dateTimeFactory); + // DST for PST 2017 end on November 5 + final extent = new DateTimeExtents( + start: new DateTime(2017, 11, 4), + end: new DateTime(2017, 11, 6), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(1); + final steps = stepIterable.toList(); + + expect(steps.length, equals(3)); + expect( + steps, + equals([ + new DateTime(2017, 11, 4), + new DateTime(2017, 11, 5), + new DateTime(2017, 11, 6), + ])); + }); + }); + + group('Hour time stepper', () { + test('gets steps in 1 hour increments', () { + final stepper = new HourTimeStepper(dateTimeFactory); + final extent = new DateTimeExtents( + start: new DateTime(2017, 8, 20, 10), + end: new DateTime(2017, 8, 20, 15), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(1); + final steps = stepIterable.toList(); + + expect(steps.length, equals(6)); + expect( + steps, + equals([ + new DateTime(2017, 8, 20, 10), + new DateTime(2017, 8, 20, 11), + new DateTime(2017, 8, 20, 12), + new DateTime(2017, 8, 20, 13), + new DateTime(2017, 8, 20, 14), + new DateTime(2017, 8, 20, 15), + ])); + }); + + test('gets steps in 4 hour increments', () { + final stepper = new HourTimeStepper(dateTimeFactory); + final extent = new DateTimeExtents( + start: new DateTime(2017, 8, 20, 10), + end: new DateTime(2017, 8, 21, 10), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(4); + final steps = stepIterable.toList(); + + expect(steps.length, equals(6)); + expect( + steps, + equals([ + new DateTime(2017, 8, 20, 12), + new DateTime(2017, 8, 20, 16), + new DateTime(2017, 8, 20, 20), + new DateTime(2017, 8, 21, 0), + new DateTime(2017, 8, 21, 4), + new DateTime(2017, 8, 21, 8), + ])); + }); + + test('step through daylight saving forward change in 1 hour increments', + () { + final stepper = new HourTimeStepper(dateTimeFactory); + // DST for PST 2017 begin on March 12. At 2am clocks are turned to 3am. + final extent = new DateTimeExtents( + start: new DateTime(2017, 3, 12, 0), + end: new DateTime(2017, 3, 12, 5), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(1); + final steps = stepIterable.toList(); + + expect(steps.length, equals(5)); + expect( + steps, + equals([ + new DateTime(2017, 3, 12, 0), + new DateTime(2017, 3, 12, 1), + new DateTime(2017, 3, 12, 3), + new DateTime(2017, 3, 12, 4), + new DateTime(2017, 3, 12, 5), + ])); + }); + + test('step through daylight saving backward change in 1 hour increments', + () { + final stepper = new HourTimeStepper(dateTimeFactory); + // DST for PST 2017 end on November 5. At 2am, clocks are turned to 1am. + final extent = new DateTimeExtents( + start: new DateTime(2017, 11, 5, 0), + end: new DateTime(2017, 11, 5, 4), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(1); + final steps = stepIterable.toList(); + + expect(steps.length, equals(6)); + expect( + steps, + equals([ + new DateTime(2017, 11, 5, 0), + new DateTime(2017, 11, 5, 0) + .add(new Duration(milliseconds: millisecondsInHour)), + new DateTime(2017, 11, 5, 0) + .add(new Duration(milliseconds: millisecondsInHour * 2)), + new DateTime(2017, 11, 5, 2), + new DateTime(2017, 11, 5, 3), + new DateTime(2017, 11, 5, 4), + ])); + }); + + test('step through daylight saving forward change in 4 hour increments', + () { + final stepper = new HourTimeStepper(dateTimeFactory); + // DST for PST 2017 begin on March 12. At 2am clocks are turned to 3am. + final extent = new DateTimeExtents( + start: new DateTime(2017, 3, 12, 0), + end: new DateTime(2017, 3, 13, 0), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(4); + final steps = stepIterable.toList(); + + expect(steps.length, equals(6)); + expect( + steps, + equals([ + new DateTime(2017, 3, 12, 4), + new DateTime(2017, 3, 12, 8), + new DateTime(2017, 3, 12, 12), + new DateTime(2017, 3, 12, 16), + new DateTime(2017, 3, 12, 20), + new DateTime(2017, 3, 13, 0), + ])); + }); + + test('step through daylight saving backward change in 4 hour increments', + () { + final stepper = new HourTimeStepper(dateTimeFactory); + // DST for PST 2017 end on November 5. + // At 2am, clocks are turned to 1am. + final extent = new DateTimeExtents( + start: new DateTime(2017, 11, 5, 0), + end: new DateTime(2017, 11, 6, 0), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(4); + final steps = stepIterable.toList(); + + expect(steps.length, equals(7)); + expect( + steps, + equals([ + new DateTime(2017, 11, 5, 0) + .add(new Duration(milliseconds: millisecondsInHour)), + new DateTime(2017, 11, 5, 4), + new DateTime(2017, 11, 5, 8), + new DateTime(2017, 11, 5, 12), + new DateTime(2017, 11, 5, 16), + new DateTime(2017, 11, 5, 20), + new DateTime(2017, 11, 6, 0), + ])); + }); + }); + + group('Minute time stepper', () { + test('gets steps with 5 minute increments', () { + final stepper = new MinuteTimeStepper(dateTimeFactory); + final extent = new DateTimeExtents( + start: new DateTime(2017, 8, 20, 3, 46), + end: new DateTime(2017, 8, 20, 4, 02), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(5); + final steps = stepIterable.toList(); + + expect(steps.length, equals(3)); + expect( + steps, + equals([ + new DateTime(2017, 8, 20, 3, 50), + new DateTime(2017, 8, 20, 3, 55), + new DateTime(2017, 8, 20, 4), + ])); + }); + + test('step through daylight saving forward change', () { + final stepper = new MinuteTimeStepper(dateTimeFactory); + // DST for PST 2017 begin on March 12. At 2am clocks are turned to 3am. + final extent = new DateTimeExtents( + start: new DateTime(2017, 3, 12, 1, 40), + end: new DateTime(2017, 3, 12, 4, 02), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(15); + final steps = stepIterable.toList(); + + expect(steps.length, equals(6)); + expect( + steps, + equals([ + new DateTime(2017, 3, 12, 1, 45), + new DateTime(2017, 3, 12, 3), + new DateTime(2017, 3, 12, 3, 15), + new DateTime(2017, 3, 12, 3, 30), + new DateTime(2017, 3, 12, 3, 45), + new DateTime(2017, 3, 12, 4), + ])); + }); + + test('steps correctly after daylight saving forward change', () { + final stepper = new MinuteTimeStepper(dateTimeFactory); + // DST for PST 2017 begin on March 12. At 2am clocks are turned to 3am. + final extent = new DateTimeExtents( + start: new DateTime(2017, 3, 12, 3, 02), + end: new DateTime(2017, 3, 12, 4, 02), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(30); + final steps = stepIterable.toList(); + + expect(steps.length, equals(2)); + expect( + steps, + equals([ + new DateTime(2017, 3, 12, 3, 30), + new DateTime(2017, 3, 12, 4), + ])); + }); + + test('step through daylight saving backward change', () { + final stepper = new MinuteTimeStepper(dateTimeFactory); + // DST for PST 2017 end on November 5. + // At 2am, clocks are turned to 1am. + final extent = new DateTimeExtents( + start: new DateTime(2017, 11, 5) + .add(new Duration(hours: 1, minutes: 29)), + end: new DateTime(2017, 11, 5, 3, 02)); + final stepIterable = stepper.getSteps(extent)..iterator.reset(30); + final steps = stepIterable.toList(); + + expect(steps.length, equals(6)); + expect( + steps, + equals([ + // The first 1:30am + new DateTime(2017, 11, 5).add(new Duration(hours: 1, minutes: 30)), + // The 2nd 1am. + new DateTime(2017, 11, 5).add(new Duration(hours: 2)), + // The 2nd 1:30am + new DateTime(2017, 11, 5).add(new Duration(hours: 2, minutes: 30)), + // 2am + new DateTime(2017, 11, 5).add(new Duration(hours: 3)), + // 2:30am + new DateTime(2017, 11, 5).add(new Duration(hours: 3, minutes: 30)), + // 3am + new DateTime(2017, 11, 5, 3) + ])); + }); + }); + + group('Month time stepper', () { + test('steps crosses the year', () { + final stepper = new MonthTimeStepper(dateTimeFactory); + final extent = new DateTimeExtents( + start: new DateTime(2017, 5), + end: new DateTime(2018, 9), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(4); + final steps = stepIterable.toList(); + + expect(steps.length, equals(4)); + expect( + steps, + equals([ + new DateTime(2017, 8), + new DateTime(2017, 12), + new DateTime(2018, 4), + new DateTime(2018, 8), + ])); + }); + + test('steps within one year', () { + final stepper = new MonthTimeStepper(dateTimeFactory); + final extent = new DateTimeExtents( + start: new DateTime(2017, 1), + end: new DateTime(2017, 5), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(2); + final steps = stepIterable.toList(); + + expect(steps.length, equals(2)); + expect( + steps, + equals([ + new DateTime(2017, 2), + new DateTime(2017, 4), + ])); + }); + + test('step before would allow ticks to include last month of the year', () { + final stepper = new MonthTimeStepper(dateTimeFactory); + final time = new DateTime(2017, 10); + + expect(stepper.getStepTimeBeforeInclusive(time, 1), + equals(new DateTime(2017, 10))); + + // Months - 3, 6, 9, 12 + expect(stepper.getStepTimeBeforeInclusive(time, 3), + equals(new DateTime(2017, 9))); + + // Months - 6, 12 + expect(stepper.getStepTimeBeforeInclusive(time, 6), + equals(new DateTime(2017, 6))); + }); + + test('step before for January', () { + final stepper = new MonthTimeStepper(dateTimeFactory); + final time = new DateTime(2017, 1); + + expect(stepper.getStepTimeBeforeInclusive(time, 1), + equals(new DateTime(2017, 1))); + + // Months - 3, 6, 9, 12 + expect(stepper.getStepTimeBeforeInclusive(time, 3), + equals(new DateTime(2016, 12))); + + // Months - 6, 12 + expect(stepper.getStepTimeBeforeInclusive(time, 6), + equals(new DateTime(2016, 12))); + }); + + test('step before for December', () { + final stepper = new MonthTimeStepper(dateTimeFactory); + final time = new DateTime(2017, 12); + + expect(stepper.getStepTimeBeforeInclusive(time, 1), + equals(new DateTime(2017, 12))); + + // Months - 3, 6, 9, 12 + expect(stepper.getStepTimeBeforeInclusive(time, 3), + equals(new DateTime(2017, 12))); + + // Months - 6, 12 + expect(stepper.getStepTimeBeforeInclusive(time, 6), + equals(new DateTime(2017, 12))); + }); + }); + + group('Year stepper', () { + test('steps in 10 year increments', () { + final stepper = new YearTimeStepper(dateTimeFactory); + final extent = new DateTimeExtents( + start: new DateTime(2017), + end: new DateTime(2042), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(10); + final steps = stepIterable.toList(); + + expect(steps.length, equals(3)); + expect( + steps, + equals([ + new DateTime(2020), + new DateTime(2030), + new DateTime(2040), + ])); + }); + + test('steps through negative year', () { + final stepper = new YearTimeStepper(dateTimeFactory); + final extent = new DateTimeExtents( + start: new DateTime(-420), + end: new DateTime(240), + ); + final stepIterable = stepper.getSteps(extent)..iterator.reset(200); + final steps = stepIterable.toList(); + + expect(steps.length, equals(4)); + expect( + steps, + equals([ + new DateTime(-400), + new DateTime(-200), + new DateTime(0), + new DateTime(200), + ])); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/axis/time/time_tick_provider_test.dart b/web/charts/common/test/chart/cartesian/axis/time/time_tick_provider_test.dart new file mode 100644 index 000000000..19c871e09 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/axis/time/time_tick_provider_test.dart @@ -0,0 +1,67 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/axis/time/auto_adjusting_date_time_tick_provider.dart'; +import 'package:test/test.dart'; +import 'simple_date_time_factory.dart' show SimpleDateTimeFactory; + +const EPSILON = 0.001; + +void main() { + const dateTimeFactory = const SimpleDateTimeFactory(); + + group('Find closest step size from stepper', () { + test('from exactly matching step size', () { + final stepper = AutoAdjustingDateTimeTickProvider.createHourTickProvider( + dateTimeFactory); + final oneHourMs = (new Duration(hours: 1)).inMilliseconds; + final closestStepSize = stepper.getClosestStepSize(oneHourMs); + + expect(closestStepSize, equals(oneHourMs)); + }); + + test('choose smallest increment if step is smaller than smallest increment', + () { + final stepper = AutoAdjustingDateTimeTickProvider.createHourTickProvider( + dateTimeFactory); + final oneHourMs = (new Duration(hours: 1)).inMilliseconds; + final closestStepSize = stepper + .getClosestStepSize((new Duration(minutes: 56)).inMilliseconds); + + expect(closestStepSize, equals(oneHourMs)); + }); + + test('choose largest increment if step is larger than largest increment', + () { + final stepper = AutoAdjustingDateTimeTickProvider.createHourTickProvider( + dateTimeFactory); + final oneDayMs = (new Duration(hours: 24)).inMilliseconds; + final closestStepSize = + stepper.getClosestStepSize((new Duration(hours: 25)).inMilliseconds); + + expect(closestStepSize, equals(oneDayMs)); + }); + + test('choose closest increment if exact not found', () { + final stepper = AutoAdjustingDateTimeTickProvider.createHourTickProvider( + dateTimeFactory); + final threeHoursMs = (new Duration(hours: 3)).inMilliseconds; + final closestStepSize = stepper.getClosestStepSize( + (new Duration(hours: 3, minutes: 28)).inMilliseconds); + + expect(closestStepSize, equals(threeHoursMs)); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/cartesian_chart_test.dart b/web/charts/common/test/chart/cartesian/cartesian_chart_test.dart new file mode 100644 index 000000000..7028786d3 --- /dev/null +++ b/web/charts/common/test/chart/cartesian/cartesian_chart_test.dart @@ -0,0 +1,105 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart'; +import 'package:charts_common/src/chart/cartesian/axis/spec/date_time_axis_spec.dart'; +import 'package:charts_common/src/chart/cartesian/axis/spec/ordinal_axis_spec.dart'; +import 'package:charts_common/src/chart/cartesian/axis/spec/numeric_axis_spec.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/time_series/time_series_chart.dart'; +import 'package:charts_common/src/common/graphics_factory.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockContext extends Mock implements ChartContext {} + +class MockGraphicsFactory extends Mock implements GraphicsFactory {} + +class FakeNumericChart extends NumericCartesianChart { + FakeNumericChart() { + context = new MockContext(); + graphicsFactory = new MockGraphicsFactory(); + } + + @override + void initDomainAxis() { + // Purposely bypass the renderer code. + } +} + +class FakeOrdinalChart extends OrdinalCartesianChart { + FakeOrdinalChart() { + context = new MockContext(); + graphicsFactory = new MockGraphicsFactory(); + } + + @override + void initDomainAxis() { + // Purposely bypass the renderer code. + } +} + +class FakeTimeSeries extends TimeSeriesChart { + FakeTimeSeries() { + context = new MockContext(); + graphicsFactory = new MockGraphicsFactory(); + } + + @override + void initDomainAxis() { + // Purposely bypass the renderer code. + } +} + +void main() { + group('Axis reset with new axis spec', () { + test('for ordinal chart', () { + final chart = new FakeOrdinalChart(); + chart.configurationChanged(); + final domainAxis = chart.domainAxis; + expect(domainAxis, isNotNull); + + chart.domainAxisSpec = new OrdinalAxisSpec(); + chart.configurationChanged(); + + expect(domainAxis, isNot(chart.domainAxis)); + }); + + test('for numeric chart', () { + final chart = new FakeNumericChart(); + chart.configurationChanged(); + final domainAxis = chart.domainAxis; + expect(domainAxis, isNotNull); + + chart.domainAxisSpec = new NumericAxisSpec(); + chart.configurationChanged(); + + expect(domainAxis, isNot(chart.domainAxis)); + }); + + test('for time series chart', () { + final chart = new FakeTimeSeries(); + chart.configurationChanged(); + final domainAxis = chart.domainAxis; + expect(domainAxis, isNotNull); + + chart.domainAxisSpec = new DateTimeAxisSpec(); + chart.configurationChanged(); + + expect(domainAxis, isNot(chart.domainAxis)); + }); + }); +} diff --git a/web/charts/common/test/chart/cartesian/cartesian_renderer_test.dart b/web/charts/common/test/chart/cartesian/cartesian_renderer_test.dart new file mode 100644 index 000000000..574b5373e --- /dev/null +++ b/web/charts/common/test/chart/cartesian/cartesian_renderer_test.dart @@ -0,0 +1,295 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/cartesian/cartesian_renderer.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/datum_details.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/common/series_datum.dart'; +import 'package:charts_common/src/common/symbol_renderer.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +/// For testing viewport start / end. +class FakeCartesianRenderer extends BaseCartesianRenderer { + @override + List getNearestDatumDetailPerSeries(Point chartPoint, + bool byDomain, Rectangle boundsOverride) => + null; + + @override + void paint(ChartCanvas canvas, double animationPercent) {} + + @override + void update(List seriesList, bool isAnimating) {} + + @override + SymbolRenderer get symbolRenderer => null; + + DatumDetails addPositionToDetailsForSeriesDatum( + DatumDetails details, SeriesDatum seriesDatum) { + return details; + } +} + +class MockAxis extends Mock implements Axis {} + +void main() { + BaseCartesianRenderer renderer; + + setUp(() { + renderer = new FakeCartesianRenderer(); + }); + + group('find viewport start', () { + test('several domains are in the viewport', () { + final data = [0, 1, 2, 3, 4, 5, 6]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(-1); + when(axis.compareDomainValueToViewport(1)).thenReturn(-1); + when(axis.compareDomainValueToViewport(2)).thenReturn(0); + when(axis.compareDomainValueToViewport(3)).thenReturn(0); + when(axis.compareDomainValueToViewport(4)).thenReturn(0); + when(axis.compareDomainValueToViewport(5)).thenReturn(1); + when(axis.compareDomainValueToViewport(6)).thenReturn(1); + + final start = renderer.findNearestViewportStart(axis, domainFn, data); + + expect(start, equals(2)); + }); + + test('extents are all in the viewport, use the first domain', () { + // Start of viewport is the same as the start of the domain. + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(any)).thenReturn(0); + + final start = renderer.findNearestViewportStart(axis, domainFn, data); + + expect(start, equals(0)); + }); + + test('is the first domain', () { + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(0); + when(axis.compareDomainValueToViewport(1)).thenReturn(1); + when(axis.compareDomainValueToViewport(2)).thenReturn(1); + when(axis.compareDomainValueToViewport(3)).thenReturn(1); + + final start = renderer.findNearestViewportStart(axis, domainFn, data); + + expect(start, equals(0)); + }); + + test('is the last domain', () { + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(-1); + when(axis.compareDomainValueToViewport(1)).thenReturn(-1); + when(axis.compareDomainValueToViewport(2)).thenReturn(-1); + when(axis.compareDomainValueToViewport(3)).thenReturn(0); + + final start = renderer.findNearestViewportStart(axis, domainFn, data); + + expect(start, equals(3)); + }); + + test('is the middle', () { + final data = [0, 1, 2, 3, 4, 5, 6]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(-1); + when(axis.compareDomainValueToViewport(1)).thenReturn(-1); + when(axis.compareDomainValueToViewport(2)).thenReturn(-1); + when(axis.compareDomainValueToViewport(3)).thenReturn(0); + when(axis.compareDomainValueToViewport(4)).thenReturn(1); + when(axis.compareDomainValueToViewport(5)).thenReturn(1); + when(axis.compareDomainValueToViewport(6)).thenReturn(1); + + final start = renderer.findNearestViewportStart(axis, domainFn, data); + + expect(start, equals(3)); + }); + + test('viewport is between data', () { + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(-1); + when(axis.compareDomainValueToViewport(1)).thenReturn(-1); + when(axis.compareDomainValueToViewport(2)).thenReturn(1); + when(axis.compareDomainValueToViewport(3)).thenReturn(1); + + final start = renderer.findNearestViewportStart(axis, domainFn, data); + + expect(start, equals(1)); + }); + + // Error case, viewport shouldn't be set to the outside of the extents. + // We still want to provide a value. + test('all extents greater than viewport ', () { + // Return the right most value as start of viewport. + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(any)).thenReturn(1); + + final start = renderer.findNearestViewportStart(axis, domainFn, data); + + expect(start, equals(3)); + }); + + // Error case, viewport shouldn't be set to the outside of the extents. + // We still want to provide a value. + test('all extents less than viewport ', () { + // Return the left most value as the start of the viewport. + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(any)).thenReturn(-1); + + final start = renderer.findNearestViewportStart(axis, domainFn, data); + + expect(start, equals(0)); + }); + }); + + group('find viewport end', () { + test('several domains are in the viewport', () { + final data = [0, 1, 2, 3, 4, 5, 6]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(-1); + when(axis.compareDomainValueToViewport(1)).thenReturn(-1); + when(axis.compareDomainValueToViewport(2)).thenReturn(0); + when(axis.compareDomainValueToViewport(3)).thenReturn(0); + when(axis.compareDomainValueToViewport(4)).thenReturn(0); + when(axis.compareDomainValueToViewport(5)).thenReturn(1); + when(axis.compareDomainValueToViewport(6)).thenReturn(1); + + final start = renderer.findNearestViewportEnd(axis, domainFn, data); + + expect(start, equals(4)); + }); + + test('extents are all in the viewport, use the last domain', () { + // Start of viewport is the same as the end of the domain. + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(any)).thenReturn(0); + + final start = renderer.findNearestViewportEnd(axis, domainFn, data); + + expect(start, equals(3)); + }); + + test('is the first domain', () { + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(0); + when(axis.compareDomainValueToViewport(1)).thenReturn(1); + when(axis.compareDomainValueToViewport(2)).thenReturn(1); + when(axis.compareDomainValueToViewport(3)).thenReturn(1); + + final start = renderer.findNearestViewportEnd(axis, domainFn, data); + + expect(start, equals(0)); + }); + + test('is the last domain', () { + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(-1); + when(axis.compareDomainValueToViewport(1)).thenReturn(-1); + when(axis.compareDomainValueToViewport(2)).thenReturn(-1); + when(axis.compareDomainValueToViewport(3)).thenReturn(0); + + final start = renderer.findNearestViewportEnd(axis, domainFn, data); + + expect(start, equals(3)); + }); + + test('is the middle', () { + final data = [0, 1, 2, 3, 4, 5, 6]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(-1); + when(axis.compareDomainValueToViewport(1)).thenReturn(-1); + when(axis.compareDomainValueToViewport(2)).thenReturn(-1); + when(axis.compareDomainValueToViewport(3)).thenReturn(0); + when(axis.compareDomainValueToViewport(4)).thenReturn(1); + when(axis.compareDomainValueToViewport(5)).thenReturn(1); + when(axis.compareDomainValueToViewport(6)).thenReturn(1); + + final start = renderer.findNearestViewportEnd(axis, domainFn, data); + + expect(start, equals(3)); + }); + + test('viewport is between data', () { + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(0)).thenReturn(-1); + when(axis.compareDomainValueToViewport(1)).thenReturn(-1); + when(axis.compareDomainValueToViewport(2)).thenReturn(1); + when(axis.compareDomainValueToViewport(3)).thenReturn(1); + + final start = renderer.findNearestViewportEnd(axis, domainFn, data); + + expect(start, equals(2)); + }); + + // Error case, viewport shouldn't be set to the outside of the extents. + // We still want to provide a value. + test('all extents greater than viewport ', () { + // Return the right most value as start of viewport. + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(any)).thenReturn(1); + + final start = renderer.findNearestViewportEnd(axis, domainFn, data); + + expect(start, equals(3)); + }); + + // Error case, viewport shouldn't be set to the outside of the extents. + // We still want to provide a value. + test('all extents less than viewport ', () { + // Return the left most value as the start of the viewport. + final data = [0, 1, 2, 3]; + final domainFn = (int index) => data[index]; + final axis = new MockAxis(); + when(axis.compareDomainValueToViewport(any)).thenReturn(-1); + + final start = renderer.findNearestViewportEnd(axis, domainFn, data); + + expect(start, equals(0)); + }); + }); +} diff --git a/web/charts/common/test/chart/common/behavior/a11y/domain_a11y_explore_behavior_test.dart b/web/charts/common/test/chart/common/behavior/a11y/domain_a11y_explore_behavior_test.dart new file mode 100644 index 000000000..6d7903b0c --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/a11y/domain_a11y_explore_behavior_test.dart @@ -0,0 +1,253 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Rectangle; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/common/behavior/a11y/domain_a11y_explore_behavior.dart'; +import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart'; +import 'package:charts_common/src/data/series.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockContext extends Mock implements ChartContext {} + +class MockAxis extends Mock implements Axis {} + +class FakeCartesianChart extends CartesianChart { + @override + Rectangle drawAreaBounds; + + void callFireOnPostprocess(List> seriesList) { + fireOnPostprocess(seriesList); + } + + @override + initDomainAxis() {} +} + +void main() { + FakeCartesianChart chart; + DomainA11yExploreBehavior behavior; + MockAxis domainAxis; + + MutableSeries _series1; + final _s1D1 = new MyRow('s1d1', 11, 'a11yd1'); + final _s1D2 = new MyRow('s1d2', 12, 'a11yd2'); + final _s1D3 = new MyRow('s1d3', 13, 'a11yd3'); + + setUp(() { + chart = new FakeCartesianChart() + ..drawAreaBounds = new Rectangle(50, 20, 150, 80); + + behavior = new DomainA11yExploreBehavior( + vocalizationCallback: domainVocalization); + behavior.attachTo(chart); + + domainAxis = new MockAxis(); + _series1 = new MutableSeries(new Series( + id: 's1', + data: [_s1D1, _s1D2, _s1D3], + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.count, + )) + ..setAttr(domainAxisKey, domainAxis); + }); + + test('creates nodes for vertically drawn charts', () { + // A LTR chart + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(false); + when(context.isRtl).thenReturn(false); + chart.context = context; + // Drawn vertically + chart.vertical = true; + // Set step size of 50, which should be the width of the bounding box + when(domainAxis.stepSize).thenReturn(50.0); + when(domainAxis.getLocation('s1d1')).thenReturn(75.0); + when(domainAxis.getLocation('s1d2')).thenReturn(125.0); + when(domainAxis.getLocation('s1d3')).thenReturn(175.0); + // Call fire on post process for the behavior to get the series list. + chart.callFireOnPostprocess([_series1]); + + final nodes = behavior.createA11yNodes(); + + expect(nodes, hasLength(3)); + expect(nodes[0].label, equals('s1d1')); + expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 50, 80))); + expect(nodes[1].label, equals('s1d2')); + expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80))); + expect(nodes[2].label, equals('s1d3')); + expect(nodes[2].boundingBox, equals(new Rectangle(150, 20, 50, 80))); + }); + + test('creates nodes for vertically drawn RTL charts', () { + // A RTL chart + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(true); + when(context.isRtl).thenReturn(true); + chart.context = context; + // Drawn vertically + chart.vertical = true; + // Set step size of 50, which should be the width of the bounding box + when(domainAxis.stepSize).thenReturn(50.0); + when(domainAxis.getLocation('s1d1')).thenReturn(175.0); + when(domainAxis.getLocation('s1d2')).thenReturn(125.0); + when(domainAxis.getLocation('s1d3')).thenReturn(75.0); + // Call fire on post process for the behavior to get the series list. + chart.callFireOnPostprocess([_series1]); + + final nodes = behavior.createA11yNodes(); + + expect(nodes, hasLength(3)); + expect(nodes[0].label, equals('s1d1')); + expect(nodes[0].boundingBox, equals(new Rectangle(150, 20, 50, 80))); + expect(nodes[1].label, equals('s1d2')); + expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80))); + expect(nodes[2].label, equals('s1d3')); + expect(nodes[2].boundingBox, equals(new Rectangle(50, 20, 50, 80))); + }); + + test('creates nodes for horizontally drawn charts', () { + // A LTR chart + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(false); + when(context.isRtl).thenReturn(false); + chart.context = context; + // Drawn horizontally + chart.vertical = false; + // Set step size of 20, which should be the height of the bounding box + when(domainAxis.stepSize).thenReturn(20.0); + when(domainAxis.getLocation('s1d1')).thenReturn(30.0); + when(domainAxis.getLocation('s1d2')).thenReturn(50.0); + when(domainAxis.getLocation('s1d3')).thenReturn(70.0); + // Call fire on post process for the behavior to get the series list. + chart.callFireOnPostprocess([_series1]); + + final nodes = behavior.createA11yNodes(); + + expect(nodes, hasLength(3)); + expect(nodes[0].label, equals('s1d1')); + expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 150, 20))); + expect(nodes[1].label, equals('s1d2')); + expect(nodes[1].boundingBox, equals(new Rectangle(50, 40, 150, 20))); + expect(nodes[2].label, equals('s1d3')); + expect(nodes[2].boundingBox, equals(new Rectangle(50, 60, 150, 20))); + }); + + test('creates nodes for horizontally drawn RTL charts', () { + // A LTR chart + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(true); + when(context.isRtl).thenReturn(true); + chart.context = context; + // Drawn horizontally + chart.vertical = false; + // Set step size of 20, which should be the height of the bounding box + when(domainAxis.stepSize).thenReturn(20.0); + when(domainAxis.getLocation('s1d1')).thenReturn(30.0); + when(domainAxis.getLocation('s1d2')).thenReturn(50.0); + when(domainAxis.getLocation('s1d3')).thenReturn(70.0); + // Call fire on post process for the behavior to get the series list. + chart.callFireOnPostprocess([_series1]); + + final nodes = behavior.createA11yNodes(); + + expect(nodes, hasLength(3)); + expect(nodes[0].label, equals('s1d1')); + expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 150, 20))); + expect(nodes[1].label, equals('s1d2')); + expect(nodes[1].boundingBox, equals(new Rectangle(50, 40, 150, 20))); + expect(nodes[2].label, equals('s1d3')); + expect(nodes[2].boundingBox, equals(new Rectangle(50, 60, 150, 20))); + }); + + test('nodes ordered correctly with a series missing a domain', () { + // A LTR chart + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(false); + when(context.isRtl).thenReturn(false); + chart.context = context; + // Drawn vertically + chart.vertical = true; + // Set step size of 50, which should be the width of the bounding box + when(domainAxis.stepSize).thenReturn(50.0); + when(domainAxis.getLocation('s1d1')).thenReturn(75.0); + when(domainAxis.getLocation('s1d2')).thenReturn(125.0); + when(domainAxis.getLocation('s1d3')).thenReturn(175.0); + // Create a series with a missing domain + final seriesWithMissingDomain = new MutableSeries(new Series( + id: 'm1', + data: [_s1D1, _s1D3], + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.count, + )) + ..setAttr(domainAxisKey, domainAxis); + + // Call fire on post process for the behavior to get the series list. + chart.callFireOnPostprocess([seriesWithMissingDomain, _series1]); + + final nodes = behavior.createA11yNodes(); + + expect(nodes, hasLength(3)); + expect(nodes[0].label, equals('s1d1')); + expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 50, 80))); + expect(nodes[1].label, equals('s1d2')); + expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80))); + expect(nodes[2].label, equals('s1d3')); + expect(nodes[2].boundingBox, equals(new Rectangle(150, 20, 50, 80))); + }); + + test('creates nodes with minimum width', () { + // A behavior with minimum width of 50 + final behaviorWithMinWidth = + new DomainA11yExploreBehavior(minimumWidth: 50.0); + behaviorWithMinWidth.attachTo(chart); + + // A LTR chart + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(false); + when(context.isRtl).thenReturn(false); + chart.context = context; + // Drawn vertically + chart.vertical = true; + // Return a step size of 20, which is less than the minimum width. + // Expect the results to use the minimum width of 50 instead. + when(domainAxis.stepSize).thenReturn(20.0); + when(domainAxis.getLocation('s1d1')).thenReturn(75.0); + when(domainAxis.getLocation('s1d2')).thenReturn(125.0); + when(domainAxis.getLocation('s1d3')).thenReturn(175.0); + // Call fire on post process for the behavior to get the series list. + chart.callFireOnPostprocess([_series1]); + + final nodes = behaviorWithMinWidth.createA11yNodes(); + + expect(nodes, hasLength(3)); + expect(nodes[0].label, equals('s1d1')); + expect(nodes[0].boundingBox, equals(new Rectangle(50, 20, 50, 80))); + expect(nodes[1].label, equals('s1d2')); + expect(nodes[1].boundingBox, equals(new Rectangle(100, 20, 50, 80))); + expect(nodes[2].label, equals('s1d3')); + expect(nodes[2].boundingBox, equals(new Rectangle(150, 20, 50, 80))); + }); +} + +class MyRow { + final String campaign; + final int count; + final String a11yDescription; + MyRow(this.campaign, this.count, this.a11yDescription); +} diff --git a/web/charts/common/test/chart/common/behavior/calculation/percent_injector_test.dart b/web/charts/common/test/chart/common/behavior/calculation/percent_injector_test.dart new file mode 100644 index 000000000..ae897b156 --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/calculation/percent_injector_test.dart @@ -0,0 +1,593 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart'; +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart' + show MutableSeries; +import 'package:charts_common/src/chart/common/behavior/calculation/percent_injector.dart'; +import 'package:charts_common/src/data/series.dart' show Series; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +/// Datum/Row for the chart. +class MyRow { + final String campaign; + final int clickCount; + final int clickCountLower; + final int clickCountUpper; + MyRow(this.campaign, this.clickCount, this.clickCountLower, + this.clickCountUpper); +} + +class MockChart extends Mock implements CartesianChart { + LifecycleListener lastLifecycleListener; + + bool vertical = true; + + @override + addLifecycleListener(LifecycleListener listener) => + lastLifecycleListener = listener; + + @override + removeLifecycleListener(LifecycleListener listener) { + expect(listener, equals(lastLifecycleListener)); + lastLifecycleListener = null; + return true; + } +} + +void main() { + MockChart _chart; + List> seriesList; + + PercentInjector _makeBehavior( + {PercentInjectorTotalType totalType = PercentInjectorTotalType.domain}) { + final behavior = new PercentInjector(totalType: totalType); + + behavior.attachTo(_chart); + + return behavior; + } + + setUp(() { + _chart = new MockChart(); + + final myFakeDesktopAData = [ + new MyRow('MyCampaign1', 1, 1, 1), + new MyRow('MyCampaign2', 2, 2, 2), + new MyRow('MyCampaign3', 3, 3, 3), + ]; + + final myFakeTabletAData = [ + new MyRow('MyCampaign1', 2, 2, 2), + new MyRow('MyCampaign2', 3, 3, 3), + new MyRow('MyCampaign3', 4, 4, 4), + ]; + + final myFakeMobileAData = [ + new MyRow('MyCampaign1', 3, 3, 3), + new MyRow('MyCampaign2', 4, 4, 4), + new MyRow('MyCampaign3', 5, 5, 5), + ]; + + final myFakeDesktopBData = [ + new MyRow('MyCampaign1', 10, 8, 12), + new MyRow('MyCampaign2', 20, 18, 22), + new MyRow('MyCampaign3', 30, 28, 32), + ]; + + final myFakeTabletBData = [ + new MyRow('MyCampaign1', 20, 18, 22), + new MyRow('MyCampaign2', 30, 28, 32), + new MyRow('MyCampaign3', 40, 38, 42), + ]; + + final myFakeMobileBData = [ + new MyRow('MyCampaign1', 30, 28, 32), + new MyRow('MyCampaign2', 40, 38, 42), + new MyRow('MyCampaign3', 50, 48, 52), + ]; + + seriesList = [ + new MutableSeries(new Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeDesktopAData)), + new MutableSeries(new Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeTabletAData)), + new MutableSeries(new Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeMobileAData)), + new MutableSeries(new Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureLowerBoundFn: (MyRow row, _) => row.clickCountLower, + measureUpperBoundFn: (MyRow row, _) => row.clickCountUpper, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeDesktopBData)), + new MutableSeries(new Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureLowerBoundFn: (MyRow row, _) => row.clickCountLower, + measureUpperBoundFn: (MyRow row, _) => row.clickCountUpper, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeTabletBData)), + new MutableSeries(new Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureLowerBoundFn: (MyRow row, _) => row.clickCountLower, + measureUpperBoundFn: (MyRow row, _) => row.clickCountUpper, + measureOffsetFn: (MyRow row, _) => 0, + data: myFakeMobileBData)) + ]; + }); + + group('Inject', () { + test('percent of domain', () { + // Setup behavior. + _makeBehavior(totalType: PercentInjectorTotalType.domain); + + // Act + _chart.lastLifecycleListener.onData(seriesList); + _chart.lastLifecycleListener.onPreprocess(seriesList); + + // Verify first series. + var series = seriesList[0]; + + expect(series.measureFn(0), equals(1 / 66)); + expect(series.measureFn(1), equals(2 / 99)); + expect(series.measureFn(2), equals(3 / 132)); + + expect(series.rawMeasureFn(0), equals(1)); + expect(series.rawMeasureFn(1), equals(2)); + expect(series.rawMeasureFn(2), equals(3)); + + // Verify second series. + series = seriesList[1]; + + expect(series.measureFn(0), equals(2 / 66)); + expect(series.measureFn(1), equals(3 / 99)); + expect(series.measureFn(2), equals(4 / 132)); + + expect(series.rawMeasureFn(0), equals(2)); + expect(series.rawMeasureFn(1), equals(3)); + expect(series.rawMeasureFn(2), equals(4)); + + // Verify third series. + series = seriesList[2]; + + expect(series.measureFn(0), equals(3 / 66)); + expect(series.measureFn(1), equals(4 / 99)); + expect(series.measureFn(2), equals(5 / 132)); + + expect(series.rawMeasureFn(0), equals(3)); + expect(series.rawMeasureFn(1), equals(4)); + expect(series.rawMeasureFn(2), equals(5)); + + // Verify fourth series. + series = seriesList[3]; + + expect(series.measureFn(0), equals(10 / 66)); + expect(series.measureFn(1), equals(20 / 99)); + expect(series.measureFn(2), equals(30 / 132)); + + expect(series.rawMeasureFn(0), equals(10)); + expect(series.rawMeasureFn(1), equals(20)); + expect(series.rawMeasureFn(2), equals(30)); + + expect(series.measureLowerBoundFn(0), equals(8 / 66)); + expect(series.measureLowerBoundFn(1), equals(18 / 99)); + expect(series.measureLowerBoundFn(2), equals(28 / 132)); + + expect(series.rawMeasureLowerBoundFn(0), equals(8)); + expect(series.rawMeasureLowerBoundFn(1), equals(18)); + expect(series.rawMeasureLowerBoundFn(2), equals(28)); + + expect(series.measureUpperBoundFn(0), equals(12 / 66)); + expect(series.measureUpperBoundFn(1), equals(22 / 99)); + expect(series.measureUpperBoundFn(2), equals(32 / 132)); + + expect(series.rawMeasureUpperBoundFn(0), equals(12)); + expect(series.rawMeasureUpperBoundFn(1), equals(22)); + expect(series.rawMeasureUpperBoundFn(2), equals(32)); + + // Verify fifth series. + series = seriesList[4]; + + expect(series.measureFn(0), equals(20 / 66)); + expect(series.measureFn(1), equals(30 / 99)); + expect(series.measureFn(2), equals(40 / 132)); + + expect(series.rawMeasureFn(0), equals(20)); + expect(series.rawMeasureFn(1), equals(30)); + expect(series.rawMeasureFn(2), equals(40)); + + expect(series.measureLowerBoundFn(0), equals(18 / 66)); + expect(series.measureLowerBoundFn(1), equals(28 / 99)); + expect(series.measureLowerBoundFn(2), equals(38 / 132)); + + expect(series.rawMeasureLowerBoundFn(0), equals(18)); + expect(series.rawMeasureLowerBoundFn(1), equals(28)); + expect(series.rawMeasureLowerBoundFn(2), equals(38)); + + expect(series.measureUpperBoundFn(0), equals(22 / 66)); + expect(series.measureUpperBoundFn(1), equals(32 / 99)); + expect(series.measureUpperBoundFn(2), equals(42 / 132)); + + expect(series.rawMeasureUpperBoundFn(0), equals(22)); + expect(series.rawMeasureUpperBoundFn(1), equals(32)); + expect(series.rawMeasureUpperBoundFn(2), equals(42)); + + // Verify sixth series. + series = seriesList[5]; + + expect(series.measureFn(0), equals(30 / 66)); + expect(series.measureFn(1), equals(40 / 99)); + expect(series.measureFn(2), equals(50 / 132)); + + expect(series.rawMeasureFn(0), equals(30)); + expect(series.rawMeasureFn(1), equals(40)); + expect(series.rawMeasureFn(2), equals(50)); + + expect(series.measureLowerBoundFn(0), equals(28 / 66)); + expect(series.measureLowerBoundFn(1), equals(38 / 99)); + expect(series.measureLowerBoundFn(2), equals(48 / 132)); + + expect(series.rawMeasureLowerBoundFn(0), equals(28)); + expect(series.rawMeasureLowerBoundFn(1), equals(38)); + expect(series.rawMeasureLowerBoundFn(2), equals(48)); + + expect(series.measureUpperBoundFn(0), equals(32 / 66)); + expect(series.measureUpperBoundFn(1), equals(42 / 99)); + expect(series.measureUpperBoundFn(2), equals(52 / 132)); + + expect(series.rawMeasureUpperBoundFn(0), equals(32)); + expect(series.rawMeasureUpperBoundFn(1), equals(42)); + expect(series.rawMeasureUpperBoundFn(2), equals(52)); + }); + + test('percent of domain, grouped by series category', () { + // Setup behavior. + _makeBehavior(totalType: PercentInjectorTotalType.domainBySeriesCategory); + + // Act + _chart.lastLifecycleListener.onData(seriesList); + _chart.lastLifecycleListener.onPreprocess(seriesList); + + // Verify first series. + var series = seriesList[0]; + + expect(series.measureFn(0), equals(1 / 6)); + expect(series.measureFn(1), equals(2 / 9)); + expect(series.measureFn(2), equals(3 / 12)); + + expect(series.rawMeasureFn(0), equals(1)); + expect(series.rawMeasureFn(1), equals(2)); + expect(series.rawMeasureFn(2), equals(3)); + + // Verify second series. + series = seriesList[1]; + + expect(series.measureFn(0), equals(2 / 6)); + expect(series.measureFn(1), equals(3 / 9)); + expect(series.measureFn(2), equals(4 / 12)); + + expect(series.rawMeasureFn(0), equals(2)); + expect(series.rawMeasureFn(1), equals(3)); + expect(series.rawMeasureFn(2), equals(4)); + + // Verify third series. + series = seriesList[2]; + + expect(series.measureFn(0), equals(3 / 6)); + expect(series.measureFn(1), equals(4 / 9)); + expect(series.measureFn(2), equals(5 / 12)); + + expect(series.rawMeasureFn(0), equals(3)); + expect(series.rawMeasureFn(1), equals(4)); + expect(series.rawMeasureFn(2), equals(5)); + + // Verify fourth series. + series = seriesList[3]; + + expect(series.measureFn(0), equals(10 / 60)); + expect(series.measureFn(1), equals(20 / 90)); + expect(series.measureFn(2), equals(30 / 120)); + + expect(series.rawMeasureFn(0), equals(10)); + expect(series.rawMeasureFn(1), equals(20)); + expect(series.rawMeasureFn(2), equals(30)); + + expect(series.measureLowerBoundFn(0), equals(8 / 60)); + expect(series.measureLowerBoundFn(1), equals(18 / 90)); + expect(series.measureLowerBoundFn(2), equals(28 / 120)); + + expect(series.rawMeasureLowerBoundFn(0), equals(8)); + expect(series.rawMeasureLowerBoundFn(1), equals(18)); + expect(series.rawMeasureLowerBoundFn(2), equals(28)); + + expect(series.measureUpperBoundFn(0), equals(12 / 60)); + expect(series.measureUpperBoundFn(1), equals(22 / 90)); + expect(series.measureUpperBoundFn(2), equals(32 / 120)); + + expect(series.rawMeasureUpperBoundFn(0), equals(12)); + expect(series.rawMeasureUpperBoundFn(1), equals(22)); + expect(series.rawMeasureUpperBoundFn(2), equals(32)); + + // Verify fifth series. + series = seriesList[4]; + + expect(series.measureFn(0), equals(20 / 60)); + expect(series.measureFn(1), equals(30 / 90)); + expect(series.measureFn(2), equals(40 / 120)); + + expect(series.rawMeasureFn(0), equals(20)); + expect(series.rawMeasureFn(1), equals(30)); + expect(series.rawMeasureFn(2), equals(40)); + + expect(series.measureLowerBoundFn(0), equals(18 / 60)); + expect(series.measureLowerBoundFn(1), equals(28 / 90)); + expect(series.measureLowerBoundFn(2), equals(38 / 120)); + + expect(series.rawMeasureLowerBoundFn(0), equals(18)); + expect(series.rawMeasureLowerBoundFn(1), equals(28)); + expect(series.rawMeasureLowerBoundFn(2), equals(38)); + + expect(series.measureUpperBoundFn(0), equals(22 / 60)); + expect(series.measureUpperBoundFn(1), equals(32 / 90)); + expect(series.measureUpperBoundFn(2), equals(42 / 120)); + + expect(series.rawMeasureUpperBoundFn(0), equals(22)); + expect(series.rawMeasureUpperBoundFn(1), equals(32)); + expect(series.rawMeasureUpperBoundFn(2), equals(42)); + + // Verify sixth series. + series = seriesList[5]; + + expect(series.measureFn(0), equals(30 / 60)); + expect(series.measureFn(1), equals(40 / 90)); + expect(series.measureFn(2), equals(50 / 120)); + + expect(series.rawMeasureFn(0), equals(30)); + expect(series.rawMeasureFn(1), equals(40)); + expect(series.rawMeasureFn(2), equals(50)); + + expect(series.measureLowerBoundFn(0), equals(28 / 60)); + expect(series.measureLowerBoundFn(1), equals(38 / 90)); + expect(series.measureLowerBoundFn(2), equals(48 / 120)); + + expect(series.rawMeasureLowerBoundFn(0), equals(28)); + expect(series.rawMeasureLowerBoundFn(1), equals(38)); + expect(series.rawMeasureLowerBoundFn(2), equals(48)); + + expect(series.measureUpperBoundFn(0), equals(32 / 60)); + expect(series.measureUpperBoundFn(1), equals(42 / 90)); + expect(series.measureUpperBoundFn(2), equals(52 / 120)); + + expect(series.rawMeasureUpperBoundFn(0), equals(32)); + expect(series.rawMeasureUpperBoundFn(1), equals(42)); + expect(series.rawMeasureUpperBoundFn(2), equals(52)); + }); + + test('percent of series', () { + // Setup behavior. + _makeBehavior(totalType: PercentInjectorTotalType.series); + + // Act + _chart.lastLifecycleListener.onData(seriesList); + _chart.lastLifecycleListener.onPreprocess(seriesList); + + // Verify that every series has a total measure value. Technically this is + // handled in MutableSeries, but it is a pre-condition for this behavior + // functioning properly. + expect(seriesList[0].seriesMeasureTotal, equals(6)); + expect(seriesList[1].seriesMeasureTotal, equals(9)); + expect(seriesList[2].seriesMeasureTotal, equals(12)); + expect(seriesList[3].seriesMeasureTotal, equals(60)); + expect(seriesList[4].seriesMeasureTotal, equals(90)); + expect(seriesList[5].seriesMeasureTotal, equals(120)); + + // Verify first series. + var series = seriesList[0]; + + expect(series.measureFn(0), equals(1 / 6)); + expect(series.measureFn(1), equals(2 / 6)); + expect(series.measureFn(2), equals(3 / 6)); + + expect(series.rawMeasureFn(0), equals(1)); + expect(series.rawMeasureFn(1), equals(2)); + expect(series.rawMeasureFn(2), equals(3)); + + // Verify second series. + series = seriesList[1]; + + expect(series.measureFn(0), equals(2 / 9)); + expect(series.measureFn(1), equals(3 / 9)); + expect(series.measureFn(2), equals(4 / 9)); + + expect(series.rawMeasureFn(0), equals(2)); + expect(series.rawMeasureFn(1), equals(3)); + expect(series.rawMeasureFn(2), equals(4)); + + // Verify third series. + series = seriesList[2]; + + expect(series.measureFn(0), equals(3 / 12)); + expect(series.measureFn(1), equals(4 / 12)); + expect(series.measureFn(2), equals(5 / 12)); + + expect(series.rawMeasureFn(0), equals(3)); + expect(series.rawMeasureFn(1), equals(4)); + expect(series.rawMeasureFn(2), equals(5)); + + // Verify fourth series. + series = seriesList[3]; + + expect(series.measureFn(0), equals(10 / 60)); + expect(series.measureFn(1), equals(20 / 60)); + expect(series.measureFn(2), equals(30 / 60)); + + expect(series.rawMeasureFn(0), equals(10)); + expect(series.rawMeasureFn(1), equals(20)); + expect(series.rawMeasureFn(2), equals(30)); + + expect(series.measureLowerBoundFn(0), equals(8 / 60)); + expect(series.measureLowerBoundFn(1), equals(18 / 60)); + expect(series.measureLowerBoundFn(2), equals(28 / 60)); + + expect(series.rawMeasureLowerBoundFn(0), equals(8)); + expect(series.rawMeasureLowerBoundFn(1), equals(18)); + expect(series.rawMeasureLowerBoundFn(2), equals(28)); + + expect(series.measureUpperBoundFn(0), equals(12 / 60)); + expect(series.measureUpperBoundFn(1), equals(22 / 60)); + expect(series.measureUpperBoundFn(2), equals(32 / 60)); + + expect(series.rawMeasureUpperBoundFn(0), equals(12)); + expect(series.rawMeasureUpperBoundFn(1), equals(22)); + expect(series.rawMeasureUpperBoundFn(2), equals(32)); + + // Verify fifth series. + series = seriesList[4]; + + expect(series.measureFn(0), equals(20 / 90)); + expect(series.measureFn(1), equals(30 / 90)); + expect(series.measureFn(2), equals(40 / 90)); + + expect(series.rawMeasureFn(0), equals(20)); + expect(series.rawMeasureFn(1), equals(30)); + expect(series.rawMeasureFn(2), equals(40)); + + expect(series.measureLowerBoundFn(0), equals(18 / 90)); + expect(series.measureLowerBoundFn(1), equals(28 / 90)); + expect(series.measureLowerBoundFn(2), equals(38 / 90)); + + expect(series.rawMeasureLowerBoundFn(0), equals(18)); + expect(series.rawMeasureLowerBoundFn(1), equals(28)); + expect(series.rawMeasureLowerBoundFn(2), equals(38)); + + expect(series.measureUpperBoundFn(0), equals(22 / 90)); + expect(series.measureUpperBoundFn(1), equals(32 / 90)); + expect(series.measureUpperBoundFn(2), equals(42 / 90)); + + expect(series.rawMeasureUpperBoundFn(0), equals(22)); + expect(series.rawMeasureUpperBoundFn(1), equals(32)); + expect(series.rawMeasureUpperBoundFn(2), equals(42)); + + // Verify sixth series. + series = seriesList[5]; + + expect(series.measureFn(0), equals(30 / 120)); + expect(series.measureFn(1), equals(40 / 120)); + expect(series.measureFn(2), equals(50 / 120)); + + expect(series.rawMeasureFn(0), equals(30)); + expect(series.rawMeasureFn(1), equals(40)); + expect(series.rawMeasureFn(2), equals(50)); + + expect(series.measureLowerBoundFn(0), equals(28 / 120)); + expect(series.measureLowerBoundFn(1), equals(38 / 120)); + expect(series.measureLowerBoundFn(2), equals(48 / 120)); + + expect(series.rawMeasureLowerBoundFn(0), equals(28)); + expect(series.rawMeasureLowerBoundFn(1), equals(38)); + expect(series.rawMeasureLowerBoundFn(2), equals(48)); + + expect(series.measureUpperBoundFn(0), equals(32 / 120)); + expect(series.measureUpperBoundFn(1), equals(42 / 120)); + expect(series.measureUpperBoundFn(2), equals(52 / 120)); + + expect(series.rawMeasureUpperBoundFn(0), equals(32)); + expect(series.rawMeasureUpperBoundFn(1), equals(42)); + expect(series.rawMeasureUpperBoundFn(2), equals(52)); + }); + }); + + group('Life cycle', () { + test('sets injected flag for percent of domain', () { + // Setup behavior. + _makeBehavior(totalType: PercentInjectorTotalType.domain); + + // Act + _chart.lastLifecycleListener.onData(seriesList); + + // Verify that each series has an initially false flag. + expect(seriesList[0].getAttr(percentInjectedKey), isFalse); + expect(seriesList[1].getAttr(percentInjectedKey), isFalse); + expect(seriesList[2].getAttr(percentInjectedKey), isFalse); + expect(seriesList[3].getAttr(percentInjectedKey), isFalse); + expect(seriesList[4].getAttr(percentInjectedKey), isFalse); + expect(seriesList[5].getAttr(percentInjectedKey), isFalse); + + // Act + _chart.lastLifecycleListener.onPreprocess(seriesList); + + // Verify that each series has a true flag. + expect(seriesList[0].getAttr(percentInjectedKey), isTrue); + expect(seriesList[1].getAttr(percentInjectedKey), isTrue); + expect(seriesList[2].getAttr(percentInjectedKey), isTrue); + expect(seriesList[3].getAttr(percentInjectedKey), isTrue); + expect(seriesList[4].getAttr(percentInjectedKey), isTrue); + expect(seriesList[5].getAttr(percentInjectedKey), isTrue); + }); + + test('sets injected flag for percent of series', () { + // Setup behavior. + _makeBehavior(totalType: PercentInjectorTotalType.series); + + // Act + _chart.lastLifecycleListener.onData(seriesList); + + // Verify that each series has an initially false flag. + expect(seriesList[0].getAttr(percentInjectedKey), isFalse); + expect(seriesList[1].getAttr(percentInjectedKey), isFalse); + expect(seriesList[2].getAttr(percentInjectedKey), isFalse); + expect(seriesList[3].getAttr(percentInjectedKey), isFalse); + expect(seriesList[4].getAttr(percentInjectedKey), isFalse); + expect(seriesList[5].getAttr(percentInjectedKey), isFalse); + + // Act + _chart.lastLifecycleListener.onPreprocess(seriesList); + + // Verify that each series has a true flag. + expect(seriesList[0].getAttr(percentInjectedKey), isTrue); + expect(seriesList[1].getAttr(percentInjectedKey), isTrue); + expect(seriesList[2].getAttr(percentInjectedKey), isTrue); + expect(seriesList[3].getAttr(percentInjectedKey), isTrue); + expect(seriesList[4].getAttr(percentInjectedKey), isTrue); + expect(seriesList[5].getAttr(percentInjectedKey), isTrue); + }); + }); +} diff --git a/web/charts/common/test/chart/common/behavior/chart_behavior_test.dart b/web/charts/common/test/chart/common/behavior/chart_behavior_test.dart new file mode 100644 index 000000000..72c98b06f --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/chart_behavior_test.dart @@ -0,0 +1,153 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/common/series_renderer.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/behavior/chart_behavior.dart'; +import 'package:charts_common/src/chart/common/datum_details.dart'; +import 'package:charts_common/src/chart/common/selection_model/selection_model.dart'; + +import 'package:test/test.dart'; + +class MockBehavior extends Mock implements ChartBehavior {} + +class ParentBehavior implements ChartBehavior { + final ChartBehavior child; + + ParentBehavior(this.child); + + String get role => null; + + @override + void attachTo(BaseChart chart) { + chart.addBehavior(child); + } + + @override + void removeFrom(BaseChart chart) { + chart.removeBehavior(child); + } +} + +class ConcreteChart extends BaseChart { + @override + SeriesRenderer makeDefaultRenderer() => null; + + @override + List> getDatumDetails(SelectionModelType _) => null; +} + +void main() { + ConcreteChart chart; + MockBehavior namedBehavior; + MockBehavior unnamedBehavior; + + setUp(() { + chart = new ConcreteChart(); + + namedBehavior = new MockBehavior(); + when(namedBehavior.role).thenReturn('foo'); + + unnamedBehavior = new MockBehavior(); + when(unnamedBehavior.role).thenReturn(null); + }); + + group('Attach & Detach', () { + test('attach is called once', () { + chart.addBehavior(namedBehavior); + verify(namedBehavior.attachTo(chart)).called(1); + + verify(namedBehavior.role); + verifyNoMoreInteractions(namedBehavior); + }); + + test('deteach is called once', () { + chart.addBehavior(namedBehavior); + verify(namedBehavior.attachTo(chart)).called(1); + + chart.removeBehavior(namedBehavior); + verify(namedBehavior.removeFrom(chart)).called(1); + + verify(namedBehavior.role); + verifyNoMoreInteractions(namedBehavior); + }); + + test('detach is called when name is reused', () { + final otherBehavior = new MockBehavior(); + when(otherBehavior.role).thenReturn('foo'); + + chart.addBehavior(namedBehavior); + verify(namedBehavior.attachTo(chart)).called(1); + + chart.addBehavior(otherBehavior); + verify(namedBehavior.removeFrom(chart)).called(1); + verify(otherBehavior.attachTo(chart)).called(1); + + verify(namedBehavior.role); + verify(otherBehavior.role); + verifyNoMoreInteractions(namedBehavior); + verifyNoMoreInteractions(otherBehavior); + }); + + test('detach is not called when name is null', () { + chart.addBehavior(namedBehavior); + verify(namedBehavior.attachTo(chart)).called(1); + + chart.addBehavior(unnamedBehavior); + verify(unnamedBehavior.attachTo(chart)).called(1); + + verify(namedBehavior.role); + verify(unnamedBehavior.role); + verifyNoMoreInteractions(namedBehavior); + verifyNoMoreInteractions(unnamedBehavior); + }); + + test('detach is not called when name is different', () { + final otherBehavior = new MockBehavior(); + when(otherBehavior.role).thenReturn('bar'); + + chart.addBehavior(namedBehavior); + verify(namedBehavior.attachTo(chart)).called(1); + + chart.addBehavior(otherBehavior); + verify(otherBehavior.attachTo(chart)).called(1); + + verify(namedBehavior.role); + verify(otherBehavior.role); + verifyNoMoreInteractions(namedBehavior); + verifyNoMoreInteractions(otherBehavior); + }); + + test('behaviors are removed when chart is destroyed', () { + final parentBehavior = new ParentBehavior(unnamedBehavior); + + chart.addBehavior(parentBehavior); + // The parent should add the child behavoir. + verify(unnamedBehavior.attachTo(chart)).called(1); + + chart.destroy(); + + // The parent should remove the child behavior. + verify(unnamedBehavior.removeFrom(chart)).called(1); + + // Remove should only be called once and shouldn't trigger a concurrent + // modification exception. + verify(unnamedBehavior.role); + verifyNoMoreInteractions(unnamedBehavior); + }); + }); +} diff --git a/web/charts/common/test/chart/common/behavior/domain_highlighter_test.dart b/web/charts/common/test/chart/common/behavior/domain_highlighter_test.dart new file mode 100644 index 000000000..af051be3a --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/domain_highlighter_test.dart @@ -0,0 +1,185 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/behavior/domain_highlighter.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/common/selection_model/selection_model.dart'; +import 'package:charts_common/src/common/material_palette.dart'; +import 'package:charts_common/src/data/series.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockChart extends Mock implements BaseChart { + LifecycleListener lastListener; + + @override + addLifecycleListener(LifecycleListener listener) => lastListener = listener; + + @override + removeLifecycleListener(LifecycleListener listener) { + expect(listener, equals(lastListener)); + lastListener = null; + return true; + } +} + +class MockSelectionModel extends Mock implements MutableSelectionModel { + SelectionModelListener lastListener; + + @override + addSelectionChangedListener(SelectionModelListener listener) => + lastListener = listener; + + @override + removeSelectionChangedListener(SelectionModelListener listener) { + expect(listener, equals(lastListener)); + lastListener = null; + } +} + +void main() { + MockChart _chart; + MockSelectionModel _selectionModel; + + MutableSeries _series1; + final _s1D1 = new MyRow('s1d1', 11); + final _s1D2 = new MyRow('s1d2', 12); + final _s1D3 = new MyRow('s1d3', 13); + + MutableSeries _series2; + final _s2D1 = new MyRow('s2d1', 21); + final _s2D2 = new MyRow('s2d2', 22); + final _s2D3 = new MyRow('s2d3', 23); + + _setupSelection(List selected) { + for (var i = 0; i < _series1.data.length; i++) { + when(_selectionModel.isDatumSelected(_series1, i)) + .thenReturn(selected.contains(_series1.data[i])); + } + for (var i = 0; i < _series2.data.length; i++) { + when(_selectionModel.isDatumSelected(_series2, i)) + .thenReturn(selected.contains(_series2.data[i])); + } + } + + setUp(() { + _chart = new MockChart(); + + _selectionModel = new MockSelectionModel(); + when(_chart.getSelectionModel(SelectionModelType.info)) + .thenReturn(_selectionModel); + + _series1 = new MutableSeries(new Series( + id: 's1', + data: [_s1D1, _s1D2, _s1D3], + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.count, + colorFn: (_, __) => MaterialPalette.blue.shadeDefault)) + ..measureFn = (_) => 0.0; + + _series2 = new MutableSeries(new Series( + id: 's2', + data: [_s2D1, _s2D2, _s2D3], + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.count, + colorFn: (_, __) => MaterialPalette.red.shadeDefault)) + ..measureFn = (_) => 0.0; + }); + + group('DomainHighligher', () { + test('darkens the selected bars', () { + // Setup + final behavior = new DomainHighlighter(SelectionModelType.info); + behavior.attachTo(_chart); + _setupSelection([_s1D2, _s2D2]); + final seriesList = [_series1, _series2]; + + // Act + _selectionModel.lastListener(_selectionModel); + verify(_chart.redraw(skipAnimation: true, skipLayout: true)); + _chart.lastListener.onPostprocess(seriesList); + + // Verify + final s1ColorFn = _series1.colorFn; + expect(s1ColorFn(0), equals(MaterialPalette.blue.shadeDefault)); + expect(s1ColorFn(1), equals(MaterialPalette.blue.shadeDefault.darker)); + expect(s1ColorFn(2), equals(MaterialPalette.blue.shadeDefault)); + + final s2ColorFn = _series2.colorFn; + expect(s2ColorFn(0), equals(MaterialPalette.red.shadeDefault)); + expect(s2ColorFn(1), equals(MaterialPalette.red.shadeDefault.darker)); + expect(s2ColorFn(2), equals(MaterialPalette.red.shadeDefault)); + }); + + test('listens to other selection models', () { + // Setup + final behavior = new DomainHighlighter(SelectionModelType.action); + when(_chart.getSelectionModel(SelectionModelType.action)) + .thenReturn(_selectionModel); + + // Act + behavior.attachTo(_chart); + + // Verify + verify(_chart.getSelectionModel(SelectionModelType.action)); + verifyNever(_chart.getSelectionModel(SelectionModelType.info)); + }); + + test('leaves everything alone with no selection', () { + // Setup + final behavior = new DomainHighlighter(SelectionModelType.info); + behavior.attachTo(_chart); + _setupSelection([]); + final seriesList = [_series1, _series2]; + + // Act + _selectionModel.lastListener(_selectionModel); + verify(_chart.redraw(skipAnimation: true, skipLayout: true)); + _chart.lastListener.onPostprocess(seriesList); + + // Verify + final s1ColorFn = _series1.colorFn; + expect(s1ColorFn(0), equals(MaterialPalette.blue.shadeDefault)); + expect(s1ColorFn(1), equals(MaterialPalette.blue.shadeDefault)); + expect(s1ColorFn(2), equals(MaterialPalette.blue.shadeDefault)); + + final s2ColorFn = _series2.colorFn; + expect(s2ColorFn(0), equals(MaterialPalette.red.shadeDefault)); + expect(s2ColorFn(1), equals(MaterialPalette.red.shadeDefault)); + expect(s2ColorFn(2), equals(MaterialPalette.red.shadeDefault)); + }); + + test('cleans up', () { + // Setup + final behavior = new DomainHighlighter(SelectionModelType.info); + behavior.attachTo(_chart); + _setupSelection([_s1D2, _s2D2]); + + // Act + behavior.removeFrom(_chart); + + // Verify + expect(_chart.lastListener, isNull); + expect(_selectionModel.lastListener, isNull); + }); + }); +} + +class MyRow { + final String campaign; + final int count; + MyRow(this.campaign, this.count); +} diff --git a/web/charts/common/test/chart/common/behavior/initial_selection_test.dart b/web/charts/common/test/chart/common/behavior/initial_selection_test.dart new file mode 100644 index 000000000..c1fefd118 --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/initial_selection_test.dart @@ -0,0 +1,213 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/datum_details.dart'; +import 'package:charts_common/src/chart/common/behavior/initial_selection.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/common/series_datum.dart'; +import 'package:charts_common/src/chart/common/series_renderer.dart'; +import 'package:charts_common/src/chart/common/selection_model/selection_model.dart'; +import 'package:charts_common/src/data/series.dart'; + +import 'package:test/test.dart'; + +class FakeRenderer extends BaseSeriesRenderer { + @override + DatumDetails addPositionToDetailsForSeriesDatum( + DatumDetails details, SeriesDatum seriesDatum) { + return null; + } + + @override + List getNearestDatumDetailPerSeries( + Point chartPoint, bool byDomain, Rectangle boundsOverride) { + return null; + } + + @override + void paint(ChartCanvas canvas, double animationPercent) {} + + @override + void update(List seriesList, bool isAnimating) {} +} + +class FakeChart extends BaseChart { + @override + List getDatumDetails(SelectionModelType type) => []; + + @override + SeriesRenderer makeDefaultRenderer() => new FakeRenderer(); + + void requestOnDraw(List seriesList) { + fireOnDraw(seriesList); + } +} + +void main() { + FakeChart _chart; + ImmutableSeries _series1; + ImmutableSeries _series2; + ImmutableSeries _series3; + ImmutableSeries _series4; + final infoSelectionType = SelectionModelType.info; + + InitialSelection _makeBehavior(SelectionModelType selectionModelType, + {List selectedSeries, List selectedData}) { + InitialSelection behavior = new InitialSelection( + selectionModelType: selectionModelType, + selectedSeriesConfig: selectedSeries, + selectedDataConfig: selectedData); + + behavior.attachTo(_chart); + + return behavior; + } + + setUp(() { + _chart = new FakeChart(); + + _series1 = new MutableSeries(new Series( + id: 'mySeries1', + data: ['A', 'B', 'C', 'D'], + domainFn: (dynamic datum, __) => datum, + measureFn: (_, __) {})); + + _series2 = new MutableSeries(new Series( + id: 'mySeries2', + data: ['W', 'X', 'Y', 'Z'], + domainFn: (dynamic datum, __) => datum, + measureFn: (_, __) {})); + + _series3 = new MutableSeries(new Series( + id: 'mySeries3', + data: ['W', 'X', 'Y', 'Z'], + domainFn: (dynamic datum, __) => datum, + measureFn: (_, __) {})); + + _series4 = new MutableSeries(new Series( + id: 'mySeries4', + data: ['W', 'X', 'Y', 'Z'], + domainFn: (dynamic datum, __) => datum, + measureFn: (_, __) {})); + }); + + test('selects initial datum', () { + _makeBehavior(infoSelectionType, + selectedData: [new SeriesDatumConfig('mySeries1', 'C')]); + + _chart.requestOnDraw([_series1, _series2]); + + final model = _chart.getSelectionModel(infoSelectionType); + + expect(model.selectedSeries, hasLength(1)); + expect(model.selectedSeries[0], equals(_series1)); + expect(model.selectedDatum, hasLength(1)); + expect(model.selectedDatum[0].series, equals(_series1)); + expect(model.selectedDatum[0].datum, equals('C')); + }); + + test('selects multiple initial data', () { + _makeBehavior(infoSelectionType, selectedData: [ + new SeriesDatumConfig('mySeries1', 'C'), + new SeriesDatumConfig('mySeries1', 'D') + ]); + + _chart.requestOnDraw([_series1, _series2]); + + final model = _chart.getSelectionModel(infoSelectionType); + + expect(model.selectedSeries, hasLength(1)); + expect(model.selectedSeries[0], equals(_series1)); + expect(model.selectedDatum, hasLength(2)); + expect(model.selectedDatum[0].series, equals(_series1)); + expect(model.selectedDatum[0].datum, equals('C')); + expect(model.selectedDatum[1].series, equals(_series1)); + expect(model.selectedDatum[1].datum, equals('D')); + }); + + test('selects initial series', () { + _makeBehavior(infoSelectionType, selectedSeries: ['mySeries2']); + + _chart.requestOnDraw([_series1, _series2, _series3, _series4]); + + final model = _chart.getSelectionModel(infoSelectionType); + + expect(model.selectedSeries, hasLength(1)); + expect(model.selectedSeries[0], equals(_series2)); + expect(model.selectedDatum, isEmpty); + }); + + test('selects multiple series', () { + _makeBehavior(infoSelectionType, + selectedSeries: ['mySeries2', 'mySeries4']); + + _chart.requestOnDraw([_series1, _series2, _series3, _series4]); + + final model = _chart.getSelectionModel(infoSelectionType); + + expect(model.selectedSeries, hasLength(2)); + expect(model.selectedSeries[0], equals(_series2)); + expect(model.selectedSeries[1], equals(_series4)); + expect(model.selectedDatum, isEmpty); + }); + + test('selects series and datum', () { + _makeBehavior(infoSelectionType, + selectedData: [new SeriesDatumConfig('mySeries1', 'C')], + selectedSeries: ['mySeries4']); + + _chart.requestOnDraw([_series1, _series2, _series3, _series4]); + + final model = _chart.getSelectionModel(infoSelectionType); + + expect(model.selectedSeries, hasLength(2)); + expect(model.selectedSeries[0], equals(_series1)); + expect(model.selectedSeries[1], equals(_series4)); + expect(model.selectedDatum[0].series, equals(_series1)); + expect(model.selectedDatum[0].datum, equals('C')); + }); + + test('selection model is reset when a new series is drawn', () { + _makeBehavior(infoSelectionType, selectedSeries: ['mySeries2']); + + _chart.requestOnDraw([_series1, _series2, _series3, _series4]); + + final model = _chart.getSelectionModel(infoSelectionType); + + // Verify initial selection is selected on first draw + expect(model.selectedSeries, hasLength(1)); + expect(model.selectedSeries[0], equals(_series2)); + expect(model.selectedDatum, isEmpty); + + // Request a draw with a new series list. + _chart.draw( + [ + new Series( + id: 'mySeries2', + data: ['W', 'X', 'Y', 'Z'], + domainFn: (dynamic datum, __) => datum, + measureFn: (_, __) {}) + ], + ); + + // Verify selection is cleared. + expect(model.selectedSeries, isEmpty); + expect(model.selectedDatum, isEmpty); + }); +} diff --git a/web/charts/common/test/chart/common/behavior/line_point_highlighter_test.dart b/web/charts/common/test/chart/common/behavior/line_point_highlighter_test.dart new file mode 100644 index 000000000..1753df0c6 --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/line_point_highlighter_test.dart @@ -0,0 +1,270 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point, Rectangle; + +import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart'; +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/behavior/line_point_highlighter.dart'; +import 'package:charts_common/src/chart/common/datum_details.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/common/series_datum.dart'; +import 'package:charts_common/src/chart/common/series_renderer.dart'; +import 'package:charts_common/src/chart/common/selection_model/selection_model.dart'; +import 'package:charts_common/src/common/material_palette.dart'; +import 'package:charts_common/src/data/series.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockChart extends Mock implements CartesianChart { + LifecycleListener lastListener; + + @override + addLifecycleListener(LifecycleListener listener) => lastListener = listener; + + @override + removeLifecycleListener(LifecycleListener listener) { + expect(listener, equals(lastListener)); + lastListener = null; + return true; + } + + @override + bool get vertical => true; +} + +class MockSelectionModel extends Mock implements MutableSelectionModel { + SelectionModelListener lastListener; + + @override + addSelectionChangedListener(SelectionModelListener listener) => + lastListener = listener; + + @override + removeSelectionChangedListener(SelectionModelListener listener) { + expect(listener, equals(lastListener)); + lastListener = null; + } +} + +class MockNumericAxis extends Mock implements NumericAxis { + @override + getLocation(num domain) { + return 10.0; + } +} + +class MockSeriesRenderer extends BaseSeriesRenderer { + @override + void update(_, __) {} + + @override + void paint(_, __) {} + + List getNearestDatumDetailPerSeries( + Point chartPoint, bool byDomain, Rectangle boundsOverride) { + return null; + } + + DatumDetails addPositionToDetailsForSeriesDatum( + DatumDetails details, SeriesDatum seriesDatum) { + return new DatumDetails.from(details, + chartPosition: new Point(0.0, 0.0)); + } +} + +void main() { + MockChart _chart; + MockSelectionModel _selectionModel; + MockSeriesRenderer _seriesRenderer; + + MutableSeries _series1; + final _s1D1 = new MyRow(1, 11); + final _s1D2 = new MyRow(2, 12); + final _s1D3 = new MyRow(3, 13); + + MutableSeries _series2; + final _s2D1 = new MyRow(4, 21); + final _s2D2 = new MyRow(5, 22); + final _s2D3 = new MyRow(6, 23); + + List _mockGetSelectedDatumDetails(List selection) { + final details = []; + + for (SeriesDatum seriesDatum in selection) { + details.add(_seriesRenderer.getDetailsForSeriesDatum(seriesDatum)); + } + + return details; + } + + _setupSelection(List selection) { + final selected = []; + + for (var i = 0; i < selection.length; i++) { + selected.add(selection[0].datum); + } + + for (int i = 0; i < _series1.data.length; i++) { + when(_selectionModel.isDatumSelected(_series1, i)) + .thenReturn(selected.contains(_series1.data[i])); + } + for (int i = 0; i < _series2.data.length; i++) { + when(_selectionModel.isDatumSelected(_series2, i)) + .thenReturn(selected.contains(_series2.data[i])); + } + + when(_selectionModel.selectedDatum).thenReturn(selection); + + final selectedDetails = _mockGetSelectedDatumDetails(selection); + + when(_chart.getSelectedDatumDetails(SelectionModelType.info)) + .thenReturn(selectedDetails); + } + + setUp(() { + _chart = new MockChart(); + + _seriesRenderer = new MockSeriesRenderer(); + + _selectionModel = new MockSelectionModel(); + when(_chart.getSelectionModel(SelectionModelType.info)) + .thenReturn(_selectionModel); + + _series1 = new MutableSeries(new Series( + id: 's1', + data: [_s1D1, _s1D2, _s1D3], + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.count, + colorFn: (_, __) => MaterialPalette.blue.shadeDefault)) + ..measureFn = (_) => 0.0; + + _series2 = new MutableSeries(new Series( + id: 's2', + data: [_s2D1, _s2D2, _s2D3], + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.count, + colorFn: (_, __) => MaterialPalette.red.shadeDefault)) + ..measureFn = (_) => 0.0; + }); + + group('LinePointHighlighter', () { + test('highlights the selected points', () { + // Setup + final behavior = + new LinePointHighlighter(selectionModelType: SelectionModelType.info); + final tester = new LinePointHighlighterTester(behavior); + behavior.attachTo(_chart); + _setupSelection([ + new SeriesDatum(_series1, _s1D2), + new SeriesDatum(_series2, _s2D2), + ]); + + // Mock axes for returning fake domain locations. + Axis domainAxis = new MockNumericAxis(); + Axis primaryMeasureAxis = new MockNumericAxis(); + + _series1.setAttr(domainAxisKey, domainAxis); + _series1.setAttr(measureAxisKey, primaryMeasureAxis); + _series1.measureOffsetFn = (_) => 0.0; + + _series2.setAttr(domainAxisKey, domainAxis); + _series2.setAttr(measureAxisKey, primaryMeasureAxis); + _series2.measureOffsetFn = (_) => 0.0; + + // Act + _selectionModel.lastListener(_selectionModel); + verify(_chart.redraw(skipAnimation: true, skipLayout: true)); + + _chart.lastListener.onAxisConfigured(); + + // Verify + expect(tester.getSelectionLength(), equals(2)); + + expect(tester.isDatumSelected(_series1.data[0]), equals(false)); + expect(tester.isDatumSelected(_series1.data[1]), equals(true)); + expect(tester.isDatumSelected(_series1.data[2]), equals(false)); + + expect(tester.isDatumSelected(_series2.data[0]), equals(false)); + expect(tester.isDatumSelected(_series2.data[1]), equals(true)); + expect(tester.isDatumSelected(_series2.data[2]), equals(false)); + }); + + test('listens to other selection models', () { + // Setup + final behavior = new LinePointHighlighter( + selectionModelType: SelectionModelType.action); + when(_chart.getSelectionModel(SelectionModelType.action)) + .thenReturn(_selectionModel); + + // Act + behavior.attachTo(_chart); + + // Verify + verify(_chart.getSelectionModel(SelectionModelType.action)); + verifyNever(_chart.getSelectionModel(SelectionModelType.info)); + }); + + test('leaves everything alone with no selection', () { + // Setup + final behavior = + new LinePointHighlighter(selectionModelType: SelectionModelType.info); + final tester = new LinePointHighlighterTester(behavior); + behavior.attachTo(_chart); + _setupSelection([]); + + // Act + _selectionModel.lastListener(_selectionModel); + verify(_chart.redraw(skipAnimation: true, skipLayout: true)); + _chart.lastListener.onAxisConfigured(); + + // Verify + expect(tester.getSelectionLength(), equals(0)); + + expect(tester.isDatumSelected(_series1.data[0]), equals(false)); + expect(tester.isDatumSelected(_series1.data[1]), equals(false)); + expect(tester.isDatumSelected(_series1.data[2]), equals(false)); + + expect(tester.isDatumSelected(_series2.data[0]), equals(false)); + expect(tester.isDatumSelected(_series2.data[1]), equals(false)); + expect(tester.isDatumSelected(_series2.data[2]), equals(false)); + }); + + test('cleans up', () { + // Setup + final behavior = + new LinePointHighlighter(selectionModelType: SelectionModelType.info); + behavior.attachTo(_chart); + _setupSelection([ + new SeriesDatum(_series1, _s1D2), + new SeriesDatum(_series2, _s2D2), + ]); + + // Act + behavior.removeFrom(_chart); + + // Verify + expect(_chart.lastListener, isNull); + expect(_selectionModel.lastListener, isNull); + }); + }); +} + +class MyRow { + final int campaign; + final int count; + MyRow(this.campaign, this.count); +} diff --git a/web/charts/common/test/chart/common/behavior/range_annotation_test.dart b/web/charts/common/test/chart/common/behavior/range_annotation_test.dart new file mode 100644 index 000000000..b83ea39fd --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/range_annotation_test.dart @@ -0,0 +1,351 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Rectangle; + +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/cartesian/axis/numeric_tick_provider.dart'; +import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart'; +import 'package:charts_common/src/chart/cartesian/axis/linear/linear_scale.dart'; +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/chart_context.dart'; +import 'package:charts_common/src/chart/common/behavior/range_annotation.dart'; +import 'package:charts_common/src/chart/line/line_chart.dart'; +import 'package:charts_common/src/common/material_palette.dart'; +import 'package:charts_common/src/data/series.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockContext extends Mock implements ChartContext {} + +class ConcreteChart extends LineChart { + LifecycleListener lastListener; + + final _domainAxis = new ConcreteNumericAxis(); + + final _primaryMeasureAxis = new ConcreteNumericAxis(); + + @override + addLifecycleListener(LifecycleListener listener) { + lastListener = listener; + return super.addLifecycleListener(listener); + } + + @override + removeLifecycleListener(LifecycleListener listener) { + expect(listener, equals(lastListener)); + lastListener = null; + return super.removeLifecycleListener(listener); + } + + @override + Axis get domainAxis => _domainAxis; + + @override + Axis getMeasureAxis({String axisId}) => _primaryMeasureAxis; +} + +class ConcreteNumericAxis extends Axis { + ConcreteNumericAxis() + : super( + tickProvider: new MockTickProvider(), + tickFormatter: new NumericTickFormatter(), + scale: new LinearScale(), + ); +} + +class MockTickProvider extends Mock implements NumericTickProvider {} + +void main() { + Rectangle drawBounds; + Rectangle domainAxisBounds; + Rectangle measureAxisBounds; + + ConcreteChart _chart; + + Series _series1; + final _s1D1 = new MyRow(0, 11); + final _s1D2 = new MyRow(1, 12); + final _s1D3 = new MyRow(2, 13); + + Series _series2; + final _s2D1 = new MyRow(3, 21); + final _s2D2 = new MyRow(4, 22); + final _s2D3 = new MyRow(5, 23); + + const _dashPattern = const [2, 3]; + + List> _annotations1; + + List> _annotations2; + + List> _annotations3; + + ConcreteChart _makeChart() { + final chart = new ConcreteChart(); + + final context = new MockContext(); + when(context.chartContainerIsRtl).thenReturn(false); + when(context.isRtl).thenReturn(false); + chart.context = context; + + return chart; + } + + /// Initializes the [chart], draws the [seriesList], and configures mock axis + /// layout bounds. + _drawSeriesList(ConcreteChart chart, List> seriesList) { + _chart.domainAxis.autoViewport = true; + _chart.domainAxis.resetDomains(); + + _chart.getMeasureAxis().autoViewport = true; + _chart.getMeasureAxis().resetDomains(); + + _chart.draw(seriesList); + + _chart.domainAxis.layout(domainAxisBounds, drawBounds); + + _chart.getMeasureAxis().layout(measureAxisBounds, drawBounds); + + _chart.lastListener.onAxisConfigured(); + } + + setUpAll(() { + drawBounds = new Rectangle(0, 0, 100, 100); + domainAxisBounds = new Rectangle(0, 0, 100, 100); + measureAxisBounds = new Rectangle(0, 0, 100, 100); + }); + + setUp(() { + _chart = _makeChart(); + + _series1 = new Series( + id: 's1', + data: [_s1D1, _s1D2, _s1D3], + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.count, + colorFn: (_, __) => MaterialPalette.blue.shadeDefault); + + _series2 = new Series( + id: 's2', + data: [_s2D1, _s2D2, _s2D3], + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.count, + colorFn: (_, __) => MaterialPalette.red.shadeDefault); + + _annotations1 = [ + new RangeAnnotationSegment(1, 2, RangeAnnotationAxisType.domain, + startLabel: 'Ann 1'), + new RangeAnnotationSegment(4, 5, RangeAnnotationAxisType.domain, + color: MaterialPalette.gray.shade200, endLabel: 'Ann 2'), + new RangeAnnotationSegment(5, 5.5, RangeAnnotationAxisType.measure, + startLabel: 'Really long tick start label', + endLabel: 'Really long tick end label'), + new RangeAnnotationSegment(10, 15, RangeAnnotationAxisType.measure, + startLabel: 'Ann 4 Start', endLabel: 'Ann 4 End'), + new RangeAnnotationSegment(16, 22, RangeAnnotationAxisType.measure, + startLabel: 'Ann 5 Start', endLabel: 'Ann 5 End'), + ]; + + _annotations2 = [ + new RangeAnnotationSegment(1, 2, RangeAnnotationAxisType.domain), + new RangeAnnotationSegment(4, 5, RangeAnnotationAxisType.domain, + color: MaterialPalette.gray.shade200), + new RangeAnnotationSegment(8, 10, RangeAnnotationAxisType.domain, + color: MaterialPalette.gray.shade300), + ]; + + _annotations3 = [ + new LineAnnotationSegment(1, RangeAnnotationAxisType.measure, + startLabel: 'Ann 1 Start', endLabel: 'Ann 1 End'), + new LineAnnotationSegment(4, RangeAnnotationAxisType.measure, + startLabel: 'Ann 2 Start', + endLabel: 'Ann 2 End', + color: MaterialPalette.gray.shade200, + dashPattern: _dashPattern), + ]; + }); + + group('RangeAnnotation', () { + test('renders the annotations', () { + // Setup + final behavior = new RangeAnnotation(_annotations1); + final tester = new RangeAnnotationTester(behavior); + behavior.attachTo(_chart); + + final seriesList = [_series1, _series2]; + + // Act + _drawSeriesList(_chart, seriesList); + + // Verify + expect(_chart.domainAxis.getLocation(2), equals(40.0)); + expect( + tester.doesAnnotationExist( + startPosition: 20.0, + endPosition: 40.0, + color: MaterialPalette.gray.shade100, + startLabel: 'Ann 1', + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.vertical, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + expect( + tester.doesAnnotationExist( + startPosition: 80.0, + endPosition: 100.0, + color: MaterialPalette.gray.shade200, + endLabel: 'Ann 2', + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.vertical, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + + // Verify measure annotations + expect(_chart.getMeasureAxis().getLocation(11).round(), equals(33)); + expect( + tester.doesAnnotationExist( + startPosition: 0.0, + endPosition: 2.78, + color: MaterialPalette.gray.shade100, + startLabel: 'Really long tick start label', + endLabel: 'Really long tick end label', + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.horizontal, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + expect( + tester.doesAnnotationExist( + startPosition: 27.78, + endPosition: 55.56, + color: MaterialPalette.gray.shade100, + startLabel: 'Ann 4 Start', + endLabel: 'Ann 4 End', + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.horizontal, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + expect( + tester.doesAnnotationExist( + startPosition: 61.11, + endPosition: 94.44, + color: MaterialPalette.gray.shade100, + startLabel: 'Ann 5 Start', + endLabel: 'Ann 5 End', + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.horizontal, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + }); + + test('extends the domain axis when annotations fall outside the range', () { + // Setup + final behavior = new RangeAnnotation(_annotations2); + final tester = new RangeAnnotationTester(behavior); + behavior.attachTo(_chart); + + final seriesList = [_series1, _series2]; + + // Act + _drawSeriesList(_chart, seriesList); + + // Verify + expect(_chart.domainAxis.getLocation(2), equals(20.0)); + expect( + tester.doesAnnotationExist( + startPosition: 10.0, + endPosition: 20.0, + color: MaterialPalette.gray.shade100, + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.vertical, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + expect( + tester.doesAnnotationExist( + startPosition: 40.0, + endPosition: 50.0, + color: MaterialPalette.gray.shade200, + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.vertical, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + expect( + tester.doesAnnotationExist( + startPosition: 80.0, + endPosition: 100.0, + color: MaterialPalette.gray.shade300, + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.vertical, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + }); + + test('test dash pattern equality', () { + // Setup + final behavior = new RangeAnnotation(_annotations3); + final tester = new RangeAnnotationTester(behavior); + behavior.attachTo(_chart); + + final seriesList = [_series1, _series2]; + + // Act + _drawSeriesList(_chart, seriesList); + + // Verify + expect(_chart.domainAxis.getLocation(2), equals(40.0)); + expect( + tester.doesAnnotationExist( + startPosition: 0.0, + endPosition: 0.0, + color: MaterialPalette.gray.shade100, + startLabel: 'Ann 1 Start', + endLabel: 'Ann 1 End', + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.horizontal, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + expect( + tester.doesAnnotationExist( + startPosition: 13.64, + endPosition: 13.64, + color: MaterialPalette.gray.shade200, + dashPattern: _dashPattern, + startLabel: 'Ann 2 Start', + endLabel: 'Ann 2 End', + labelAnchor: AnnotationLabelAnchor.end, + labelDirection: AnnotationLabelDirection.horizontal, + labelPosition: AnnotationLabelPosition.auto), + equals(true)); + }); + + test('cleans up', () { + // Setup + final behavior = new RangeAnnotation(_annotations2); + behavior.attachTo(_chart); + + // Act + behavior.removeFrom(_chart); + + // Verify + expect(_chart.lastListener, isNull); + }); + }); +} + +class MyRow { + final int campaign; + final int count; + MyRow(this.campaign, this.count); +} diff --git a/web/charts/common/test/chart/common/behavior/selection/lock_selection_test.dart b/web/charts/common/test/chart/common/behavior/selection/lock_selection_test.dart new file mode 100644 index 000000000..f480f9028 --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/selection/lock_selection_test.dart @@ -0,0 +1,160 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/behavior/selection/lock_selection.dart'; +import 'package:charts_common/src/chart/common/selection_model/selection_model.dart'; +import 'package:charts_common/src/common/gesture_listener.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockChart extends Mock implements BaseChart { + GestureListener lastListener; + + @override + GestureListener addGestureListener(GestureListener listener) { + lastListener = listener; + return listener; + } + + @override + void removeGestureListener(GestureListener listener) { + expect(listener, equals(lastListener)); + lastListener = null; + } +} + +class MockSelectionModel extends Mock implements MutableSelectionModel { + bool locked = false; +} + +void main() { + MockChart _chart; + MockSelectionModel _hoverSelectionModel; + MockSelectionModel _clickSelectionModel; + + LockSelection _makeLockSelectionBehavior( + SelectionModelType selectionModelType) { + LockSelection behavior = + new LockSelection(selectionModelType: selectionModelType); + + behavior.attachTo(_chart); + + return behavior; + } + + _setupChart({Point forPoint, bool isWithinRenderer}) { + if (isWithinRenderer != null) { + when(_chart.pointWithinRenderer(forPoint)).thenReturn(isWithinRenderer); + } + } + + setUp(() { + _hoverSelectionModel = new MockSelectionModel(); + _clickSelectionModel = new MockSelectionModel(); + + _chart = new MockChart(); + when(_chart.getSelectionModel(SelectionModelType.info)) + .thenReturn(_hoverSelectionModel); + when(_chart.getSelectionModel(SelectionModelType.action)) + .thenReturn(_clickSelectionModel); + }); + + group('LockSelection trigger handling', () { + test('can lock model with a selection', () { + // Setup chart matches point with single domain single series. + _makeLockSelectionBehavior(SelectionModelType.info); + Point point = new Point(100.0, 100.0); + _setupChart(forPoint: point, isWithinRenderer: true); + + when(_hoverSelectionModel.hasAnySelection).thenReturn(true); + + // Act + _chart.lastListener.onTapTest(point); + _chart.lastListener.onTap(point); + + // Validate + verify(_hoverSelectionModel.hasAnySelection); + expect(_hoverSelectionModel.locked, equals(true)); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + + test('can lock and unlock model', () { + // Setup chart matches point with single domain single series. + _makeLockSelectionBehavior(SelectionModelType.info); + Point point = new Point(100.0, 100.0); + _setupChart(forPoint: point, isWithinRenderer: true); + + when(_hoverSelectionModel.hasAnySelection).thenReturn(true); + + // Act + _chart.lastListener.onTapTest(point); + _chart.lastListener.onTap(point); + + // Validate + verify(_hoverSelectionModel.hasAnySelection); + expect(_hoverSelectionModel.locked, equals(true)); + + // Act + _chart.lastListener.onTapTest(point); + _chart.lastListener.onTap(point); + + // Validate + verify(_hoverSelectionModel.clearSelection()); + expect(_hoverSelectionModel.locked, equals(false)); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + + test('does not lock model with empty selection', () { + // Setup chart matches point with single domain single series. + _makeLockSelectionBehavior(SelectionModelType.info); + Point point = new Point(100.0, 100.0); + _setupChart(forPoint: point, isWithinRenderer: true); + + when(_hoverSelectionModel.hasAnySelection).thenReturn(false); + + // Act + _chart.lastListener.onTapTest(point); + _chart.lastListener.onTap(point); + + // Validate + verify(_hoverSelectionModel.hasAnySelection); + expect(_hoverSelectionModel.locked, equals(false)); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + }); + + group('Cleanup', () { + test('detach removes listener', () { + // Setup + final behavior = _makeLockSelectionBehavior(SelectionModelType.info); + Point point = new Point(100.0, 100.0); + _setupChart(forPoint: point, isWithinRenderer: true); + expect(_chart.lastListener, isNotNull); + + // Act + behavior.removeFrom(_chart); + + // Validate + expect(_chart.lastListener, isNull); + }); + }); +} diff --git a/web/charts/common/test/chart/common/behavior/selection/select_nearest_test.dart b/web/charts/common/test/chart/common/behavior/selection/select_nearest_test.dart new file mode 100644 index 000000000..2f931fb16 --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/selection/select_nearest_test.dart @@ -0,0 +1,491 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/behavior/selection/select_nearest.dart'; +import 'package:charts_common/src/chart/common/behavior/selection/selection_trigger.dart'; +import 'package:charts_common/src/chart/common/datum_details.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/common/series_datum.dart'; +import 'package:charts_common/src/chart/common/selection_model/selection_model.dart'; +import 'package:charts_common/src/common/gesture_listener.dart'; +import 'package:charts_common/src/data/series.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockChart extends Mock implements BaseChart { + GestureListener lastListener; + + @override + GestureListener addGestureListener(GestureListener listener) { + lastListener = listener; + return listener; + } + + @override + void removeGestureListener(GestureListener listener) { + expect(listener, equals(lastListener)); + lastListener = null; + } +} + +class MockSelectionModel extends Mock implements MutableSelectionModel { +} + +void main() { + MockChart _chart; + MockSelectionModel _hoverSelectionModel; + MockSelectionModel _clickSelectionModel; + List _series1Data; + List _series2Data; + MutableSeries _series1; + MutableSeries _series2; + DatumDetails _details1; + DatumDetails _details1Series2; + DatumDetails _details2; + DatumDetails _details3; + + SelectNearest _makeBehavior( + SelectionModelType selectionModelType, SelectionTrigger eventTrigger, + {bool expandToDomain, + bool selectClosestSeries, + int maximumDomainDistancePx}) { + SelectNearest behavior = new SelectNearest( + selectionModelType: selectionModelType, + expandToDomain: expandToDomain, + selectClosestSeries: selectClosestSeries, + eventTrigger: eventTrigger, + maximumDomainDistancePx: maximumDomainDistancePx); + + behavior.attachTo(_chart); + + return behavior; + } + + _setupChart( + {Point forPoint, + bool isWithinRenderer, + List> respondWithDetails, + List> seriesList}) { + if (isWithinRenderer != null) { + when(_chart.pointWithinRenderer(forPoint)).thenReturn(isWithinRenderer); + } + if (respondWithDetails != null) { + when(_chart.getNearestDatumDetailPerSeries(forPoint, true)) + .thenReturn(respondWithDetails); + } + if (seriesList != null) { + when(_chart.currentSeriesList).thenReturn(seriesList); + } + } + + setUp(() { + _hoverSelectionModel = new MockSelectionModel(); + _clickSelectionModel = new MockSelectionModel(); + + _chart = new MockChart(); + when(_chart.getSelectionModel(SelectionModelType.info)) + .thenReturn(_hoverSelectionModel); + when(_chart.getSelectionModel(SelectionModelType.action)) + .thenReturn(_clickSelectionModel); + + _series1Data = ['myDomain1', 'myDomain2', 'myDomain3']; + + _series1 = new MutableSeries(new Series( + id: 'mySeries1', + data: ['myDatum1', 'myDatum2', 'myDatum3'], + domainFn: (_, int i) => _series1Data[i], + measureFn: (_, __) {})); + + _details1 = new DatumDetails( + datum: 'myDatum1', + domain: 'myDomain1', + series: _series1, + domainDistance: 10.0, + measureDistance: 20.0); + _details2 = new DatumDetails( + datum: 'myDatum2', + domain: 'myDomain2', + series: _series1, + domainDistance: 10.0, + measureDistance: 20.0); + _details3 = new DatumDetails( + datum: 'myDatum3', + domain: 'myDomain3', + series: _series1, + domainDistance: 10.0, + measureDistance: 20.0); + + _series2Data = ['myDomain1']; + + _series2 = new MutableSeries(new Series( + id: 'mySeries2', + data: ['myDatum1s2'], + domainFn: (_, int i) => _series2Data[i], + measureFn: (_, __) {})); + + _details1Series2 = new DatumDetails( + datum: 'myDatum1s2', + domain: 'myDomain1', + series: _series2, + domainDistance: 10.0, + measureDistance: 20.0); + }); + + tearDown(resetMockitoState); + + group('SelectNearest trigger handling', () { + test('single series selects detail', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionModelType.info, SelectionTrigger.hover, + expandToDomain: true, selectClosestSeries: true); + Point point = new Point(100.0, 100.0); + _setupChart( + forPoint: point, + isWithinRenderer: true, + respondWithDetails: [_details1], + seriesList: [_series1]); + + // Act + _chart.lastListener.onHover(point); + + // Validate + verify(_hoverSelectionModel.updateSelection( + [new SeriesDatum(_series1, _details1.datum)], [_series1])); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + // Shouldn't be listening to anything else. + expect(_chart.lastListener.onTap, isNull); + expect(_chart.lastListener.onDragStart, isNull); + }); + + test('can listen to tap', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionModelType.action, SelectionTrigger.tap, + expandToDomain: true, selectClosestSeries: true); + Point point = new Point(100.0, 100.0); + _setupChart( + forPoint: point, + isWithinRenderer: true, + respondWithDetails: [_details1], + seriesList: [_series1]); + + // Act + _chart.lastListener.onTapTest(point); + _chart.lastListener.onTap(point); + + // Validate + verify(_clickSelectionModel.updateSelection( + [new SeriesDatum(_series1, _details1.datum)], [_series1])); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + + test('can listen to drag', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionModelType.info, SelectionTrigger.pressHold, + expandToDomain: true, selectClosestSeries: true); + + Point startPoint = new Point(100.0, 100.0); + _setupChart( + forPoint: startPoint, + isWithinRenderer: true, + respondWithDetails: [_details1], + seriesList: [_series1]); + + Point updatePoint1 = new Point(200.0, 100.0); + _setupChart( + forPoint: updatePoint1, + isWithinRenderer: true, + respondWithDetails: [_details1], + seriesList: [_series1]); + + Point updatePoint2 = new Point(300.0, 100.0); + _setupChart( + forPoint: updatePoint2, + isWithinRenderer: true, + respondWithDetails: [_details2], + seriesList: [_series1]); + + Point endPoint = new Point(400.0, 100.0); + _setupChart( + forPoint: endPoint, + isWithinRenderer: true, + respondWithDetails: [_details3], + seriesList: [_series1]); + + // Act + _chart.lastListener.onTapTest(startPoint); + _chart.lastListener.onDragStart(startPoint); + _chart.lastListener.onDragUpdate(updatePoint1, 1.0); + _chart.lastListener.onDragUpdate(updatePoint2, 1.0); + _chart.lastListener.onDragEnd(endPoint, 1.0, 0.0); + + // Validate + // details1 was tripped 2 times (startPoint & updatePoint1) + verify(_hoverSelectionModel.updateSelection( + [new SeriesDatum(_series1, _details1.datum)], [_series1])).called(2); + // details2 was tripped for updatePoint2 + verify(_hoverSelectionModel.updateSelection( + [new SeriesDatum(_series1, _details2.datum)], [_series1])); + // dragEnd deselects even though we are over details3. + verify(_hoverSelectionModel.updateSelection([], [])); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + + test('can listen to drag after long press', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionModelType.info, SelectionTrigger.longPressHold, + expandToDomain: true, selectClosestSeries: true); + + Point startPoint = new Point(100.0, 100.0); + _setupChart( + forPoint: startPoint, + isWithinRenderer: true, + respondWithDetails: [_details1], + seriesList: [_series1]); + + Point updatePoint1 = new Point(200.0, 100.0); + _setupChart( + forPoint: updatePoint1, + isWithinRenderer: true, + respondWithDetails: [_details2], + seriesList: [_series1]); + + Point endPoint = new Point(400.0, 100.0); + _setupChart( + forPoint: endPoint, + isWithinRenderer: true, + respondWithDetails: [_details3], + seriesList: [_series1]); + + // Act 1 + _chart.lastListener.onTapTest(startPoint); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + + // Act 2 + // verify no interaction yet. + _chart.lastListener.onLongPress(startPoint); + _chart.lastListener.onDragStart(startPoint); + _chart.lastListener.onDragUpdate(updatePoint1, 1.0); + _chart.lastListener.onDragEnd(endPoint, 1.0, 0.0); + + // Validate + // details1 was tripped 2 times (longPress & dragStart) + verify(_hoverSelectionModel.updateSelection( + [new SeriesDatum(_series1, _details1.datum)], [_series1])).called(2); + verify(_hoverSelectionModel.updateSelection( + [new SeriesDatum(_series1, _details2.datum)], [_series1])); + // dragEnd deselects even though we are over details3. + verify(_hoverSelectionModel.updateSelection([], [])); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + + test('no trigger before long press', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionModelType.info, SelectionTrigger.longPressHold, + expandToDomain: true, selectClosestSeries: true); + + Point startPoint = new Point(100.0, 100.0); + _setupChart( + forPoint: startPoint, + isWithinRenderer: true, + respondWithDetails: [_details1], + seriesList: [_series1]); + + Point updatePoint1 = new Point(200.0, 100.0); + _setupChart( + forPoint: updatePoint1, + isWithinRenderer: true, + respondWithDetails: [_details2], + seriesList: [_series1]); + + Point endPoint = new Point(400.0, 100.0); + _setupChart( + forPoint: endPoint, + isWithinRenderer: true, + respondWithDetails: [_details3], + seriesList: [_series1]); + + // Act + _chart.lastListener.onTapTest(startPoint); + _chart.lastListener.onDragStart(startPoint); + _chart.lastListener.onDragUpdate(updatePoint1, 1.0); + _chart.lastListener.onDragEnd(endPoint, 1.0, 0.0); + + // Validate + // No interaction, didn't long press first. + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + }); + + group('Details', () { + test('expands to domain and includes closest series', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionModelType.info, SelectionTrigger.hover, + expandToDomain: true, selectClosestSeries: true); + Point point = new Point(100.0, 100.0); + _setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [ + _details1, + _details1Series2, + ], seriesList: [ + _series1, + _series2 + ]); + + // Act + _chart.lastListener.onHover(point); + + // Validate + verify(_hoverSelectionModel.updateSelection([ + new SeriesDatum(_series1, _details1.datum), + new SeriesDatum(_series2, _details1Series2.datum) + ], [ + _series1 + ])); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + + test('does not expand to domain', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionModelType.info, SelectionTrigger.hover, + expandToDomain: false, selectClosestSeries: true); + Point point = new Point(100.0, 100.0); + _setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [ + _details1, + _details1Series2, + ], seriesList: [ + _series1, + _series2 + ]); + + // Act + _chart.lastListener.onHover(point); + + // Validate + verify(_hoverSelectionModel.updateSelection( + [new SeriesDatum(_series1, _details1.datum)], [_series1])); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + + test('does not include closest series', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionModelType.info, SelectionTrigger.hover, + expandToDomain: true, selectClosestSeries: false); + Point point = new Point(100.0, 100.0); + _setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [ + _details1, + _details1Series2, + ], seriesList: [ + _series1, + _series2 + ]); + + // Act + _chart.lastListener.onHover(point); + + // Validate + verify(_hoverSelectionModel.updateSelection([ + new SeriesDatum(_series1, _details1.datum), + new SeriesDatum(_series2, _details1Series2.datum) + ], [])); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + + test('does not include overlay series', () { + // Setup chart with an overlay series. + _series2.overlaySeries = true; + + _makeBehavior(SelectionModelType.info, SelectionTrigger.hover, + expandToDomain: true, selectClosestSeries: true); + Point point = new Point(100.0, 100.0); + _setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [ + _details1, + _details1Series2, + ], seriesList: [ + _series1, + _series2 + ]); + + // Act + _chart.lastListener.onHover(point); + + // Validate + verify(_hoverSelectionModel.updateSelection([ + new SeriesDatum(_series1, _details1.datum), + ], [ + _series1 + ])); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + + test('selection does not exceed maximumDomainDistancePx', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionModelType.info, SelectionTrigger.hover, + expandToDomain: true, + selectClosestSeries: true, + maximumDomainDistancePx: 1); + Point point = new Point(100.0, 100.0); + _setupChart(forPoint: point, isWithinRenderer: true, respondWithDetails: [ + _details1, + _details1Series2, + ], seriesList: [ + _series1, + _series2 + ]); + + // Act + _chart.lastListener.onHover(point); + + // Validate + verify(_hoverSelectionModel.updateSelection([], [])); + verifyNoMoreInteractions(_hoverSelectionModel); + verifyNoMoreInteractions(_clickSelectionModel); + }); + }); + + group('Cleanup', () { + test('detach removes listener', () { + // Setup + SelectNearest behavior = _makeBehavior( + SelectionModelType.info, SelectionTrigger.hover, + expandToDomain: true, selectClosestSeries: true); + Point point = new Point(100.0, 100.0); + _setupChart( + forPoint: point, + isWithinRenderer: true, + respondWithDetails: [_details1], + seriesList: [_series1]); + expect(_chart.lastListener, isNotNull); + + // Act + behavior.removeFrom(_chart); + + // Validate + expect(_chart.lastListener, isNull); + }); + }); +} diff --git a/web/charts/common/test/chart/common/behavior/series_legend_behavior_test.dart b/web/charts/common/test/chart/common/behavior/series_legend_behavior_test.dart new file mode 100644 index 000000000..7f682acc1 --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/series_legend_behavior_test.dart @@ -0,0 +1,474 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/common/series_datum.dart'; +import 'package:charts_common/src/chart/common/series_renderer.dart'; +import 'package:charts_common/src/chart/common/behavior/legend/legend_entry_generator.dart'; +import 'package:charts_common/src/chart/common/behavior/legend/series_legend.dart'; +import 'package:charts_common/src/chart/common/datum_details.dart'; +import 'package:charts_common/src/chart/common/selection_model/selection_model.dart'; +import 'package:charts_common/src/common/color.dart'; +import 'package:charts_common/src/data/series.dart'; +import 'package:test/test.dart'; + +class ConcreteChart extends BaseChart { + List> _seriesList; + + ConcreteChart(this._seriesList); + + @override + SeriesRenderer makeDefaultRenderer() => null; + + @override + List> get currentSeriesList => _seriesList; + + @override + List> getDatumDetails(SelectionModelType _) => null; + + set seriesList(List> seriesList) { + _seriesList = seriesList; + } + + void callOnDraw() { + fireOnDraw(_seriesList); + } + + void callOnPreProcess() { + fireOnPreprocess(_seriesList); + } + + void callOnPostProcess() { + fireOnPostprocess(_seriesList); + } +} + +class ConcreteSeriesLegend extends SeriesLegend { + ConcreteSeriesLegend( + {SelectionModelType selectionModelType, + LegendEntryGenerator legendEntryGenerator}) + : super( + selectionModelType: selectionModelType, + legendEntryGenerator: legendEntryGenerator); + + @override + bool isSeriesRenderer = false; + + @override + void hideSeries(String seriesId) { + super.hideSeries(seriesId); + } + + @override + void showSeries(String seriesId) { + super.showSeries(seriesId); + } + + @override + bool isSeriesHidden(String seriesId) { + return super.isSeriesHidden(seriesId); + } +} + +void main() { + MutableSeries series1; + final s1D1 = new MyRow('s1d1', 11); + final s1D2 = new MyRow('s1d2', 12); + final s1D3 = new MyRow('s1d3', 13); + + MutableSeries series2; + final s2D1 = new MyRow('s2d1', 21); + final s2D2 = new MyRow('s2d2', 22); + final s2D3 = new MyRow('s2d3', 23); + + final blue = new Color(r: 0x21, g: 0x96, b: 0xF3); + final red = new Color(r: 0xF4, g: 0x43, b: 0x36); + + ConcreteChart chart; + + setUp(() { + series1 = new MutableSeries(new Series( + id: 's1', + data: [s1D1, s1D2, s1D3], + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.count, + colorFn: (_, __) => blue)); + + series2 = new MutableSeries(new Series( + id: 's2', + data: [s2D1, s2D2, s2D3], + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.count, + colorFn: (_, __) => red)); + }); + + test('Legend entries created on chart post process', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final legend = new SeriesLegend(selectionModelType: selectionType); + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + chart.callOnDraw(); + chart.callOnPreProcess(); + chart.callOnPostProcess(); + + final legendEntries = legend.legendState.legendEntries; + expect(legendEntries, hasLength(2)); + expect(legendEntries[0].series, equals(series1)); + expect(legendEntries[0].label, equals('s1')); + expect(legendEntries[0].color, equals(blue)); + expect(legendEntries[0].isSelected, isFalse); + + expect(legendEntries[1].series, equals(series2)); + expect(legendEntries[1].label, equals('s2')); + expect(legendEntries[1].color, equals(red)); + expect(legendEntries[1].isSelected, isFalse); + }); + + test('default hidden series are removed from list during pre process', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final legend = + new ConcreteSeriesLegend(selectionModelType: selectionType); + + legend.defaultHiddenSeries = ['s2']; + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + chart.callOnDraw(); + chart.callOnPreProcess(); + + expect(legend.isSeriesHidden('s1'), isFalse); + expect(legend.isSeriesHidden('s2'), isTrue); + + expect(seriesList, hasLength(1)); + expect(seriesList[0].id, equals('s1')); + }); + + test('hidden series are removed from list after chart pre process', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final legend = + new ConcreteSeriesLegend(selectionModelType: selectionType); + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + legend.hideSeries('s1'); + chart.callOnDraw(); + chart.callOnPreProcess(); + + expect(legend.isSeriesHidden('s1'), isTrue); + expect(legend.isSeriesHidden('s2'), isFalse); + + expect(seriesList, hasLength(1)); + expect(seriesList[0].id, equals('s2')); + }); + + test('hidden and re-shown series is in the list after chart pre process', () { + final seriesList = [series1, series2]; + final seriesList2 = [series1, series2]; + final selectionType = SelectionModelType.info; + final legend = + new ConcreteSeriesLegend(selectionModelType: selectionType); + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + + // First hide the series. + legend.hideSeries('s1'); + chart.callOnDraw(); + chart.callOnPreProcess(); + + expect(legend.isSeriesHidden('s1'), isTrue); + expect(legend.isSeriesHidden('s2'), isFalse); + + expect(seriesList, hasLength(1)); + expect(seriesList[0].id, equals('s2')); + + // Then un-hide the series. This second list imitates the behavior of the + // chart, which creates a fresh copy of the original data from the user + // during each draw cycle. + legend.showSeries('s1'); + chart.seriesList = seriesList2; + chart.callOnDraw(); + chart.callOnPreProcess(); + + expect(legend.isSeriesHidden('s1'), isFalse); + expect(legend.isSeriesHidden('s2'), isFalse); + + expect(seriesList2, hasLength(2)); + expect(seriesList2[0].id, equals('s1')); + expect(seriesList2[1].id, equals('s2')); + }); + + test('selected series legend entry is updated', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final legend = new SeriesLegend(selectionModelType: selectionType); + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + chart.callOnDraw(); + chart.callOnPreProcess(); + chart.callOnPostProcess(); + chart.getSelectionModel(selectionType).updateSelection([], [series1]); + + final legendEntries = legend.legendState.legendEntries; + expect(legendEntries, hasLength(2)); + expect(legendEntries[0].series, equals(series1)); + expect(legendEntries[0].label, equals('s1')); + expect(legendEntries[0].color, equals(blue)); + expect(legendEntries[0].isSelected, isTrue); + + expect(legendEntries[1].series, equals(series2)); + expect(legendEntries[1].label, equals('s2')); + expect(legendEntries[1].color, equals(red)); + expect(legendEntries[1].isSelected, isFalse); + }); + + test('hidden series removed from chart and later readded is visible', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final legend = + new ConcreteSeriesLegend(selectionModelType: selectionType); + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + + // First hide the series. + legend.hideSeries('s1'); + chart.callOnDraw(); + chart.callOnPreProcess(); + + expect(legend.isSeriesHidden('s1'), isTrue); + expect(legend.isSeriesHidden('s2'), isFalse); + + expect(seriesList, hasLength(1)); + expect(seriesList[0].id, equals('s2')); + + // Validate that drawing the same set of series again maintains the hidden + // states. + final seriesList2 = [series1, series2]; + chart.seriesList = seriesList2; + chart.callOnDraw(); + chart.callOnPreProcess(); + + expect(legend.isSeriesHidden('s1'), isTrue); + expect(legend.isSeriesHidden('s2'), isFalse); + + expect(seriesList2, hasLength(1)); + expect(seriesList2[0].id, equals('s2')); + + // Next, redraw the chart with only the visible series2. + final seriesList3 = [series2]; + + chart.seriesList = seriesList3; + chart.callOnDraw(); + chart.callOnPreProcess(); + + expect(legend.isSeriesHidden('s2'), isFalse); + + expect(seriesList3, hasLength(1)); + expect(seriesList3[0].id, equals('s2')); + + // Finally, add series1 back to the chart, and validate that it is not + // hidden. + final seriesList4 = [series1, series2]; + chart.seriesList = seriesList4; + chart.callOnDraw(); + chart.callOnPreProcess(); + + expect(legend.isSeriesHidden('s1'), isFalse); + expect(legend.isSeriesHidden('s2'), isFalse); + + expect(seriesList4, hasLength(2)); + expect(seriesList4[0].id, equals('s1')); + expect(seriesList4[1].id, equals('s2')); + }); + + test('generated legend entries use provided formatters', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final measureFormatter = + (num value) => 'measure ${value?.toStringAsFixed(0)}'; + final secondaryMeasureFormatter = + (num value) => 'secondary ${value?.toStringAsFixed(0)}'; + final legend = new SeriesLegend( + selectionModelType: selectionType, + measureFormatter: measureFormatter, + secondaryMeasureFormatter: secondaryMeasureFormatter); + + series2.setAttr(measureAxisIdKey, 'secondaryMeasureAxisId'); + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + chart.callOnDraw(); + chart.callOnPreProcess(); + chart.callOnPostProcess(); + chart.getSelectionModel(selectionType).updateSelection( + [new SeriesDatum(series1, s1D1), new SeriesDatum(series2, s2D1)], + [series1, series2]); + + final legendEntries = legend.legendState.legendEntries; + expect(legendEntries, hasLength(2)); + expect(legendEntries[0].series, equals(series1)); + expect(legendEntries[0].label, equals('s1')); + expect(legendEntries[0].isSelected, isTrue); + expect(legendEntries[0].value, equals(11.0)); + expect(legendEntries[0].formattedValue, equals('measure 11')); + + expect(legendEntries[1].series, equals(series2)); + expect(legendEntries[1].label, equals('s2')); + expect(legendEntries[1].isSelected, isTrue); + expect(legendEntries[1].value, equals(21.0)); + expect(legendEntries[1].formattedValue, equals('secondary 21')); + }); + + test('series legend - show measure sum when there is no selection', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}'; + final legend = new SeriesLegend( + selectionModelType: selectionType, + legendDefaultMeasure: LegendDefaultMeasure.sum, + measureFormatter: measureFormatter); + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + chart.callOnDraw(); + chart.callOnPreProcess(); + chart.callOnPostProcess(); + + final legendEntries = legend.legendState.legendEntries; + expect(legendEntries, hasLength(2)); + expect(legendEntries[0].series, equals(series1)); + expect(legendEntries[0].label, equals('s1')); + expect(legendEntries[0].color, equals(blue)); + expect(legendEntries[0].isSelected, isFalse); + expect(legendEntries[0].value, equals(36.0)); + expect(legendEntries[0].formattedValue, equals('36')); + + expect(legendEntries[1].series, equals(series2)); + expect(legendEntries[1].label, equals('s2')); + expect(legendEntries[1].color, equals(red)); + expect(legendEntries[1].isSelected, isFalse); + expect(legendEntries[1].value, equals(66.0)); + expect(legendEntries[1].formattedValue, equals('66')); + }); + + test('series legend - show measure average when there is no selection', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}'; + final legend = new SeriesLegend( + selectionModelType: selectionType, + legendDefaultMeasure: LegendDefaultMeasure.average, + measureFormatter: measureFormatter); + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + chart.callOnDraw(); + chart.callOnPreProcess(); + chart.callOnPostProcess(); + + final legendEntries = legend.legendState.legendEntries; + expect(legendEntries, hasLength(2)); + expect(legendEntries[0].series, equals(series1)); + expect(legendEntries[0].label, equals('s1')); + expect(legendEntries[0].color, equals(blue)); + expect(legendEntries[0].isSelected, isFalse); + expect(legendEntries[0].value, equals(12.0)); + expect(legendEntries[0].formattedValue, equals('12')); + + expect(legendEntries[1].series, equals(series2)); + expect(legendEntries[1].label, equals('s2')); + expect(legendEntries[1].color, equals(red)); + expect(legendEntries[1].isSelected, isFalse); + expect(legendEntries[1].value, equals(22.0)); + expect(legendEntries[1].formattedValue, equals('22')); + }); + + test('series legend - show first measure when there is no selection', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}'; + final legend = new SeriesLegend( + selectionModelType: selectionType, + legendDefaultMeasure: LegendDefaultMeasure.firstValue, + measureFormatter: measureFormatter); + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + chart.callOnDraw(); + chart.callOnPreProcess(); + chart.callOnPostProcess(); + + final legendEntries = legend.legendState.legendEntries; + expect(legendEntries, hasLength(2)); + expect(legendEntries[0].series, equals(series1)); + expect(legendEntries[0].label, equals('s1')); + expect(legendEntries[0].color, equals(blue)); + expect(legendEntries[0].isSelected, isFalse); + expect(legendEntries[0].value, equals(11.0)); + expect(legendEntries[0].formattedValue, equals('11')); + + expect(legendEntries[1].series, equals(series2)); + expect(legendEntries[1].label, equals('s2')); + expect(legendEntries[1].color, equals(red)); + expect(legendEntries[1].isSelected, isFalse); + expect(legendEntries[1].value, equals(21.0)); + expect(legendEntries[1].formattedValue, equals('21')); + }); + + test('series legend - show last measure when there is no selection', () { + final seriesList = [series1, series2]; + final selectionType = SelectionModelType.info; + final measureFormatter = (num value) => '${value?.toStringAsFixed(0)}'; + final legend = new SeriesLegend( + selectionModelType: selectionType, + legendDefaultMeasure: LegendDefaultMeasure.lastValue, + measureFormatter: measureFormatter); + + chart = new ConcreteChart(seriesList); + legend.attachTo(chart); + chart.callOnDraw(); + chart.callOnPreProcess(); + chart.callOnPostProcess(); + + final legendEntries = legend.legendState.legendEntries; + expect(legendEntries, hasLength(2)); + expect(legendEntries[0].series, equals(series1)); + expect(legendEntries[0].label, equals('s1')); + expect(legendEntries[0].color, equals(blue)); + expect(legendEntries[0].isSelected, isFalse); + expect(legendEntries[0].value, equals(13.0)); + expect(legendEntries[0].formattedValue, equals('13')); + + expect(legendEntries[1].series, equals(series2)); + expect(legendEntries[1].label, equals('s2')); + expect(legendEntries[1].color, equals(red)); + expect(legendEntries[1].isSelected, isFalse); + expect(legendEntries[1].value, equals(23.0)); + expect(legendEntries[1].formattedValue, equals('23')); + }); +} + +class MyRow { + final String campaign; + final int count; + MyRow(this.campaign, this.count); +} diff --git a/web/charts/common/test/chart/common/behavior/slider/slider_test.dart b/web/charts/common/test/chart/common/behavior/slider/slider_test.dart new file mode 100644 index 000000000..ebc25e7f3 --- /dev/null +++ b/web/charts/common/test/chart/common/behavior/slider/slider_test.dart @@ -0,0 +1,611 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart'; +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/common/base_chart.dart'; +import 'package:charts_common/src/chart/common/datum_details.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/common/behavior/slider/slider.dart'; +import 'package:charts_common/src/chart/common/behavior/selection/selection_trigger.dart'; +import 'package:charts_common/src/common/gesture_listener.dart'; +import 'package:charts_common/src/data/series.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockChart extends Mock implements CartesianChart { + GestureListener lastGestureListener; + + LifecycleListener lastLifecycleListener; + + bool vertical = true; + + @override + GestureListener addGestureListener(GestureListener listener) { + lastGestureListener = listener; + return listener; + } + + @override + void removeGestureListener(GestureListener listener) { + expect(listener, equals(lastGestureListener)); + lastGestureListener = null; + } + + @override + addLifecycleListener(LifecycleListener listener) => + lastLifecycleListener = listener; + + @override + removeLifecycleListener(LifecycleListener listener) { + expect(listener, equals(lastLifecycleListener)); + lastLifecycleListener = null; + return true; + } +} + +class MockDomainAxis extends Mock implements NumericAxis { + @override + double getDomain(num location) { + return (location / 20.0).toDouble(); + } + + @override + double getLocation(num domain) { + return (domain * 20.0).toDouble(); + } +} + +void main() { + MockChart _chart; + MockDomainAxis _domainAxis; + ImmutableSeries _series1; + DatumDetails _details1; + DatumDetails _details2; + DatumDetails _details3; + + SliderTester tester; + + Slider _makeBehavior(SelectionTrigger eventTrigger, + {Point handleOffset, + Rectangle handleSize, + double initialDomainValue, + SliderListenerCallback onChangeCallback, + bool snapToDatum = false, + SliderHandlePosition handlePosition = SliderHandlePosition.middle}) { + Slider behavior = new Slider( + eventTrigger: eventTrigger, + initialDomainValue: initialDomainValue, + onChangeCallback: onChangeCallback, + snapToDatum: snapToDatum, + style: new SliderStyle( + handleOffset: handleOffset, handlePosition: handlePosition)); + + behavior.attachTo(_chart); + + tester = new SliderTester(behavior); + + // Mock out chart layout by assigning bounds to the layout view. + tester.layout( + new Rectangle(0, 0, 200, 200), new Rectangle(0, 0, 200, 200)); + + return behavior; + } + + _setupChart( + {Point forPoint, + bool isWithinRenderer, + List respondWithDetails}) { + when(_chart.domainAxis).thenReturn(_domainAxis); + + if (isWithinRenderer != null) { + when(_chart.pointWithinRenderer(forPoint)).thenReturn(isWithinRenderer); + } + if (respondWithDetails != null) { + when(_chart.getNearestDatumDetailPerSeries(forPoint, true)) + .thenReturn(respondWithDetails); + } + } + + setUp(() { + _chart = new MockChart(); + + _domainAxis = new MockDomainAxis(); + + _series1 = new MutableSeries(new Series( + id: 'mySeries1', + data: [], + domainFn: (_, __) {}, + measureFn: (_, __) {})); + + _details1 = new DatumDetails( + chartPosition: new Point(20.0, 80.0), + datum: 'myDatum1', + domain: 1.0, + series: _series1, + domainDistance: 10.0, + measureDistance: 20.0); + _details2 = new DatumDetails( + chartPosition: new Point(40.0, 80.0), + datum: 'myDatum2', + domain: 2.0, + series: _series1, + domainDistance: 10.0, + measureDistance: 20.0); + _details3 = new DatumDetails( + chartPosition: new Point(90.0, 80.0), + datum: 'myDatum3', + domain: 4.5, + series: _series1, + domainDistance: 10.0, + measureDistance: 20.0); + }); + + group('Slider trigger handling', () { + test('can listen to tap and drag', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionTrigger.tapAndDrag, + handleOffset: new Point(0.0, 0.0), + handleSize: new Rectangle(0, 0, 10, 20)); + + Point startPoint = new Point(100.0, 100.0); + _setupChart( + forPoint: startPoint, + isWithinRenderer: true, + respondWithDetails: [_details1]); + + Point updatePoint1 = new Point(50.0, 100.0); + _setupChart( + forPoint: updatePoint1, + isWithinRenderer: true, + respondWithDetails: [_details2]); + + Point updatePoint2 = new Point(100.0, 100.0); + _setupChart( + forPoint: updatePoint2, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + Point endPoint = new Point(120.0, 100.0); + _setupChart( + forPoint: endPoint, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + // Act + _chart.lastLifecycleListener.onAxisConfigured(); + + _chart.lastGestureListener.onTapTest(startPoint); + _chart.lastGestureListener.onTap(startPoint); + + // Start the drag. + _chart.lastGestureListener.onDragStart(startPoint); + expect(tester.domainCenterPoint, equals(startPoint)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Drag to first update point. + _chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0); + expect(tester.domainCenterPoint, equals(updatePoint1)); + expect(tester.domainValue, equals(2.5)); + expect(tester.handleBounds, equals(new Rectangle(45, 90, 10, 20))); + + // Drag to first update point. + _chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0); + expect(tester.domainCenterPoint, equals(updatePoint2)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Drag the point to the end point. + _chart.lastGestureListener.onDragUpdate(endPoint, 1.0); + expect(tester.domainCenterPoint, equals(endPoint)); + expect(tester.domainValue, equals(6.0)); + expect(tester.handleBounds, equals(new Rectangle(115, 90, 10, 20))); + + // Simulate onDragEnd. + _chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0); + + expect(tester.domainCenterPoint, equals(endPoint)); + expect(tester.domainValue, equals(6.0)); + expect(tester.handleBounds, equals(new Rectangle(115, 90, 10, 20))); + }); + + test('slider handle can render at top', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionTrigger.tapAndDrag, + handleOffset: new Point(0.0, 0.0), + handleSize: new Rectangle(0, 0, 10, 20), + handlePosition: SliderHandlePosition.top); + + Point startPoint = new Point(100.0, 0.0); + _setupChart( + forPoint: startPoint, + isWithinRenderer: true, + respondWithDetails: [_details1]); + + Point updatePoint1 = new Point(50.0, 0.0); + _setupChart( + forPoint: updatePoint1, + isWithinRenderer: true, + respondWithDetails: [_details2]); + + Point updatePoint2 = new Point(100.0, 0.0); + _setupChart( + forPoint: updatePoint2, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + Point endPoint = new Point(120.0, 0.0); + _setupChart( + forPoint: endPoint, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + // Act + _chart.lastLifecycleListener.onAxisConfigured(); + + _chart.lastGestureListener.onTapTest(startPoint); + _chart.lastGestureListener.onTap(startPoint); + + // Start the drag. + _chart.lastGestureListener.onDragStart(startPoint); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, -10, 10, 20))); + + // Drag to first update point. + _chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0); + expect(tester.domainValue, equals(2.5)); + expect(tester.handleBounds, equals(new Rectangle(45, -10, 10, 20))); + + // Drag to first update point. + _chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, -10, 10, 20))); + + // Drag the point to the end point. + _chart.lastGestureListener.onDragUpdate(endPoint, 1.0); + expect(tester.domainValue, equals(6.0)); + expect(tester.handleBounds, equals(new Rectangle(115, -10, 10, 20))); + + // Simulate onDragEnd. + _chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0); + + expect(tester.domainValue, equals(6.0)); + expect(tester.handleBounds, equals(new Rectangle(115, -10, 10, 20))); + }); + + test('can listen to press hold', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionTrigger.pressHold, + handleOffset: new Point(0.0, 0.0), + handleSize: new Rectangle(0, 0, 10, 20)); + + Point startPoint = new Point(100.0, 100.0); + _setupChart( + forPoint: startPoint, + isWithinRenderer: true, + respondWithDetails: [_details1]); + + Point updatePoint1 = new Point(50.0, 100.0); + _setupChart( + forPoint: updatePoint1, + isWithinRenderer: true, + respondWithDetails: [_details2]); + + Point updatePoint2 = new Point(100.0, 100.0); + _setupChart( + forPoint: updatePoint2, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + Point endPoint = new Point(120.0, 100.0); + _setupChart( + forPoint: endPoint, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + // Act + _chart.lastLifecycleListener.onAxisConfigured(); + + _chart.lastGestureListener.onTapTest(startPoint); + _chart.lastGestureListener.onLongPress(startPoint); + + // Start the drag. + _chart.lastGestureListener.onDragStart(startPoint); + expect(tester.domainCenterPoint, equals(startPoint)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Drag to first update point. + _chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0); + expect(tester.domainCenterPoint, equals(updatePoint1)); + expect(tester.domainValue, equals(2.5)); + expect(tester.handleBounds, equals(new Rectangle(45, 90, 10, 20))); + + // Drag to first update point. + _chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0); + expect(tester.domainCenterPoint, equals(updatePoint2)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Drag the point to the end point. + _chart.lastGestureListener.onDragUpdate(endPoint, 1.0); + expect(tester.domainCenterPoint, equals(endPoint)); + expect(tester.domainValue, equals(6.0)); + expect(tester.handleBounds, equals(new Rectangle(115, 90, 10, 20))); + + // Simulate onDragEnd. + _chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0); + + expect(tester.domainCenterPoint, equals(endPoint)); + expect(tester.domainValue, equals(6.0)); + expect(tester.handleBounds, equals(new Rectangle(115, 90, 10, 20))); + }); + + test('can listen to long press hold', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionTrigger.longPressHold, + handleOffset: new Point(0.0, 0.0), + handleSize: new Rectangle(0, 0, 10, 20)); + + Point startPoint = new Point(100.0, 100.0); + _setupChart( + forPoint: startPoint, + isWithinRenderer: true, + respondWithDetails: [_details1]); + + Point updatePoint1 = new Point(50.0, 100.0); + _setupChart( + forPoint: updatePoint1, + isWithinRenderer: true, + respondWithDetails: [_details2]); + + Point updatePoint2 = new Point(100.0, 100.0); + _setupChart( + forPoint: updatePoint2, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + Point endPoint = new Point(120.0, 100.0); + _setupChart( + forPoint: endPoint, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + // Act + _chart.lastLifecycleListener.onAxisConfigured(); + + _chart.lastGestureListener.onTapTest(startPoint); + _chart.lastGestureListener.onLongPress(startPoint); + + // Start the drag. + _chart.lastGestureListener.onDragStart(startPoint); + expect(tester.domainCenterPoint, equals(startPoint)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Drag to first update point. + _chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0); + expect(tester.domainCenterPoint, equals(updatePoint1)); + expect(tester.domainValue, equals(2.5)); + expect(tester.handleBounds, equals(new Rectangle(45, 90, 10, 20))); + + // Drag to first update point. + _chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0); + expect(tester.domainCenterPoint, equals(updatePoint2)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Drag the point to the end point. + _chart.lastGestureListener.onDragUpdate(endPoint, 1.0); + expect(tester.domainCenterPoint, equals(endPoint)); + expect(tester.domainValue, equals(6.0)); + expect(tester.handleBounds, equals(new Rectangle(115, 90, 10, 20))); + + // Simulate onDragEnd. + _chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0); + + expect(tester.domainCenterPoint, equals(endPoint)); + expect(tester.domainValue, equals(6.0)); + expect(tester.handleBounds, equals(new Rectangle(115, 90, 10, 20))); + }); + + test('no position update before long press', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionTrigger.longPressHold, + handleOffset: new Point(0.0, 0.0), + handleSize: new Rectangle(0, 0, 10, 20)); + + Point startPoint = new Point(100.0, 100.0); + _setupChart( + forPoint: startPoint, + isWithinRenderer: true, + respondWithDetails: [_details1]); + + Point updatePoint1 = new Point(50.0, 100.0); + _setupChart( + forPoint: updatePoint1, + isWithinRenderer: true, + respondWithDetails: [_details2]); + + Point updatePoint2 = new Point(100.0, 100.0); + _setupChart( + forPoint: updatePoint2, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + Point endPoint = new Point(120.0, 100.0); + _setupChart( + forPoint: endPoint, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + // Act + _chart.lastLifecycleListener.onAxisConfigured(); + + _chart.lastGestureListener.onTapTest(startPoint); + + // Start the drag. + _chart.lastGestureListener.onDragStart(startPoint); + expect(tester.domainCenterPoint, equals(startPoint)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Drag the point to the end point. + _chart.lastGestureListener.onDragUpdate(endPoint, 1.0); + expect(tester.domainCenterPoint, equals(startPoint)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Simulate onDragEnd. + _chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0); + + expect(tester.domainCenterPoint, equals(startPoint)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + }); + + test('can snap to datum', () { + // Setup chart matches point with single domain single series. + _makeBehavior(SelectionTrigger.tapAndDrag, + handleOffset: new Point(0.0, 0.0), + handleSize: new Rectangle(0, 0, 10, 20), + snapToDatum: true); + + Point startPoint = new Point(100.0, 100.0); + _setupChart( + forPoint: startPoint, + isWithinRenderer: true, + respondWithDetails: [_details1]); + + Point updatePoint1 = new Point(50.0, 100.0); + _setupChart( + forPoint: updatePoint1, + isWithinRenderer: true, + respondWithDetails: [_details2]); + + Point updatePoint2 = new Point(100.0, 100.0); + _setupChart( + forPoint: updatePoint2, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + Point endPoint = new Point(120.0, 100.0); + _setupChart( + forPoint: endPoint, + isWithinRenderer: true, + respondWithDetails: [_details3]); + + // Act + _chart.lastLifecycleListener.onAxisConfigured(); + + _chart.lastGestureListener.onTapTest(startPoint); + _chart.lastGestureListener.onTap(startPoint); + + // Start the drag. + _chart.lastGestureListener.onDragStart(startPoint); + expect(tester.domainCenterPoint, equals(startPoint)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Drag to first update point. The slider should follow the mouse during + // each drag update. + _chart.lastGestureListener.onDragUpdate(updatePoint1, 1.0); + expect(tester.domainCenterPoint, equals(updatePoint1)); + expect(tester.domainValue, equals(2.5)); + expect(tester.handleBounds, equals(new Rectangle(45, 90, 10, 20))); + + // Drag to first update point. + _chart.lastGestureListener.onDragUpdate(updatePoint2, 1.0); + expect(tester.domainCenterPoint, equals(updatePoint2)); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Drag the point to the end point. + _chart.lastGestureListener.onDragUpdate(endPoint, 1.0); + expect(tester.domainCenterPoint, equals(endPoint)); + expect(tester.domainValue, equals(6.0)); + expect(tester.handleBounds, equals(new Rectangle(115, 90, 10, 20))); + + // Simulate onDragEnd. This is where we expect the snap to occur. + _chart.lastGestureListener.onDragEnd(endPoint, 1.0, 1.0); + + expect(tester.domainCenterPoint, equals(new Point(90, 100))); + expect(tester.domainValue, equals(4.5)); + expect(tester.handleBounds, equals(new Rectangle(85, 90, 10, 20))); + }); + }); + + group('Slider manual control', () { + test('can set domain position', () { + // Setup chart matches point with single domain single series. + final slider = _makeBehavior(SelectionTrigger.tapAndDrag, + handleOffset: new Point(0.0, 0.0), + handleSize: new Rectangle(0, 0, 10, 20), + initialDomainValue: 1.0); + + _setupChart(); + + // Act + _chart.lastLifecycleListener.onAxisConfigured(); + + // Verify initial position. + expect(tester.domainCenterPoint, equals(new Point(20.0, 100.0))); + expect(tester.domainValue, equals(1.0)); + expect(tester.handleBounds, equals(new Rectangle(15, 90, 10, 20))); + + // Move to first domain value. + slider.moveSliderToDomain(2); + expect(tester.domainCenterPoint, equals(new Point(40.0, 100.0))); + expect(tester.domainValue, equals(2.0)); + expect(tester.handleBounds, equals(new Rectangle(35, 90, 10, 20))); + + // Move to second domain value. + slider.moveSliderToDomain(5); + expect(tester.domainCenterPoint, equals(new Point(100.0, 100.0))); + expect(tester.domainValue, equals(5.0)); + expect(tester.handleBounds, equals(new Rectangle(95, 90, 10, 20))); + + // Move to second domain value. + slider.moveSliderToDomain(7.5); + expect(tester.domainCenterPoint, equals(new Point(150.0, 100.0))); + expect(tester.domainValue, equals(7.5)); + expect(tester.handleBounds, equals(new Rectangle(145, 90, 10, 20))); + }); + }); + + group('Cleanup', () { + test('detach removes listener', () { + // Setup + Slider behavior = _makeBehavior(SelectionTrigger.tapAndDrag); + + Point point = new Point(100.0, 100.0); + _setupChart( + forPoint: point, + isWithinRenderer: true, + respondWithDetails: [_details1]); + expect(_chart.lastGestureListener, isNotNull); + + // Act + behavior.removeFrom(_chart); + + // Validate + expect(_chart.lastGestureListener, isNull); + }); + }); +} diff --git a/web/charts/common/test/chart/common/gesture_listener_test.dart b/web/charts/common/test/chart/common/gesture_listener_test.dart new file mode 100644 index 000000000..6b1880a53 --- /dev/null +++ b/web/charts/common/test/chart/common/gesture_listener_test.dart @@ -0,0 +1,249 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point; +import 'package:charts_common/src/common/gesture_listener.dart'; +import 'package:charts_common/src/common/proxy_gesture_listener.dart'; +import 'package:test/test.dart'; + +void main() { + ProxyGestureListener _proxy; + Point _point; + setUp(() { + _proxy = new ProxyGestureListener(); + _point = new Point(10.0, 12.0); + }); + + group('Tap gesture', () { + test('notified for simple case', () { + // Setup + final tapListener = new MockListener(consumeEvent: true); + _proxy.add(new GestureListener(onTap: tapListener.callback)); + + // Act + _proxy.onTapTest(_point); + _proxy.onTap(_point); + + // Verify + tapListener.verify(arg1: _point); + }); + + test('notifies new listener for second event', () { + // Setup + final tapListener1 = new MockListener(); + _proxy.add(new GestureListener( + onTap: tapListener1.callback, + )); + + // Act + _proxy.onTapTest(_point); + _proxy.onTap(_point); + + // Verify + tapListener1.verify(arg1: _point); + + // Setup Another + final tapListener2 = new MockListener(); + _proxy.add(new GestureListener( + onTap: tapListener2.callback, + )); + + // Act + _proxy.onTapTest(_point); + _proxy.onTap(_point); + + // Verify + tapListener1.verify(callCount: 2, arg1: _point); + tapListener2.verify(arg1: _point); + }); + + test('notifies claiming listener registered first', () { + // Setup + final claimingTapDownListener = new MockListener(consumeEvent: true); + final claimingTapListener = new MockListener(consumeEvent: true); + + _proxy.add(new GestureListener( + onTapTest: claimingTapDownListener.callback, + onTap: claimingTapListener.callback, + )); + + final nonclaimingTapDownListener = new MockListener(consumeEvent: false); + final nonclaimingTapListener = new MockListener(consumeEvent: false); + + _proxy.add(new GestureListener( + onTapTest: nonclaimingTapDownListener.callback, + onTap: nonclaimingTapListener.callback, + )); + + // Act + _proxy.onTapTest(_point); + _proxy.onTap(_point); + + // Verify + claimingTapDownListener.verify(arg1: _point); + claimingTapListener.verify(arg1: _point); + nonclaimingTapDownListener.verify(arg1: _point); + nonclaimingTapListener.verify(callCount: 0); + }); + + test('notifies claiming listener registered second', () { + // Setup + final nonclaimingTapDownListener = new MockListener(consumeEvent: false); + final nonclaimingTapListener = new MockListener(consumeEvent: false); + + _proxy.add(new GestureListener( + onTapTest: nonclaimingTapDownListener.callback, + onTap: nonclaimingTapListener.callback, + )); + + final claimingTapDownListener = new MockListener(consumeEvent: true); + final claimingTapListener = new MockListener(consumeEvent: true); + + _proxy.add(new GestureListener( + onTapTest: claimingTapDownListener.callback, + onTap: claimingTapListener.callback, + )); + + // Act + _proxy.onTapTest(_point); + _proxy.onTap(_point); + + // Verify + nonclaimingTapDownListener.verify(arg1: _point); + nonclaimingTapListener.verify(callCount: 0); + claimingTapDownListener.verify(arg1: _point); + claimingTapListener.verify(arg1: _point); + }); + }); + + group('LongPress gesture', () { + test('notifies with tap', () { + // Setup + final tapDown = new MockListener(consumeEvent: true); + final tap = new MockListener(consumeEvent: true); + final tapCancel = new MockListener(consumeEvent: true); + + _proxy.add(new GestureListener( + onTapTest: tapDown.callback, + onTap: tap.callback, + onTapCancel: tapCancel.callback, + )); + + final pressTapDown = new MockListener(consumeEvent: true); + final longPress = new MockListener(consumeEvent: true); + final pressCancel = new MockListener(consumeEvent: true); + + _proxy.add(new GestureListener( + onTapTest: pressTapDown.callback, + onLongPress: longPress.callback, + onTapCancel: pressCancel.callback, + )); + + // Act + _proxy.onTapTest(_point); + _proxy.onLongPress(_point); + _proxy.onTap(_point); + + // Verify + tapDown.verify(arg1: _point); + tap.verify(callCount: 0); + tapCancel.verify(callCount: 1); + + pressTapDown.verify(arg1: _point); + longPress.verify(arg1: _point); + pressCancel.verify(callCount: 0); + }); + }); + + group('Drag gesture', () { + test('wins over tap', () { + // Setup + final tapDown = new MockListener(consumeEvent: true); + final tap = new MockListener(consumeEvent: true); + final tapCancel = new MockListener(consumeEvent: true); + + _proxy.add(new GestureListener( + onTapTest: tapDown.callback, + onTap: tap.callback, + onTapCancel: tapCancel.callback, + )); + + final dragTapDown = new MockListener(consumeEvent: true); + final dragStart = new MockListener(consumeEvent: true); + final dragUpdate = new MockListener(consumeEvent: true); + final dragEnd = new MockListener(consumeEvent: true); + final dragCancel = new MockListener(consumeEvent: true); + + _proxy.add(new GestureListener( + onTapTest: dragTapDown.callback, + onDragStart: dragStart.callback, + onDragUpdate: dragUpdate.callback, + onDragEnd: dragEnd.callback, + onTapCancel: dragCancel.callback, + )); + + // Act + _proxy.onTapTest(_point); + _proxy.onDragStart(_point); + _proxy.onDragUpdate(_point, 1.0); + _proxy.onDragUpdate(_point, 1.0); + _proxy.onDragEnd(_point, 2.0, 3.0); + _proxy.onTap(_point); + + // Verify + tapDown.verify(arg1: _point); + tap.verify(callCount: 0); + tapCancel.verify(callCount: 1); + + dragTapDown.verify(arg1: _point); + dragStart.verify(arg1: _point); + dragUpdate.verify(callCount: 2, arg1: _point, arg2: 1.0); + dragEnd.verify(arg1: _point, arg2: 2.0, arg3: 3.0); + dragCancel.verify(callCount: 0); + }); + }); +} + +class MockListener { + Object _arg1; + Object _arg2; + Object _arg3; + int _callCount = 0; + + final bool consumeEvent; + + MockListener({this.consumeEvent = false}); + + bool callback([Object arg1, Object arg2, Object arg3]) { + _arg1 = arg1; + _arg2 = arg2; + _arg3 = arg3; + + _callCount++; + + return consumeEvent; + } + + verify({int callCount = 1, Object arg1, Object arg2, Object arg3}) { + if (callCount != any) { + expect(_callCount, equals(callCount)); + } + expect(_arg1, equals(arg1)); + expect(_arg2, equals(arg2)); + expect(_arg3, equals(arg3)); + } +} + +const any = -1; diff --git a/web/charts/common/test/chart/common/selection_model/selection_model_test.dart b/web/charts/common/test/chart/common/selection_model/selection_model_test.dart new file mode 100644 index 000000000..641ffa6a4 --- /dev/null +++ b/web/charts/common/test/chart/common/selection_model/selection_model_test.dart @@ -0,0 +1,331 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/common/selection_model/selection_model.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/common/series_datum.dart'; +import 'package:charts_common/src/data/series.dart'; +import 'package:test/test.dart'; + +void main() { + MutableSelectionModel _selectionModel; + + ImmutableSeries _closestSeries; + MyDatum _closestDatumClosestSeries; + SeriesDatum _closestDatumClosestSeriesPair; + MyDatum _otherDatumClosestSeries; + SeriesDatum _otherDatumClosestSeriesPair; + + ImmutableSeries _otherSeries; + MyDatum _closestDatumOtherSeries; + SeriesDatum _closestDatumOtherSeriesPair; + MyDatum _otherDatumOtherSeries; + SeriesDatum _otherDatumOtherSeriesPair; + + setUp(() { + _selectionModel = new MutableSelectionModel(); + + _closestDatumClosestSeries = new MyDatum('cDcS'); + _otherDatumClosestSeries = new MyDatum('oDcS'); + _closestSeries = new MutableSeries(new Series( + id: 'closest', + data: [_closestDatumClosestSeries, _otherDatumClosestSeries], + domainFn: (dynamic d, _) => d.id, + measureFn: (_, __) => 0)); + _closestDatumClosestSeriesPair = + new SeriesDatum(_closestSeries, _closestDatumClosestSeries); + _otherDatumClosestSeriesPair = + new SeriesDatum(_closestSeries, _otherDatumClosestSeries); + + _closestDatumOtherSeries = new MyDatum('cDoS'); + _otherDatumOtherSeries = new MyDatum('oDoS'); + _otherSeries = new MutableSeries(new Series( + id: 'other', + data: [_closestDatumOtherSeries, _otherDatumOtherSeries], + domainFn: (dynamic d, _) => d.id, + measureFn: (_, __) => 0)); + _closestDatumOtherSeriesPair = + new SeriesDatum(_otherSeries, _closestDatumOtherSeries); + _otherDatumOtherSeriesPair = + new SeriesDatum(_otherSeries, _otherDatumOtherSeries); + }); + + group('SelectionModel persists values', () { + test('selection model is empty by default', () { + expect(_selectionModel.hasDatumSelection, isFalse); + expect(_selectionModel.hasSeriesSelection, isFalse); + }); + + test('all datum are selected but only the first Series is', () { + // Select the 'closest' datum for each Series. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + new SeriesDatum(_otherSeries, _closestDatumOtherSeries), + ], [ + _closestSeries + ]); + + expect(_selectionModel.hasDatumSelection, isTrue); + expect(_selectionModel.selectedDatum, hasLength(2)); + expect(_selectionModel.selectedDatum, + contains(_closestDatumClosestSeriesPair)); + expect(_selectionModel.selectedDatum, + contains(_closestDatumOtherSeriesPair)); + expect( + _selectionModel.selectedDatum.contains(_otherDatumClosestSeriesPair), + isFalse); + expect(_selectionModel.selectedDatum.contains(_otherDatumOtherSeriesPair), + isFalse); + + expect(_selectionModel.hasSeriesSelection, isTrue); + expect(_selectionModel.selectedSeries, hasLength(1)); + expect(_selectionModel.selectedSeries, contains(_closestSeries)); + expect(_selectionModel.selectedSeries.contains(_otherSeries), isFalse); + }); + + test('selection can change', () { + // Select the 'closest' datum for each Series. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + new SeriesDatum(_otherSeries, _closestDatumOtherSeries), + ], [ + _closestSeries + ]); + + // Change selection to just the other datum on the other series. + _selectionModel.updateSelection([ + new SeriesDatum(_otherSeries, _otherDatumOtherSeries), + ], [ + _otherSeries + ]); + + expect(_selectionModel.selectedDatum, hasLength(1)); + expect( + _selectionModel.selectedDatum, contains(_otherDatumOtherSeriesPair)); + + expect(_selectionModel.selectedSeries, hasLength(1)); + expect(_selectionModel.selectedSeries, contains(_otherSeries)); + }); + + test('selection can be series only', () { + // Select the 'closest' Series without datum to simulate legend hovering. + _selectionModel.updateSelection([], [_closestSeries]); + + expect(_selectionModel.hasDatumSelection, isFalse); + expect(_selectionModel.selectedDatum, hasLength(0)); + + expect(_selectionModel.hasSeriesSelection, isTrue); + expect(_selectionModel.selectedSeries, hasLength(1)); + expect(_selectionModel.selectedSeries, contains(_closestSeries)); + }); + + test('selection lock prevents change', () { + // Prevent selection changes. + _selectionModel.locked = true; + + // Try to the 'closest' datum for each Series. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + new SeriesDatum(_otherSeries, _closestDatumOtherSeries), + ], [ + _closestSeries + ]); + + expect(_selectionModel.hasDatumSelection, isFalse); + expect(_selectionModel.hasSeriesSelection, isFalse); + + // Allow selection changes. + _selectionModel.locked = false; + + // Try to the 'closest' datum for each Series. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + new SeriesDatum(_otherSeries, _closestDatumOtherSeries), + ], [ + _closestSeries + ]); + + expect(_selectionModel.hasDatumSelection, isTrue); + expect(_selectionModel.hasSeriesSelection, isTrue); + + // Prevent selection changes. + _selectionModel.locked = true; + + // Attempt to change selection + _selectionModel.updateSelection([ + new SeriesDatum(_otherSeries, _otherDatumOtherSeries), + ], [ + _otherSeries + ]); + + // Previous selection should still be set. + expect(_selectionModel.selectedDatum, hasLength(2)); + expect(_selectionModel.selectedDatum, + contains(_closestDatumClosestSeriesPair)); + expect(_selectionModel.selectedDatum, + contains(_closestDatumOtherSeriesPair)); + + expect(_selectionModel.selectedSeries, hasLength(1)); + expect(_selectionModel.selectedSeries, contains(_closestSeries)); + }); + }); + + group('SelectionModel changed listeners', () { + test('listener triggered for change', () { + SelectionModel triggeredModel; + // Listen + _selectionModel + .addSelectionChangedListener((SelectionModel model) { + triggeredModel = model; + }); + + // Set the selection to closest datum. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + ], [ + _closestSeries + ]); + + // Callback should have been triggered. + expect(triggeredModel, equals(_selectionModel)); + }); + + test('listener not triggered for no change', () { + SelectionModel triggeredModel; + // Set the selection to closest datum. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + ], [ + _closestSeries + ]); + + // Listen + _selectionModel + .addSelectionChangedListener((SelectionModel model) { + triggeredModel = model; + }); + + // Try to update the model with the same value. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + ], [ + _closestSeries + ]); + + // Callback should not have been triggered. + expect(triggeredModel, isNull); + }); + + test('removed listener not triggered for change', () { + SelectionModel triggeredModel; + + Function cb = (SelectionModel model) { + triggeredModel = model; + }; + + // Listen + _selectionModel.addSelectionChangedListener(cb); + + // Unlisten + _selectionModel.removeSelectionChangedListener(cb); + + // Set the selection to closest datum. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + ], [ + _closestSeries + ]); + + // Callback should not have been triggered. + expect(triggeredModel, isNull); + }); + }); + + group('SelectionModel updated listeners', () { + test('listener triggered for change', () { + SelectionModel triggeredModel; + // Listen + _selectionModel + .addSelectionUpdatedListener((SelectionModel model) { + triggeredModel = model; + }); + + // Set the selection to closest datum. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + ], [ + _closestSeries + ]); + + // Callback should have been triggered. + expect(triggeredModel, equals(_selectionModel)); + }); + + test('listener triggered for no change', () { + SelectionModel triggeredModel; + // Set the selection to closest datum. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + ], [ + _closestSeries + ]); + + // Listen + _selectionModel + .addSelectionUpdatedListener((SelectionModel model) { + triggeredModel = model; + }); + + // Try to update the model with the same value. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + ], [ + _closestSeries + ]); + + // Callback should have been triggered. + expect(triggeredModel, equals(_selectionModel)); + }); + + test('removed listener not triggered for change', () { + SelectionModel triggeredModel; + + Function cb = (SelectionModel model) { + triggeredModel = model; + }; + + // Listen + _selectionModel.addSelectionUpdatedListener(cb); + + // Unlisten + _selectionModel.removeSelectionUpdatedListener(cb); + + // Set the selection to closest datum. + _selectionModel.updateSelection([ + new SeriesDatum(_closestSeries, _closestDatumClosestSeries), + ], [ + _closestSeries + ]); + + // Callback should not have been triggered. + expect(triggeredModel, isNull); + }); + }); +} + +class MyDatum { + final String id; + MyDatum(this.id); +} diff --git a/web/charts/common/test/chart/layout/layout_manager_impl_test.dart b/web/charts/common/test/chart/layout/layout_manager_impl_test.dart new file mode 100644 index 000000000..88f0a8a99 --- /dev/null +++ b/web/charts/common/test/chart/layout/layout_manager_impl_test.dart @@ -0,0 +1,48 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/layout/layout_config.dart'; +import 'package:charts_common/src/chart/layout/layout_manager_impl.dart'; + +import 'package:test/test.dart'; + +void main() { + test('default layout', () { + var layout = LayoutManagerImpl(); + layout.measure(400, 300); + + expect(layout.marginTop, equals(0)); + expect(layout.marginRight, equals(0)); + expect(layout.marginBottom, equals(0)); + expect(layout.marginLeft, equals(0)); + }); + + test('all fixed margin', () { + var layout = LayoutManagerImpl( + config: LayoutConfig( + topSpec: MarginSpec.fixedPixel(12), + rightSpec: MarginSpec.fixedPixel(11), + bottomSpec: MarginSpec.fixedPixel(10), + leftSpec: MarginSpec.fixedPixel(9), + ), + ); + layout.measure(400, 300); + + expect(layout.marginTop, equals(12)); + expect(layout.marginRight, equals(11)); + expect(layout.marginBottom, equals(10)); + expect(layout.marginLeft, equals(9)); + }); +} diff --git a/web/charts/common/test/chart/line/line_renderer_test.dart b/web/charts/common/test/chart/line/line_renderer_test.dart new file mode 100644 index 000000000..e76613ae9 --- /dev/null +++ b/web/charts/common/test/chart/line/line_renderer_test.dart @@ -0,0 +1,644 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/line/line_renderer.dart'; +import 'package:charts_common/src/chart/line/line_renderer_config.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart' + show MutableSeries, ImmutableSeries; +import 'package:charts_common/src/common/color.dart'; +import 'package:charts_common/src/common/material_palette.dart' + show MaterialPalette; +import 'package:charts_common/src/data/series.dart' show Series; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +/// Datum/Row for the chart. +class MyRow { + final String campaignString; + final int campaign; + final int clickCount; + final Color color; + final List dashPattern; + final double strokeWidthPx; + MyRow(this.campaignString, this.campaign, this.clickCount, this.color, + this.dashPattern, this.strokeWidthPx); +} + +class MockImmutableSeries extends Mock implements ImmutableSeries { + String _id; + MockImmutableSeries(this._id); + + @override + String get id => _id; +} + +void main() { + LineRenderer renderer; + List> numericSeriesList; + List> ordinalSeriesList; + + List myFakeDesktopData; + List myFakeTabletData; + List myFakeMobileData; + + setUp(() { + myFakeDesktopData = [ + new MyRow( + 'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, null, 2.0), + new MyRow( + 'MyCampaign2', 2, 25, MaterialPalette.green.shadeDefault, null, 2.0), + new MyRow( + 'MyCampaign3', 3, 100, MaterialPalette.red.shadeDefault, null, 2.0), + new MyRow('MyOtherCampaign', 4, 75, MaterialPalette.red.shadeDefault, + null, 2.0), + ]; + + myFakeTabletData = [ + new MyRow( + 'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, [2, 2], 2.0), + new MyRow( + 'MyCampaign2', 2, 25, MaterialPalette.blue.shadeDefault, [3, 3], 2.0), + new MyRow('MyCampaign3', 3, 100, MaterialPalette.blue.shadeDefault, + [4, 4], 2.0), + new MyRow('MyOtherCampaign', 4, 75, MaterialPalette.blue.shadeDefault, + [4, 4], 2.0), + ]; + + myFakeMobileData = [ + new MyRow( + 'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, null, 2.0), + new MyRow( + 'MyCampaign2', 2, 25, MaterialPalette.blue.shadeDefault, null, 3.0), + new MyRow( + 'MyCampaign3', 3, 100, MaterialPalette.blue.shadeDefault, null, 4.0), + new MyRow('MyOtherCampaign', 4, 75, MaterialPalette.blue.shadeDefault, + null, 4.0), + ]; + + numericSeriesList = [ + new MutableSeries(new Series( + id: 'Desktop', + colorFn: (_, __) => MaterialPalette.blue.shadeDefault, + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + data: myFakeDesktopData)), + new MutableSeries(new Series( + id: 'Tablet', + colorFn: (_, __) => MaterialPalette.red.shadeDefault, + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + strokeWidthPxFn: (_, __) => 1.25, + data: myFakeTabletData)), + new MutableSeries(new Series( + id: 'Mobile', + colorFn: (_, __) => MaterialPalette.green.shadeDefault, + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + strokeWidthPxFn: (_, __) => 3.0, + data: myFakeMobileData)) + ]; + + ordinalSeriesList = [ + new MutableSeries(new Series( + id: 'Desktop', + colorFn: (_, __) => MaterialPalette.blue.shadeDefault, + domainFn: (dynamic row, _) => row.campaignString, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + data: myFakeDesktopData)), + new MutableSeries(new Series( + id: 'Tablet', + colorFn: (_, __) => MaterialPalette.red.shadeDefault, + domainFn: (dynamic row, _) => row.campaignString, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + strokeWidthPxFn: (_, __) => 1.25, + data: myFakeTabletData)), + new MutableSeries(new Series( + id: 'Mobile', + colorFn: (_, __) => MaterialPalette.green.shadeDefault, + domainFn: (dynamic row, _) => row.campaignString, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + strokeWidthPxFn: (_, __) => 3.0, + data: myFakeMobileData)) + ]; + }); + + group('preprocess', () { + test('with numeric data and simple lines', () { + renderer = new LineRenderer( + config: new LineRendererConfig(strokeWidthPx: 2.0)); + + renderer.configureSeries(numericSeriesList); + renderer.preprocessSeries(numericSeriesList); + + expect(numericSeriesList.length, equals(3)); + + // Validate Desktop series. + var series = numericSeriesList[0]; + + var styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(1)); + + var segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(4)); + expect(segment.strokeWidthPx, equals(2.0)); + + expect(series.measureOffsetFn(0), 0); + expect(series.measureOffsetFn(1), 0); + expect(series.measureOffsetFn(2), 0); + expect(series.measureOffsetFn(3), 0); + + // Validate Tablet series. + series = numericSeriesList[1]; + + styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(1)); + + segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.red.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(4)); + expect(segment.strokeWidthPx, equals(1.25)); + + expect(series.measureOffsetFn(0), 0); + expect(series.measureOffsetFn(1), 0); + expect(series.measureOffsetFn(2), 0); + expect(series.measureOffsetFn(3), 0); + + // Validate Mobile series. + series = numericSeriesList[2]; + + styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(1)); + + segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.green.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(4)); + expect(segment.strokeWidthPx, equals(3.0)); + + expect(series.measureOffsetFn(0), 0); + expect(series.measureOffsetFn(1), 0); + expect(series.measureOffsetFn(2), 0); + expect(series.measureOffsetFn(3), 0); + }); + + test('with numeric data and stacked lines', () { + renderer = new LineRenderer( + config: new LineRendererConfig(stacked: true, strokeWidthPx: 2.0)); + + renderer.configureSeries(numericSeriesList); + renderer.preprocessSeries(numericSeriesList); + + expect(numericSeriesList.length, equals(3)); + + // Validate Desktop series. + var series = numericSeriesList[0]; + + var styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(1)); + + var segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(4)); + expect(segment.strokeWidthPx, equals(2.0)); + + expect(series.measureOffsetFn(0), 0); + expect(series.measureOffsetFn(1), 0); + expect(series.measureOffsetFn(2), 0); + expect(series.measureOffsetFn(3), 0); + + // Validate Tablet series. + series = numericSeriesList[1]; + + styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(1)); + + segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.red.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(4)); + expect(segment.strokeWidthPx, equals(1.25)); + + expect(series.measureOffsetFn(0), 5); + expect(series.measureOffsetFn(1), 25); + expect(series.measureOffsetFn(2), 100); + expect(series.measureOffsetFn(3), 75); + + // Validate Mobile series. + series = numericSeriesList[2]; + + styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(1)); + + segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.green.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(4)); + expect(segment.strokeWidthPx, equals(3.0)); + + expect(series.measureOffsetFn(0), 10); + expect(series.measureOffsetFn(1), 50); + expect(series.measureOffsetFn(2), 200); + expect(series.measureOffsetFn(3), 150); + }); + + test('with numeric data and changes in style', () { + numericSeriesList = [ + new MutableSeries(new Series( + id: 'Desktop', + colorFn: (MyRow row, _) => row.color, + dashPatternFn: (MyRow row, _) => row.dashPattern, + strokeWidthPxFn: (MyRow row, _) => row.strokeWidthPx, + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + data: myFakeDesktopData)), + new MutableSeries(new Series( + id: 'Tablet', + colorFn: (MyRow row, _) => row.color, + dashPatternFn: (MyRow row, _) => row.dashPattern, + strokeWidthPxFn: (MyRow row, _) => row.strokeWidthPx, + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + data: myFakeTabletData)), + new MutableSeries(new Series( + id: 'Mobile', + colorFn: (MyRow row, _) => row.color, + dashPatternFn: (MyRow row, _) => row.dashPattern, + strokeWidthPxFn: (MyRow row, _) => row.strokeWidthPx, + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + data: myFakeMobileData)) + ]; + + renderer = new LineRenderer( + config: new LineRendererConfig(strokeWidthPx: 2.0)); + + renderer.configureSeries(numericSeriesList); + renderer.preprocessSeries(numericSeriesList); + + expect(numericSeriesList.length, equals(3)); + + // Validate Desktop series. + var series = numericSeriesList[0]; + + var styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(3)); + + var segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(2)); + expect(segment.strokeWidthPx, equals(2.0)); + + segment = styleSegments[1]; + expect(segment.color, equals(MaterialPalette.green.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(2)); + expect(segment.domainExtent.end, equals(3)); + expect(segment.strokeWidthPx, equals(2.0)); + + segment = styleSegments[2]; + expect(segment.color, equals(MaterialPalette.red.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(3)); + expect(segment.domainExtent.end, equals(4)); + expect(segment.strokeWidthPx, equals(2.0)); + + expect(series.measureOffsetFn(0), 0); + expect(series.measureOffsetFn(1), 0); + expect(series.measureOffsetFn(2), 0); + expect(series.measureOffsetFn(3), 0); + + // Validate Tablet series. + series = numericSeriesList[1]; + + styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(3)); + + segment = segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, equals([2, 2])); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(2)); + expect(segment.strokeWidthPx, equals(2.0)); + + segment = styleSegments[1]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, equals([3, 3])); + expect(segment.domainExtent.start, equals(2)); + expect(segment.domainExtent.end, equals(3)); + expect(segment.strokeWidthPx, equals(2.0)); + + segment = styleSegments[2]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, equals([4, 4])); + expect(segment.domainExtent.start, equals(3)); + expect(segment.domainExtent.end, equals(4)); + expect(segment.strokeWidthPx, equals(2.0)); + + expect(series.measureOffsetFn(0), 0); + expect(series.measureOffsetFn(1), 0); + expect(series.measureOffsetFn(2), 0); + expect(series.measureOffsetFn(3), 0); + + // Validate Mobile series. + series = numericSeriesList[2]; + + styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(3)); + + segment = segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(2)); + expect(segment.strokeWidthPx, equals(2.0)); + + segment = styleSegments[1]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(2)); + expect(segment.domainExtent.end, equals(3)); + expect(segment.strokeWidthPx, equals(3.0)); + + segment = styleSegments[2]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals(3)); + expect(segment.domainExtent.end, equals(4)); + expect(segment.strokeWidthPx, equals(4.0)); + + expect(series.measureOffsetFn(0), 0); + expect(series.measureOffsetFn(1), 0); + expect(series.measureOffsetFn(2), 0); + expect(series.measureOffsetFn(3), 0); + }); + + test('with numeric data and repeats in style', () { + var myFakeData = [ + new MyRow( + 'MyCampaign1', 1, 5, MaterialPalette.blue.shadeDefault, null, 2.0), + new MyRow('MyCampaign2', 2, 25, MaterialPalette.green.shadeDefault, + null, 2.0), + new MyRow('MyCampaign3', 3, 100, MaterialPalette.blue.shadeDefault, + null, 2.0), + new MyRow('MyCampaign4', 4, 75, MaterialPalette.green.shadeDefault, + null, 2.0), + new MyRow( + 'MyCampaign1', 5, 5, MaterialPalette.blue.shadeDefault, null, 2.0), + new MyRow('MyCampaign2', 6, 25, MaterialPalette.green.shadeDefault, + null, 2.0), + new MyRow('MyCampaign3', 7, 100, MaterialPalette.blue.shadeDefault, + null, 2.0), + new MyRow('MyCampaign4', 8, 75, MaterialPalette.green.shadeDefault, + null, 2.0), + ]; + + numericSeriesList = [ + new MutableSeries(new Series( + id: 'Desktop', + colorFn: (MyRow row, _) => row.color, + dashPatternFn: (MyRow row, _) => row.dashPattern, + strokeWidthPxFn: (MyRow row, _) => row.strokeWidthPx, + domainFn: (dynamic row, _) => row.campaign, + measureFn: (dynamic row, _) => row.clickCount, + measureOffsetFn: (_, __) => 0, + data: myFakeData)), + ]; + + renderer = new LineRenderer( + config: new LineRendererConfig(strokeWidthPx: 2.0)); + + renderer.configureSeries(numericSeriesList); + renderer.preprocessSeries(numericSeriesList); + + expect(numericSeriesList.length, equals(1)); + + // Validate Desktop series. + var series = numericSeriesList[0]; + + var styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(8)); + + var segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.domainExtent.start, equals(1)); + expect(segment.domainExtent.end, equals(2)); + + segment = styleSegments[1]; + expect(segment.color, equals(MaterialPalette.green.shadeDefault)); + expect(segment.domainExtent.start, equals(2)); + expect(segment.domainExtent.end, equals(3)); + + segment = styleSegments[2]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.domainExtent.start, equals(3)); + expect(segment.domainExtent.end, equals(4)); + + segment = styleSegments[3]; + expect(segment.color, equals(MaterialPalette.green.shadeDefault)); + expect(segment.domainExtent.start, equals(4)); + expect(segment.domainExtent.end, equals(5)); + + segment = styleSegments[4]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.domainExtent.start, equals(5)); + expect(segment.domainExtent.end, equals(6)); + + segment = styleSegments[5]; + expect(segment.color, equals(MaterialPalette.green.shadeDefault)); + expect(segment.domainExtent.start, equals(6)); + expect(segment.domainExtent.end, equals(7)); + + segment = styleSegments[6]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.domainExtent.start, equals(7)); + expect(segment.domainExtent.end, equals(8)); + + segment = styleSegments[7]; + expect(segment.color, equals(MaterialPalette.green.shadeDefault)); + expect(segment.domainExtent.start, equals(8)); + expect(segment.domainExtent.end, equals(8)); + }); + + test('with ordinal data and simple lines', () { + renderer = new LineRenderer( + config: new LineRendererConfig(strokeWidthPx: 2.0)); + + renderer.configureSeries(ordinalSeriesList); + renderer.preprocessSeries(ordinalSeriesList); + + expect(ordinalSeriesList.length, equals(3)); + + // Validate Desktop series. + var series = ordinalSeriesList[0]; + + var styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(1)); + + var segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.blue.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals('MyCampaign1')); + expect(segment.domainExtent.end, equals('MyOtherCampaign')); + expect(segment.strokeWidthPx, equals(2.0)); + + // Validate Tablet series. + series = ordinalSeriesList[1]; + + styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(1)); + + segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.red.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals('MyCampaign1')); + expect(segment.domainExtent.end, equals('MyOtherCampaign')); + expect(segment.strokeWidthPx, equals(1.25)); + + // Validate Mobile series. + series = ordinalSeriesList[2]; + + styleSegments = series.getAttr(styleSegmentsKey); + expect(styleSegments.length, equals(1)); + + segment = styleSegments[0]; + expect(segment.color, equals(MaterialPalette.green.shadeDefault)); + expect(segment.dashPattern, isNull); + expect(segment.domainExtent.start, equals('MyCampaign1')); + expect(segment.domainExtent.end, equals('MyOtherCampaign')); + expect(segment.strokeWidthPx, equals(3.0)); + }); + }); + + group('Line merging', () { + List> series(List keys) { + return keys.map((key) => MockImmutableSeries(key)).toList(); + } + + test('simple beginning removal', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c']); + tester.merge(series(['b', 'c'])); + + // The series should still be there so that it can be animated out. + expect(tester.seriesKeys, equals(['a', 'b', 'c'])); + }); + + test('simple middle removal', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c']); + tester.merge(series(['a', 'c'])); + + // The series should still be there so that it can be animated out. + expect(tester.seriesKeys, equals(['a', 'b', 'c'])); + }); + + test('simple end removal', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c']); + tester.merge(series(['a', 'b'])); + + // The series should still be there so that it can be animated out. + expect(tester.seriesKeys, equals(['a', 'b', 'c'])); + }); + + test('simple beginning addition', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c']); + tester.merge(series(['d', 'a', 'b', 'c'])); + + expect(tester.seriesKeys, equals(['d', 'a', 'b', 'c'])); + }); + + test('simple middle addition', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c']); + tester.merge(series(['a', 'd', 'b', 'c'])); + + expect(tester.seriesKeys, equals(['a', 'd', 'b', 'c'])); + }); + + test('simple end addition', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c']); + tester.merge(series(['a', 'b', 'c', 'd'])); + + expect(tester.seriesKeys, equals(['a', 'b', 'c', 'd'])); + }); + + test('replacement begining', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c']); + tester.merge(series(['d', 'b', 'c'])); + + expect(tester.seriesKeys, equals(['a', 'd', 'b', 'c'])); + }); + + test('replacement end', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c']); + tester.merge(series(['a', 'b', 'd'])); + + expect(tester.seriesKeys, equals(['a', 'b', 'c', 'd'])); + }); + + test('full replacement', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c']); + tester.merge(series(['d', 'e', 'f'])); + + expect(tester.seriesKeys, equals(['a', 'b', 'c', 'd', 'e', 'f'])); + }); + + test('mixed replacement', () { + final tester = LineRendererTester(LineRenderer()); + + tester.setSeriesKeys(['a', 'b', 'c', 'd']); + tester.merge(series(['d', 'a', 'f', 'c'])); + + expect(tester.seriesKeys, equals(['d', 'a', 'b', 'f', 'c'])); + }); + }); +} diff --git a/web/charts/common/test/chart/line/renderer_nearest_detail_test.dart b/web/charts/common/test/chart/line/renderer_nearest_detail_test.dart new file mode 100644 index 000000000..28f29765f --- /dev/null +++ b/web/charts/common/test/chart/line/renderer_nearest_detail_test.dart @@ -0,0 +1,354 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:charts_common/src/chart/cartesian/axis/axis.dart'; +import 'package:charts_common/src/chart/cartesian/cartesian_chart.dart'; +import 'package:charts_common/src/chart/common/chart_canvas.dart'; +import 'package:charts_common/src/chart/common/processed_series.dart'; +import 'package:charts_common/src/chart/line/line_renderer.dart'; +import 'package:charts_common/src/chart/line/line_renderer_config.dart'; +import 'package:charts_common/src/common/color.dart'; +import 'package:charts_common/src/data/series.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +/// Datum/Row for the chart. +class MyRow { + final int timestamp; + int clickCount; + MyRow(this.timestamp, this.clickCount); +} + +// TODO: Test in RTL context as well. + +class MockChart extends Mock implements CartesianChart {} + +class MockDomainAxis extends Mock implements Axis {} + +class MockMeasureAxis extends Mock implements Axis {} + +class MockCanvas extends Mock implements ChartCanvas {} + +void main() { + ///////////////////////////////////////// + // Convenience methods for creating mocks. + ///////////////////////////////////////// + MutableSeries _makeSeries({String id, int measureOffset = 0}) { + final data = [ + new MyRow(1000, measureOffset + 10), + new MyRow(2000, measureOffset + 20), + new MyRow(3000, measureOffset + 30), + ]; + + final series = new MutableSeries(new Series( + id: id, + data: data, + domainFn: (MyRow row, _) => row.timestamp, + measureFn: (MyRow row, _) => row.clickCount, + )); + + series.measureOffsetFn = (_) => 0.0; + series.colorFn = (_) => new Color.fromHex(code: '#000000'); + + // Mock the Domain axis results. + final domainAxis = new MockDomainAxis(); + when(domainAxis.rangeBand).thenReturn(100.0); + when(domainAxis.getLocation(1000)).thenReturn(70.0); + when(domainAxis.getLocation(2000)).thenReturn(70.0 + 100); + when(domainAxis.getLocation(3000)).thenReturn(70.0 + 200.0); + series.setAttr(domainAxisKey, domainAxis); + + // Mock the Measure axis results. + final measureAxis = new MockMeasureAxis(); + for (var i = 0; i <= 100; i++) { + when(measureAxis.getLocation(i.toDouble())) + .thenReturn(20.0 + 100.0 - i.toDouble()); + } + // Special case where measure is above drawArea. + when(measureAxis.getLocation(500)).thenReturn(20.0 + 100.0 - 500); + + series.setAttr(measureAxisKey, measureAxis); + + return series; + } + + LineRenderer renderer; + + bool selectNearestByDomain; + + setUp(() { + selectNearestByDomain = true; + + renderer = new LineRenderer( + config: new LineRendererConfig(strokeWidthPx: 1.0)); + final layoutBounds = new Rectangle(70, 20, 200, 100); + renderer.layout(layoutBounds, layoutBounds); + return renderer; + }); + + ///////////////////////////////////////// + // Additional edge test cases + ///////////////////////////////////////// + group('edge cases', () { + test('hit target with missing data in series still selects others', () { + // Setup + final seriesList = >[ + _makeSeries(id: 'foo')..data.clear(), + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act Point just below barSeries.data[0] + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + + final closest = details[0]; + expect(closest.domain, equals(1000)); + expect(closest.series.id, equals('bar')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(10)); + expect(closest.measureDistance, equals(5)); + }); + + test('all series without data is skipped', () { + // Setup + final seriesList = >[ + _makeSeries(id: 'foo')..data.clear(), + _makeSeries(id: 'bar')..data.clear(), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(0)); + }); + + test('single overlay series is skipped', () { + // Setup + final seriesList = >[ + _makeSeries(id: 'foo')..overlaySeries = true, + _makeSeries(id: 'bar'), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + + final closest = details[0]; + expect(closest.domain, equals(1000)); + expect(closest.series.id, equals('bar')); + expect(closest.datum, equals(seriesList[1].data[0])); + expect(closest.domainDistance, equals(10)); + expect(closest.measureDistance, equals(5)); + }); + + test('all overlay series is skipped', () { + // Setup + final seriesList = >[ + _makeSeries(id: 'foo')..overlaySeries = true, + _makeSeries(id: 'bar')..overlaySeries = true, + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(0)); + }); + }); + + ///////////////////////////////////////// + // Vertical BarRenderer + ///////////////////////////////////////// + group('LineRenderer', () { + test('hit test works', () { + // Setup + final seriesList = >[_makeSeries(id: 'foo')]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + final closest = details[0]; + expect(closest.domain, equals(1000)); + expect(closest.series, equals(seriesList[0])); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(10)); + expect(closest.measureDistance, equals(5)); + }); + + test('hit test expands to multiple series', () { + // Setup bar series is 20 measure higher than foo. + final seriesList = >[ + _makeSeries(id: 'foo'), + _makeSeries(id: 'bar', measureOffset: 20), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 10.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals(1000)); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[0])); + expect(closest.domainDistance, equals(10)); + expect(closest.measureDistance, equals(5)); + + final next = details[1]; + expect(next.domain, equals(1000)); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[0])); + expect(next.domainDistance, equals(10)); + expect(next.measureDistance, equals(25)); // 20offset + 10measure - 5pt + }); + + test('hit test expands with missing data in series', () { + // Setup bar series is 20 measure higher than foo and is missing the + // middle point. + final seriesList = >[ + _makeSeries(id: 'foo'), + _makeSeries(id: 'bar', measureOffset: 20)..data.removeAt(1), + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 100.0 + 10.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(2)); + + final closest = details[0]; + expect(closest.domain, equals(2000)); + expect(closest.series.id, equals('foo')); + expect(closest.datum, equals(seriesList[0].data[1])); + expect(closest.domainDistance, equals(10)); + expect(closest.measureDistance, equals(15)); + + // bar series jumps to last point since it is missing middle. + final next = details[1]; + expect(next.domain, equals(3000)); + expect(next.series.id, equals('bar')); + expect(next.datum, equals(seriesList[1].data[1])); + expect(next.domainDistance, equals(90)); + expect(next.measureDistance, equals(45.0)); + }); + + test('hit test works for points above drawArea', () { + // Setup + final seriesList = >[ + _makeSeries(id: 'foo')..data[1].clickCount = 500 + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + final details = renderer.getNearestDatumDetailPerSeries( + new Point(70.0 + 100.0 + 10.0, 20.0 + 10.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(1)); + final closest = details[0]; + expect(closest.domain, equals(2000)); + expect(closest.series, equals(seriesList[0])); + expect(closest.datum, equals(seriesList[0].data[1])); + expect(closest.domainDistance, equals(10)); + expect(closest.measureDistance, equals(410)); // 500 - 100 + 10 + }); + + test('no selection for points outside of viewport', () { + // Setup + final seriesList = >[ + _makeSeries(id: 'foo')..data.add(new MyRow(-1000, 20)) + ]; + renderer.configureSeries(seriesList); + renderer.preprocessSeries(seriesList); + renderer.update(seriesList, false); + renderer.paint(new MockCanvas(), 1.0); + + // Act + // Note: point is in the axis, over a bar outside of the viewport. + final details = renderer.getNearestDatumDetailPerSeries( + new Point(-0.0, 20.0 + 100.0 - 5.0), + selectNearestByDomain, + null); + + // Verify + expect(details.length, equals(0)); + }); + }); +} diff --git a/web/charts/common/test/chart/pie/arc_label_decorator_test.dart b/web/charts/common/test/chart/pie/arc_label_decorator_test.dart new file mode 100644 index 000000000..5b4821da4 --- /dev/null +++ b/web/charts/common/test/chart/pie/arc_label_decorator_test.dart @@ -0,0 +1,323 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show pi, Point, Rectangle; +import 'package:charts_common/src/chart/common/processed_series.dart' + show ImmutableSeries; +import 'package:charts_common/src/common/color.dart' show Color; +import 'package:charts_common/src/common/graphics_factory.dart' + show GraphicsFactory; +import 'package:charts_common/src/common/line_style.dart' show LineStyle; +import 'package:charts_common/src/common/text_element.dart' + show TextDirection, TextElement, MaxWidthStrategy; +import 'package:charts_common/src/common/text_measurement.dart' + show TextMeasurement; +import 'package:charts_common/src/common/text_style.dart' show TextStyle; +import 'package:charts_common/src/chart/cartesian/axis/spec/axis_spec.dart' + show TextStyleSpec; +import 'package:charts_common/src/chart/common/chart_canvas.dart' + show ChartCanvas; +import 'package:charts_common/src/chart/pie/arc_label_decorator.dart' + show ArcLabelDecorator, ArcLabelPosition; +import 'package:charts_common/src/chart/pie/arc_renderer.dart' + show ArcRendererElement, ArcRendererElementList; +import 'package:charts_common/src/data/series.dart' show AccessorFn; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockCanvas extends Mock implements ChartCanvas {} + +/// A fake [GraphicsFactory] that returns [FakeTextStyle] and [FakeTextElement]. +class FakeGraphicsFactory extends GraphicsFactory { + @override + TextStyle createTextPaint() => new FakeTextStyle(); + + @override + TextElement createTextElement(String text) => new FakeTextElement(text); + + @override + LineStyle createLinePaint() => new MockLinePaint(); +} + +/// Stores [TextStyle] properties for test to verify. +class FakeTextStyle implements TextStyle { + Color color; + int fontSize; + String fontFamily; +} + +/// Fake [TextElement] which returns text length as [horizontalSliceWidth]. +/// +/// Font size is returned for [verticalSliceWidth] and [baseline]. +class FakeTextElement implements TextElement { + final String text; + TextStyle textStyle; + int maxWidth; + MaxWidthStrategy maxWidthStrategy; + TextDirection textDirection; + double opacity; + + FakeTextElement(this.text); + + TextMeasurement get measurement => new TextMeasurement( + horizontalSliceWidth: text.length.toDouble(), + verticalSliceWidth: textStyle.fontSize.toDouble(), + baseline: textStyle.fontSize.toDouble()); +} + +class MockLinePaint extends Mock implements LineStyle {} + +class FakeArcRendererElement extends ArcRendererElement { + final _series = new MockImmutableSeries(); + final AccessorFn labelAccessor; + final List data; + + FakeArcRendererElement(this.labelAccessor, this.data) { + when(_series.labelAccessorFn).thenReturn(labelAccessor); + when(_series.data).thenReturn(data); + } + + ImmutableSeries get series => _series; +} + +class MockImmutableSeries extends Mock implements ImmutableSeries {} + +void main() { + ChartCanvas canvas; + GraphicsFactory graphicsFactory; + Rectangle drawBounds; + + setUpAll(() { + canvas = new MockCanvas(); + graphicsFactory = new FakeGraphicsFactory(); + drawBounds = new Rectangle(0, 0, 200, 200); + }); + + group('pie chart', () { + test('Paint labels with default settings', () { + final data = ['A', 'B']; + final arcElements = new ArcRendererElementList() + ..arcs = [ + // 'A' is small enough to fit inside the arc. + // 'LongLabelB' should not fit inside the arc because it has length + // greater than 10. + new FakeArcRendererElement((_) => 'A', data) + ..startAngle = -pi / 2 + ..endAngle = pi / 2, + new FakeArcRendererElement((_) => 'LongLabelB', data) + ..startAngle = pi / 2 + ..endAngle = 3 * pi / 2, + ] + ..center = new Point(100.0, 100.0) + ..innerRadius = 30.0 + ..radius = 40.0 + ..startAngle = -pi / 2; + + final decorator = new ArcLabelDecorator(); + + decorator.decorate(arcElements, canvas, graphicsFactory, + drawBounds: drawBounds, animationPercent: 1.0); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + // Draw text is called twice (once for each arc) and all 3 parameters were + // captured. Total parameters captured expected to be 6. + expect(captured, hasLength(6)); + // For arc 'A'. + expect(captured[0].maxWidth, equals(10 - decorator.labelPadding)); + expect(captured[0].textDirection, equals(TextDirection.center)); + expect(captured[1], equals(135)); + expect(captured[2], + equals(100 - decorator.insideLabelStyleSpec.fontSize ~/ 2)); + // For arc 'B'. + expect(captured[3].maxWidth, equals(80)); + expect(captured[3].textDirection, equals(TextDirection.rtl)); + expect( + captured[4], + equals(60 - + decorator.leaderLineStyleSpec.length - + decorator.labelPadding * 3)); + expect(captured[5], + equals(100 - decorator.outsideLabelStyleSpec.fontSize ~/ 2)); + }); + + test('LabelPosition.inside always paints inside the arc', () { + final arcElements = new ArcRendererElementList() + ..arcs = [ + // 'LongLabelABC' would not fit inside the arc because it has length + // greater than 10. [ArcLabelPosition.inside] should override this. + new FakeArcRendererElement((_) => 'LongLabelABC', ['A']) + ..startAngle = -pi / 2 + ..endAngle = pi / 2, + ] + ..center = new Point(100.0, 100.0) + ..innerRadius = 30.0 + ..radius = 40.0 + ..startAngle = -pi / 2; + + final decorator = new ArcLabelDecorator( + labelPosition: ArcLabelPosition.inside, + insideLabelStyleSpec: new TextStyleSpec(fontSize: 10)); + + decorator.decorate(arcElements, canvas, graphicsFactory, + drawBounds: drawBounds, animationPercent: 1.0); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + expect(captured, hasLength(3)); + expect(captured[0].maxWidth, equals(10 - decorator.labelPadding)); + expect(captured[0].textDirection, equals(TextDirection.center)); + expect(captured[1], equals(135)); + expect(captured[2], + equals(100 - decorator.insideLabelStyleSpec.fontSize ~/ 2)); + }); + + test('LabelPosition.outside always paints outside the arc', () { + final arcElements = new ArcRendererElementList() + ..arcs = [ + // 'A' will fit inside the arc because it has length less than 10. + // [ArcLabelPosition.outside] should override this. + new FakeArcRendererElement((_) => 'A', ['A']) + ..startAngle = -pi / 2 + ..endAngle = pi / 2, + ] + ..center = new Point(100.0, 100.0) + ..innerRadius = 30.0 + ..radius = 40.0 + ..startAngle = -pi / 2; + + final decorator = new ArcLabelDecorator( + labelPosition: ArcLabelPosition.outside, + outsideLabelStyleSpec: new TextStyleSpec(fontSize: 10)); + + decorator.decorate(arcElements, canvas, graphicsFactory, + drawBounds: drawBounds, animationPercent: 1.0); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + expect(captured, hasLength(3)); + expect(captured[0].maxWidth, equals(40)); + expect(captured[0].textDirection, equals(TextDirection.ltr)); + expect( + captured[1], + equals(140 + + decorator.leaderLineStyleSpec.length + + decorator.labelPadding * 3)); + expect(captured[2], + equals(100 - decorator.outsideLabelStyleSpec.fontSize ~/ 2)); + }); + + test('Inside and outside label styles are applied', () { + final data = ['A', 'B']; + final arcElements = new ArcRendererElementList() + ..arcs = [ + // 'A' is small enough to fit inside the arc. + // 'LongLabelB' should not fit inside the arc because it has length + // greater than 10. + new FakeArcRendererElement((_) => 'A', data) + ..startAngle = -pi / 2 + ..endAngle = pi / 2, + new FakeArcRendererElement((_) => 'LongLabelB', data) + ..startAngle = pi / 2 + ..endAngle = 3 * pi / 2, + ] + ..center = new Point(100.0, 100.0) + ..innerRadius = 30.0 + ..radius = 40.0 + ..startAngle = -pi / 2; + + final insideColor = new Color(r: 0, g: 0, b: 0); + final outsideColor = new Color(r: 255, g: 255, b: 255); + final decorator = new ArcLabelDecorator( + labelPadding: 0, + insideLabelStyleSpec: new TextStyleSpec( + fontSize: 10, fontFamily: 'insideFont', color: insideColor), + outsideLabelStyleSpec: new TextStyleSpec( + fontSize: 8, fontFamily: 'outsideFont', color: outsideColor)); + + decorator.decorate(arcElements, canvas, graphicsFactory, + drawBounds: drawBounds, animationPercent: 1.0); + + final captured = + verify(canvas.drawText(captureAny, captureAny, captureAny)).captured; + // Draw text is called twice (once for each arc) and all 3 parameters were + // captured. Total parameters captured expected to be 6. + expect(captured, hasLength(6)); + // For arc 'A'. + expect(captured[0].maxWidth, equals(10 - decorator.labelPadding)); + expect(captured[0].textDirection, equals(TextDirection.center)); + expect(captured[0].textStyle.fontFamily, equals('insideFont')); + expect(captured[0].textStyle.color, equals(insideColor)); + expect(captured[1], equals(135)); + expect(captured[2], + equals(100 - decorator.insideLabelStyleSpec.fontSize ~/ 2)); + // For arc 'B'. + expect(captured[3].maxWidth, equals(90)); + expect(captured[3].textDirection, equals(TextDirection.rtl)); + expect(captured[3].textStyle.fontFamily, equals('outsideFont')); + expect(captured[3].textStyle.color, equals(outsideColor)); + expect( + captured[4], + equals(50 - + decorator.leaderLineStyleSpec.length - + decorator.labelPadding * 3)); + expect(captured[5], + equals(100 - decorator.outsideLabelStyleSpec.fontSize ~/ 2)); + }); + }); + + group('Null and empty label scenarios', () { + test('Skip label if label accessor does not exist', () { + final arcElements = new ArcRendererElementList() + ..arcs = [ + new FakeArcRendererElement(null, ['A']) + ..startAngle = -pi / 2 + ..endAngle = pi / 2, + ] + ..center = new Point(100.0, 100.0) + ..innerRadius = 30.0 + ..radius = 40.0 + ..startAngle = -pi / 2; + + new ArcLabelDecorator().decorate(arcElements, canvas, graphicsFactory, + drawBounds: drawBounds, animationPercent: 1.0); + + verifyNever(canvas.drawText(any, any, any)); + }); + + test('Skip label if label is null or empty', () { + final data = ['A', 'B']; + final arcElements = new ArcRendererElementList() + ..arcs = [ + new FakeArcRendererElement(null, data) + ..startAngle = -pi / 2 + ..endAngle = pi / 2, + new FakeArcRendererElement((_) => '', data) + ..startAngle = pi / 2 + ..endAngle = 3 * pi / 2, + ] + ..center = new Point(100.0, 100.0) + ..innerRadius = 30.0 + ..radius = 40.0 + ..startAngle = -pi / 2; + + new ArcLabelDecorator().decorate(arcElements, canvas, graphicsFactory, + drawBounds: drawBounds, animationPercent: 1.0); + + verifyNever(canvas.drawText(any, any, any)); + }); + }); +} diff --git a/web/charts/common/test/chart/scatter_plot/comparison_points_decorator_test.dart b/web/charts/common/test/chart/scatter_plot/comparison_points_decorator_test.dart new file mode 100644 index 000000000..858294dbc --- /dev/null +++ b/web/charts/common/test/chart/scatter_plot/comparison_points_decorator_test.dart @@ -0,0 +1,220 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point, Rectangle; +import 'package:charts_common/src/chart/scatter_plot/comparison_points_decorator.dart'; +import 'package:charts_common/src/chart/scatter_plot/point_renderer.dart'; + +import 'package:test/test.dart'; + +/// Datum/Row for the chart. +class MyRow { + final int campaign; + final int clickCount; + MyRow(this.campaign, this.clickCount); +} + +class TestComparisonPointsDecorator extends ComparisonPointsDecorator { + List> testComputeBoundedPointsForElement( + PointRendererElement pointElement, Rectangle drawBounds) { + return computeBoundedPointsForElement(pointElement, drawBounds); + } +} + +void main() { + TestComparisonPointsDecorator decorator; + Rectangle bounds; + + setUp(() { + decorator = new TestComparisonPointsDecorator(); + bounds = new Rectangle(0, 0, 100, 100); + }); + + group('compute bounded points', () { + test('with line inside bounds', () { + final element = new PointRendererElement() + ..point = new DatumPoint( + x: 10.0, + xLower: 5.0, + xUpper: 50.0, + y: 20.0, + yLower: 20.0, + yUpper: 20.0); + + final points = + decorator.testComputeBoundedPointsForElement(element, bounds); + + expect(points.length, equals(2)); + + expect(points[0].x, equals(5.0)); + expect(points[0].y, equals(20.0)); + + expect(points[1].x, equals(50.0)); + expect(points[1].y, equals(20.0)); + }); + + test('with line entirely above bounds', () { + final element = new PointRendererElement() + ..point = new DatumPoint( + x: 10.0, + xLower: 5.0, + xUpper: 50.0, + y: -20.0, + yLower: -20.0, + yUpper: -20.0); + + final points = + decorator.testComputeBoundedPointsForElement(element, bounds); + + expect(points, isNull); + }); + + test('with line entirely below bounds', () { + final element = new PointRendererElement() + ..point = new DatumPoint( + x: 10.0, + xLower: 5.0, + xUpper: 50.0, + y: 120.0, + yLower: 120.0, + yUpper: 120.0); + + final points = + decorator.testComputeBoundedPointsForElement(element, bounds); + + expect(points, isNull); + }); + + test('with line entirely left of bounds', () { + final element = new PointRendererElement() + ..point = new DatumPoint( + x: -10.0, + xLower: -5.0, + xUpper: -50.0, + y: 20.0, + yLower: 20.0, + yUpper: 50.0); + + final points = + decorator.testComputeBoundedPointsForElement(element, bounds); + + expect(points, isNull); + }); + + test('with line entirely right of bounds', () { + final element = new PointRendererElement() + ..point = new DatumPoint( + x: 110.0, + xLower: 105.0, + xUpper: 150.0, + y: 20.0, + yLower: 20.0, + yUpper: 50.0); + + final points = + decorator.testComputeBoundedPointsForElement(element, bounds); + + expect(points, isNull); + }); + + test('with horizontal line extending beyond bounds', () { + final element = new PointRendererElement() + ..point = new DatumPoint( + x: 10.0, + xLower: -10.0, + xUpper: 110.0, + y: 20.0, + yLower: 20.0, + yUpper: 20.0); + + final points = + decorator.testComputeBoundedPointsForElement(element, bounds); + + expect(points.length, equals(2)); + + expect(points[0].x, equals(0.0)); + expect(points[0].y, equals(20.0)); + + expect(points[1].x, equals(100.0)); + expect(points[1].y, equals(20.0)); + }); + + test('with vertical line extending beyond bounds', () { + final element = new PointRendererElement() + ..point = new DatumPoint( + x: 20.0, + xLower: 20.0, + xUpper: 20.0, + y: 10.0, + yLower: -10.0, + yUpper: 110.0); + + final points = + decorator.testComputeBoundedPointsForElement(element, bounds); + + expect(points.length, equals(2)); + + expect(points[0].x, equals(20.0)); + expect(points[0].y, equals(0.0)); + + expect(points[1].x, equals(20.0)); + expect(points[1].y, equals(100.0)); + }); + + test('with diagonal from top left to bottom right', () { + final element = new PointRendererElement() + ..point = new DatumPoint( + x: 50.0, + xLower: -50.0, + xUpper: 150.0, + y: 50.0, + yLower: -50.0, + yUpper: 150.0); + + final points = + decorator.testComputeBoundedPointsForElement(element, bounds); + + expect(points.length, equals(2)); + + expect(points[0].x, equals(0.0)); + expect(points[0].y, equals(0.0)); + + expect(points[1].x, equals(100.0)); + expect(points[1].y, equals(100.0)); + }); + + test('with diagonal from bottom left to top right', () { + final element = new PointRendererElement() + ..point = new DatumPoint( + x: 50.0, + xLower: -50.0, + xUpper: 150.0, + y: 50.0, + yLower: 150.0, + yUpper: -50.0); + + final points = + decorator.testComputeBoundedPointsForElement(element, bounds); + + expect(points.length, equals(2)); + + expect(points[0].x, equals(0.0)); + expect(points[0].y, equals(100.0)); + + expect(points[1].x, equals(100.0)); + expect(points[1].y, equals(0.0)); + }); + }); +} diff --git a/web/charts/common/test/chart/scatter_plot/point_renderer_test.dart b/web/charts/common/test/chart/scatter_plot/point_renderer_test.dart new file mode 100644 index 000000000..8788f086c --- /dev/null +++ b/web/charts/common/test/chart/scatter_plot/point_renderer_test.dart @@ -0,0 +1,192 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/common/processed_series.dart' + show MutableSeries; +import 'package:charts_common/src/chart/scatter_plot/point_renderer.dart'; +import 'package:charts_common/src/chart/scatter_plot/point_renderer_config.dart'; +import 'package:charts_common/src/common/material_palette.dart' + show MaterialPalette; +import 'package:charts_common/src/data/series.dart' show Series; + +import 'package:test/test.dart'; + +/// Datum/Row for the chart. +class MyRow { + final String campaignString; + final int campaign; + final int clickCount; + final double radius; + final double boundsRadius; + final String shape; + MyRow(this.campaignString, this.campaign, this.clickCount, this.radius, + this.boundsRadius, this.shape); +} + +void main() { + PointRenderer renderer; + List> numericSeriesList; + + setUp(() { + var myFakeDesktopData = [ + // This datum should get a default bounds line radius value. + new MyRow('MyCampaign1', 0, 5, 3.0, null, null), + new MyRow('MyCampaign2', 10, 25, 5.0, 4.0, 'shape 1'), + new MyRow('MyCampaign3', 12, 75, 4.0, 4.0, 'shape 2'), + // This datum should always get default radius values. + new MyRow('MyCampaign4', 13, 225, null, null, null), + ]; + + final maxMeasure = 300; + + numericSeriesList = [ + new MutableSeries(new Series( + id: 'Desktop', + colorFn: (MyRow row, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = row.clickCount / maxMeasure; + + if (bucket < 1 / 3) { + return MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return MaterialPalette.red.shadeDefault; + } else { + return MaterialPalette.green.shadeDefault; + } + }, + domainFn: (MyRow row, _) => row.campaign, + measureFn: (MyRow row, _) => row.clickCount, + measureOffsetFn: (MyRow row, _) => 0, + radiusPxFn: (MyRow row, _) => row.radius, + data: myFakeDesktopData) + // Define a bounds line radius function. + ..setAttribute(boundsLineRadiusPxFnKey, + (int index) => myFakeDesktopData[index].boundsRadius)) + ]; + }); + + group('preprocess', () { + test('with numeric data and simple points', () { + renderer = new PointRenderer(config: new PointRendererConfig()); + + renderer.preprocessSeries(numericSeriesList); + + expect(numericSeriesList.length, equals(1)); + + // Validate Desktop series. + var series = numericSeriesList[0]; + + var keyFn = series.keyFn; + + var elementsList = series.getAttr(pointElementsKey); + expect(elementsList.length, equals(4)); + + expect(elementsList[0].radiusPx, equals(3.0)); + expect(elementsList[1].radiusPx, equals(5.0)); + expect(elementsList[2].radiusPx, equals(4.0)); + expect(elementsList[3].radiusPx, equals(3.5)); + + expect(elementsList[0].boundsLineRadiusPx, equals(3.0)); + expect(elementsList[1].boundsLineRadiusPx, equals(4.0)); + expect(elementsList[2].boundsLineRadiusPx, equals(4.0)); + expect(elementsList[3].boundsLineRadiusPx, equals(3.5)); + + expect(elementsList[0].symbolRendererId, equals(defaultSymbolRendererId)); + expect(elementsList[1].symbolRendererId, equals(defaultSymbolRendererId)); + expect(elementsList[2].symbolRendererId, equals(defaultSymbolRendererId)); + expect(elementsList[3].symbolRendererId, equals(defaultSymbolRendererId)); + + expect(keyFn(0), equals('Desktop__0__5')); + expect(keyFn(1), equals('Desktop__10__25')); + expect(keyFn(2), equals('Desktop__12__75')); + expect(keyFn(3), equals('Desktop__13__225')); + }); + + test('with numeric data and missing radiusPxFn', () { + renderer = new PointRenderer( + config: + new PointRendererConfig(radiusPx: 2.0, boundsLineRadiusPx: 1.5)); + + // Remove the radius functions to test configured defaults. + numericSeriesList[0].radiusPxFn = null; + numericSeriesList[0].setAttr(boundsLineRadiusPxFnKey, null); + + renderer.preprocessSeries(numericSeriesList); + + expect(numericSeriesList.length, equals(1)); + + // Validate Desktop series. + var series = numericSeriesList[0]; + + var elementsList = series.getAttr(pointElementsKey); + expect(elementsList.length, equals(4)); + + expect(elementsList[0].radiusPx, equals(2.0)); + expect(elementsList[1].radiusPx, equals(2.0)); + expect(elementsList[2].radiusPx, equals(2.0)); + expect(elementsList[3].radiusPx, equals(2.0)); + + expect(elementsList[0].boundsLineRadiusPx, equals(1.5)); + expect(elementsList[1].boundsLineRadiusPx, equals(1.5)); + expect(elementsList[2].boundsLineRadiusPx, equals(1.5)); + expect(elementsList[3].boundsLineRadiusPx, equals(1.5)); + }); + + test('with custom symbol renderer ID in data', () { + renderer = new PointRenderer(config: new PointRendererConfig()); + + numericSeriesList[0].setAttr(pointSymbolRendererFnKey, + (int index) => numericSeriesList[0].data[index].shape as String); + + renderer.preprocessSeries(numericSeriesList); + + expect(numericSeriesList.length, equals(1)); + + // Validate Desktop series. + var series = numericSeriesList[0]; + + var elementsList = series.getAttr(pointElementsKey); + expect(elementsList.length, equals(4)); + + expect(elementsList[0].symbolRendererId, equals(defaultSymbolRendererId)); + expect(elementsList[1].symbolRendererId, equals('shape 1')); + expect(elementsList[2].symbolRendererId, equals('shape 2')); + expect(elementsList[3].symbolRendererId, equals(defaultSymbolRendererId)); + }); + + test('with custom symbol renderer ID in series and data', () { + renderer = new PointRenderer(config: new PointRendererConfig()); + + numericSeriesList[0].setAttr(pointSymbolRendererFnKey, + (int index) => numericSeriesList[0].data[index].shape as String); + numericSeriesList[0].setAttr(pointSymbolRendererIdKey, 'shape 0'); + + renderer.preprocessSeries(numericSeriesList); + + expect(numericSeriesList.length, equals(1)); + + // Validate Desktop series. + var series = numericSeriesList[0]; + + var elementsList = series.getAttr(pointElementsKey); + expect(elementsList.length, equals(4)); + + expect(elementsList[0].symbolRendererId, equals('shape 0')); + expect(elementsList[1].symbolRendererId, equals('shape 1')); + expect(elementsList[2].symbolRendererId, equals('shape 2')); + expect(elementsList[3].symbolRendererId, equals('shape 0')); + }); + }); +} diff --git a/web/charts/common/test/chart/scatter_plot/symbol_annotation_renderer_test.dart b/web/charts/common/test/chart/scatter_plot/symbol_annotation_renderer_test.dart new file mode 100644 index 000000000..3419a1309 --- /dev/null +++ b/web/charts/common/test/chart/scatter_plot/symbol_annotation_renderer_test.dart @@ -0,0 +1,109 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/src/chart/common/processed_series.dart' + show MutableSeries; +import 'package:charts_common/src/chart/scatter_plot/point_renderer.dart'; +import 'package:charts_common/src/chart/scatter_plot/symbol_annotation_renderer.dart'; +import 'package:charts_common/src/chart/scatter_plot/symbol_annotation_renderer_config.dart'; +import 'package:charts_common/src/common/material_palette.dart' + show MaterialPalette; +import 'package:charts_common/src/data/series.dart' show Series; + +import 'package:test/test.dart'; + +/// Datum/Row for the chart. +class MyRow { + final String campaignString; + final int campaign; + final int campaignLower; + final int campaignUpper; + final double radius; + final double boundsRadius; + final String shape; + MyRow(this.campaignString, this.campaign, this.campaignLower, + this.campaignUpper, this.radius, this.boundsRadius, this.shape); +} + +void main() { + SymbolAnnotationRenderer renderer; + List> numericSeriesList; + + setUp(() { + var myFakeDesktopData = [ + // This datum should get a default bounds line radius value. + new MyRow('MyCampaign1', 0, 0, 0, 3.0, null, null), + new MyRow('MyCampaign2', 10, 10, 12, 5.0, 4.0, 'shape 1'), + new MyRow('MyCampaign3', 10, 10, 14, 4.0, 4.0, 'shape 2'), + // This datum should always get default radius values. + new MyRow('MyCampaign4', 13, 12, 15, null, null, null), + ]; + + numericSeriesList = [ + new MutableSeries(new Series( + id: 'Desktop', + colorFn: (MyRow row, _) => MaterialPalette.blue.shadeDefault, + domainFn: (MyRow row, _) => row.campaign, + domainLowerBoundFn: (MyRow row, _) => row.campaignLower, + domainUpperBoundFn: (MyRow row, _) => row.campaignUpper, + measureFn: (MyRow row, _) => 0, + measureOffsetFn: (MyRow row, _) => 0, + radiusPxFn: (MyRow row, _) => row.radius, + data: myFakeDesktopData) + // Define a bounds line radius function. + ..setAttribute(boundsLineRadiusPxFnKey, + (int index) => myFakeDesktopData[index].boundsRadius)) + ]; + }); + + group('preprocess', () { + test('with numeric data and simple points', () { + renderer = new SymbolAnnotationRenderer( + config: new SymbolAnnotationRendererConfig()); + + renderer.preprocessSeries(numericSeriesList); + + expect(numericSeriesList.length, equals(1)); + + // Validate Desktop series. + var series = numericSeriesList[0]; + + var keyFn = series.keyFn; + + var elementsList = series.getAttr(pointElementsKey); + expect(elementsList.length, equals(4)); + + expect(elementsList[0].radiusPx, equals(3.0)); + expect(elementsList[1].radiusPx, equals(5.0)); + expect(elementsList[2].radiusPx, equals(4.0)); + expect(elementsList[3].radiusPx, equals(5.0)); + + expect(elementsList[0].boundsLineRadiusPx, equals(3.0)); + expect(elementsList[1].boundsLineRadiusPx, equals(4.0)); + expect(elementsList[2].boundsLineRadiusPx, equals(4.0)); + expect(elementsList[3].boundsLineRadiusPx, equals(5.0)); + + expect(elementsList[0].symbolRendererId, equals(defaultSymbolRendererId)); + expect(elementsList[1].symbolRendererId, equals(defaultSymbolRendererId)); + expect(elementsList[2].symbolRendererId, equals(defaultSymbolRendererId)); + expect(elementsList[3].symbolRendererId, equals(defaultSymbolRendererId)); + + expect(keyFn(0), equals('Desktop__0__0__0')); + expect(keyFn(1), equals('Desktop__10__10__12')); + expect(keyFn(2), equals('Desktop__10__10__14')); + expect(keyFn(3), equals('Desktop__13__12__15')); + }); + }); +} diff --git a/web/charts/example/README.md b/web/charts/example/README.md new file mode 100644 index 000000000..b9828c865 --- /dev/null +++ b/web/charts/example/README.md @@ -0,0 +1,8 @@ +Examples of the [charts_flutter](https://pub.dev/packages/charts_flutter) package running on the web. + +Original source at [github.com/google/charts](https://github.com/google/charts). + +Copied from [github.com/google/charts](https://github.com/google/charts) at +[35aeffe7c9](https://github.com/google/charts/commit/35aeffe7c96aa7d231c90fddd9766998545f1080). + +With changes to run on the web. diff --git a/web/charts/example/lib/a11y/a11y_gallery.dart b/web/charts/example/lib/a11y/a11y_gallery.dart new file mode 100644 index 000000000..d5e785528 --- /dev/null +++ b/web/charts/example/lib/a11y/a11y_gallery.dart @@ -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 'package:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'domain_a11y_explore_bar_chart.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.accessibility), + title: 'Screen reader enabled bar chart', + subtitle: 'Requires TalkBack or Voiceover turned on to work. ' + 'Bar chart with domain selection explore mode behavior.', + childBuilder: () => new DomainA11yExploreBarChart.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/a11y/domain_a11y_explore_bar_chart.dart b/web/charts/example/lib/a11y/domain_a11y_explore_bar_chart.dart new file mode 100644 index 000000000..1bcac0ede --- /dev/null +++ b/web/charts/example/lib/a11y/domain_a11y_explore_bar_chart.dart @@ -0,0 +1,215 @@ +// 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. + +/// Example of a bar chart with domain selection A11y behavior. +/// +/// The OS screen reader (TalkBack / VoiceOver) setting must be turned on, or +/// the behavior does not do anything. +/// +/// Note that the screenshot does not show any visual differences but when the +/// OS screen reader is enabled, the node that is being read out loud will be +/// surrounded by a rectangle. +/// +/// When [DomainA11yExploreBehavior] is added to the chart, the chart will +/// listen for the gesture that triggers "explore mode". +/// "Explore mode" creates semantic nodes for each domain value in the chart +/// with a description (customizable, defaults to domain value) and a bounding +/// box that surrounds the domain. +/// +/// These semantic node descriptions are read out loud by the OS screen reader +/// when the user taps within the bounding box, or when the user cycles through +/// the screen's elements (such as swiping left and right). +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class DomainA11yExploreBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + DomainA11yExploreBarChart(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory DomainA11yExploreBarChart.withSampleData() { + return new DomainA11yExploreBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DomainA11yExploreBarChart.withRandomData() { + return new DomainA11yExploreBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final mobileData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletData = [ + // Purposely missing data to show that only measures that are available + // are vocalized. + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Mobile Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileData, + ), + new charts.Series( + id: 'Tablet Sales', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletData, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + /// An example of how to generate a customized vocalization for + /// [DomainA11yExploreBehavior] from a list of [SeriesDatum]s. + /// + /// The list of series datums is for one domain. + /// + /// This example vocalizes the domain, then for each series that has that + /// domain, it vocalizes the series display name and the measure and a + /// description of that measure. + String vocalizeDomainAndMeasures(List seriesDatums) { + final buffer = new StringBuffer(); + + // The datum's type in this case is [OrdinalSales]. + // So we can access year and sales information here. + buffer.write(seriesDatums.first.datum.year); + + for (charts.SeriesDatum seriesDatum in seriesDatums) { + final series = seriesDatum.series; + final datum = seriesDatum.datum; + + buffer.write(' ${series.displayName} ' + '${datum.sales / 1000} thousand dollars'); + } + + return buffer.toString(); + } + + @override + Widget build(BuildContext context) { + return new Semantics( + // Describe your chart + label: 'Yearly sales bar chart', + // Optionally provide a hint for the user to know how to trigger + // explore mode. + hint: 'Press and hold to enable explore', + child: new charts.BarChart( + seriesList, + animate: animate, + // To prevent conflict with the select nearest behavior that uses the + // tap gesture, turn off default interactions when the user is using + // an accessibility service like TalkBack or VoiceOver to interact + // with the application. + defaultInteractions: !MediaQuery.of(context).accessibleNavigation, + behaviors: [ + new charts.DomainA11yExploreBehavior( + // Callback for generating the message that is vocalized. + // An example of how to use is in [vocalizeDomainAndMeasures]. + // If none is set, the default only vocalizes the domain value. + vocalizationCallback: vocalizeDomainAndMeasures, + // The following settings are optional, but shown here for + // demonstration purchases. + // [exploreModeTrigger] Default is press and hold, can be + // changed to tap. + exploreModeTrigger: charts.ExploreModeTrigger.pressHold, + // [exploreModeEnabledAnnouncement] Optionally notify the OS + // when explore mode is enabled. + exploreModeEnabledAnnouncement: 'Explore mode enabled', + // [exploreModeDisabledAnnouncement] Optionally notify the OS + // when explore mode is disabled. + exploreModeDisabledAnnouncement: 'Explore mode disabled', + // [minimumWidth] Default and minimum is 1.0. This is the + // minimum width of the screen reader bounding box. The bounding + // box width is calculated based on the domain axis step size. + // Minimum width will be used if the step size is smaller. + minimumWidth: 1.0, + ), + // Optionally include domain highlighter as a behavior. + // This behavior is included in this example to show that when an + // a11y node has focus, the chart's internal selection model is + // also updated. + new charts.DomainHighlighter(charts.SelectionModelType.info), + ], + )); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final mobileData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletData = [ + // Purposely missing data to show that only measures that are available + // are vocalized. + new OrdinalSales('2016', 25), + new OrdinalSales('2017', 50), + ]; + + return [ + new charts.Series( + id: 'Mobile Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileData, + ), + new charts.Series( + id: 'Tablet Sales', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletData, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/app_config.dart b/web/charts/example/lib/app_config.dart new file mode 100644 index 000000000..3e937eef2 --- /dev/null +++ b/web/charts/example/lib/app_config.dart @@ -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 'package:flutter_web/material.dart'; + +/// A particular configuration of the app. +class AppConfig { + final String appName; + final String appLink; + final ThemeData theme; + final bool showPerformanceOverlay; + + AppConfig( + {this.appName, this.appLink, this.theme, this.showPerformanceOverlay}); +} + +/// The default configuration of the app. +AppConfig get defaultConfig { + return new AppConfig( + appName: 'Charts Gallery', + appLink: '', + theme: new ThemeData( + brightness: Brightness.light, + primarySwatch: Colors.lightBlue, + ), + showPerformanceOverlay: false, + ); +} diff --git a/web/charts/example/lib/axes/axes_gallery.dart b/web/charts/example/lib/axes/axes_gallery.dart new file mode 100644 index 000000000..69fafd962 --- /dev/null +++ b/web/charts/example/lib/axes/axes_gallery.dart @@ -0,0 +1,137 @@ +// 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:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'bar_secondary_axis.dart'; +import 'bar_secondary_axis_only.dart'; +import 'custom_axis_tick_formatters.dart'; +import 'custom_font_size_and_color.dart'; +import 'custom_measure_tick_count.dart'; +import 'gridline_dash_pattern.dart'; +import 'hidden_ticks_and_labels_axis.dart'; +import 'horizontal_bar_secondary_axis.dart'; +import 'integer_only_measure_axis.dart'; +import 'line_disjoint_axis.dart'; +import 'measure_axis_label_alignment.dart'; +import 'numeric_initial_viewport.dart'; +import 'nonzero_bound_measure_axis.dart'; +import 'ordinal_initial_viewport.dart'; +import 'short_tick_length_axis.dart'; +import 'statically_provided_ticks.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Bar chart with Secondary Measure Axis', + subtitle: 'Bar chart with a series using a secondary measure axis', + childBuilder: () => new BarChartWithSecondaryAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Bar chart with Secondary Measure Axis only', + subtitle: 'Bar chart with both series using secondary measure axis', + childBuilder: () => new BarChartWithSecondaryAxisOnly.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal bar chart with Secondary Measure Axis', + subtitle: + 'Horizontal Bar chart with a series using secondary measure axis', + childBuilder: () => + new HorizontalBarChartWithSecondaryAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Short Ticks Axis', + subtitle: 'Bar chart with the primary measure axis having short ticks', + childBuilder: () => new ShortTickLengthAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Custom Axis Fonts', + subtitle: 'Bar chart with custom axis font size and color', + childBuilder: () => new CustomFontSizeAndColor.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Label Alignment Axis', + subtitle: 'Bar chart with custom measure axis label alignments', + childBuilder: () => new MeasureAxisLabelAlignment.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'No Axis', + subtitle: 'Bar chart with only the axis line drawn', + childBuilder: () => new HiddenTicksAndLabelsAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Statically Provided Ticks', + subtitle: 'Bar chart with statically provided ticks', + childBuilder: () => new StaticallyProvidedTicks.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Custom Formatter', + subtitle: 'Timeseries with custom domain and measure tick formatters', + childBuilder: () => new CustomAxisTickFormatters.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Custom Tick Count', + subtitle: 'Timeseries with custom measure axis tick count', + childBuilder: () => new CustomMeasureTickCount.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Integer Measure Ticks', + subtitle: 'Timeseries with only whole number measure axis ticks', + childBuilder: () => new IntegerOnlyMeasureAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Non-zero bound Axis', + subtitle: 'Timeseries with measure axis that does not include zero', + childBuilder: () => new NonzeroBoundMeasureAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Ordinal axis with initial viewport', + subtitle: 'Single series with initial viewport', + childBuilder: () => new OrdinalInitialViewport.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Numeric axis with initial viewport', + subtitle: 'Initial viewport is set to a subset of the data', + childBuilder: () => new NumericInitialViewport.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Gridline dash pattern', + subtitle: 'Timeseries with measure gridlines that have a dash pattern', + childBuilder: () => new GridlineDashPattern.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Disjoint Measure Axes', + subtitle: 'Line chart with disjoint measure axes', + childBuilder: () => new DisjointMeasureAxisLineChart.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/axes/bar_secondary_axis.dart b/web/charts/example/lib/axes/bar_secondary_axis.dart new file mode 100644 index 000000000..517221951 --- /dev/null +++ b/web/charts/example/lib/axes/bar_secondary_axis.dart @@ -0,0 +1,158 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a primary and secondary axis (left & right respectively) +/// for a set of grouped bars. This is useful for comparing Series that have +/// different units (revenue vs clicks by region), or different magnitudes (2017 +/// revenue vs 1/1/2017 revenue by region). +/// +/// The first series plots using the primary axis to position its measure +/// values (bar height). This is the default axis used if the measureAxisId is +/// not set. +/// +/// The second series plots using the secondary axis due to the measureAxisId of +/// secondaryMeasureAxisId. +/// +/// Note: primary and secondary may flip left and right positioning when +/// RTL.flipAxisLocations is set. +class BarChartWithSecondaryAxis extends StatelessWidget { + static const secondaryMeasureAxisId = 'secondaryMeasureAxisId'; + final List seriesList; + final bool animate; + + BarChartWithSecondaryAxis(this.seriesList, {this.animate}); + + factory BarChartWithSecondaryAxis.withSampleData() { + return new BarChartWithSecondaryAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory BarChartWithSecondaryAxis.withRandomData() { + return new BarChartWithSecondaryAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + final losAngelesSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + new charts.Series( + id: 'Los Angeles Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: losAngelesSalesData, + )..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId) + // Set the 'Los Angeles Revenue' series to use the secondary measure axis. + // All series that have this set will use the secondary measure axis. + // All other series will use the primary measure axis. + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // It is important when using both primary and secondary axes to choose + // the same number of ticks for both sides to get the gridlines to line + // up. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 3)), + secondaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 3)), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + final losAngelesSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + new charts.Series( + id: 'Los Angeles Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: losAngelesSalesData, + )..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId) + // Set the 'Los Angeles Revenue' series to use the secondary measure axis. + // All series that have this set will use the secondary measure axis. + // All other series will use the primary measure axis. + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/axes/bar_secondary_axis_only.dart b/web/charts/example/lib/axes/bar_secondary_axis_only.dart new file mode 100644 index 000000000..622408b32 --- /dev/null +++ b/web/charts/example/lib/axes/bar_secondary_axis_only.dart @@ -0,0 +1,114 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using only a secondary axis (on the right) for a set of grouped +/// bars. +/// +/// Both series plots using the secondary axis due to the measureAxisId of +/// secondaryMeasureAxisId. +/// +/// Note: secondary may flip left and right positioning when +/// RTL.flipAxisLocations is set. +class BarChartWithSecondaryAxisOnly extends StatelessWidget { + static const secondaryMeasureAxisId = 'secondaryMeasureAxisId'; + final List seriesList; + final bool animate; + + BarChartWithSecondaryAxisOnly(this.seriesList, {this.animate}); + + factory BarChartWithSecondaryAxisOnly.withSampleData() { + return new BarChartWithSecondaryAxisOnly( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory BarChartWithSecondaryAxisOnly.withRandomData() { + return new BarChartWithSecondaryAxisOnly(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ) + // Set series to use the secondary measure axis. + ..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 500), + new OrdinalSales('2015', 2500), + new OrdinalSales('2016', 1000), + new OrdinalSales('2017', 7500), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ) + // Set series to use the secondary measure axis. + ..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/axes/custom_axis_tick_formatters.dart b/web/charts/example/lib/axes/custom_axis_tick_formatters.dart new file mode 100644 index 000000000..476ed2a38 --- /dev/null +++ b/web/charts/example/lib/axes/custom_axis_tick_formatters.dart @@ -0,0 +1,144 @@ +// 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. + +/// Example of timeseries chart with custom measure and domain formatters. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; +import 'package:intl/intl.dart'; + +class CustomAxisTickFormatters extends StatelessWidget { + final List seriesList; + final bool animate; + + CustomAxisTickFormatters(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory CustomAxisTickFormatters.withSampleData() { + return new CustomAxisTickFormatters( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory CustomAxisTickFormatters.withRandomData() { + return new CustomAxisTickFormatters(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 26), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 27), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 28), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 29), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 30), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 01), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 02), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 03), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 04), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 05), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + /// Formatter for numeric ticks using [NumberFormat] to format into currency + /// + /// This is what is used in the [NumericAxisSpec] below. + final simpleCurrencyFormatter = + new charts.BasicNumericTickFormatterSpec.fromNumberFormat( + new NumberFormat.compactSimpleCurrency()); + + /// Formatter for numeric ticks that uses the callback provided. + /// + /// Use this formatter if you need to format values that [NumberFormat] + /// cannot provide. + /// + /// To see this formatter, change [NumericAxisSpec] to use this formatter. + // final customTickFormatter = + // charts.BasicNumericTickFormatterSpec((num value) => 'MyValue: $value'); + + return new charts.TimeSeriesChart(seriesList, + animate: animate, + // Sets up a currency formatter for the measure axis. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickFormatterSpec: simpleCurrencyFormatter), + + /// Customizes the date tick formatter. It will print the day of month + /// as the default format, but include the month and year if it + /// transitions to a new month. + /// + /// minute, hour, day, month, and year are all provided by default and + /// you can override them following this pattern. + domainAxis: new charts.DateTimeAxisSpec( + tickFormatterSpec: new charts.AutoDateTimeTickFormatterSpec( + day: new charts.TimeFormatterSpec( + format: 'd', transitionFormat: 'MM/dd/yyyy')))); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 6), + new MyRow(new DateTime(2017, 9, 26), 8), + new MyRow(new DateTime(2017, 9, 27), 6), + new MyRow(new DateTime(2017, 9, 28), 9), + new MyRow(new DateTime(2017, 9, 29), 11), + new MyRow(new DateTime(2017, 9, 30), 15), + new MyRow(new DateTime(2017, 10, 01), 25), + new MyRow(new DateTime(2017, 10, 02), 33), + new MyRow(new DateTime(2017, 10, 03), 27), + new MyRow(new DateTime(2017, 10, 04), 31), + new MyRow(new DateTime(2017, 10, 05), 23), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int cost; + MyRow(this.timeStamp, this.cost); +} diff --git a/web/charts/example/lib/axes/custom_font_size_and_color.dart b/web/charts/example/lib/axes/custom_font_size_and_color.dart new file mode 100644 index 000000000..d139963d6 --- /dev/null +++ b/web/charts/example/lib/axes/custom_font_size_and_color.dart @@ -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. + +/// Custom Font Style Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a custom primary measure and domain axis replacing the +/// renderSpec with one with a custom font size and a custom color. +/// +/// There are many axis styling options in the SmallTickRenderer allowing you +/// to customize the font, tick lengths, and offsets. +class CustomFontSizeAndColor extends StatelessWidget { + final List seriesList; + final bool animate; + + CustomFontSizeAndColor(this.seriesList, {this.animate}); + + factory CustomFontSizeAndColor.withSampleData() { + return new CustomFontSizeAndColor( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory CustomFontSizeAndColor.withRandomData() { + return new CustomFontSizeAndColor(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Assign a custom style for the domain axis. + /// + /// This is an OrdinalAxisSpec to match up with BarChart's default + /// ordinal domain axis (use NumericAxisSpec or DateTimeAxisSpec for + /// other charts). + domainAxis: new charts.OrdinalAxisSpec( + renderSpec: new charts.SmallTickRendererSpec( + + // Tick and Label styling here. + labelStyle: new charts.TextStyleSpec( + fontSize: 18, // size in Pts. + color: charts.MaterialPalette.black), + + // Change the line colors to match text color. + lineStyle: new charts.LineStyleSpec( + color: charts.MaterialPalette.black))), + + /// Assign a custom style for the measure axis. + primaryMeasureAxis: new charts.NumericAxisSpec( + renderSpec: new charts.GridlineRendererSpec( + + // Tick and Label styling here. + labelStyle: new charts.TextStyleSpec( + fontSize: 18, // size in Pts. + color: charts.MaterialPalette.black), + + // Change the line colors to match text color. + lineStyle: new charts.LineStyleSpec( + color: charts.MaterialPalette.black))), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/axes/custom_measure_tick_count.dart b/web/charts/example/lib/axes/custom_measure_tick_count.dart new file mode 100644 index 000000000..33de075cf --- /dev/null +++ b/web/charts/example/lib/axes/custom_measure_tick_count.dart @@ -0,0 +1,122 @@ +// 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. + +/// Example of timeseries chart with a custom number of ticks +/// +/// The tick count can be set by setting the [desiredMinTickCount] and +/// [desiredMaxTickCount] for automatically adjusted tick counts (based on +/// how 'nice' the ticks are) or [desiredTickCount] for a fixed tick count. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class CustomMeasureTickCount extends StatelessWidget { + final List seriesList; + final bool animate; + + CustomMeasureTickCount(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory CustomMeasureTickCount.withSampleData() { + return new CustomMeasureTickCount( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory CustomMeasureTickCount.withRandomData() { + return new CustomMeasureTickCount(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 26), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 27), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 28), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 29), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 30), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 01), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 02), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 03), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 04), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 05), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, + animate: animate, + + /// Customize the measure axis to have 2 ticks, + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 2))); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 6), + new MyRow(new DateTime(2017, 9, 26), 8), + new MyRow(new DateTime(2017, 9, 27), 6), + new MyRow(new DateTime(2017, 9, 28), 9), + new MyRow(new DateTime(2017, 9, 29), 11), + new MyRow(new DateTime(2017, 9, 30), 15), + new MyRow(new DateTime(2017, 10, 01), 25), + new MyRow(new DateTime(2017, 10, 02), 33), + new MyRow(new DateTime(2017, 10, 03), 27), + new MyRow(new DateTime(2017, 10, 04), 31), + new MyRow(new DateTime(2017, 10, 05), 23), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int cost; + MyRow(this.timeStamp, this.cost); +} diff --git a/web/charts/example/lib/axes/flipped_vertical_axis.dart b/web/charts/example/lib/axes/flipped_vertical_axis.dart new file mode 100644 index 000000000..f59f1c43a --- /dev/null +++ b/web/charts/example/lib/axes/flipped_vertical_axis.dart @@ -0,0 +1,114 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of flipping the vertical measure axis direction so that larger +/// values render downward instead of the usual rendering up. +/// +/// flipVerticalAxis, when set, flips the vertical axis from its default +/// direction. +/// +/// Note: primary and secondary may flip left and right positioning when +/// RTL.flipAxisLocations is set. +class FlippedVerticalAxis extends StatelessWidget { + final List seriesList; + final bool animate; + + FlippedVerticalAxis(this.seriesList, {this.animate}); + + factory FlippedVerticalAxis.withSampleData() { + return new FlippedVerticalAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory FlippedVerticalAxis.withRandomData() { + return new FlippedVerticalAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + const runners = ['Smith', 'Jones', 'Brown', 'Doe']; + + // Randomly assign runners, but leave the order of the places. + final raceData = [ + new RunnerRank(runners.removeAt(random.nextInt(runners.length)), 1), + new RunnerRank(runners.removeAt(random.nextInt(runners.length)), 2), + new RunnerRank(runners.removeAt(random.nextInt(runners.length)), 3), + new RunnerRank(runners.removeAt(random.nextInt(runners.length)), 4), + ]; + + return [ + new charts.Series( + id: 'Race Results', + domainFn: (RunnerRank row, _) => row.name, + measureFn: (RunnerRank row, _) => row.place, + data: raceData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // Known Issue, the bar chart cannot render negative direction bars at this + // time so the result is an empty chart. + // TODO: Remove this comment + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + flipVerticalAxis: true, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final raceData = [ + new RunnerRank('Smith', 1), + new RunnerRank('Jones', 2), + new RunnerRank('Brown', 3), + new RunnerRank('Doe', 4), + ]; + + return [ + new charts.Series( + id: 'Race Results', + domainFn: (RunnerRank row, _) => row.name, + measureFn: (RunnerRank row, _) => row.place, + data: raceData), + ]; + } +} + +/// Datum/Row for the chart. +class RunnerRank { + final String name; + final int place; + RunnerRank(this.name, this.place); +} diff --git a/web/charts/example/lib/axes/gridline_dash_pattern.dart b/web/charts/example/lib/axes/gridline_dash_pattern.dart new file mode 100644 index 000000000..7980c2ac7 --- /dev/null +++ b/web/charts/example/lib/axes/gridline_dash_pattern.dart @@ -0,0 +1,120 @@ +// 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. + +/// Example of timeseries chart with gridlines that have a dash pattern. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class GridlineDashPattern extends StatelessWidget { + final List seriesList; + final bool animate; + + GridlineDashPattern(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory GridlineDashPattern.withSampleData() { + return new GridlineDashPattern( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GridlineDashPattern.withRandomData() { + return new GridlineDashPattern(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 26), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 27), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 28), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 29), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 30), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 01), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 02), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 03), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 04), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 05), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, + animate: animate, + + /// Customize the gridlines to use a dash pattern. + primaryMeasureAxis: new charts.NumericAxisSpec( + renderSpec: charts.GridlineRendererSpec( + lineStyle: charts.LineStyleSpec( + dashPattern: [4, 4], + )))); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 6), + new MyRow(new DateTime(2017, 9, 26), 8), + new MyRow(new DateTime(2017, 9, 27), 6), + new MyRow(new DateTime(2017, 9, 28), 9), + new MyRow(new DateTime(2017, 9, 29), 11), + new MyRow(new DateTime(2017, 9, 30), 15), + new MyRow(new DateTime(2017, 10, 01), 25), + new MyRow(new DateTime(2017, 10, 02), 33), + new MyRow(new DateTime(2017, 10, 03), 27), + new MyRow(new DateTime(2017, 10, 04), 31), + new MyRow(new DateTime(2017, 10, 05), 23), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int cost; + MyRow(this.timeStamp, this.cost); +} diff --git a/web/charts/example/lib/axes/hidden_ticks_and_labels_axis.dart b/web/charts/example/lib/axes/hidden_ticks_and_labels_axis.dart new file mode 100644 index 000000000..94f28ebbd --- /dev/null +++ b/web/charts/example/lib/axes/hidden_ticks_and_labels_axis.dart @@ -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. + +/// No Axis Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of hiding both axis. +class HiddenTicksAndLabelsAxis extends StatelessWidget { + final List seriesList; + final bool animate; + + HiddenTicksAndLabelsAxis(this.seriesList, {this.animate}); + + factory HiddenTicksAndLabelsAxis.withSampleData() { + return new HiddenTicksAndLabelsAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HiddenTicksAndLabelsAxis.withRandomData() { + return new HiddenTicksAndLabelsAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Assign a custom style for the measure axis. + /// + /// The NoneRenderSpec can still draw an axis line with + /// showAxisLine=true. + primaryMeasureAxis: + new charts.NumericAxisSpec(renderSpec: new charts.NoneRenderSpec()), + + /// This is an OrdinalAxisSpec to match up with BarChart's default + /// ordinal domain axis (use NumericAxisSpec or DateTimeAxisSpec for + /// other charts). + domainAxis: new charts.OrdinalAxisSpec( + // Make sure that we draw the domain axis line. + showAxisLine: true, + // But don't draw anything else. + renderSpec: new charts.NoneRenderSpec()), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/axes/horizontal_bar_secondary_axis.dart b/web/charts/example/lib/axes/horizontal_bar_secondary_axis.dart new file mode 100644 index 000000000..82269865a --- /dev/null +++ b/web/charts/example/lib/axes/horizontal_bar_secondary_axis.dart @@ -0,0 +1,160 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a primary and secondary axis (left & right respectively) +/// for a set of grouped bars. This is useful for comparing Series that have +/// different units (revenue vs clicks by region), or different magnitudes (2017 +/// revenue vs 1/1/2017 revenue by region). +/// +/// The first series plots using the primary axis to position its measure +/// values (bar height). This is the default axis used if the measureAxisId is +/// not set. +/// +/// The second series plots using the secondary axis due to the measureAxisId of +/// secondaryMeasureAxisId. +/// +/// Note: primary and secondary may flip left and right positioning when +/// RTL.flipAxisLocations is set. +class HorizontalBarChartWithSecondaryAxis extends StatelessWidget { + static const secondaryMeasureAxisId = 'secondaryMeasureAxisId'; + final List seriesList; + final bool animate; + + HorizontalBarChartWithSecondaryAxis(this.seriesList, {this.animate}); + + factory HorizontalBarChartWithSecondaryAxis.withSampleData() { + return new HorizontalBarChartWithSecondaryAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalBarChartWithSecondaryAxis.withRandomData() { + return new HorizontalBarChartWithSecondaryAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + final losAngelesSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + new charts.Series( + id: 'Los Angeles Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: losAngelesSalesData, + )..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId) + // Set the 'Los Angeles Revenue' series to use the secondary measure axis. + // All series that have this set will use the secondary measure axis. + // All other series will use the primary measure axis. + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // For horizontal bar charts, set the [vertical] flag to false. + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + vertical: false, + // It is important when using both primary and secondary axes to choose + // the same number of ticks for both sides to get the gridlines to line + // up. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 3)), + secondaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 3)), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + final losAngelesSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + new charts.Series( + id: 'Los Angeles Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: losAngelesSalesData, + )..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId) + // Set the 'Los Angeles Revenue' series to use the secondary measure axis. + // All series that have this set will use the secondary measure axis. + // All other series will use the primary measure axis. + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/axes/integer_only_measure_axis.dart b/web/charts/example/lib/axes/integer_only_measure_axis.dart new file mode 100644 index 000000000..dc5e33a06 --- /dev/null +++ b/web/charts/example/lib/axes/integer_only_measure_axis.dart @@ -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. + +/// Example of timeseries chart forcing the measure axis to have whole number +/// ticks. This is useful if the measure units don't make sense to present as +/// fractional. +/// +/// This is done by customizing the measure axis and setting +/// [dataIsInWholeNumbers] on the tick provider. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class IntegerOnlyMeasureAxis extends StatelessWidget { + final List seriesList; + final bool animate; + + IntegerOnlyMeasureAxis(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory IntegerOnlyMeasureAxis.withSampleData() { + return new IntegerOnlyMeasureAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory IntegerOnlyMeasureAxis.withRandomData() { + return new IntegerOnlyMeasureAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 26), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 27), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 28), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 29), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 30), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 01), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 02), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 03), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 04), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 05), random.nextDouble().round()), + ]; + + return [ + new charts.Series( + id: 'Headcount', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.headcount, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Provides a custom axis ensuring that the ticks are in whole numbers. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: new charts.BasicNumericTickProviderSpec( + // Make sure we don't have values less than 1 as ticks + // (ie: counts). + dataIsInWholeNumbers: true, + // Fixed tick count to highlight the integer only behavior + // generating ticks [0, 1, 2, 3, 4]. + desiredTickCount: 5)), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 0), + new MyRow(new DateTime(2017, 9, 26), 0), + new MyRow(new DateTime(2017, 9, 27), 0), + new MyRow(new DateTime(2017, 9, 28), 0), + new MyRow(new DateTime(2017, 9, 29), 0), + new MyRow(new DateTime(2017, 9, 30), 0), + new MyRow(new DateTime(2017, 10, 01), 1), + new MyRow(new DateTime(2017, 10, 02), 1), + new MyRow(new DateTime(2017, 10, 03), 1), + new MyRow(new DateTime(2017, 10, 04), 1), + new MyRow(new DateTime(2017, 10, 05), 1), + ]; + + return [ + new charts.Series( + id: 'Headcount', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.headcount, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int headcount; + MyRow(this.timeStamp, this.headcount); +} diff --git a/web/charts/example/lib/axes/line_disjoint_axis.dart b/web/charts/example/lib/axes/line_disjoint_axis.dart new file mode 100644 index 000000000..91059601d --- /dev/null +++ b/web/charts/example/lib/axes/line_disjoint_axis.dart @@ -0,0 +1,268 @@ +// 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. + +/// Example of using disjoint measure axes to render 4 series of lines with +/// separate scales. 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 measure axes will be used to scale the series associated with them, +/// but they will not render any tick elements on either side of the chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:collection' show LinkedHashMap; +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class DisjointMeasureAxisLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + DisjointMeasureAxisLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory DisjointMeasureAxisLineChart.withSampleData() { + return new DisjointMeasureAxisLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DisjointMeasureAxisLineChart.withRandomData() { + return new DisjointMeasureAxisLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + // The first three series contain similar data with different magnitudes. + // This demonstrates the ability to graph the trends in each series relative + // to each other, without the largest magnitude series compressing the + // smallest. + final myFakeDesktopData = [ + new LinearClicks(0, clickCount: random.nextInt(100)), + new LinearClicks(1, clickCount: random.nextInt(100)), + new LinearClicks(2, clickCount: random.nextInt(100)), + new LinearClicks(3, clickCount: random.nextInt(100)), + ]; + + final myFakeTabletData = [ + new LinearClicks(0, clickCount: random.nextInt(100) * 100), + new LinearClicks(1, clickCount: random.nextInt(100) * 100), + new LinearClicks(2, clickCount: random.nextInt(100) * 100), + new LinearClicks(3, clickCount: random.nextInt(100) * 100), + ]; + + final myFakeMobileData = [ + new LinearClicks(0, clickCount: random.nextInt(100) * 1000), + new LinearClicks(1, clickCount: random.nextInt(100) * 1000), + new LinearClicks(2, clickCount: random.nextInt(100) * 1000), + new LinearClicks(3, clickCount: random.nextInt(100) * 1000), + ]; + + // The fourth series renders with decimal values, representing a very + // different sort ratio-based data. If this was on the same axis as any of + // the other series, it would be squashed near zero. + final myFakeClickRateData = [ + new LinearClicks(0, clickRate: .25), + new LinearClicks(1, clickRate: .65), + new LinearClicks(2, clickRate: .50), + new LinearClicks(3, clickRate: .30), + ]; + + return [ + // We render an empty series on the primary measure axis to ensure that + // the axis itself gets rendered. This helps us draw the gridlines on the + // chart. + new charts.Series( + id: 'Fake Series', + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: []), + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeDesktopData, + ) + // Set the 'Desktop' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 1'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeTabletData, + ) + // Set the 'Tablet' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 2'), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeMobileData, + ) + // Set the 'Mobile' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 3'), + new charts.Series( + id: 'Click Rate', + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeClickRateData, + ) + // Set the 'Click Rate' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 4'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + animate: animate, + // Configure a primary measure axis that will render gridlines across + // the chart. This axis uses fake ticks with no labels to ensure that we + // get 5 grid lines. + // + // We do this because disjoint measure axes do not draw any tick + // elements on the chart. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: new charts.StaticNumericTickProviderSpec( + // Create the ticks to be used the domain axis. + >[ + new charts.TickSpec(0, label: ''), + new charts.TickSpec(1, label: ''), + new charts.TickSpec(2, label: ''), + new charts.TickSpec(3, label: ''), + new charts.TickSpec(4, label: ''), + ], + )), + // Create one disjoint measure axis per series on the chart. + // + // Disjoint measure axes will be used to scale the rendered data, + // without drawing any tick elements on either side of the chart. + disjointMeasureAxes: + new LinkedHashMap.from({ + 'axis 1': new charts.NumericAxisSpec(), + 'axis 2': new charts.NumericAxisSpec(), + 'axis 3': new charts.NumericAxisSpec(), + 'axis 4': new charts.NumericAxisSpec(), + })); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + // The first three series contain similar data with different magnitudes. + // This demonstrates the ability to graph the trends in each series relative + // to each other, without the largest magnitude series compressing the + // smallest. + final myFakeDesktopData = [ + new LinearClicks(0, clickCount: 25), + new LinearClicks(1, clickCount: 125), + new LinearClicks(2, clickCount: 920), + new LinearClicks(3, clickCount: 375), + ]; + + final myFakeTabletData = [ + new LinearClicks(0, clickCount: 375), + new LinearClicks(1, clickCount: 1850), + new LinearClicks(2, clickCount: 9700), + new LinearClicks(3, clickCount: 5000), + ]; + + final myFakeMobileData = [ + new LinearClicks(0, clickCount: 5000), + new LinearClicks(1, clickCount: 25000), + new LinearClicks(2, clickCount: 100000), + new LinearClicks(3, clickCount: 75000), + ]; + + // The fourth series renders with decimal values, representing a very + // different sort ratio-based data. If this was on the same axis as any of + // the other series, it would be squashed near zero. + final myFakeClickRateData = [ + new LinearClicks(0, clickRate: .25), + new LinearClicks(1, clickRate: .65), + new LinearClicks(2, clickRate: .50), + new LinearClicks(3, clickRate: .30), + ]; + + return [ + // We render an empty series on the primary measure axis to ensure that + // the axis itself gets rendered. This helps us draw the gridlines on the + // chart. + new charts.Series( + id: 'Fake Series', + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: []), + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeDesktopData, + ) + // Set the 'Desktop' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 1'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeTabletData, + ) + // Set the 'Tablet' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 2'), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeMobileData, + ) + // Set the 'Mobile' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 3'), + new charts.Series( + id: 'Click Rate', + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickRate, + data: myFakeClickRateData, + ) + // Set the 'Click Rate' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 4'), + ]; + } +} + +/// Sample linear data type. +class LinearClicks { + final int year; + final int clickCount; + final double clickRate; + + LinearClicks(this.year, {this.clickCount = null, this.clickRate = null}); +} diff --git a/web/charts/example/lib/axes/measure_axis_label_alignment.dart b/web/charts/example/lib/axes/measure_axis_label_alignment.dart new file mode 100644 index 000000000..b9cfeda18 --- /dev/null +++ b/web/charts/example/lib/axes/measure_axis_label_alignment.dart @@ -0,0 +1,122 @@ +// 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. + +/// Custom Tick Label Alignment Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a custom primary measure replacing the renderSpec with one +/// that aligns the text under the tick and left justifies. +class MeasureAxisLabelAlignment extends StatelessWidget { + final List seriesList; + final bool animate; + + MeasureAxisLabelAlignment(this.seriesList, {this.animate}); + + factory MeasureAxisLabelAlignment.withSampleData() { + return new MeasureAxisLabelAlignment( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory MeasureAxisLabelAlignment.withRandomData() { + return new MeasureAxisLabelAlignment(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Customize the primary measure axis using a small tick renderer. + /// Use String instead of num for ordinal domain axis + /// (typically bar charts). + primaryMeasureAxis: new charts.NumericAxisSpec( + renderSpec: new charts.GridlineRendererSpec( + // Display the measure axis labels below the gridline. + // + // 'Before' & 'after' follow the axis value direction. + // Vertical axes draw 'before' below & 'after' above the tick. + // Horizontal axes draw 'before' left & 'after' right the tick. + labelAnchor: charts.TickLabelAnchor.before, + + // Left justify the text in the axis. + // + // Note: outside means that the secondary measure axis would right + // justify. + labelJustification: charts.TickLabelJustification.outside, + )), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/axes/nonzero_bound_measure_axis.dart b/web/charts/example/lib/axes/nonzero_bound_measure_axis.dart new file mode 100644 index 000000000..fa216f061 --- /dev/null +++ b/web/charts/example/lib/axes/nonzero_bound_measure_axis.dart @@ -0,0 +1,119 @@ +// 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. + +/// Example of timeseries chart that has a measure axis that does NOT include +/// zero. It starts at 100 and goes to 140. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class NonzeroBoundMeasureAxis extends StatelessWidget { + final List seriesList; + final bool animate; + + NonzeroBoundMeasureAxis(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory NonzeroBoundMeasureAxis.withSampleData() { + return new NonzeroBoundMeasureAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory NonzeroBoundMeasureAxis.withRandomData() { + return new NonzeroBoundMeasureAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 26), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 27), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 28), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 29), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 30), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 01), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 02), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 03), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 04), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 05), random.nextInt(100) + 100), + ]; + + return [ + new charts.Series( + id: 'Headcount', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.headcount, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, + animate: animate, + // Provide a tickProviderSpec which does NOT require that zero is + // included. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(zeroBound: false))); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 106), + new MyRow(new DateTime(2017, 9, 26), 108), + new MyRow(new DateTime(2017, 9, 27), 106), + new MyRow(new DateTime(2017, 9, 28), 109), + new MyRow(new DateTime(2017, 9, 29), 111), + new MyRow(new DateTime(2017, 9, 30), 115), + new MyRow(new DateTime(2017, 10, 01), 125), + new MyRow(new DateTime(2017, 10, 02), 133), + new MyRow(new DateTime(2017, 10, 03), 127), + new MyRow(new DateTime(2017, 10, 04), 131), + new MyRow(new DateTime(2017, 10, 05), 123), + ]; + + return [ + new charts.Series( + id: 'Headcount', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.headcount, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int headcount; + MyRow(this.timeStamp, this.headcount); +} diff --git a/web/charts/example/lib/axes/numeric_initial_viewport.dart b/web/charts/example/lib/axes/numeric_initial_viewport.dart new file mode 100644 index 000000000..958319a5e --- /dev/null +++ b/web/charts/example/lib/axes/numeric_initial_viewport.dart @@ -0,0 +1,133 @@ +// 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. + +/// Example of setting an initial viewport for ordinal axis. +/// +/// This allows for specifying the specific range of data to show that differs +/// from what was provided in the series list. +/// +/// In this example, the series list has numeric data from 0 to 10, but we +/// want to show from 3 to 7. +/// We can do this by specifying an [NumericExtents] in [NumericAxisSpec]. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class NumericInitialViewport extends StatelessWidget { + final List seriesList; + final bool animate; + + NumericInitialViewport(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory NumericInitialViewport.withSampleData() { + return new NumericInitialViewport( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory NumericInitialViewport.withRandomData() { + return new NumericInitialViewport(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + new LinearSales(7, random.nextInt(100)), + new LinearSales(8, random.nextInt(100)), + new LinearSales(9, random.nextInt(100)), + new LinearSales(10, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart( + seriesList, + animate: animate, + domainAxis: new charts.NumericAxisSpec( + // Set the initial viewport by providing a new AxisSpec with the + // desired viewport, in NumericExtents. + viewport: new charts.NumericExtents(3.0, 7.0)), + // Optionally add a pan or pan and zoom behavior. + // If pan/zoom is not added, the viewport specified remains the viewport. + behaviors: [new charts.PanAndZoomBehavior()], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + new LinearSales(4, 55), + new LinearSales(5, 66), + new LinearSales(6, 110), + new LinearSales(7, 70), + new LinearSales(8, 20), + new LinearSales(9, 25), + new LinearSales(10, 45), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/axes/ordinal_initial_viewport.dart b/web/charts/example/lib/axes/ordinal_initial_viewport.dart new file mode 100644 index 000000000..5cb6f9894 --- /dev/null +++ b/web/charts/example/lib/axes/ordinal_initial_viewport.dart @@ -0,0 +1,145 @@ +// 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. + +/// Example of setting an initial viewport for ordinal axis. +/// +/// This allows for specifying the specific range of data to show that differs +/// from what was provided in the series list. +/// +/// In this example, the series list has ordinal data from year 2014 to 2030, +/// but we want to show starting at 2018 and we only want to show 4 values. +/// We can do this by specifying an [OrdinalViewport] in [OrdinalAxisSpec]. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class OrdinalInitialViewport extends StatelessWidget { + final List seriesList; + final bool animate; + + OrdinalInitialViewport(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory OrdinalInitialViewport.withSampleData() { + return new OrdinalInitialViewport( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory OrdinalInitialViewport.withRandomData() { + return new OrdinalInitialViewport(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + new OrdinalSales('2018', random.nextInt(100)), + new OrdinalSales('2019', random.nextInt(100)), + new OrdinalSales('2020', random.nextInt(100)), + new OrdinalSales('2021', random.nextInt(100)), + new OrdinalSales('2022', random.nextInt(100)), + new OrdinalSales('2023', random.nextInt(100)), + new OrdinalSales('2024', random.nextInt(100)), + new OrdinalSales('2025', random.nextInt(100)), + new OrdinalSales('2026', random.nextInt(100)), + new OrdinalSales('2027', random.nextInt(100)), + new OrdinalSales('2028', random.nextInt(100)), + new OrdinalSales('2029', random.nextInt(100)), + new OrdinalSales('2030', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Set the initial viewport by providing a new AxisSpec with the + // desired viewport: a starting domain and the data size. + domainAxis: new charts.OrdinalAxisSpec( + viewport: new charts.OrdinalViewport('2018', 4)), + // Optionally add a pan or pan and zoom behavior. + // If pan/zoom is not added, the viewport specified remains the viewport. + behaviors: [new charts.PanAndZoomBehavior()], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + new OrdinalSales('2018', 33), + new OrdinalSales('2019', 80), + new OrdinalSales('2020', 21), + new OrdinalSales('2021', 77), + new OrdinalSales('2022', 8), + new OrdinalSales('2023', 12), + new OrdinalSales('2024', 42), + new OrdinalSales('2025', 70), + new OrdinalSales('2026', 77), + new OrdinalSales('2027', 55), + new OrdinalSales('2028', 19), + new OrdinalSales('2029', 66), + new OrdinalSales('2030', 27), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/axes/short_tick_length_axis.dart b/web/charts/example/lib/axes/short_tick_length_axis.dart new file mode 100644 index 000000000..7356e95f7 --- /dev/null +++ b/web/charts/example/lib/axes/short_tick_length_axis.dart @@ -0,0 +1,115 @@ +// 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. + +/// Custom Tick Style Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a custom primary measure axis replacing the default +/// gridline rendering with a short tick rendering. It also turns on the axis +/// line so that the ticks have something to line up against. +/// +/// There are many axis styling options in the SmallTickRenderer allowing you +/// to customize the font, tick lengths, and offsets. +class ShortTickLengthAxis extends StatelessWidget { + final List seriesList; + final bool animate; + + ShortTickLengthAxis(this.seriesList, {this.animate}); + + factory ShortTickLengthAxis.withSampleData() { + return new ShortTickLengthAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ShortTickLengthAxis.withRandomData() { + return new ShortTickLengthAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Customize the primary measure axis using a small tick renderer. + /// Note: use String instead of num for ordinal domain axis + /// (typically bar charts). + primaryMeasureAxis: new charts.NumericAxisSpec( + renderSpec: new charts.SmallTickRendererSpec( + // Tick and Label styling here. + )), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/axes/statically_provided_ticks.dart b/web/charts/example/lib/axes/statically_provided_ticks.dart new file mode 100644 index 000000000..0b32c3ad6 --- /dev/null +++ b/web/charts/example/lib/axes/statically_provided_ticks.dart @@ -0,0 +1,134 @@ +// 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. + +/// Example of axis using statically provided ticks. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of specifying a custom set of ticks to be used on the domain axis. +/// +/// Specifying custom set of ticks allows specifying exactly what ticks are +/// used in the axis. Each tick is also allowed to have a different style set. +/// +/// For an ordinal axis, the [StaticOrdinalTickProviderSpec] is shown in this +/// example defining ticks to be used with [TickSpec] of String. +/// +/// For numeric axis, the [StaticNumericTickProviderSpec] can be used by passing +/// in a list of ticks defined with [TickSpec] of num. +/// +/// For datetime axis, the [StaticDateTimeTickProviderSpec] can be used by +/// passing in a list of ticks defined with [TickSpec] of datetime. +class StaticallyProvidedTicks extends StatelessWidget { + final List seriesList; + final bool animate; + + StaticallyProvidedTicks(this.seriesList, {this.animate}); + + factory StaticallyProvidedTicks.withSampleData() { + return new StaticallyProvidedTicks( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StaticallyProvidedTicks.withRandomData() { + return new StaticallyProvidedTicks(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Create the ticks to be used the domain axis. + final staticTicks = >[ + new charts.TickSpec( + // Value must match the domain value. + '2014', + // Optional label for this tick, defaults to domain value if not set. + label: 'Year 2014', + // The styling for this tick. + style: new charts.TextStyleSpec( + color: new charts.Color(r: 0x4C, g: 0xAF, b: 0x50))), + // If no text style is specified - the style from renderSpec will be used + // if one is specified. + new charts.TickSpec('2015'), + new charts.TickSpec('2016'), + new charts.TickSpec('2017'), + ]; + + return new charts.BarChart( + seriesList, + animate: animate, + domainAxis: new charts.OrdinalAxisSpec( + tickProviderSpec: + new charts.StaticOrdinalTickProviderSpec(staticTicks)), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/bar_gallery.dart b/web/charts/example/lib/bar_chart/bar_gallery.dart new file mode 100644 index 000000000..0a737d9a6 --- /dev/null +++ b/web/charts/example/lib/bar_chart/bar_gallery.dart @@ -0,0 +1,156 @@ +// 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:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'custom_rounded_bars.dart'; +import 'grouped.dart'; +import 'grouped_fill_color.dart'; +import 'grouped_single_target_line.dart'; +import 'grouped_stacked.dart'; +import 'grouped_stacked_weight_pattern.dart'; +import 'grouped_target_line.dart'; +import 'horizontal.dart'; +import 'horizontal_bar_label.dart'; +import 'horizontal_bar_label_custom.dart'; +import 'horizontal_pattern_forward_hatch.dart'; +import 'pattern_forward_hatch.dart'; +import 'simple.dart'; +import 'stacked.dart'; +import 'stacked_fill_color.dart'; +import 'stacked_horizontal.dart'; +import 'stacked_target_line.dart'; +import 'spark_bar.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Simple Bar Chart', + subtitle: 'Simple bar chart with a single series', + childBuilder: () => new SimpleBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Stacked Bar Chart', + subtitle: 'Stacked bar chart with multiple series', + childBuilder: () => new StackedBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Bar Chart', + subtitle: 'Grouped bar chart with multiple series', + childBuilder: () => new GroupedBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Stacked Bar Chart', + subtitle: 'Grouped and stacked bar chart with multiple series', + childBuilder: () => new GroupedStackedBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Bar Target Line Chart', + subtitle: 'Grouped bar target line chart with multiple series', + childBuilder: () => new GroupedBarTargetLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Bar Single Target Line Chart', + subtitle: + 'Grouped bar target line chart with multiple series and a single target', + childBuilder: () => new GroupedBarSingleTargetLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Stacked Bar Target Line Chart', + subtitle: 'Stacked bar target line chart with multiple series', + childBuilder: () => new StackedBarTargetLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal Bar Chart', + subtitle: 'Horizontal bar chart with a single series', + childBuilder: () => new HorizontalBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Stacked Horizontal Bar Chart', + subtitle: 'Stacked horizontal bar chart with multiple series', + childBuilder: () => new StackedHorizontalBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal Bar Chart with Bar Labels', + subtitle: 'Horizontal bar chart with a single series and bar labels', + childBuilder: () => new HorizontalBarLabelChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal Bar Chart with Custom Bar Labels', + subtitle: 'Bar labels with customized styling', + childBuilder: () => new HorizontalBarLabelCustomChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Spark Bar Chart', + subtitle: 'Spark Bar Chart', + childBuilder: () => new SparkBar.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Fill Color Bar Chart', + subtitle: 'Grouped bar chart with fill colors', + childBuilder: () => new GroupedFillColorBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Stacked Fill Color Bar Chart', + subtitle: 'Stacked bar chart with fill colors', + childBuilder: () => new StackedFillColorBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Pattern Forward Hatch Bar Chart', + subtitle: 'Pattern Forward Hatch Bar Chart', + childBuilder: () => new PatternForwardHatchBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal Pattern Forward Hatch Bar Chart', + subtitle: 'Horizontal Pattern Forward Hatch Bar Chart', + childBuilder: () => + new HorizontalPatternForwardHatchBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Weight Pattern Bar Chart', + subtitle: 'Grouped and stacked bar chart with a weight pattern', + childBuilder: () => + new GroupedStackedWeightPatternBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Bar Chart with custom bar radius', + subtitle: 'Custom rounded bar corners', + childBuilder: () => new CustomRoundedBars.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/bar_chart/custom_rounded_bars.dart b/web/charts/example/lib/bar_chart/custom_rounded_bars.dart new file mode 100644 index 000000000..a8952285e --- /dev/null +++ b/web/charts/example/lib/bar_chart/custom_rounded_bars.dart @@ -0,0 +1,110 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class CustomRoundedBars extends StatelessWidget { + final List seriesList; + final bool animate; + + CustomRoundedBars(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with custom rounded bars. + factory CustomRoundedBars.withSampleData() { + return new CustomRoundedBars( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory CustomRoundedBars.withRandomData() { + return new CustomRoundedBars(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + defaultRenderer: new charts.BarRendererConfig( + // By default, bar renderer will draw rounded bars with a constant + // radius of 30. + // To not have any rounded corners, use [NoCornerStrategy] + // To change the radius of the bars, use [ConstCornerStrategy] + cornerStrategy: const charts.ConstCornerStrategy(30)), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/grouped.dart b/web/charts/example/lib/bar_chart/grouped.dart new file mode 100644 index 000000000..c0e687897 --- /dev/null +++ b/web/charts/example/lib/bar_chart/grouped.dart @@ -0,0 +1,154 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + GroupedBarChart(this.seriesList, {this.animate}); + + factory GroupedBarChart.withSampleData() { + return new GroupedBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedBarChart.withRandomData() { + return new GroupedBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/grouped_fill_color.dart b/web/charts/example/lib/bar_chart/grouped_fill_color.dart new file mode 100644 index 000000000..a84178342 --- /dev/null +++ b/web/charts/example/lib/bar_chart/grouped_fill_color.dart @@ -0,0 +1,178 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of a grouped bar chart with three series, each rendered with +/// different fill colors. +class GroupedFillColorBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + GroupedFillColorBarChart(this.seriesList, {this.animate}); + + factory GroupedFillColorBarChart.withSampleData() { + return new GroupedFillColorBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedFillColorBarChart.withRandomData() { + return new GroupedFillColorBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + // Blue bars with a lighter center color. + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + fillColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + ), + // Solid red bars. Fill color will default to the series color if no + // fillColorFn is configured. + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + ), + // Hollow green bars. + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + fillColorFn: (_, __) => charts.MaterialPalette.transparent, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Configure a stroke width to enable borders on the bars. + defaultRenderer: new charts.BarRendererConfig( + groupingType: charts.BarGroupingType.grouped, strokeWidthPx: 2.0), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + // Blue bars with a lighter center color. + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + fillColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + ), + // Solid red bars. Fill color will default to the series color if no + // fillColorFn is configured. + new charts.Series( + id: 'Tablet', + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + ), + // Hollow green bars. + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + fillColorFn: (_, __) => charts.MaterialPalette.transparent, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/grouped_single_target_line.dart b/web/charts/example/lib/bar_chart/grouped_single_target_line.dart new file mode 100644 index 000000000..fdb6a63a5 --- /dev/null +++ b/web/charts/example/lib/bar_chart/grouped_single_target_line.dart @@ -0,0 +1,180 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedBarSingleTargetLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + GroupedBarSingleTargetLineChart(this.seriesList, {this.animate}); + + factory GroupedBarSingleTargetLineChart.withSampleData() { + return new GroupedBarSingleTargetLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedBarSingleTargetLineChart.withRandomData() { + return new GroupedBarSingleTargetLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final targetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: targetLineData) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart(seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + customSeriesRenderers: [ + new charts.BarTargetLineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customTargetLine', + groupingType: charts.BarGroupingType.grouped) + ]); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final targetLineData = [ + new OrdinalSales('2014', 30), + new OrdinalSales('2015', 55), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 25), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: targetLineData) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/grouped_stacked.dart b/web/charts/example/lib/bar_chart/grouped_stacked.dart new file mode 100644 index 000000000..ecf178744 --- /dev/null +++ b/web/charts/example/lib/bar_chart/grouped_stacked.dart @@ -0,0 +1,244 @@ +// 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. + +/// Example of a bar chart with grouped, stacked series oriented vertically. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedStackedBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + GroupedStackedBarChart(this.seriesList, {this.animate}); + + factory GroupedStackedBarChart.withSampleData() { + return new GroupedStackedBarChart( + createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedStackedBarChart.withRandomData() { + return new GroupedStackedBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.groupedStacked, + ); + } + + /// Create series list with multiple series + static List> createSampleData() { + final desktopSalesDataA = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/grouped_stacked_weight_pattern.dart b/web/charts/example/lib/bar_chart/grouped_stacked_weight_pattern.dart new file mode 100644 index 000000000..9cf0a8705 --- /dev/null +++ b/web/charts/example/lib/bar_chart/grouped_stacked_weight_pattern.dart @@ -0,0 +1,256 @@ +// 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. + +/// Example of a bar chart with grouped, stacked series oriented vertically with +/// a custom 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. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedStackedWeightPatternBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + GroupedStackedWeightPatternBarChart(this.seriesList, {this.animate}); + + factory GroupedStackedWeightPatternBarChart.withSampleData() { + return new GroupedStackedWeightPatternBarChart( + createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedStackedWeightPatternBarChart.withRandomData() { + return new GroupedStackedWeightPatternBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Configure the bar renderer in grouped stacked rendering mode with a + // custom weight pattern. + // + // The first stack of bars in each group is configured to be twice as wide + // as the second stack of bars in each group. + defaultRenderer: new charts.BarRendererConfig( + groupingType: charts.BarGroupingType.groupedStacked, + weightPattern: [2, 1], + ), + ); + } + + /// Create series list with multiple series + static List> createSampleData() { + final desktopSalesDataA = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/grouped_target_line.dart b/web/charts/example/lib/bar_chart/grouped_target_line.dart new file mode 100644 index 000000000..21c0f8e41 --- /dev/null +++ b/web/charts/example/lib/bar_chart/grouped_target_line.dart @@ -0,0 +1,248 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedBarTargetLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + GroupedBarTargetLineChart(this.seriesList, {this.animate}); + + factory GroupedBarTargetLineChart.withSampleData() { + return new GroupedBarTargetLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedBarTargetLineChart.withRandomData() { + return new GroupedBarTargetLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Tablet Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Mobile Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart(seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + customSeriesRenderers: [ + new charts.BarTargetLineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customTargetLine', + groupingType: charts.BarGroupingType.grouped) + ]); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopTargetLineData = [ + new OrdinalSales('2014', 4), + new OrdinalSales('2015', 20), + new OrdinalSales('2016', 80), + new OrdinalSales('2017', 65), + ]; + + final tableTargetLineData = [ + new OrdinalSales('2014', 30), + new OrdinalSales('2015', 55), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 25), + ]; + + final mobileTargetLineData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 5), + new OrdinalSales('2016', 45), + new OrdinalSales('2017', 35), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Tablet Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Mobile Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/horizontal.dart b/web/charts/example/lib/bar_chart/horizontal.dart new file mode 100644 index 000000000..5e27583bd --- /dev/null +++ b/web/charts/example/lib/bar_chart/horizontal.dart @@ -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. + +/// Horizontal bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class HorizontalBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + HorizontalBarChart(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory HorizontalBarChart.withSampleData() { + return new HorizontalBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalBarChart.withRandomData() { + return new HorizontalBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // For horizontal bar charts, set the [vertical] flag to false. + return new charts.BarChart( + seriesList, + animate: animate, + vertical: false, + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/horizontal_bar_label.dart b/web/charts/example/lib/bar_chart/horizontal_bar_label.dart new file mode 100644 index 000000000..c59d81bef --- /dev/null +++ b/web/charts/example/lib/bar_chart/horizontal_bar_label.dart @@ -0,0 +1,123 @@ +// 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. + +/// Horizontal bar chart with bar label renderer example and hidden domain axis. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class HorizontalBarLabelChart extends StatelessWidget { + final List seriesList; + final bool animate; + + HorizontalBarLabelChart(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory HorizontalBarLabelChart.withSampleData() { + return new HorizontalBarLabelChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalBarLabelChart.withRandomData() { + return new HorizontalBarLabelChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '${sales.year}: \$${sales.sales.toString()}') + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // [BarLabelDecorator] will automatically position the label + // inside the bar if the label will fit. If the label will not fit and the + // area outside of the bar is larger than the bar, it will draw outside of the + // bar. Labels can always display inside or outside using [LabelPosition]. + // + // Text style for inside / outside can be controlled independently by setting + // [insideLabelStyleSpec] and [outsideLabelStyleSpec]. + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + vertical: false, + // Set a bar label decorator. + // Example configuring different styles for inside/outside: + // barRendererDecorator: new charts.BarLabelDecorator( + // insideLabelStyleSpec: new charts.TextStyleSpec(...), + // outsideLabelStyleSpec: new charts.TextStyleSpec(...)), + barRendererDecorator: new charts.BarLabelDecorator(), + // Hide domain axis. + domainAxis: + new charts.OrdinalAxisSpec(renderSpec: new charts.NoneRenderSpec()), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '${sales.year}: \$${sales.sales.toString()}') + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/horizontal_bar_label_custom.dart b/web/charts/example/lib/bar_chart/horizontal_bar_label_custom.dart new file mode 100644 index 000000000..c247b51b9 --- /dev/null +++ b/web/charts/example/lib/bar_chart/horizontal_bar_label_custom.dart @@ -0,0 +1,140 @@ +// 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. + +/// Horizontal bar chart with custom style for each datum in the bar label. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class HorizontalBarLabelCustomChart extends StatelessWidget { + final List seriesList; + final bool animate; + + HorizontalBarLabelCustomChart(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + static HorizontalBarLabelCustomChart createWithSampleData() { + return new HorizontalBarLabelCustomChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalBarLabelCustomChart.withRandomData() { + return new HorizontalBarLabelCustomChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '${sales.year}: \$${sales.sales.toString()}', + insideLabelStyleAccessorFn: (OrdinalSales sales, _) { + final color = (sales.year == '2014') + ? charts.MaterialPalette.red.shadeDefault + : charts.MaterialPalette.yellow.shadeDefault.darker; + return new charts.TextStyleSpec(color: color); + }, + outsideLabelStyleAccessorFn: (OrdinalSales sales, _) { + final color = (sales.year == '2014') + ? charts.MaterialPalette.red.shadeDefault + : charts.MaterialPalette.yellow.shadeDefault.darker; + return new charts.TextStyleSpec(color: color); + }, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // The [BarLabelDecorator] has settings to set the text style for all labels + // for inside the bar and outside the bar. To be able to control each datum's + // style, set the style accessor functions on the series. + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + vertical: false, + barRendererDecorator: new charts.BarLabelDecorator(), + // Hide domain axis. + domainAxis: + new charts.OrdinalAxisSpec(renderSpec: new charts.NoneRenderSpec()), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '${sales.year}: \$${sales.sales.toString()}', + insideLabelStyleAccessorFn: (OrdinalSales sales, _) { + final color = (sales.year == '2014') + ? charts.MaterialPalette.red.shadeDefault + : charts.MaterialPalette.yellow.shadeDefault.darker; + return new charts.TextStyleSpec(color: color); + }, + outsideLabelStyleAccessorFn: (OrdinalSales sales, _) { + final color = (sales.year == '2014') + ? charts.MaterialPalette.red.shadeDefault + : charts.MaterialPalette.yellow.shadeDefault.darker; + return new charts.TextStyleSpec(color: color); + }, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/horizontal_pattern_forward_hatch.dart b/web/charts/example/lib/bar_chart/horizontal_pattern_forward_hatch.dart new file mode 100644 index 000000000..edff5618d --- /dev/null +++ b/web/charts/example/lib/bar_chart/horizontal_pattern_forward_hatch.dart @@ -0,0 +1,163 @@ +// 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. + +/// Forward pattern hatch bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Forward hatch pattern horizontal bar chart example. +/// +/// The second series of bars is rendered with a pattern by defining a +/// fillPatternFn mapping function. +class HorizontalPatternForwardHatchBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + HorizontalPatternForwardHatchBarChart(this.seriesList, {this.animate}); + + factory HorizontalPatternForwardHatchBarChart.withSampleData() { + return new HorizontalPatternForwardHatchBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalPatternForwardHatchBarChart.withRandomData() { + return new HorizontalPatternForwardHatchBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + fillPatternFn: (OrdinalSales sales, _) => + charts.FillPatternType.forwardHatch, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + vertical: false, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + fillPatternFn: (OrdinalSales sales, _) => + charts.FillPatternType.forwardHatch, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/pattern_forward_hatch.dart b/web/charts/example/lib/bar_chart/pattern_forward_hatch.dart new file mode 100644 index 000000000..d951a712c --- /dev/null +++ b/web/charts/example/lib/bar_chart/pattern_forward_hatch.dart @@ -0,0 +1,161 @@ +// 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. + +/// Forward hatch pattern bar chart example. +/// +/// The second series of bars is rendered with a pattern by defining a +/// fillPatternFn mapping function. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class PatternForwardHatchBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + PatternForwardHatchBarChart(this.seriesList, {this.animate}); + + factory PatternForwardHatchBarChart.withSampleData() { + return new PatternForwardHatchBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PatternForwardHatchBarChart.withRandomData() { + return new PatternForwardHatchBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + fillPatternFn: (OrdinalSales sales, _) => + charts.FillPatternType.forwardHatch, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + fillPatternFn: (OrdinalSales sales, _) => + charts.FillPatternType.forwardHatch, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/simple.dart b/web/charts/example/lib/bar_chart/simple.dart new file mode 100644 index 000000000..51ed6e43b --- /dev/null +++ b/web/charts/example/lib/bar_chart/simple.dart @@ -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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SimpleBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + SimpleBarChart(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory SimpleBarChart.withSampleData() { + return new SimpleBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleBarChart.withRandomData() { + return new SimpleBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/spark_bar.dart b/web/charts/example/lib/bar_chart/spark_bar.dart new file mode 100644 index 000000000..fe4749f84 --- /dev/null +++ b/web/charts/example/lib/bar_chart/spark_bar.dart @@ -0,0 +1,140 @@ +// 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. + +/// Spark Bar Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of a Spark Bar by hiding both axis, reducing the chart margins. +class SparkBar extends StatelessWidget { + final List seriesList; + final bool animate; + + SparkBar(this.seriesList, {this.animate}); + + factory SparkBar.withSampleData() { + return new SparkBar( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SparkBar.withRandomData() { + return new SparkBar(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2007', random.nextInt(100)), + new OrdinalSales('2008', random.nextInt(100)), + new OrdinalSales('2009', random.nextInt(100)), + new OrdinalSales('2010', random.nextInt(100)), + new OrdinalSales('2011', random.nextInt(100)), + new OrdinalSales('2012', random.nextInt(100)), + new OrdinalSales('2013', random.nextInt(100)), + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Assign a custom style for the measure axis. + /// + /// The NoneRenderSpec only draws an axis line (and even that can be hidden + /// with showAxisLine=false). + primaryMeasureAxis: + new charts.NumericAxisSpec(renderSpec: new charts.NoneRenderSpec()), + + /// This is an OrdinalAxisSpec to match up with BarChart's default + /// ordinal domain axis (use NumericAxisSpec or DateTimeAxisSpec for + /// other charts). + domainAxis: new charts.OrdinalAxisSpec( + // Make sure that we draw the domain axis line. + showAxisLine: true, + // But don't draw anything else. + renderSpec: new charts.NoneRenderSpec()), + + // With a spark chart we likely don't want large chart margins. + // 1px is the smallest we can make each margin. + layoutConfig: new charts.LayoutConfig( + leftMarginSpec: new charts.MarginSpec.fixedPixel(0), + topMarginSpec: new charts.MarginSpec.fixedPixel(0), + rightMarginSpec: new charts.MarginSpec.fixedPixel(0), + bottomMarginSpec: new charts.MarginSpec.fixedPixel(0)), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2007', 3100), + new OrdinalSales('2008', 3500), + new OrdinalSales('2009', 5000), + new OrdinalSales('2010', 2500), + new OrdinalSales('2011', 3200), + new OrdinalSales('2012', 4500), + new OrdinalSales('2013', 4400), + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 5000), + new OrdinalSales('2016', 4500), + new OrdinalSales('2017', 4300), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/stacked.dart b/web/charts/example/lib/bar_chart/stacked.dart new file mode 100644 index 000000000..37fc75002 --- /dev/null +++ b/web/charts/example/lib/bar_chart/stacked.dart @@ -0,0 +1,155 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class StackedBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + StackedBarChart(this.seriesList, {this.animate}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory StackedBarChart.withSampleData() { + return new StackedBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedBarChart.withRandomData() { + return new StackedBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.stacked, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/stacked_fill_color.dart b/web/charts/example/lib/bar_chart/stacked_fill_color.dart new file mode 100644 index 000000000..b401a8d37 --- /dev/null +++ b/web/charts/example/lib/bar_chart/stacked_fill_color.dart @@ -0,0 +1,178 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of a stacked bar chart with three series, each rendered with +/// different fill colors. +class StackedFillColorBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + StackedFillColorBarChart(this.seriesList, {this.animate}); + + factory StackedFillColorBarChart.withSampleData() { + return new StackedFillColorBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedFillColorBarChart.withRandomData() { + return new StackedFillColorBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + // Blue bars with a lighter center color. + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + fillColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + ), + // Solid red bars. Fill color will default to the series color if no + // fillColorFn is configured. + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + ), + // Hollow green bars. + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + fillColorFn: (_, __) => charts.MaterialPalette.transparent, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Configure a stroke width to enable borders on the bars. + defaultRenderer: new charts.BarRendererConfig( + groupingType: charts.BarGroupingType.stacked, strokeWidthPx: 2.0), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + // Blue bars with a lighter center color. + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + fillColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + ), + // Solid red bars. Fill color will default to the series color if no + // fillColorFn is configured. + new charts.Series( + id: 'Tablet', + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + ), + // Hollow green bars. + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + fillColorFn: (_, __) => charts.MaterialPalette.transparent, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/stacked_horizontal.dart b/web/charts/example/lib/bar_chart/stacked_horizontal.dart new file mode 100644 index 000000000..691313aee --- /dev/null +++ b/web/charts/example/lib/bar_chart/stacked_horizontal.dart @@ -0,0 +1,157 @@ +// 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. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class StackedHorizontalBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + StackedHorizontalBarChart(this.seriesList, {this.animate}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory StackedHorizontalBarChart.withSampleData() { + return new StackedHorizontalBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedHorizontalBarChart.withRandomData() { + return new StackedHorizontalBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // For horizontal bar charts, set the [vertical] flag to false. + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.stacked, + vertical: false, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/bar_chart/stacked_target_line.dart b/web/charts/example/lib/bar_chart/stacked_target_line.dart new file mode 100644 index 000000000..be795bff4 --- /dev/null +++ b/web/charts/example/lib/bar_chart/stacked_target_line.dart @@ -0,0 +1,249 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class StackedBarTargetLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + StackedBarTargetLineChart(this.seriesList, {this.animate}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory StackedBarTargetLineChart.withSampleData() { + return new StackedBarTargetLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedBarTargetLineChart.withRandomData() { + return new StackedBarTargetLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Tablet Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Mobile Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart(seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.stacked, + customSeriesRenderers: [ + new charts.BarTargetLineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customTargetLine', + groupingType: charts.BarGroupingType.stacked) + ]); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopTargetLineData = [ + new OrdinalSales('2014', 4), + new OrdinalSales('2015', 20), + new OrdinalSales('2016', 80), + new OrdinalSales('2017', 65), + ]; + + final tableTargetLineData = [ + new OrdinalSales('2014', 30), + new OrdinalSales('2015', 55), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 25), + ]; + + final mobileTargetLineData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 5), + new OrdinalSales('2016', 45), + new OrdinalSales('2017', 35), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Tablet Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Mobile Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/behaviors_gallery.dart b/web/charts/example/lib/behaviors/behaviors_gallery.dart new file mode 100644 index 000000000..aa8ea004d --- /dev/null +++ b/web/charts/example/lib/behaviors/behaviors_gallery.dart @@ -0,0 +1,126 @@ +// 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:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'chart_title.dart'; +import 'initial_hint_animation.dart'; +import 'initial_selection.dart'; +import 'percent_of_domain.dart'; +import 'percent_of_domain_by_category.dart'; +import 'percent_of_series.dart'; +import 'selection_bar_highlight.dart'; +import 'selection_line_highlight.dart'; +import 'selection_line_highlight_custom_shape.dart'; +import 'selection_callback_example.dart'; +import 'selection_scatter_plot_highlight.dart'; +import 'selection_user_managed.dart'; +import 'slider.dart'; +import 'sliding_viewport_on_selection.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Bar Highlight', + subtitle: 'Simple bar chart with tap activation', + childBuilder: () => new SelectionBarHighlight.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Line Highlight', + subtitle: 'Line chart with tap and drag activation', + childBuilder: () => new SelectionLineHighlight.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Line Highlight Custom Shape', + subtitle: 'Line chart with tap and drag activation and a custom shape', + childBuilder: () => + new SelectionLineHighlightCustomShape.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Scatter Plot Highlight', + subtitle: 'Scatter plot chart with tap and drag activation', + childBuilder: () => new SelectionScatterPlotHighlight.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Callback Example', + subtitle: 'Timeseries that updates external components on selection', + childBuilder: () => new SelectionCallbackExample.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'User managed selection', + subtitle: + 'Example where selection can be set and cleared programmatically', + childBuilder: () => new SelectionUserManaged.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Bar Chart with initial selection', + subtitle: 'Single series with initial selection', + childBuilder: () => new InitialSelection.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Line Chart with Chart Titles', + subtitle: 'Line chart with four chart titles', + childBuilder: () => new ChartTitleLine.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Line Chart with Slider', + subtitle: 'Line chart with a slider behavior', + childBuilder: () => new SliderLine.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Percent of Domain', + subtitle: 'Stacked bar chart with measures calculated as percent of ' + + 'domain', + childBuilder: () => new PercentOfDomainBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Percent of Domain by Category', + subtitle: 'Grouped stacked bar chart with measures calculated as ' + 'percent of domain and series category', + childBuilder: () => + new PercentOfDomainByCategoryBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Percent of Series', + subtitle: 'Grouped bar chart with measures calculated as percent of ' + + 'series', + childBuilder: () => new PercentOfSeriesBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Sliding viewport on domain selection', + subtitle: 'Center viewport on selected domain', + childBuilder: () => new SlidingViewportOnSelection.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Initial hint animation ', + subtitle: 'Animate into final viewport', + childBuilder: () => new InitialHintAnimation.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/behaviors/chart_title.dart b/web/charts/example/lib/behaviors/chart_title.dart new file mode 100644 index 000000000..ce0a7c730 --- /dev/null +++ b/web/charts/example/lib/behaviors/chart_title.dart @@ -0,0 +1,131 @@ +// 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. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +/// This is a line chart with a title text in every margin. +/// +/// A series of [ChartTitle] behaviors are used to render titles, one per +/// margin. +class ChartTitleLine extends StatelessWidget { + final List seriesList; + final bool animate; + + ChartTitleLine(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory ChartTitleLine.withSampleData() { + return new ChartTitleLine( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ChartTitleLine.withRandomData() { + return new ChartTitleLine(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart( + seriesList, + animate: animate, + // Configures four [ChartTitle] behaviors to render titles in each chart + // margin. The top title has a sub-title, and is aligned to the left edge + // of the chart. The other titles are aligned with the middle of the draw + // area. + behaviors: [ + new charts.ChartTitle('Top title text', + subTitle: 'Top sub-title text', + behaviorPosition: charts.BehaviorPosition.top, + titleOutsideJustification: charts.OutsideJustification.start, + // Set a larger inner padding than the default (10) to avoid + // rendering the text too close to the top measure axis tick label. + // The top tick label may extend upwards into the top margin region + // if it is located at the top of the draw area. + innerPadding: 18), + new charts.ChartTitle('Bottom title text', + behaviorPosition: charts.BehaviorPosition.bottom, + titleOutsideJustification: + charts.OutsideJustification.middleDrawArea), + new charts.ChartTitle('Start title', + behaviorPosition: charts.BehaviorPosition.start, + titleOutsideJustification: + charts.OutsideJustification.middleDrawArea), + new charts.ChartTitle('End title', + behaviorPosition: charts.BehaviorPosition.end, + titleOutsideJustification: + charts.OutsideJustification.middleDrawArea), + ], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/initial_hint_animation.dart b/web/charts/example/lib/behaviors/initial_hint_animation.dart new file mode 100644 index 000000000..cc1e47b99 --- /dev/null +++ b/web/charts/example/lib/behaviors/initial_hint_animation.dart @@ -0,0 +1,175 @@ +// 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. + +/// Example of initial hint animation behavior. +/// +/// To see the animation, please run the example app and select +/// "Initial hint animation". +/// +/// This behavior is intended to be used with charts that also have pan/zoom +/// behaviors added and/or the initial viewport set in [AxisSpec]. +/// +/// Adding this behavior will cause the chart to animate from a scale and/or +/// offset of the desired final viewport. If the user taps the widget prior +/// to the animation being completed, animation will stop. +/// +/// [maxHintScaleFactor] is the amount the domain axis will be scaled at the +/// start of te hint. By default, this is null, indicating that there will be +/// no scale factor hint. A value of 1.0 means the viewport is showing all +/// domains in the viewport. If a value is provided, it cannot be less than 1.0. +/// +/// [maxHintTranslate] is the amount of ordinal values to translate the viewport +/// from the desired initial viewport. Currently only works for ordinal axis. +/// +/// In this example, the series list has ordinal data from year 2014 to 2030, +/// and we have the initial viewport set to start at 2018 that shows 4 values by +/// specifying an [OrdinalViewport] in [OrdinalAxisSpec]. We can add the hint +/// animation by adding behavior [InitialHintBehavior] with [maxHintTranslate] +/// of 4. When the chart is drawn for the first time, the viewport will show +/// 2022 as the first value and the viewport will animate by panning values to +/// the right until 2018 is the first value in the viewport. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class InitialHintAnimation extends StatelessWidget { + final List seriesList; + final bool animate; + + InitialHintAnimation(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory InitialHintAnimation.withSampleData() { + return new InitialHintAnimation( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory InitialHintAnimation.withRandomData() { + return new InitialHintAnimation(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + new OrdinalSales('2018', random.nextInt(100)), + new OrdinalSales('2019', random.nextInt(100)), + new OrdinalSales('2020', random.nextInt(100)), + new OrdinalSales('2021', random.nextInt(100)), + new OrdinalSales('2022', random.nextInt(100)), + new OrdinalSales('2023', random.nextInt(100)), + new OrdinalSales('2024', random.nextInt(100)), + new OrdinalSales('2025', random.nextInt(100)), + new OrdinalSales('2026', random.nextInt(100)), + new OrdinalSales('2027', random.nextInt(100)), + new OrdinalSales('2028', random.nextInt(100)), + new OrdinalSales('2029', random.nextInt(100)), + new OrdinalSales('2030', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Optionally turn off the animation that animates values up from the + // bottom of the domain axis. If animation is on, the bars will animate up + // and then animate to the final viewport. + animationDuration: Duration.zero, + // Set the initial viewport by providing a new AxisSpec with the + // desired viewport: a starting domain and the data size. + domainAxis: new charts.OrdinalAxisSpec( + viewport: new charts.OrdinalViewport('2018', 4)), + behaviors: [ + // Add this behavior to show initial hint animation that will pan to the + // final desired viewport. + // The duration of the animation can be adjusted by pass in + // [hintDuration]. By default this is 3000ms. + new charts.InitialHintBehavior(maxHintTranslate: 4.0), + // Optionally add a pan or pan and zoom behavior. + // If pan/zoom is not added, the viewport specified remains the viewport + new charts.PanAndZoomBehavior(), + ], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + new OrdinalSales('2018', 33), + new OrdinalSales('2019', 80), + new OrdinalSales('2020', 21), + new OrdinalSales('2021', 77), + new OrdinalSales('2022', 8), + new OrdinalSales('2023', 12), + new OrdinalSales('2024', 42), + new OrdinalSales('2025', 70), + new OrdinalSales('2026', 77), + new OrdinalSales('2027', 55), + new OrdinalSales('2028', 19), + new OrdinalSales('2029', 66), + new OrdinalSales('2030', 27), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/initial_selection.dart b/web/charts/example/lib/behaviors/initial_selection.dart new file mode 100644 index 000000000..95573ef92 --- /dev/null +++ b/web/charts/example/lib/behaviors/initial_selection.dart @@ -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. + +/// Example of adding an initial selection behavior. +/// +/// This example adds initial selection to a bar chart, but any chart can use +/// the initial selection behavior. +/// +/// Initial selection is only set on the very first draw and will not be set +/// again in subsequent draws unless the behavior is reconfigured. +/// +/// The selection will remain on the chart unless another behavior is added +/// that updates the selection. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class InitialSelection extends StatelessWidget { + final List seriesList; + final bool animate; + + InitialSelection(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with initial selection behavior. + factory InitialSelection.withSampleData() { + return new InitialSelection( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory InitialSelection.withRandomData() { + return new InitialSelection(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + behaviors: [ + // Initial selection can be configured by passing in: + // + // A list of datum config, specified with series ID and domain value. + // A list of series config, which is a list of series ID(s). + // + // Initial selection can be applied to any chart type. + // + // [BarChart] by default includes behaviors [SelectNearest] and + // [DomainHighlighter]. So this behavior shows the initial selection + // highlighted and when another datum is tapped, the selection changes. + new charts.InitialSelection(selectedDataConfig: [ + new charts.SeriesDatumConfig('Sales', '2016') + ]) + ], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/percent_of_domain.dart b/web/charts/example/lib/behaviors/percent_of_domain.dart new file mode 100644 index 000000000..b9ef4dfcb --- /dev/null +++ b/web/charts/example/lib/behaviors/percent_of_domain.dart @@ -0,0 +1,167 @@ +// 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. + +/// Example of a percentage bar chart with stacked series oriented vertically. +/// +/// Each bar stack shows the percentage of each measure out of the total measure +/// value of the stack. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class PercentOfDomainBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + PercentOfDomainBarChart(this.seriesList, {this.animate}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory PercentOfDomainBarChart.withSampleData() { + return new PercentOfDomainBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PercentOfDomainBarChart.withRandomData() { + return new PercentOfDomainBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.stacked, + // Configures a [PercentInjector] behavior that will calculate measure + // values as the percentage of the total of all data that shares a + // domain value. + behaviors: [ + new charts.PercentInjector( + totalType: charts.PercentInjectorTotalType.domain) + ], + // Configure the axis spec to show percentage values. + primaryMeasureAxis: new charts.PercentAxisSpec(), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/percent_of_domain_by_category.dart b/web/charts/example/lib/behaviors/percent_of_domain_by_category.dart new file mode 100644 index 000000000..648304224 --- /dev/null +++ b/web/charts/example/lib/behaviors/percent_of_domain_by_category.dart @@ -0,0 +1,261 @@ +// 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. + +/// Example of a percentage bar chart with grouped, stacked series oriented +/// vertically. +/// +/// Each bar stack shows the percentage of each measure out of the total measure +/// value of the stack. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class PercentOfDomainByCategoryBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + PercentOfDomainByCategoryBarChart(this.seriesList, {this.animate}); + + factory PercentOfDomainByCategoryBarChart.withSampleData() { + return new PercentOfDomainByCategoryBarChart( + createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PercentOfDomainByCategoryBarChart.withRandomData() { + return new PercentOfDomainByCategoryBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.groupedStacked, + // Configures a [PercentInjector] behavior that will calculate measure + // values as the percentage of the total of all data that shares both a + // domain and a series category. + // + // We use this option on a grouped stacked bar chart to ensure that the + // total value for each bar stack is 100%. A stacked bar chart that does + // not group by series category would use the "domain" option. + behaviors: [ + new charts.PercentInjector( + totalType: charts.PercentInjectorTotalType.domainBySeriesCategory) + ], + // Configure the axis spec to show percentage values. + primaryMeasureAxis: new charts.PercentAxisSpec(), + ); + } + + /// Create series list with multiple series + static List> createSampleData() { + final desktopSalesDataA = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/percent_of_series.dart b/web/charts/example/lib/behaviors/percent_of_series.dart new file mode 100644 index 000000000..60e30c4b1 --- /dev/null +++ b/web/charts/example/lib/behaviors/percent_of_series.dart @@ -0,0 +1,120 @@ +// 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. + +/// Example of a percentage bar chart which shows each bar as the percentage of +/// the total series measure value. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class PercentOfSeriesBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + PercentOfSeriesBarChart(this.seriesList, {this.animate}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory PercentOfSeriesBarChart.withSampleData() { + return new PercentOfSeriesBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PercentOfSeriesBarChart.withRandomData() { + return new PercentOfSeriesBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Configures a [PercentInjector] behavior that will calculate measure + // values as the percentage of the total of all data in its series. + behaviors: [ + new charts.PercentInjector( + totalType: charts.PercentInjectorTotalType.series) + ], + // Configure the axis spec to show percentage values. + primaryMeasureAxis: new charts.PercentAxisSpec(), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2011', 5), + new OrdinalSales('2012', 25), + new OrdinalSales('2013', 50), + new OrdinalSales('2014', 75), + new OrdinalSales('2015', 100), + new OrdinalSales('2016', 125), + new OrdinalSales('2017', 200), + new OrdinalSales('2018', 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/selection_bar_highlight.dart b/web/charts/example/lib/behaviors/selection_bar_highlight.dart new file mode 100644 index 000000000..34bdbc500 --- /dev/null +++ b/web/charts/example/lib/behaviors/selection_bar_highlight.dart @@ -0,0 +1,110 @@ +// 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. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SelectionBarHighlight extends StatelessWidget { + final List seriesList; + final bool animate; + + SelectionBarHighlight(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory SelectionBarHighlight.withSampleData() { + return new SelectionBarHighlight( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionBarHighlight.withRandomData() { + return new SelectionBarHighlight(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // This is just a simple bar chart with optional property + // [defaultInteractions] set to true to include the default + // interactions/behaviors when building the chart. + // This includes bar highlighting. + // + // Note: defaultInteractions defaults to true. + // + // [defaultInteractions] can be set to false to avoid the default + // interactions. + return new charts.BarChart( + seriesList, + animate: animate, + defaultInteractions: true, + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/selection_callback_example.dart b/web/charts/example/lib/behaviors/selection_callback_example.dart new file mode 100644 index 000000000..66c17d896 --- /dev/null +++ b/web/charts/example/lib/behaviors/selection_callback_example.dart @@ -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. + +/// Timeseries chart with example of updating external state based on selection. +/// +/// A SelectionModelConfig can be provided for each of the different +/// [SelectionModel] (currently info and action). +/// +/// [SelectionModelType.info] is the default selection chart exploration type +/// initiated by some tap event. This is a different model from +/// [SelectionModelType.action] which is typically used to select some value as +/// an input to some other UI component. This allows dual state of exploring +/// and selecting data via different touch events. +/// +/// See [SelectNearest] behavior on setting the different ways of triggering +/// [SelectionModel] updates from hover & click events. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SelectionCallbackExample extends StatefulWidget { + final List seriesList; + final bool animate; + + SelectionCallbackExample(this.seriesList, {this.animate}); + + /// Creates a [charts.TimeSeriesChart] with sample data and no transition. + factory SelectionCallbackExample.withSampleData() { + return new SelectionCallbackExample( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionCallbackExample.withRandomData() { + return new SelectionCallbackExample(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final us_data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + final uk_data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'US Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: us_data, + ), + new charts.Series( + id: 'UK Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: uk_data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // We need a Stateful widget to build the selection details with the current + // selection as the state. + @override + State createState() => new _SelectionCallbackState(); + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final us_data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 78), + new TimeSeriesSales(new DateTime(2017, 10, 10), 54), + ]; + + final uk_data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 15), + new TimeSeriesSales(new DateTime(2017, 9, 26), 33), + new TimeSeriesSales(new DateTime(2017, 10, 3), 68), + new TimeSeriesSales(new DateTime(2017, 10, 10), 48), + ]; + + return [ + new charts.Series( + id: 'US Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: us_data, + ), + new charts.Series( + id: 'UK Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: uk_data, + ) + ]; + } +} + +class _SelectionCallbackState extends State { + DateTime _time; + Map _measures; + + // Listens to the underlying selection changes, and updates the information + // relevant to building the primitive legend like information under the + // chart. + _onSelectionChanged(charts.SelectionModel model) { + final selectedDatum = model.selectedDatum; + + DateTime time; + final measures = {}; + + // We get the model that updated with a list of [SeriesDatum] which is + // simply a pair of series & datum. + // + // Walk the selection updating the measures map, storing off the sales and + // series name for each selection point. + if (selectedDatum.isNotEmpty) { + time = selectedDatum.first.datum.time; + selectedDatum.forEach((charts.SeriesDatum datumPair) { + measures[datumPair.series.displayName] = datumPair.datum.sales; + }); + } + + // Request a build. + setState(() { + _time = time; + _measures = measures; + }); + } + + @override + Widget build(BuildContext context) { + // The children consist of a Chart and Text widgets below to hold the info. + final children = [ + new SizedBox( + height: 150.0, + child: new charts.TimeSeriesChart( + widget.seriesList, + animate: widget.animate, + selectionModels: [ + new charts.SelectionModelConfig( + type: charts.SelectionModelType.info, + changedListener: _onSelectionChanged, + ) + ], + )), + ]; + + // If there is a selection, then include the details. + if (_time != null) { + children.add(new Padding( + padding: new EdgeInsets.only(top: 5.0), + child: new Text(_time.toString()))); + } + _measures?.forEach((String series, num value) { + children.add(new Text('${series}: ${value}')); + }); + + return new Column(children: children); + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/web/charts/example/lib/behaviors/selection_line_highlight.dart b/web/charts/example/lib/behaviors/selection_line_highlight.dart new file mode 100644 index 000000000..a1964049f --- /dev/null +++ b/web/charts/example/lib/behaviors/selection_line_highlight.dart @@ -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. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SelectionLineHighlight extends StatelessWidget { + final List seriesList; + final bool animate; + + SelectionLineHighlight(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory SelectionLineHighlight.withSampleData() { + return new SelectionLineHighlight( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionLineHighlight.withRandomData() { + return new SelectionLineHighlight(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // This is just a simple line chart with a behavior that highlights the + // selected points along the lines. A point will be drawn at the selected + // datum's x,y coordinate, and a vertical follow line will be drawn through + // it. + // + // A [Charts.LinePointHighlighter] behavior is added manually to enable the + // highlighting effect. + // + // As an alternative, [defaultInteractions] can be set to true to include + // the default chart interactions, including a LinePointHighlighter. + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + // Optional - Configures a [LinePointHighlighter] behavior with a + // vertical follow line. A vertical follow line is included by + // default, but is shown here as an example configuration. + // + // By default, the line has default dash pattern of [1,3]. This can be + // set by providing a [dashPattern] or it can be turned off by passing in + // an empty list. An empty list is necessary because passing in a null + // value will be treated the same as not passing in a value at all. + new charts.LinePointHighlighter( + showHorizontalFollowLine: + charts.LinePointHighlighterFollowLineType.none, + showVerticalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest), + // Optional - By default, select nearest is configured to trigger + // with tap so that a user can have pan/zoom behavior and line point + // highlighter. Changing the trigger to tap and drag allows the + // highlighter to follow the dragging gesture but it is not + // recommended to be used when pan/zoom behavior is enabled. + new charts.SelectNearest(eventTrigger: charts.SelectionTrigger.tapAndDrag) + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/selection_line_highlight_custom_shape.dart b/web/charts/example/lib/behaviors/selection_line_highlight_custom_shape.dart new file mode 100644 index 000000000..843d22488 --- /dev/null +++ b/web/charts/example/lib/behaviors/selection_line_highlight_custom_shape.dart @@ -0,0 +1,130 @@ +// 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. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SelectionLineHighlightCustomShape extends StatelessWidget { + final List seriesList; + final bool animate; + + SelectionLineHighlightCustomShape(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory SelectionLineHighlightCustomShape.withSampleData() { + return new SelectionLineHighlightCustomShape( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionLineHighlightCustomShape.withRandomData() { + return new SelectionLineHighlightCustomShape(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // This is a simple line chart with a behavior that highlights hovered + // lines. A hollow rectangular shape will be drawn at the hovered datum's + // x,y coordinate, and a vertical follow line will be drawn through it. + // + // A [Charts.LinePointHighlighter] behavior is added manually to enable the + // highlighting effect. + // + // As an alternative, [defaultInteractions] can be set to true to include + // the default chart interactions, including a LinePointHighlighter. + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + // Optional - Configures a [LinePointHighlighter] behavior with a + // vertical follow line. A vertical follow line is included by + // default, but is shown here as an example configuration. + // + // By default, the line has default dash pattern of [1,3]. This can be + // set by providing a [dashPattern] or it can be turned off by passing in + // an empty list. An empty list is necessary because passing in a null + // value will be treated the same as not passing in a value at all. + // + // The symbol renderer is configured to render a hollow shape, for + // demonstration. + new charts.LinePointHighlighter( + showHorizontalFollowLine: + charts.LinePointHighlighterFollowLineType.none, + showVerticalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest, + symbolRenderer: new charts.RectSymbolRenderer(isSolid: false)), + // Optional - By default, select nearest is configured to trigger + // with tap so that a user can have pan/zoom behavior and line point + // highlighter. Changing the trigger to tap and drag allows the + // highlighter to follow the dragging gesture but it is not + // recommended to be used when pan/zoom behavior is enabled. + new charts.SelectNearest(eventTrigger: charts.SelectionTrigger.tapAndDrag) + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/selection_scatter_plot_highlight.dart b/web/charts/example/lib/behaviors/selection_scatter_plot_highlight.dart new file mode 100644 index 000000000..10657c648 --- /dev/null +++ b/web/charts/example/lib/behaviors/selection_scatter_plot_highlight.dart @@ -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. + +/// Example of a scatter plot chart using custom symbols for the points and a +/// behavior that highlights selected points. +/// +/// An optional [charts.LinePointHighlighter] behavior has been added to enable +/// a highlighting effect. This behavior will draw a larger symbol on top of the +/// point nearest to the point where a user taps on the chart. It will also draw +/// follow lines. +/// +/// The series has been configured to draw each point as a square by default. +/// +/// Some data will be drawn as a circle, indicated by defining a custom "circle" +/// value referenced by [charts.pointSymbolRendererFnKey]. +/// +/// Some other data have will be drawn as a hollow circle. In addition to the +/// custom renderer key, these data also have stroke and fillColor values +/// defined. Configuring a separate fillColor will cause the center of the shape +/// to be filled in, with white in these examples. The border of the shape will +/// be color with the color of the data. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SelectionScatterPlotHighlight extends StatelessWidget { + final List seriesList; + final bool animate; + + SelectionScatterPlotHighlight(this.seriesList, {this.animate}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory SelectionScatterPlotHighlight.withSampleData() { + return new SelectionScatterPlotHighlight( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionScatterPlotHighlight.withRandomData() { + return new SelectionScatterPlotHighlight(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + final data = [ + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + 'circle', null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + 'circle', null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow square, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, null, charts.MaterialPalette.white, 2.0), + ]; + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + fillColorFn: (LinearSales row, _) => row.fillColor, + strokeWidthPxFn: (LinearSales row, _) => row.strokeWidth, + data: data, + ) + // Accessor function that associates each datum with a symbol renderer. + ..setAttribute( + charts.pointSymbolRendererFnKey, (int index) => data[index].shape) + // Default symbol renderer ID for data that have no defined shape. + ..setAttribute(charts.pointSymbolRendererIdKey, 'rect') + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + behaviors: [ + // Optional - Configures a [LinePointHighlighter] behavior with + // horizontal and vertical follow lines. The highlighter will increase + // the size of the selected points on the chart. + // + // By default, the line has default dash pattern of [1,3]. This can be + // set by providing a [dashPattern] or it can be turned off by passing + // in an empty list. An empty list is necessary because passing in a + // null value will be treated the same as not passing in a value at + // all. + new charts.LinePointHighlighter( + showHorizontalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest, + showVerticalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest), + // Optional - By default, select nearest is configured to trigger + // with tap so that a user can have pan/zoom behavior and line point + // highlighter. Changing the trigger to tap and drag allows the + // highlighter to follow the dragging gesture but it is not + // recommended to be used when pan/zoom behavior is enabled. + new charts.SelectNearest( + eventTrigger: charts.SelectionTrigger.tapAndDrag), + ], + // Configure the point renderer to have a map of custom symbol + // renderers. + defaultRenderer: + new charts.PointRendererConfig(customSymbolRenderers: { + 'circle': new charts.CircleSymbolRenderer(), + 'rect': new charts.RectSymbolRenderer(), + })); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5, 3.0, 'circle', null, null), + new LinearSales(10, 25, 5.0, null, null, null), + new LinearSales(12, 75, 4.0, null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales( + 13, 225, 5.0, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(16, 50, 4.0, null, null, null), + new LinearSales(24, 75, 3.0, null, null, null), + new LinearSales(25, 100, 3.0, 'circle', null, null), + new LinearSales(34, 150, 5.0, null, null, null), + new LinearSales(37, 10, 4.5, null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales( + 45, 300, 8.0, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(52, 15, 4.0, null, null, null), + // Render a hollow square, filled in with white. + new LinearSales(56, 200, 7.0, null, charts.MaterialPalette.white, 2.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + fillColorFn: (LinearSales row, _) => row.fillColor, + strokeWidthPxFn: (LinearSales row, _) => row.strokeWidth, + data: data, + ) + // Accessor function that associates each datum with a symbol renderer. + ..setAttribute( + charts.pointSymbolRendererFnKey, (int index) => data[index].shape) + // Default symbol renderer ID for data that have no defined shape. + ..setAttribute(charts.pointSymbolRendererIdKey, 'rect') + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + final String shape; + final charts.Color fillColor; + final double strokeWidth; + + LinearSales(this.year, this.sales, this.radius, this.shape, this.fillColor, + this.strokeWidth); +} diff --git a/web/charts/example/lib/behaviors/selection_user_managed.dart b/web/charts/example/lib/behaviors/selection_user_managed.dart new file mode 100644 index 000000000..5293b895e --- /dev/null +++ b/web/charts/example/lib/behaviors/selection_user_managed.dart @@ -0,0 +1,164 @@ +// 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. + +/// Example of using user managed state to programmatically set selection. +/// +/// In this example, clicking the "clear selection" button sets the selection +/// to an empty selection. This example also shows that initial selection +/// behavior can still be used with user managed state. +/// +/// Note that the picture in this example is not interactive, please run the +/// gallery app to try out using the button to clear selection. +/// +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SelectionUserManaged extends StatefulWidget { + final List seriesList; + final bool animate; + + SelectionUserManaged(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory SelectionUserManaged.withSampleData() { + return new SelectionUserManaged( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionUserManaged.withRandomData() { + return new SelectionUserManaged(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + + @override + SelectionUserManagedState createState() { + return new SelectionUserManagedState(); + } +} + +class SelectionUserManagedState extends State { + final _myState = new charts.UserManagedState(); + + @override + Widget build(BuildContext context) { + final chart = new charts.BarChart( + widget.seriesList, + animate: false, //widget.animate, + selectionModels: [ + new charts.SelectionModelConfig( + type: charts.SelectionModelType.info, + updatedListener: _infoSelectionModelUpdated) + ], + // Pass in the state you manage to the chart. This will be used to + // override the internal chart state. + userManagedState: _myState, + // The initial selection can still be optionally added by adding the + // initial selection behavior. + behaviors: [ + new charts.InitialSelection(selectedDataConfig: [ + new charts.SeriesDatumConfig('Sales', '2016') + ]) + ], + ); + + final clearSelection = new MaterialButton( + onPressed: _handleClearSelection, child: new Text('Clear Selection')); + + return new Column( + children: [new SizedBox(child: chart, height: 150.0), clearSelection]); + } + + void _infoSelectionModelUpdated(charts.SelectionModel model) { + // If you want to allow the chart to continue to respond to select events + // that update the selection, add an updatedListener that saves off the + // selection model each time the selection model is updated, regardless of + // if there are changes. + // + // This also allows you to listen to the selection model update events and + // alter the selection. + _myState.selectionModels[charts.SelectionModelType.info] = + new charts.UserManagedSelectionModel(model: model); + } + + void _handleClearSelection() { + // Call set state to request a rebuild, to pass in the modified selection. + // In this case, passing in an empty [UserManagedSelectionModel] creates a + // no selection model to clear all selection when rebuilt. + setState(() { + _myState.selectionModels[charts.SelectionModelType.info] = + new charts.UserManagedSelectionModel(); + }); + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/slider.dart b/web/charts/example/lib/behaviors/slider.dart new file mode 100644 index 000000000..4099d64e2 --- /dev/null +++ b/web/charts/example/lib/behaviors/slider.dart @@ -0,0 +1,196 @@ +// 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. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; +import 'package:flutter_web/scheduler.dart'; + +/// This is just a simple line chart with a behavior that adds slider controls. +/// +/// A [Slider] behavior is added manually to enable slider controls, with an +/// initial position at 1 along the domain axis. +/// +/// An onChange event handler has been configured to demonstrate updating a div +/// with data from the slider's current position. An "initial" drag state event +/// will be fired when the chart is drawn because an initial domain value is +/// set. +/// +/// [Slider.moveSliderToDomain] can be called to programmatically position the +/// slider. This is useful for synchronizing the slider with external elements. +class SliderLine extends StatefulWidget { + final List seriesList; + final bool animate; + + SliderLine(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory SliderLine.withSampleData() { + return new SliderLine( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SliderLine.withRandomData() { + return new SliderLine(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // We need a Stateful widget to build the selection details with the current + // selection as the state. + @override + State createState() => new _SliderCallbackState(); + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +class _SliderCallbackState extends State { + num _sliderDomainValue; + String _sliderDragState; + Point _sliderPosition; + + // Handles callbacks when the user drags the slider. + _onSliderChange(Point point, dynamic domain, String roleId, + charts.SliderListenerDragState dragState) { + // Request a build. + void rebuild(_) { + setState(() { + _sliderDomainValue = (domain * 10).round() / 10; + _sliderDragState = dragState.toString(); + _sliderPosition = point; + }); + } + + SchedulerBinding.instance.addPostFrameCallback(rebuild); + } + + @override + Widget build(BuildContext context) { + // The children consist of a Chart and Text widgets below to hold the info. + final children = [ + new SizedBox( + height: 150.0, + child: new charts.LineChart( + widget.seriesList, + animate: widget.animate, + // Configures a [Slider] behavior. + // + // Available options include: + // + // [eventTrigger] configures the type of mouse gesture that controls + // 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] takes in a [SliderStyle] configuration object, and + // configures the color and sizing of the slider line and handle. + behaviors: [ + new charts.Slider( + initialDomainValue: 1.0, onChangeCallback: _onSliderChange), + ], + )), + ]; + + // If there is a slider change event, then include the details. + if (_sliderDomainValue != null) { + children.add(new Padding( + padding: new EdgeInsets.only(top: 5.0), + child: new Text('Slider domain value: ${_sliderDomainValue}'))); + } + if (_sliderPosition != null) { + children.add(new Padding( + padding: new EdgeInsets.only(top: 5.0), + child: new Text( + 'Slider position: ${_sliderPosition.x}, ${_sliderPosition.y}'))); + } + if (_sliderDragState != null) { + children.add(new Padding( + padding: new EdgeInsets.only(top: 5.0), + child: new Text('Slider drag state: ${_sliderDragState}'))); + } + + return new Column(children: children); + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/behaviors/sliding_viewport_on_selection.dart b/web/charts/example/lib/behaviors/sliding_viewport_on_selection.dart new file mode 100644 index 000000000..307f61535 --- /dev/null +++ b/web/charts/example/lib/behaviors/sliding_viewport_on_selection.dart @@ -0,0 +1,144 @@ +// 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. + +/// Example of the chart behavior that centers the viewport on domain selection. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SlidingViewportOnSelection extends StatelessWidget { + final List seriesList; + final bool animate; + + SlidingViewportOnSelection(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory SlidingViewportOnSelection.withSampleData() { + return new SlidingViewportOnSelection( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SlidingViewportOnSelection.withRandomData() { + return new SlidingViewportOnSelection(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + new OrdinalSales('2018', random.nextInt(100)), + new OrdinalSales('2019', random.nextInt(100)), + new OrdinalSales('2020', random.nextInt(100)), + new OrdinalSales('2021', random.nextInt(100)), + new OrdinalSales('2022', random.nextInt(100)), + new OrdinalSales('2023', random.nextInt(100)), + new OrdinalSales('2024', random.nextInt(100)), + new OrdinalSales('2025', random.nextInt(100)), + new OrdinalSales('2026', random.nextInt(100)), + new OrdinalSales('2027', random.nextInt(100)), + new OrdinalSales('2028', random.nextInt(100)), + new OrdinalSales('2029', random.nextInt(100)), + new OrdinalSales('2030', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + behaviors: [ + // Add the sliding viewport behavior to have the viewport center on the + // domain that is currently selected. + new charts.SlidingViewport(), + // A pan and zoom behavior helps demonstrate the sliding viewport + // behavior by allowing the data visible in the viewport to be adjusted + // dynamically. + new charts.PanAndZoomBehavior(), + ], + // Set an initial viewport to demonstrate the sliding viewport behavior on + // initial chart load. + domainAxis: new charts.OrdinalAxisSpec( + viewport: new charts.OrdinalViewport('2018', 4)), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + new OrdinalSales('2018', 33), + new OrdinalSales('2019', 80), + new OrdinalSales('2020', 21), + new OrdinalSales('2021', 77), + new OrdinalSales('2022', 8), + new OrdinalSales('2023', 12), + new OrdinalSales('2024', 42), + new OrdinalSales('2025', 70), + new OrdinalSales('2026', 77), + new OrdinalSales('2027', 55), + new OrdinalSales('2028', 19), + new OrdinalSales('2029', 66), + new OrdinalSales('2030', 27), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/combo_chart/combo_gallery.dart b/web/charts/example/lib/combo_chart/combo_gallery.dart new file mode 100644 index 000000000..487ccf0ef --- /dev/null +++ b/web/charts/example/lib/combo_chart/combo_gallery.dart @@ -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 'package:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'date_time_line_point.dart'; +import 'numeric_line_bar.dart'; +import 'numeric_line_point.dart'; +import 'ordinal_bar_line.dart'; +import 'scatter_plot_line.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Ordinal Combo Chart', + subtitle: 'Ordinal combo chart with bars and lines', + childBuilder: () => new OrdinalComboBarLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Numeric Line Bar Combo Chart', + subtitle: 'Numeric combo chart with lines and bars', + childBuilder: () => new NumericComboLineBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Numeric Line Points Combo Chart', + subtitle: 'Numeric combo chart with lines and points', + childBuilder: () => new NumericComboLinePointChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Time Series Combo Chart', + subtitle: 'Time series combo chart with lines and points', + childBuilder: () => new DateTimeComboLinePointChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Scatter Plot Combo Chart', + subtitle: 'Scatter plot combo chart with a line', + childBuilder: () => new ScatterPlotComboLineChart.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/combo_chart/date_time_line_point.dart b/web/charts/example/lib/combo_chart/date_time_line_point.dart new file mode 100644 index 000000000..86c523f50 --- /dev/null +++ b/web/charts/example/lib/combo_chart/date_time_line_point.dart @@ -0,0 +1,183 @@ +// 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. + +/// Example of a combo time series chart with two series rendered as lines, and +/// a third rendered as points along the top line with a different color. +/// +/// This example demonstrates a method for drawing points along a line using a +/// different color from the main series color. The line renderer supports +/// drawing points with the "includePoints" option, but those points will share +/// the same color as the line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class DateTimeComboLinePointChart extends StatelessWidget { + final List seriesList; + final bool animate; + + DateTimeComboLinePointChart(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory DateTimeComboLinePointChart.withSampleData() { + return new DateTimeComboLinePointChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DateTimeComboLinePointChart.withRandomData() { + return new DateTimeComboLinePointChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + final tableSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + final mobileSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), tableSalesData[0].sales), + new TimeSeriesSales(new DateTime(2017, 9, 26), tableSalesData[1].sales), + new TimeSeriesSales(new DateTime(2017, 10, 3), tableSalesData[2].sales), + new TimeSeriesSales(new DateTime(2017, 10, 10), tableSalesData[3].sales), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom point renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customPoint'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Configure the default renderer as a line renderer. This will be used + // for any series that does not define a rendererIdKey. + // + // This is the default configuration, but is shown here for illustration. + defaultRenderer: new charts.LineRendererConfig(), + // Custom renderer configuration for the point series. + customSeriesRenderers: [ + new charts.PointRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customPoint') + ], + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final desktopSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + final tableSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 10), + new TimeSeriesSales(new DateTime(2017, 9, 26), 50), + new TimeSeriesSales(new DateTime(2017, 10, 3), 200), + new TimeSeriesSales(new DateTime(2017, 10, 10), 150), + ]; + + final mobileSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 10), + new TimeSeriesSales(new DateTime(2017, 9, 26), 50), + new TimeSeriesSales(new DateTime(2017, 10, 3), 200), + new TimeSeriesSales(new DateTime(2017, 10, 10), 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom point renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customPoint'), + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/web/charts/example/lib/combo_chart/numeric_line_bar.dart b/web/charts/example/lib/combo_chart/numeric_line_bar.dart new file mode 100644 index 000000000..37b9b7c3e --- /dev/null +++ b/web/charts/example/lib/combo_chart/numeric_line_bar.dart @@ -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. + +/// Example of a numeric combo chart with two series rendered as bars, and a +/// third rendered as a line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class NumericComboLineBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + NumericComboLineBarChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory NumericComboLineBarChart.withSampleData() { + return new NumericComboLineBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory NumericComboLineBarChart.withRandomData() { + return new NumericComboLineBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + final tableSalesData = [ + new LinearSales(0, desktopSalesData[0].sales), + new LinearSales(1, desktopSalesData[1].sales), + new LinearSales(2, desktopSalesData[2].sales), + new LinearSales(3, desktopSalesData[3].sales), + ]; + + final mobileSalesData = [ + new LinearSales(0, tableSalesData[0].sales * 2), + new LinearSales(1, tableSalesData[1].sales * 2), + new LinearSales(2, tableSalesData[2].sales * 2), + new LinearSales(3, tableSalesData[3].sales * 2), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: desktopSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: tableSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: mobileSalesData), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.NumericComboChart(seriesList, + animate: animate, + // Configure the default renderer as a line renderer. This will be used + // for any series that does not define a rendererIdKey. + defaultRenderer: new charts.LineRendererConfig(), + // Custom renderer configuration for the bar series. + customSeriesRenderers: [ + new charts.BarRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customBar') + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final desktopSalesData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + final tableSalesData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + final mobileSalesData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: desktopSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: tableSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: mobileSalesData), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/combo_chart/numeric_line_point.dart b/web/charts/example/lib/combo_chart/numeric_line_point.dart new file mode 100644 index 000000000..60396dc76 --- /dev/null +++ b/web/charts/example/lib/combo_chart/numeric_line_point.dart @@ -0,0 +1,175 @@ +// 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. + +/// Example of a numeric combo chart with two series rendered as lines, and a +/// third rendered as points along the top line with a different color. +/// +/// This example demonstrates a method for drawing points along a line using a +/// different color from the main series color. The line renderer supports +/// drawing points with the "includePoints" option, but those points will share +/// the same color as the line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class NumericComboLinePointChart extends StatelessWidget { + final List seriesList; + final bool animate; + + NumericComboLinePointChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory NumericComboLinePointChart.withSampleData() { + return new NumericComboLinePointChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory NumericComboLinePointChart.withRandomData() { + return new NumericComboLinePointChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + final tableSalesData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + final mobileSalesData = [ + new LinearSales(0, tableSalesData[0].sales), + new LinearSales(1, tableSalesData[1].sales), + new LinearSales(2, tableSalesData[2].sales), + new LinearSales(3, tableSalesData[3].sales), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom point renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customPoint'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.NumericComboChart(seriesList, + animate: animate, + // Configure the default renderer as a line renderer. This will be used + // for any series that does not define a rendererIdKey. + defaultRenderer: new charts.LineRendererConfig(), + // Custom renderer configuration for the point series. + customSeriesRenderers: [ + new charts.PointRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customPoint') + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final desktopSalesData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + final tableSalesData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + final mobileSalesData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom point renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customPoint'), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/combo_chart/ordinal_bar_line.dart b/web/charts/example/lib/combo_chart/ordinal_bar_line.dart new file mode 100644 index 000000000..91fda1223 --- /dev/null +++ b/web/charts/example/lib/combo_chart/ordinal_bar_line.dart @@ -0,0 +1,166 @@ +// 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. + +/// Example of an ordinal combo chart with two series rendered as bars, and a +/// third rendered as a line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class OrdinalComboBarLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + OrdinalComboBarLineChart(this.seriesList, {this.animate}); + + factory OrdinalComboBarLineChart.withSampleData() { + return new OrdinalComboBarLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory OrdinalComboBarLineChart.withRandomData() { + return new OrdinalComboBarLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom line renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.OrdinalComboChart(seriesList, + animate: animate, + // Configure the default renderer as a bar renderer. + defaultRenderer: new charts.BarRendererConfig( + groupingType: charts.BarGroupingType.grouped), + // Custom renderer configuration for the line series. This will be used for + // any series that does not define a rendererIdKey. + customSeriesRenderers: [ + new charts.LineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customLine') + ]); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 200), + new OrdinalSales('2017', 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData), + new charts.Series( + id: 'Mobile ', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom line renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customLine'), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/combo_chart/scatter_plot_line.dart b/web/charts/example/lib/combo_chart/scatter_plot_line.dart new file mode 100644 index 000000000..ce542f614 --- /dev/null +++ b/web/charts/example/lib/combo_chart/scatter_plot_line.dart @@ -0,0 +1,198 @@ +// 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. + +/// Example of a combo scatter plot chart with a second series rendered as a +/// line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class ScatterPlotComboLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + ScatterPlotComboLineChart(this.seriesList, {this.animate}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory ScatterPlotComboLineChart.withSampleData() { + return new ScatterPlotComboLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ScatterPlotComboLineChart.withRandomData() { + return new ScatterPlotComboLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + final desktopSalesData = [ + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + ]; + + var myRegressionData = [ + new LinearSales(0, desktopSalesData[0].sales, 3.5), + new LinearSales( + 100, desktopSalesData[desktopSalesData.length - 1].sales, 7.5), + ]; + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + // Providing a radius function is optional. + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: desktopSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myRegressionData) + // Configure our custom line renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + // Configure the default renderer as a point renderer. This will be used + // for any series that does not define a rendererIdKey. + // + // This is the default configuration, but is shown here for + // illustration. + defaultRenderer: new charts.PointRendererConfig(), + // Custom renderer configuration for the line series. + customSeriesRenderers: [ + new charts.LineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customLine', + // Configure the regression line to be painted above the points. + // + // By default, series drawn by the point renderer are painted on + // top of those drawn by a line renderer. + layoutPaintOrder: charts.LayoutViewPaintOrder.point + 1) + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final desktopSalesData = [ + new LinearSales(0, 5, 3.0), + new LinearSales(10, 25, 5.0), + new LinearSales(12, 75, 4.0), + new LinearSales(13, 225, 5.0), + new LinearSales(16, 50, 4.0), + new LinearSales(24, 75, 3.0), + new LinearSales(25, 100, 3.0), + new LinearSales(34, 150, 5.0), + new LinearSales(37, 10, 4.5), + new LinearSales(45, 300, 8.0), + new LinearSales(52, 15, 4.0), + new LinearSales(56, 200, 7.0), + ]; + + var myRegressionData = [ + new LinearSales(0, 5, 3.5), + new LinearSales(56, 240, 3.5), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + // Providing a radius function is optional. + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: desktopSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myRegressionData) + // Configure our custom line renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customLine'), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + + LinearSales(this.year, this.sales, this.radius); +} diff --git a/web/charts/example/lib/drawer.dart b/web/charts/example/lib/drawer.dart new file mode 100644 index 000000000..1e76c7dcb --- /dev/null +++ b/web/charts/example/lib/drawer.dart @@ -0,0 +1,51 @@ +// 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:flutter_web/material.dart'; + +/// A menu drawer supporting toggling theme and performance overlay. +class GalleryDrawer extends StatelessWidget { + final bool showPerformanceOverlay; + final ValueChanged onShowPerformanceOverlayChanged; + + GalleryDrawer( + {Key key, + this.showPerformanceOverlay, + this.onShowPerformanceOverlayChanged}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return new Drawer( + child: new ListView(children: [ + // Performance overlay toggle. + new ListTile( + leading: new Icon(Icons.assessment), + title: new Text('Performance Overlay'), + onTap: () { + onShowPerformanceOverlayChanged(!showPerformanceOverlay); + }, + selected: showPerformanceOverlay, + trailing: new Checkbox( + value: showPerformanceOverlay, + onChanged: (bool value) { + onShowPerformanceOverlayChanged(!showPerformanceOverlay); + }, + ), + ), + ]), + ); + } +} diff --git a/web/charts/example/lib/gallery_scaffold.dart b/web/charts/example/lib/gallery_scaffold.dart new file mode 100644 index 000000000..37c428cb5 --- /dev/null +++ b/web/charts/example/lib/gallery_scaffold.dart @@ -0,0 +1,62 @@ +// 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:flutter_web/material.dart'; + +typedef Widget GalleryWidgetBuilder(); + +/// Helper to build gallery. +class GalleryScaffold extends StatefulWidget { + /// The widget used for leading in a [ListTile]. + final Widget listTileIcon; + final String title; + final String subtitle; + final GalleryWidgetBuilder childBuilder; + + GalleryScaffold( + {this.listTileIcon, this.title, this.subtitle, this.childBuilder}); + + /// Gets the gallery + Widget buildGalleryListTile(BuildContext context) => new ListTile( + leading: listTileIcon, + title: new Text(title), + subtitle: new Text(subtitle), + onTap: () { + Navigator.push(context, new MaterialPageRoute(builder: (_) => this)); + }); + + @override + _GalleryScaffoldState createState() => new _GalleryScaffoldState(); +} + +class _GalleryScaffoldState extends State { + void _handleButtonPress() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return new Scaffold( + appBar: new AppBar(title: new Text(widget.title)), + body: new Padding( + padding: const EdgeInsets.all(8.0), + child: new Column(children: [ + new SizedBox(height: 250.0, child: widget.childBuilder()), + ])), + floatingActionButton: new FloatingActionButton( + child: new Icon(Icons.refresh), onPressed: _handleButtonPress), + ); + } +} diff --git a/web/charts/example/lib/home.dart b/web/charts/example/lib/home.dart new file mode 100644 index 000000000..ce3e2ca0f --- /dev/null +++ b/web/charts/example/lib/home.dart @@ -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 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; +import 'dart:developer'; +import 'app_config.dart'; +import 'drawer.dart'; +import 'a11y/a11y_gallery.dart' as a11y show buildGallery; +import 'bar_chart/bar_gallery.dart' as bar show buildGallery; +import 'time_series_chart/time_series_gallery.dart' as time_series + show buildGallery; +import 'line_chart/line_gallery.dart' as line show buildGallery; +import 'scatter_plot_chart/scatter_plot_gallery.dart' as scatter_plot + show buildGallery; +import 'combo_chart/combo_gallery.dart' as combo show buildGallery; +import 'pie_chart/pie_gallery.dart' as pie show buildGallery; +import 'axes/axes_gallery.dart' as axes show buildGallery; +import 'behaviors/behaviors_gallery.dart' as behaviors show buildGallery; +import 'i18n/i18n_gallery.dart' as i18n show buildGallery; +import 'legends/legends_gallery.dart' as legends show buildGallery; + +/// Main entry point of the gallery app. +/// +/// This renders a list of all available demos. +class Home extends StatelessWidget { + final bool showPerformanceOverlay; + final ValueChanged onShowPerformanceOverlayChanged; + final a11yGalleries = a11y.buildGallery(); + final barGalleries = bar.buildGallery(); + final timeSeriesGalleries = time_series.buildGallery(); + final lineGalleries = line.buildGallery(); + final scatterPlotGalleries = scatter_plot.buildGallery(); + final comboGalleries = combo.buildGallery(); + final pieGalleries = pie.buildGallery(); + final axesGalleries = axes.buildGallery(); + final behaviorsGalleries = behaviors.buildGallery(); + final i18nGalleries = i18n.buildGallery(); + final legendsGalleries = legends.buildGallery(); + + Home( + {Key key, + this.showPerformanceOverlay, + this.onShowPerformanceOverlayChanged}) + : super(key: key) { + assert(onShowPerformanceOverlayChanged != null); + } + + @override + Widget build(BuildContext context) { + var galleries = []; + + galleries.addAll( + a11yGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example bar charts. + galleries.addAll( + barGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example time series charts. + galleries.addAll(timeSeriesGalleries + .map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example line charts. + galleries.addAll( + lineGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example scatter plot charts. + galleries.addAll(scatterPlotGalleries + .map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example pie charts. + galleries.addAll( + comboGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example pie charts. + galleries.addAll( + pieGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example custom axis. + galleries.addAll( + axesGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + galleries.addAll(behaviorsGalleries + .map((gallery) => gallery.buildGalleryListTile(context))); + + // Add legends examples + galleries.addAll(legendsGalleries + .map((gallery) => gallery.buildGalleryListTile(context))); + + // Add examples for i18n. + galleries.addAll( + i18nGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + _setupPerformance(); + + return new Scaffold( + drawer: new GalleryDrawer( + showPerformanceOverlay: showPerformanceOverlay, + onShowPerformanceOverlayChanged: onShowPerformanceOverlayChanged), + appBar: new AppBar(title: new Text(defaultConfig.appName)), + body: new ListView(padding: kMaterialListPadding, children: galleries), + ); + } + + void _setupPerformance() { + // Change [printPerformance] to true and set the app to release mode to + // print performance numbers to console. By default, Flutter builds in debug + // mode and this mode is slow. To build in release mode, specify the flag + // blaze-run flag "--define flutter_build_mode=release". + // The build target must also be an actual device and not the emulator. + charts.Performance.time = (String tag) => Timeline.startSync(tag); + charts.Performance.timeEnd = (_) => Timeline.finishSync(); + } +} diff --git a/web/charts/example/lib/i18n/i18n_gallery.dart b/web/charts/example/lib/i18n/i18n_gallery.dart new file mode 100644 index 000000000..09ab41d08 --- /dev/null +++ b/web/charts/example/lib/i18n/i18n_gallery.dart @@ -0,0 +1,50 @@ +// 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:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'rtl_bar_chart.dart'; +import 'rtl_line_chart.dart'; +import 'rtl_line_segments.dart'; +import 'rtl_series_legend.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'RTL Bar Chart', + subtitle: 'Simple bar chart in RTL', + childBuilder: () => new RTLBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'RTL Line Chart', + subtitle: 'Simple line chart in RTL', + childBuilder: () => new RTLLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'RTL Line Segments', + subtitle: 'Stacked area chart with style segments in RTL', + childBuilder: () => new RTLLineSegments.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'RTL Series Legend', + subtitle: 'Series legend in RTL', + childBuilder: () => new RTLSeriesLegend.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/i18n/rtl_bar_chart.dart b/web/charts/example/lib/i18n/rtl_bar_chart.dart new file mode 100644 index 000000000..bb090bb12 --- /dev/null +++ b/web/charts/example/lib/i18n/rtl_bar_chart.dart @@ -0,0 +1,119 @@ +// 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. + +/// RTL Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class RTLBarChart extends StatelessWidget { + final List seriesList; + final bool animate; + + RTLBarChart(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory RTLBarChart.withSampleData() { + return new RTLBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory RTLBarChart.withRandomData() { + return new RTLBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Charts will determine if RTL is enabled by checking the directionality by + // requesting Directionality.of(context). This returns the text direction + // from the closest instance of that encloses the context passed to build + // the chart. A [TextDirection.rtl] will be treated as a RTL chart. This + // means that the directionality widget does not have to directly wrap each + // chart. It is show here as an example only. + // + // By default, when a chart detects RTL: + // Measure axis positions are flipped. Primary measure axis is on the right + // and the secondary measure axis is on the left (when used). + // Domain axis' first domain starts on the right and grows left. + // + // Optionally, [RTLSpec] can be passed in when creating the chart to specify + // chart display settings in RTL mode. + return new Directionality( + textDirection: TextDirection.rtl, + child: new charts.BarChart( + seriesList, + animate: animate, + vertical: false, + )); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/i18n/rtl_line_chart.dart b/web/charts/example/lib/i18n/rtl_line_chart.dart new file mode 100644 index 000000000..aca6e2b52 --- /dev/null +++ b/web/charts/example/lib/i18n/rtl_line_chart.dart @@ -0,0 +1,115 @@ +// 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. + +/// RTL Line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class RTLLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + RTLLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory RTLLineChart.withSampleData() { + return new RTLLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory RTLLineChart.withRandomData() { + return new RTLLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Charts will determine if RTL is enabled by checking the directionality by + // requesting Directionality.of(context). This returns the text direction + // from the closest instance of that encloses the context passed to build + // the chart. A [TextDirection.rtl] will be treated as a RTL chart. This + // means that the directionality widget does not have to directly wrap each + // chart. It is show here as an example only. + // + // By default, when a chart detects RTL: + // Measure axis positions are flipped. Primary measure axis is on the right + // and the secondary measure axis is on the left (when used). + // Domain axis' first domain starts on the right and grows left. + return new Directionality( + textDirection: TextDirection.rtl, + child: new charts.LineChart( + seriesList, + animate: animate, + )); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/i18n/rtl_line_segments.dart b/web/charts/example/lib/i18n/rtl_line_segments.dart new file mode 100644 index 000000000..1b22c70bb --- /dev/null +++ b/web/charts/example/lib/i18n/rtl_line_segments.dart @@ -0,0 +1,248 @@ +// 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. + +/// Example of a RTL stacked area chart with changing styles within each line. +/// +/// Each series of data in this example contains different values for color, +/// dashPattern, or strokeWidthPx between each datum. The line and area skirt +/// will be rendered in segments, with the styling of the series changing when +/// these data attributes change. +/// +/// Note that if a dashPattern or strokeWidth value is not found for a +/// particular datum, then the chart will fall back to use the value defined in +/// the [charts.LineRendererConfig]. This could be used, for example, to define +/// a default dash pattern for the series, with only a specific datum called out +/// with a different pattern. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class RTLLineSegments extends StatelessWidget { + final List seriesList; + final bool animate; + + RTLLineSegments(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory RTLLineSegments.withSampleData() { + return new RTLLineSegments( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory RTLLineSegments.withRandomData() { + return new RTLLineSegments(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + // Series of data with static dash pattern and stroke width. The colorFn + // accessor will colorize each datum (for all three series). + final colorChangeData = [ + new LinearSales(0, random.nextInt(100), null, 2.0), + new LinearSales(1, random.nextInt(100), null, 2.0), + new LinearSales(2, random.nextInt(100), null, 2.0), + new LinearSales(3, random.nextInt(100), null, 2.0), + new LinearSales(4, random.nextInt(100), null, 2.0), + new LinearSales(5, random.nextInt(100), null, 2.0), + new LinearSales(6, random.nextInt(100), null, 2.0), + ]; + + // Series of data with changing color and dash pattern. + final dashPatternChangeData = [ + new LinearSales(0, random.nextInt(100), [2, 2], 2.0), + new LinearSales(1, random.nextInt(100), [2, 2], 2.0), + new LinearSales(2, random.nextInt(100), [4, 4], 2.0), + new LinearSales(3, random.nextInt(100), [4, 4], 2.0), + new LinearSales(4, random.nextInt(100), [4, 4], 2.0), + new LinearSales(5, random.nextInt(100), [8, 3, 2, 3], 2.0), + new LinearSales(6, random.nextInt(100), [8, 3, 2, 3], 2.0), + ]; + + // Series of data with changing color and stroke width. + final strokeWidthChangeData = [ + new LinearSales(0, random.nextInt(100), null, 2.0), + new LinearSales(1, random.nextInt(100), null, 2.0), + new LinearSales(2, random.nextInt(100), null, 4.0), + new LinearSales(3, random.nextInt(100), null, 4.0), + new LinearSales(4, random.nextInt(100), null, 4.0), + new LinearSales(5, random.nextInt(100), null, 6.0), + new LinearSales(6, random.nextInt(100), null, 6.0), + ]; + + // Generate 2 shades of each color so that we can style the line segments. + final blue = charts.MaterialPalette.blue.makeShades(2); + final red = charts.MaterialPalette.red.makeShades(2); + final green = charts.MaterialPalette.green.makeShades(2); + + return [ + new charts.Series( + id: 'Color Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? blue[1] : blue[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: colorChangeData, + ), + new charts.Series( + id: 'Dash Pattern Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? red[1] : red[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: dashPatternChangeData, + ), + new charts.Series( + id: 'Stroke Width Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? green[1] : green[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: strokeWidthChangeData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Charts will determine if RTL is enabled by checking the directionality by + // requesting Directionality.of(context). This returns the text direction + // from the closest instance of that encloses the context passed to build + // the chart. A [TextDirection.rtl] will be treated as a RTL chart. This + // means that the directionality widget does not have to directly wrap each + // chart. It is show here as an example only. + // + // By default, when a chart detects RTL: + // Measure axis positions are flipped. Primary measure axis is on the right + // and the secondary measure axis is on the left (when used). + // Domain axis' first domain starts on the right and grows left. + return new Directionality( + textDirection: TextDirection.rtl, + child: new charts.LineChart( + seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate, + )); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + // Series of data with static dash pattern and stroke width. The colorFn + // accessor will colorize each datum (for all three series). + final colorChangeData = [ + new LinearSales(0, 5, null, 2.0), + new LinearSales(1, 15, null, 2.0), + new LinearSales(2, 25, null, 2.0), + new LinearSales(3, 75, null, 2.0), + new LinearSales(4, 100, null, 2.0), + new LinearSales(5, 90, null, 2.0), + new LinearSales(6, 75, null, 2.0), + ]; + + // Series of data with changing color and dash pattern. + final dashPatternChangeData = [ + new LinearSales(0, 5, [2, 2], 2.0), + new LinearSales(1, 15, [2, 2], 2.0), + new LinearSales(2, 25, [4, 4], 2.0), + new LinearSales(3, 75, [4, 4], 2.0), + new LinearSales(4, 100, [4, 4], 2.0), + new LinearSales(5, 90, [8, 3, 2, 3], 2.0), + new LinearSales(6, 75, [8, 3, 2, 3], 2.0), + ]; + + // Series of data with changing color and stroke width. + final strokeWidthChangeData = [ + new LinearSales(0, 5, null, 2.0), + new LinearSales(1, 15, null, 2.0), + new LinearSales(2, 25, null, 4.0), + new LinearSales(3, 75, null, 4.0), + new LinearSales(4, 100, null, 4.0), + new LinearSales(5, 90, null, 6.0), + new LinearSales(6, 75, null, 6.0), + ]; + + // Generate 2 shades of each color so that we can style the line segments. + final blue = charts.MaterialPalette.blue.makeShades(2); + final red = charts.MaterialPalette.red.makeShades(2); + final green = charts.MaterialPalette.green.makeShades(2); + + return [ + new charts.Series( + id: 'Color Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? blue[1] : blue[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: colorChangeData, + ), + new charts.Series( + id: 'Dash Pattern Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? red[1] : red[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: dashPatternChangeData, + ), + new charts.Series( + id: 'Stroke Width Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? green[1] : green[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: strokeWidthChangeData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final List dashPattern; + final double strokeWidthPx; + + LinearSales(this.year, this.sales, this.dashPattern, this.strokeWidthPx); +} diff --git a/web/charts/example/lib/i18n/rtl_series_legend.dart b/web/charts/example/lib/i18n/rtl_series_legend.dart new file mode 100644 index 000000000..03a10c5f1 --- /dev/null +++ b/web/charts/example/lib/i18n/rtl_series_legend.dart @@ -0,0 +1,206 @@ +// 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. + +/// RTL Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class RTLSeriesLegend extends StatelessWidget { + final List seriesList; + final bool animate; + + RTLSeriesLegend(this.seriesList, {this.animate}); + + /// Creates a [BarChart] with sample data and no transition. + factory RTLSeriesLegend.withSampleData() { + return new RTLSeriesLegend( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory RTLSeriesLegend.withRandomData() { + return new RTLSeriesLegend(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Charts will determine if RTL is enabled by checking the directionality by + // requesting Directionality.of(context). This returns the text direction + // from the closest instance of that encloses the context passed to build + // the chart. A [TextDirection.rtl] will be treated as a RTL chart. This + // means that the directionality widget does not have to directly wrap each + // chart. It is show here as an example only. + // + // When the legend behavior detects RTL: + // [BehaviorPosition.start] is to the right of the chart. + // [BehaviorPosition.end] is to the left of the chart. + // + // If the [BehaviorPosition] is top or bottom, the start justification + // is to the right, and the end justification is to the left. + // + // The legend's tabular layout will also layout rows and columns from right + // to left. + // + // The below example changes the position to 'start' and max rows of 2 in + // order to show these effects, but are not required for SeriesLegend to + // work with the correct directionality. + return new Directionality( + textDirection: TextDirection.rtl, + child: new charts.BarChart( + seriesList, + animate: animate, + behaviors: [ + new charts.SeriesLegend( + position: charts.BehaviorPosition.end, desiredMaxRows: 2) + ], + )); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/legends/datum_legend_options.dart b/web/charts/example/lib/legends/datum_legend_options.dart new file mode 100644 index 000000000..3cdc327d9 --- /dev/null +++ b/web/charts/example/lib/legends/datum_legend_options.dart @@ -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. + +/// Pie chart with example of a legend with customized position, justification, +/// desired max rows, padding, and entry text styles. These options are shown as +/// an example of how to use the customizations, they do not necessary have to +/// be used together in this way. Choosing [end] as the position does not +/// require the justification to also be [endDrawArea]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class DatumLegendOptions extends StatelessWidget { + final List seriesList; + final bool animate; + + DatumLegendOptions(this.seriesList, {this.animate}); + + factory DatumLegendOptions.withSampleData() { + return new DatumLegendOptions( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DatumLegendOptions.withRandomData() { + return new DatumLegendOptions(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart( + seriesList, + animate: animate, + // Add the legend behavior to the chart to turn on legends. + // This example shows how to change the position and justification of + // the legend, in addition to altering the max rows and padding. + behaviors: [ + new charts.DatumLegend( + // Positions for "start" and "end" will be left and right respectively + // for widgets with a build context that has directionality ltr. + // For rtl, "start" and "end" will be right and left respectively. + // Since this example has directionality of ltr, the legend is + // positioned on the right side of the chart. + position: charts.BehaviorPosition.end, + // For a legend that is positioned on the left or right of the chart, + // setting the justification for [endDrawArea] is aligned to the + // bottom of the chart draw area. + outsideJustification: charts.OutsideJustification.endDrawArea, + // By default, if the position of the chart is on the left or right of + // the chart, [horizontalFirst] is set to false. This means that the + // legend entries will grow as new rows first instead of a new column. + horizontalFirst: false, + // By setting this value to 2, the legend entries will grow up to two + // rows before adding a new column. + desiredMaxRows: 2, + // This defines the padding around each legend entry. + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + // Render the legend entry text with custom styles. + entryTextStyle: charts.TextStyleSpec( + color: charts.MaterialPalette.purple.shadeDefault, + fontFamily: 'Georgia', + fontSize: 11), + ) + ], + ); + } + + /// Create series list with one series + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/legends/datum_legend_with_measures.dart b/web/charts/example/lib/legends/datum_legend_with_measures.dart new file mode 100644 index 000000000..bbbb924c9 --- /dev/null +++ b/web/charts/example/lib/legends/datum_legend_with_measures.dart @@ -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. + +/// Bar chart with example of a legend with customized position, justification, +/// desired max rows, and padding. These options are shown as an example of how +/// to use the customizations, they do not necessary have to be used together in +/// this way. Choosing [end] as the position does not require the justification +/// to also be [endDrawArea]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example that shows how to build a datum legend that shows measure values. +/// +/// Also shows the option to provide a custom measure formatter. +class DatumLegendWithMeasures extends StatelessWidget { + final List seriesList; + final bool animate; + + DatumLegendWithMeasures(this.seriesList, {this.animate}); + + factory DatumLegendWithMeasures.withSampleData() { + return new DatumLegendWithMeasures( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DatumLegendWithMeasures.withRandomData() { + return new DatumLegendWithMeasures(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(2014, random.nextInt(100)), + new LinearSales(2015, random.nextInt(100)), + new LinearSales(2016, random.nextInt(100)), + new LinearSales(2017, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart( + seriesList, + animate: animate, + // Add the legend behavior to the chart to turn on legends. + // This example shows how to optionally show measure and provide a custom + // formatter. + behaviors: [ + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // This is added in order to generate the image for the gallery to show + // an initial selection so that measure values are shown in the gallery. + new charts.InitialSelection( + selectedDataConfig: [ + new charts.SeriesDatumConfig('Sales', 0), + ], + ), + // EXCLUDE_FROM_GALLERY_DOCS_END + new charts.DatumLegend( + // Positions for "start" and "end" will be left and right respectively + // for widgets with a build context that has directionality ltr. + // For rtl, "start" and "end" will be right and left respectively. + // Since this example has directionality of ltr, the legend is + // positioned on the right side of the chart. + position: charts.BehaviorPosition.end, + // By default, if the position of the chart is on the left or right of + // the chart, [horizontalFirst] is set to false. This means that the + // legend entries will grow as new rows first instead of a new column. + horizontalFirst: false, + // This defines the padding around each legend entry. + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + // Set [showMeasures] to true to display measures in series legend. + showMeasures: true, + // Configure the measure value to be shown by default in the legend. + legendDefaultMeasure: charts.LegendDefaultMeasure.firstValue, + // Optionally provide a measure formatter to format the measure value. + // If none is specified the value is formatted as a decimal. + measureFormatter: (num value) { + return value == null ? '-' : '${value}k'; + }, + ), + ], + ); + } + + /// Create series list with one series + static List> _createSampleData() { + final data = [ + new LinearSales(2014, 100), + new LinearSales(2015, 75), + new LinearSales(2016, 25), + new LinearSales(2017, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/legends/default_hidden_series_legend.dart b/web/charts/example/lib/legends/default_hidden_series_legend.dart new file mode 100644 index 000000000..df7fe1e07 --- /dev/null +++ b/web/charts/example/lib/legends/default_hidden_series_legend.dart @@ -0,0 +1,188 @@ +// 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. + +/// Bar chart with default hidden series legend example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class DefaultHiddenSeriesLegend extends StatelessWidget { + final List seriesList; + final bool animate; + + DefaultHiddenSeriesLegend(this.seriesList, {this.animate}); + + factory DefaultHiddenSeriesLegend.withSampleData() { + return new DefaultHiddenSeriesLegend( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DefaultHiddenSeriesLegend.withRandomData() { + return new DefaultHiddenSeriesLegend(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the series legend behavior to the chart to turn on series legends. + // By default the legend will display above the chart. + behaviors: [ + new charts.SeriesLegend( + // Configures the "Other" series to be hidden on first chart draw. + defaultHiddenSeries: ['Other'], + ) + ], + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/legends/legend_custom_symbol.dart b/web/charts/example/lib/legends/legend_custom_symbol.dart new file mode 100644 index 000000000..f18552b2d --- /dev/null +++ b/web/charts/example/lib/legends/legend_custom_symbol.dart @@ -0,0 +1,209 @@ +// 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. + +/// Bar chart with custom symbol in legend example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example custom renderer that renders [IconData]. +/// +/// This is used to show that legend symbols can be assigned a custom symbol. +class IconRenderer extends charts.CustomSymbolRenderer { + final IconData iconData; + + IconRenderer(this.iconData); + + @override + Widget build(BuildContext context, {Size size, Color color, bool enabled}) { + // Lighten the color if the symbol is not enabled + // Example: If user has tapped on a Series deselecting it. + if (!enabled) { + color = color.withOpacity(0.26); + } + + return new SizedBox.fromSize( + size: size, child: new Icon(iconData, color: color, size: 12.0)); + } +} + +class LegendWithCustomSymbol extends StatelessWidget { + final List seriesList; + final bool animate; + + LegendWithCustomSymbol(this.seriesList, {this.animate}); + + factory LegendWithCustomSymbol.withSampleData() { + return new LegendWithCustomSymbol( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LegendWithCustomSymbol.withRandomData() { + return new LegendWithCustomSymbol(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the legend behavior to the chart to turn on legends. + // By default the legend will display above the chart. + // + // To change the symbol used in the legend, set the renderer attribute of + // symbolRendererKey to a SymbolRenderer. + behaviors: [new charts.SeriesLegend()], + defaultRenderer: new charts.BarRendererConfig( + symbolRenderer: new IconRenderer(Icons.cloud)), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/legends/legends_gallery.dart b/web/charts/example/lib/legends/legends_gallery.dart new file mode 100644 index 000000000..e94c3c8d3 --- /dev/null +++ b/web/charts/example/lib/legends/legends_gallery.dart @@ -0,0 +1,80 @@ +// 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:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'datum_legend_options.dart'; +import 'datum_legend_with_measures.dart'; +import 'default_hidden_series_legend.dart'; +import 'legend_custom_symbol.dart'; +import 'series_legend_options.dart'; +import 'series_legend_with_measures.dart'; +import 'simple_datum_legend.dart'; +import 'simple_series_legend.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Series Legend', + subtitle: 'A series legend for a bar chart with default settings', + childBuilder: () => new SimpleSeriesLegend.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Series Legend Options', + subtitle: + 'A series legend with custom positioning and spacing for a bar chart', + childBuilder: () => new LegendOptions.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Series Legend Custom Symbol', + subtitle: 'A series legend using a custom symbol renderer', + childBuilder: () => new LegendWithCustomSymbol.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Default Hidden Series Legend', + subtitle: 'A series legend showing a series hidden by default', + childBuilder: () => new DefaultHiddenSeriesLegend.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Series legend with measures', + subtitle: 'Series legend with measures and measure formatting', + childBuilder: () => new LegendWithMeasures.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Datum Legend', + subtitle: 'A datum legend for a pie chart with default settings', + childBuilder: () => new SimpleDatumLegend.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Datum Legend Options', + subtitle: + 'A datum legend with custom positioning and spacing for a pie chart', + childBuilder: () => new DatumLegendOptions.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Datum legend with measures', + subtitle: 'Datum legend with measures and measure formatting', + childBuilder: () => new DatumLegendWithMeasures.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/legends/series_legend_options.dart b/web/charts/example/lib/legends/series_legend_options.dart new file mode 100644 index 000000000..3f4541603 --- /dev/null +++ b/web/charts/example/lib/legends/series_legend_options.dart @@ -0,0 +1,215 @@ +// 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. + +/// Bar chart with example of a legend with customized position, justification, +/// desired max rows, padding, and entry text styles. These options are shown as +/// an example of how to use the customizations, they do not necessary have to +/// be used together in this way. Choosing [end] as the position does not +/// require the justification to also be [endDrawArea]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class LegendOptions extends StatelessWidget { + final List seriesList; + final bool animate; + + LegendOptions(this.seriesList, {this.animate}); + + factory LegendOptions.withSampleData() { + return new LegendOptions( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LegendOptions.withRandomData() { + return new LegendOptions(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the legend behavior to the chart to turn on legends. + // This example shows how to change the position and justification of + // the legend, in addition to altering the max rows and padding. + behaviors: [ + new charts.SeriesLegend( + // Positions for "start" and "end" will be left and right respectively + // for widgets with a build context that has directionality ltr. + // For rtl, "start" and "end" will be right and left respectively. + // Since this example has directionality of ltr, the legend is + // positioned on the right side of the chart. + position: charts.BehaviorPosition.end, + // For a legend that is positioned on the left or right of the chart, + // setting the justification for [endDrawArea] is aligned to the + // bottom of the chart draw area. + outsideJustification: charts.OutsideJustification.endDrawArea, + // By default, if the position of the chart is on the left or right of + // the chart, [horizontalFirst] is set to false. This means that the + // legend entries will grow as new rows first instead of a new column. + horizontalFirst: false, + // By setting this value to 2, the legend entries will grow up to two + // rows before adding a new column. + desiredMaxRows: 2, + // This defines the padding around each legend entry. + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + // Render the legend entry text with custom styles. + entryTextStyle: charts.TextStyleSpec( + color: charts.MaterialPalette.purple.shadeDefault, + fontFamily: 'Georgia', + fontSize: 11), + ) + ], + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/legends/series_legend_with_measures.dart b/web/charts/example/lib/legends/series_legend_with_measures.dart new file mode 100644 index 000000000..a5c229244 --- /dev/null +++ b/web/charts/example/lib/legends/series_legend_with_measures.dart @@ -0,0 +1,228 @@ +// 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. + +/// Bar chart with example of a legend with customized position, justification, +/// desired max rows, and padding. These options are shown as an example of how +/// to use the customizations, they do not necessary have to be used together in +/// this way. Choosing [end] as the position does not require the justification +/// to also be [endDrawArea]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example that shows how to build a series legend that shows measure values +/// when a datum is selected. +/// +/// Also shows the option to provide a custom measure formatter. +class LegendWithMeasures extends StatelessWidget { + final List seriesList; + final bool animate; + + LegendWithMeasures(this.seriesList, {this.animate}); + + factory LegendWithMeasures.withSampleData() { + return new LegendWithMeasures( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LegendWithMeasures.withRandomData() { + return new LegendWithMeasures(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the legend behavior to the chart to turn on legends. + // This example shows how to optionally show measure and provide a custom + // formatter. + behaviors: [ + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // This is added in order to generate the image for the gallery to show + // an initial selection so that measure values are shown in the gallery. + new charts.InitialSelection( + selectedDataConfig: [ + new charts.SeriesDatumConfig('Desktop', '2016'), + new charts.SeriesDatumConfig('Tablet', '2016'), + new charts.SeriesDatumConfig('Mobile', '2016'), + new charts.SeriesDatumConfig('Other', '2016'), + ], + ), + // EXCLUDE_FROM_GALLERY_DOCS_END + new charts.SeriesLegend( + // Positions for "start" and "end" will be left and right respectively + // for widgets with a build context that has directionality ltr. + // For rtl, "start" and "end" will be right and left respectively. + // Since this example has directionality of ltr, the legend is + // positioned on the right side of the chart. + position: charts.BehaviorPosition.end, + // By default, if the position of the chart is on the left or right of + // the chart, [horizontalFirst] is set to false. This means that the + // legend entries will grow as new rows first instead of a new column. + horizontalFirst: false, + // This defines the padding around each legend entry. + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + // Set show measures to true to display measures in series legend, + // when the datum is selected. + showMeasures: true, + // Optionally provide a measure formatter to format the measure value. + // If none is specified the value is formatted as a decimal. + measureFormatter: (num value) { + return value == null ? '-' : '${value}k'; + }, + ), + ], + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + // Purposely have a missing datum for 2016 to show the null measure format + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/legends/simple_datum_legend.dart b/web/charts/example/lib/legends/simple_datum_legend.dart new file mode 100644 index 000000000..da4e40972 --- /dev/null +++ b/web/charts/example/lib/legends/simple_datum_legend.dart @@ -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. + +/// Bar chart with series legend example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class SimpleDatumLegend extends StatelessWidget { + final List seriesList; + final bool animate; + + SimpleDatumLegend(this.seriesList, {this.animate}); + + factory SimpleDatumLegend.withSampleData() { + return new SimpleDatumLegend( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleDatumLegend.withRandomData() { + return new SimpleDatumLegend(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart( + seriesList, + animate: animate, + // Add the series legend behavior to the chart to turn on series legends. + // By default the legend will display above the chart. + behaviors: [new charts.DatumLegend()], + ); + } + + /// Create series list with one series + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/legends/simple_series_legend.dart b/web/charts/example/lib/legends/simple_series_legend.dart new file mode 100644 index 000000000..1284bd4f9 --- /dev/null +++ b/web/charts/example/lib/legends/simple_series_legend.dart @@ -0,0 +1,183 @@ +// 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. + +/// Bar chart with series legend example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter_web/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class SimpleSeriesLegend extends StatelessWidget { + final List seriesList; + final bool animate; + + SimpleSeriesLegend(this.seriesList, {this.animate}); + + factory SimpleSeriesLegend.withSampleData() { + return new SimpleSeriesLegend( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleSeriesLegend.withRandomData() { + return new SimpleSeriesLegend(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the series legend behavior to the chart to turn on series legends. + // By default the legend will display above the chart. + behaviors: [new charts.SeriesLegend()], + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/animation_zoom.dart b/web/charts/example/lib/line_chart/animation_zoom.dart new file mode 100644 index 000000000..1f62e3ed9 --- /dev/null +++ b/web/charts/example/lib/line_chart/animation_zoom.dart @@ -0,0 +1,101 @@ +// 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. + +/// Example of a line chart with pan and zoom enabled via +/// [Charts.PanAndZoomBehavior]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class LineAnimationZoomChart extends StatelessWidget { + final List seriesList; + final bool animate; + + LineAnimationZoomChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory LineAnimationZoomChart.withSampleData() { + return new LineAnimationZoomChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LineAnimationZoomChart.withRandomData() { + return new LineAnimationZoomChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = []; + + for (var i = 0; i < 100; i++) { + data.add(new LinearSales(i, random.nextInt(100))); + } + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + new charts.PanAndZoomBehavior(), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/area_and_line.dart b/web/charts/example/lib/line_chart/area_and_line.dart new file mode 100644 index 000000000..c660c129f --- /dev/null +++ b/web/charts/example/lib/line_chart/area_and_line.dart @@ -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. + +/// Line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class AreaAndLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + AreaAndLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory AreaAndLineChart.withSampleData() { + return new AreaAndLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory AreaAndLineChart.withRandomData() { + return new AreaAndLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customArea'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + animate: animate, + customSeriesRenderers: [ + new charts.LineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customArea', + includeArea: true, + stacked: true), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + var myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customArea'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/dash_pattern.dart b/web/charts/example/lib/line_chart/dash_pattern.dart new file mode 100644 index 000000000..385aa766c --- /dev/null +++ b/web/charts/example/lib/line_chart/dash_pattern.dart @@ -0,0 +1,162 @@ +// 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. + +/// Dash pattern line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +/// Example of a line chart rendered with dash patterns. +class DashPatternLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + DashPatternLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory DashPatternLineChart.withSampleData() { + return new DashPatternLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DashPatternLineChart.withRandomData() { + return new DashPatternLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + dashPatternFn: (_, __) => [2, 2], + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate); + } + + /// Create three series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + var myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + var myFakeMobileData = [ + new LinearSales(0, 15), + new LinearSales(1, 75), + new LinearSales(2, 300), + new LinearSales(3, 225), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + dashPatternFn: (_, __) => [2, 2], + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/line_annotation.dart b/web/charts/example/lib/line_chart/line_annotation.dart new file mode 100644 index 000000000..5d21dc43b --- /dev/null +++ b/web/charts/example/lib/line_chart/line_annotation.dart @@ -0,0 +1,124 @@ +// 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. + +/// Line chart with line annotations example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class LineLineAnnotationChart extends StatelessWidget { + final List seriesList; + final bool animate; + + LineLineAnnotationChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and line annotations. + /// + /// The second annotation extends beyond the range of the series data, + /// demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] flag. + /// This can be set to false to disable range extension. + factory LineLineAnnotationChart.withSampleData() { + return new LineLineAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LineLineAnnotationChart.withRandomData() { + return new LineLineAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + // Fix one of the points to 100 so that the annotations are consistently + // placed. + new LinearSales(3, 100), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + new charts.RangeAnnotation([ + new charts.LineAnnotationSegment( + 1.0, charts.RangeAnnotationAxisType.domain, + startLabel: 'Domain 1'), + new charts.LineAnnotationSegment( + 4, charts.RangeAnnotationAxisType.domain, + endLabel: 'Domain 2', color: charts.MaterialPalette.gray.shade200), + new charts.LineAnnotationSegment( + 20, charts.RangeAnnotationAxisType.measure, + startLabel: 'Measure 1 Start', + endLabel: 'Measure 1 End', + color: charts.MaterialPalette.gray.shade300), + new charts.LineAnnotationSegment( + 65, charts.RangeAnnotationAxisType.measure, + startLabel: 'Measure 2 Start', + endLabel: 'Measure 2 End', + color: charts.MaterialPalette.gray.shade400), + ]), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/line_gallery.dart b/web/charts/example/lib/line_chart/line_gallery.dart new file mode 100644 index 000000000..b19fc9aee --- /dev/null +++ b/web/charts/example/lib/line_chart/line_gallery.dart @@ -0,0 +1,113 @@ +// 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:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'animation_zoom.dart'; +import 'area_and_line.dart'; +import 'dash_pattern.dart'; +import 'line_annotation.dart'; +import 'points.dart'; +import 'range_annotation.dart'; +import 'range_annotation_margin.dart'; +import 'segments.dart'; +import 'simple.dart'; +import 'simple_nulls.dart'; +import 'stacked_area.dart'; +import 'stacked_area_custom_color.dart'; +import 'stacked_area_nulls.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Simple Line Chart', + subtitle: 'With a single series and default line point highlighter', + childBuilder: () => new SimpleLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Stacked Area Chart', + subtitle: 'Stacked area chart with three series', + childBuilder: () => new StackedAreaLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Stacked Area Custom Color Chart', + subtitle: 'Stacked area chart with custom area skirt color', + childBuilder: () => new StackedAreaCustomColorLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Area and Line Combo Chart', + subtitle: 'Combo chart with one line series and one area series', + childBuilder: () => new AreaAndLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Points Line Chart', + subtitle: 'Line chart with points on a single series', + childBuilder: () => new PointsLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Null Data Line Chart', + subtitle: 'With a single series and null measure values', + childBuilder: () => new SimpleNullsLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Stacked Area with Nulls Chart', + subtitle: 'Stacked area chart with three series and null measure values', + childBuilder: () => new StackedAreaNullsLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Dash Pattern Line Chart', + subtitle: 'Line chart with dash patterns', + childBuilder: () => new DashPatternLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Segments Line Chart', + subtitle: 'Line chart with changes of style for each line', + childBuilder: () => new SegmentsLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Line Annotation Line Chart', + subtitle: 'Line chart with line annotations', + childBuilder: () => new LineLineAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Range Annotation Line Chart', + subtitle: 'Line chart with range annotations', + childBuilder: () => new LineRangeAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Range Annotation Margin Labels Line Chart', + subtitle: 'Line chart with range annotations with labels in margins', + childBuilder: () => new LineRangeAnnotationMarginChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Pan and Zoom Line Chart', + subtitle: 'Simple line chart pan and zoom behaviors enabled', + childBuilder: () => new LineAnimationZoomChart.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/line_chart/points.dart b/web/charts/example/lib/line_chart/points.dart new file mode 100644 index 000000000..90659c630 --- /dev/null +++ b/web/charts/example/lib/line_chart/points.dart @@ -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. + +/// Line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class PointsLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + PointsLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory PointsLineChart.withSampleData() { + return new PointsLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PointsLineChart.withRandomData() { + return new PointsLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + animate: animate, + defaultRenderer: new charts.LineRendererConfig(includePoints: true)); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/range_annotation.dart b/web/charts/example/lib/line_chart/range_annotation.dart new file mode 100644 index 000000000..92ceea3a6 --- /dev/null +++ b/web/charts/example/lib/line_chart/range_annotation.dart @@ -0,0 +1,124 @@ +// 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. + +/// Line chart with range annotations example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class LineRangeAnnotationChart extends StatelessWidget { + final List seriesList; + final bool animate; + + LineRangeAnnotationChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and range annotations. + /// + /// The second annotation extends beyond the range of the series data, + /// demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] flag. + /// This can be set to false to disable range extension. + factory LineRangeAnnotationChart.withSampleData() { + return new LineRangeAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LineRangeAnnotationChart.withRandomData() { + return new LineRangeAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + // Fix one of the points to 100 so that the annotations are consistently + // placed. + new LinearSales(3, 100), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + new charts.RangeAnnotation([ + new charts.RangeAnnotationSegment( + 0.5, 1.0, charts.RangeAnnotationAxisType.domain, + startLabel: 'Domain 1'), + new charts.RangeAnnotationSegment( + 2, 4, charts.RangeAnnotationAxisType.domain, + endLabel: 'Domain 2', color: charts.MaterialPalette.gray.shade200), + new charts.RangeAnnotationSegment( + 15, 20, charts.RangeAnnotationAxisType.measure, + startLabel: 'Measure 1 Start', + endLabel: 'Measure 1 End', + color: charts.MaterialPalette.gray.shade300), + new charts.RangeAnnotationSegment( + 35, 65, charts.RangeAnnotationAxisType.measure, + startLabel: 'Measure 2 Start', + endLabel: 'Measure 2 End', + color: charts.MaterialPalette.gray.shade400), + ]), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/range_annotation_margin.dart b/web/charts/example/lib/line_chart/range_annotation_margin.dart new file mode 100644 index 000000000..8ff64786f --- /dev/null +++ b/web/charts/example/lib/line_chart/range_annotation_margin.dart @@ -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. + +/// Example of a line chart with range annotations configured to render labels +/// in the chart margin area. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class LineRangeAnnotationMarginChart extends StatelessWidget { + final List seriesList; + final bool animate; + + LineRangeAnnotationMarginChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and range annotations. + /// + /// The second annotation extends beyond the range of the series data, + /// demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] flag. + /// This can be set to false to disable range extension. + factory LineRangeAnnotationMarginChart.withSampleData() { + return new LineRangeAnnotationMarginChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LineRangeAnnotationMarginChart.withRandomData() { + return new LineRangeAnnotationMarginChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + // Fix one of the points to 100 so that the annotations are consistently + // placed. + new LinearSales(3, 100), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + animate: animate, + + // Allow enough space in the left and right chart margins for the + // annotations. + layoutConfig: new charts.LayoutConfig( + leftMarginSpec: new charts.MarginSpec.fixedPixel(60), + topMarginSpec: new charts.MarginSpec.fixedPixel(20), + rightMarginSpec: new charts.MarginSpec.fixedPixel(60), + bottomMarginSpec: new charts.MarginSpec.fixedPixel(20)), + behaviors: [ + // Define one domain and two measure annotations configured to render + // labels in the chart margins. + new charts.RangeAnnotation([ + new charts.RangeAnnotationSegment( + 0.5, 1.0, charts.RangeAnnotationAxisType.domain, + startLabel: 'D1 Start', + endLabel: 'D1 End', + labelAnchor: charts.AnnotationLabelAnchor.end, + color: charts.MaterialPalette.gray.shade200, + // Override the default vertical direction for domain labels. + labelDirection: charts.AnnotationLabelDirection.horizontal), + new charts.RangeAnnotationSegment( + 15, 20, charts.RangeAnnotationAxisType.measure, + startLabel: 'M1 Start', + endLabel: 'M1 End', + labelAnchor: charts.AnnotationLabelAnchor.end, + color: charts.MaterialPalette.gray.shade300), + new charts.RangeAnnotationSegment( + 35, 65, charts.RangeAnnotationAxisType.measure, + startLabel: 'M2 Start', + endLabel: 'M2 End', + labelAnchor: charts.AnnotationLabelAnchor.start, + color: charts.MaterialPalette.gray.shade400), + ], defaultLabelPosition: charts.AnnotationLabelPosition.margin), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/segments.dart b/web/charts/example/lib/line_chart/segments.dart new file mode 100644 index 000000000..603129b2b --- /dev/null +++ b/web/charts/example/lib/line_chart/segments.dart @@ -0,0 +1,233 @@ +// 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. + +/// Example of a stacked area chart with changing styles within each line. +/// +/// Each series of data in this example contains different values for color, +/// dashPattern, or strokeWidthPx between each datum. The line and area skirt +/// will be rendered in segments, with the styling of the series changing when +/// these data attributes change. +/// +/// Note that if a dashPattern or strokeWidth value is not found for a +/// particular datum, then the chart will fall back to use the value defined in +/// the [charts.LineRendererConfig]. This could be used, for example, to define +/// a default dash pattern for the series, with only a specific datum called out +/// with a different pattern. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SegmentsLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + SegmentsLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory SegmentsLineChart.withSampleData() { + return new SegmentsLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SegmentsLineChart.withRandomData() { + return new SegmentsLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + // Series of data with static dash pattern and stroke width. The colorFn + // accessor will colorize each datum (for all three series). + final colorChangeData = [ + new LinearSales(0, random.nextInt(100), null, 2.0), + new LinearSales(1, random.nextInt(100), null, 2.0), + new LinearSales(2, random.nextInt(100), null, 2.0), + new LinearSales(3, random.nextInt(100), null, 2.0), + new LinearSales(4, random.nextInt(100), null, 2.0), + new LinearSales(5, random.nextInt(100), null, 2.0), + new LinearSales(6, random.nextInt(100), null, 2.0), + ]; + + // Series of data with changing color and dash pattern. + final dashPatternChangeData = [ + new LinearSales(0, random.nextInt(100), [2, 2], 2.0), + new LinearSales(1, random.nextInt(100), [2, 2], 2.0), + new LinearSales(2, random.nextInt(100), [4, 4], 2.0), + new LinearSales(3, random.nextInt(100), [4, 4], 2.0), + new LinearSales(4, random.nextInt(100), [4, 4], 2.0), + new LinearSales(5, random.nextInt(100), [8, 3, 2, 3], 2.0), + new LinearSales(6, random.nextInt(100), [8, 3, 2, 3], 2.0), + ]; + + // Series of data with changing color and stroke width. + final strokeWidthChangeData = [ + new LinearSales(0, random.nextInt(100), null, 2.0), + new LinearSales(1, random.nextInt(100), null, 2.0), + new LinearSales(2, random.nextInt(100), null, 4.0), + new LinearSales(3, random.nextInt(100), null, 4.0), + new LinearSales(4, random.nextInt(100), null, 4.0), + new LinearSales(5, random.nextInt(100), null, 6.0), + new LinearSales(6, random.nextInt(100), null, 6.0), + ]; + + // Generate 2 shades of each color so that we can style the line segments. + final blue = charts.MaterialPalette.blue.makeShades(2); + final red = charts.MaterialPalette.red.makeShades(2); + final green = charts.MaterialPalette.green.makeShades(2); + + return [ + new charts.Series( + id: 'Color Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? blue[1] : blue[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: colorChangeData, + ), + new charts.Series( + id: 'Dash Pattern Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? red[1] : red[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: dashPatternChangeData, + ), + new charts.Series( + id: 'Stroke Width Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? green[1] : green[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: strokeWidthChangeData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + // Series of data with static dash pattern and stroke width. The colorFn + // accessor will colorize each datum (for all three series). + final colorChangeData = [ + new LinearSales(0, 5, null, 2.0), + new LinearSales(1, 15, null, 2.0), + new LinearSales(2, 25, null, 2.0), + new LinearSales(3, 75, null, 2.0), + new LinearSales(4, 100, null, 2.0), + new LinearSales(5, 90, null, 2.0), + new LinearSales(6, 75, null, 2.0), + ]; + + // Series of data with changing color and dash pattern. + final dashPatternChangeData = [ + new LinearSales(0, 5, [2, 2], 2.0), + new LinearSales(1, 15, [2, 2], 2.0), + new LinearSales(2, 25, [4, 4], 2.0), + new LinearSales(3, 75, [4, 4], 2.0), + new LinearSales(4, 100, [4, 4], 2.0), + new LinearSales(5, 90, [8, 3, 2, 3], 2.0), + new LinearSales(6, 75, [8, 3, 2, 3], 2.0), + ]; + + // Series of data with changing color and stroke width. + final strokeWidthChangeData = [ + new LinearSales(0, 5, null, 2.0), + new LinearSales(1, 15, null, 2.0), + new LinearSales(2, 25, null, 4.0), + new LinearSales(3, 75, null, 4.0), + new LinearSales(4, 100, null, 4.0), + new LinearSales(5, 90, null, 6.0), + new LinearSales(6, 75, null, 6.0), + ]; + + // Generate 2 shades of each color so that we can style the line segments. + final blue = charts.MaterialPalette.blue.makeShades(2); + final red = charts.MaterialPalette.red.makeShades(2); + final green = charts.MaterialPalette.green.makeShades(2); + + return [ + new charts.Series( + id: 'Color Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? blue[1] : blue[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: colorChangeData, + ), + new charts.Series( + id: 'Dash Pattern Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? red[1] : red[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: dashPatternChangeData, + ), + new charts.Series( + id: 'Stroke Width Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? green[1] : green[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: strokeWidthChangeData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final List dashPattern; + final double strokeWidthPx; + + LinearSales(this.year, this.sales, this.dashPattern, this.strokeWidthPx); +} diff --git a/web/charts/example/lib/line_chart/simple.dart b/web/charts/example/lib/line_chart/simple.dart new file mode 100644 index 000000000..231bc4d55 --- /dev/null +++ b/web/charts/example/lib/line_chart/simple.dart @@ -0,0 +1,101 @@ +// 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. + +/// Example of a simple line chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SimpleLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + SimpleLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory SimpleLineChart.withSampleData() { + return new SimpleLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleLineChart.withRandomData() { + return new SimpleLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/simple_nulls.dart b/web/charts/example/lib/line_chart/simple_nulls.dart new file mode 100644 index 000000000..a99c9dba4 --- /dev/null +++ b/web/charts/example/lib/line_chart/simple_nulls.dart @@ -0,0 +1,179 @@ +// 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. + +/// Example of a line chart with null measure values. +/// +/// Null values will be visible as gaps in lines and area skirts. Any data +/// points that exist between two nulls in a line will be rendered as an +/// isolated point, as seen in the green series. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SimpleNullsLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + SimpleNullsLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory SimpleNullsLineChart.withSampleData() { + return new SimpleNullsLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleNullsLineChart.withRandomData() { + return new SimpleNullsLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, null), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, null), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, null), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 15), + new LinearSales(2, null), + new LinearSales(3, 75), + new LinearSales(4, 100), + new LinearSales(5, 90), + new LinearSales(6, 75), + ]; + + final myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 30), + new LinearSales(2, 50), + new LinearSales(3, 150), + new LinearSales(4, 200), + new LinearSales(5, 180), + new LinearSales(6, 150), + ]; + + final myFakeMobileData = [ + new LinearSales(0, 15), + new LinearSales(1, 45), + new LinearSales(2, null), + new LinearSales(3, 225), + new LinearSales(4, null), + new LinearSales(5, 270), + new LinearSales(6, 225), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/stacked_area.dart b/web/charts/example/lib/line_chart/stacked_area.dart new file mode 100644 index 000000000..7ef94444b --- /dev/null +++ b/web/charts/example/lib/line_chart/stacked_area.dart @@ -0,0 +1,160 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a stacked area chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class StackedAreaLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + StackedAreaLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory StackedAreaLineChart.withSampleData() { + return new StackedAreaLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedAreaLineChart.withRandomData() { + return new StackedAreaLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + var myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + var myFakeMobileData = [ + new LinearSales(0, 15), + new LinearSales(1, 75), + new LinearSales(2, 300), + new LinearSales(3, 225), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/stacked_area_custom_color.dart b/web/charts/example/lib/line_chart/stacked_area_custom_color.dart new file mode 100644 index 000000000..42baa853c --- /dev/null +++ b/web/charts/example/lib/line_chart/stacked_area_custom_color.dart @@ -0,0 +1,175 @@ +// 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. + +/// Example of a stacked area chart with custom area colors. +/// +/// By default, the area skirt for a chart will be drawn with the same color as +/// the line, but with a 10% opacity assigned to it. An area color function can +/// be provided to override this with any custom color. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class StackedAreaCustomColorLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + StackedAreaCustomColorLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory StackedAreaCustomColorLineChart.withSampleData() { + return new StackedAreaCustomColorLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedAreaCustomColorLineChart.withRandomData() { + return new StackedAreaCustomColorLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + var myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + var myFakeMobileData = [ + new LinearSales(0, 15), + new LinearSales(1, 75), + new LinearSales(2, 300), + new LinearSales(3, 225), + ]; + + return [ + new charts.Series( + id: 'Desktop', + // colorFn specifies that the line will be blue. + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + // areaColorFn specifies that the area skirt will be light blue. + areaColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + // colorFn specifies that the line will be red. + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + // areaColorFn specifies that the area skirt will be light red. + areaColorFn: (_, __) => charts.MaterialPalette.red.shadeDefault.lighter, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + // colorFn specifies that the line will be green. + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + // areaColorFn specifies that the area skirt will be light green. + areaColorFn: (_, __) => + charts.MaterialPalette.green.shadeDefault.lighter, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/line_chart/stacked_area_nulls.dart b/web/charts/example/lib/line_chart/stacked_area_nulls.dart new file mode 100644 index 000000000..b372ee8be --- /dev/null +++ b/web/charts/example/lib/line_chart/stacked_area_nulls.dart @@ -0,0 +1,191 @@ +// 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. + +/// Example of a stacked area chart with null measure values. +/// +/// Null values will be visible as gaps in lines and area skirts. Any data +/// points that exist between two nulls in a line will be rendered as an +/// isolated point, as seen in the green series. +/// +/// In a stacked area chart, no data above a null value in the stack will be +/// rendered. In this example, the null measure value at domain 2 in the Desktop +/// series will prevent any data from being rendered at domain 2 for every +/// series because it is at the bottom of the stack. +/// +/// This will also result in an isolated point being rendered for the domain +/// value 3 in the Mobile series, because that series also contains a null at +/// domain 4. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class StackedAreaNullsLineChart extends StatelessWidget { + final List seriesList; + final bool animate; + + StackedAreaNullsLineChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory StackedAreaNullsLineChart.withSampleData() { + return new StackedAreaNullsLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedAreaNullsLineChart.withRandomData() { + return new StackedAreaNullsLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, null), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, null), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 15), + new LinearSales(2, null), + new LinearSales(3, 75), + new LinearSales(4, 100), + new LinearSales(5, 90), + new LinearSales(6, 75), + ]; + + final myFakeTabletData = [ + new LinearSales(0, 5), + new LinearSales(1, 15), + new LinearSales(2, 25), + new LinearSales(3, 75), + new LinearSales(4, 100), + new LinearSales(5, 90), + new LinearSales(6, 75), + ]; + + final myFakeMobileData = [ + new LinearSales(0, 5), + new LinearSales(1, 15), + new LinearSales(2, 25), + new LinearSales(3, 75), + new LinearSales(4, null), + new LinearSales(5, 90), + new LinearSales(6, 75), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/main.dart b/web/charts/example/lib/main.dart new file mode 100644 index 000000000..ac850e0f8 --- /dev/null +++ b/web/charts/example/lib/main.dart @@ -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:flutter_web/material.dart'; +import 'app_config.dart'; +import 'home.dart'; + +/// The main gallery app widget. +class GalleryApp extends StatefulWidget { + GalleryApp({Key key}) : super(key: key); + + @override + GalleryAppState createState() => new GalleryAppState(); +} + +/// The main gallery app state. +/// +/// Controls performance overlay, and instantiates a [Home] widget. +class GalleryAppState extends State { + // Initialize app settings from the default configuration. + bool _showPerformanceOverlay = defaultConfig.showPerformanceOverlay; + + @override + Widget build(BuildContext context) { + return new MaterialApp( + title: defaultConfig.appName, + theme: defaultConfig.theme, + showPerformanceOverlay: _showPerformanceOverlay, + home: new Home( + showPerformanceOverlay: _showPerformanceOverlay, + onShowPerformanceOverlayChanged: (bool value) { + setState(() { + _showPerformanceOverlay = value; + }); + }, + )); + } +} + +void main() { + runApp(new GalleryApp()); +} diff --git a/web/charts/example/lib/pie_chart/auto_label.dart b/web/charts/example/lib/pie_chart/auto_label.dart new file mode 100644 index 000000000..a49daecc3 --- /dev/null +++ b/web/charts/example/lib/pie_chart/auto_label.dart @@ -0,0 +1,123 @@ +// 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. + +/// Donut chart with labels example. This is a simple pie chart with a hole in +/// the middle. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class DonutAutoLabelChart extends StatelessWidget { + final List seriesList; + final bool animate; + + DonutAutoLabelChart(this.seriesList, {this.animate}); + + /// Creates a [PieChart] with sample data and no transition. + factory DonutAutoLabelChart.withSampleData() { + return new DonutAutoLabelChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DonutAutoLabelChart.withRandomData() { + return new DonutAutoLabelChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.year}: ${row.sales}', + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Configure the width of the pie slices to 60px. The remaining space in + // the chart will be left as a hole in the center. + // + // [ArcLabelDecorator] will automatically position the label inside the + // arc if the label will fit. If the label will not fit, it will draw + // outside of the arc with a leader line. Labels can always display + // inside or outside using [LabelPosition]. + // + // Text style for inside / outside can be controlled independently by + // setting [insideLabelStyleSpec] and [outsideLabelStyleSpec]. + // + // Example configuring different styles for inside/outside: + // new charts.ArcLabelDecorator( + // insideLabelStyleSpec: new charts.TextStyleSpec(...), + // outsideLabelStyleSpec: new charts.TextStyleSpec(...)), + defaultRenderer: new charts.ArcRendererConfig( + arcWidth: 60, + arcRendererDecorators: [new charts.ArcLabelDecorator()])); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.year}: ${row.sales}', + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/pie_chart/donut.dart b/web/charts/example/lib/pie_chart/donut.dart new file mode 100644 index 000000000..25fbb71a4 --- /dev/null +++ b/web/charts/example/lib/pie_chart/donut.dart @@ -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. + +/// Donut chart example. This is a simple pie chart with a hole in the middle. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class DonutPieChart extends StatelessWidget { + final List seriesList; + final bool animate; + + DonutPieChart(this.seriesList, {this.animate}); + + /// Creates a [PieChart] with sample data and no transition. + factory DonutPieChart.withSampleData() { + return new DonutPieChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DonutPieChart.withRandomData() { + return new DonutPieChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Configure the width of the pie slices to 60px. The remaining space in + // the chart will be left as a hole in the center. + defaultRenderer: new charts.ArcRendererConfig(arcWidth: 60)); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/pie_chart/gauge.dart b/web/charts/example/lib/pie_chart/gauge.dart new file mode 100644 index 000000000..59d28f2de --- /dev/null +++ b/web/charts/example/lib/pie_chart/gauge.dart @@ -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. + +/// Gauge chart example, where the data does not cover a full revolution in the +/// chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class GaugeChart extends StatelessWidget { + final List seriesList; + final bool animate; + + GaugeChart(this.seriesList, {this.animate}); + + /// Creates a [PieChart] with sample data and no transition. + factory GaugeChart.withSampleData() { + return new GaugeChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GaugeChart.withRandomData() { + return new GaugeChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new GaugeSegment('Low', random.nextInt(100)), + new GaugeSegment('Acceptable', random.nextInt(100)), + new GaugeSegment('High', random.nextInt(100)), + new GaugeSegment('Highly Unusual', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Segments', + domainFn: (GaugeSegment segment, _) => segment.segment, + measureFn: (GaugeSegment segment, _) => segment.size, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Configure the width of the pie slices to 30px. The remaining space in + // the chart will be left as a hole in the center. Adjust the start + // angle and the arc length of the pie so it resembles a gauge. + defaultRenderer: new charts.ArcRendererConfig( + arcWidth: 30, startAngle: 4 / 5 * pi, arcLength: 7 / 5 * pi)); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new GaugeSegment('Low', 75), + new GaugeSegment('Acceptable', 100), + new GaugeSegment('High', 50), + new GaugeSegment('Highly Unusual', 5), + ]; + + return [ + new charts.Series( + id: 'Segments', + domainFn: (GaugeSegment segment, _) => segment.segment, + measureFn: (GaugeSegment segment, _) => segment.size, + data: data, + ) + ]; + } +} + +/// Sample data type. +class GaugeSegment { + final String segment; + final int size; + + GaugeSegment(this.segment, this.size); +} diff --git a/web/charts/example/lib/pie_chart/outside_label.dart b/web/charts/example/lib/pie_chart/outside_label.dart new file mode 100644 index 000000000..93328c873 --- /dev/null +++ b/web/charts/example/lib/pie_chart/outside_label.dart @@ -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. + +/// Simple pie chart with outside labels example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class PieOutsideLabelChart extends StatelessWidget { + final List seriesList; + final bool animate; + + PieOutsideLabelChart(this.seriesList, {this.animate}); + + /// Creates a [PieChart] with sample data and no transition. + factory PieOutsideLabelChart.withSampleData() { + return new PieOutsideLabelChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PieOutsideLabelChart.withRandomData() { + return new PieOutsideLabelChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.year}: ${row.sales}', + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Add an [ArcLabelDecorator] configured to render labels outside of the + // arc with a leader line. + // + // Text style for inside / outside can be controlled independently by + // setting [insideLabelStyleSpec] and [outsideLabelStyleSpec]. + // + // Example configuring different styles for inside/outside: + // new charts.ArcLabelDecorator( + // insideLabelStyleSpec: new charts.TextStyleSpec(...), + // outsideLabelStyleSpec: new charts.TextStyleSpec(...)), + defaultRenderer: new charts.ArcRendererConfig(arcRendererDecorators: [ + new charts.ArcLabelDecorator( + labelPosition: charts.ArcLabelPosition.outside) + ])); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.year}: ${row.sales}', + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/pie_chart/partial_pie.dart b/web/charts/example/lib/pie_chart/partial_pie.dart new file mode 100644 index 000000000..6eb9e6aca --- /dev/null +++ b/web/charts/example/lib/pie_chart/partial_pie.dart @@ -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. + +/// Partial pie chart example, where the data does not cover a full revolution +/// in the chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class PartialPieChart extends StatelessWidget { + final List seriesList; + final bool animate; + + PartialPieChart(this.seriesList, {this.animate}); + + /// Creates a [PieChart] with sample data and no transition. + factory PartialPieChart.withSampleData() { + return new PartialPieChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PartialPieChart.withRandomData() { + return new PartialPieChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Configure the pie to display the data across only 3/4 instead of the full + // revolution. + return new charts.PieChart(seriesList, + animate: animate, + defaultRenderer: new charts.ArcRendererConfig(arcLength: 3 / 2 * pi)); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/pie_chart/pie_gallery.dart b/web/charts/example/lib/pie_chart/pie_gallery.dart new file mode 100644 index 000000000..224e96972 --- /dev/null +++ b/web/charts/example/lib/pie_chart/pie_gallery.dart @@ -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:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'auto_label.dart'; +import 'donut.dart'; +import 'gauge.dart'; +import 'simple.dart'; +import 'outside_label.dart'; +import 'partial_pie.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Simple Pie Chart', + subtitle: 'With a single series', + childBuilder: () => new SimplePieChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Outside Label Pie Chart', + subtitle: 'With a single series and labels outside the arcs', + childBuilder: () => new PieOutsideLabelChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Partial Pie Chart', + subtitle: 'That doesn\'t cover a full revolution', + childBuilder: () => new PartialPieChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Simple Donut Chart', + subtitle: 'With a single series and a hole in the middle', + childBuilder: () => new DonutPieChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Auto Label Donut Chart', + subtitle: + 'With a single series, a hole in the middle, and auto-positioned labels', + childBuilder: () => new DonutAutoLabelChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Gauge Chart', + subtitle: 'That doesn\'t cover a full revolution', + childBuilder: () => new GaugeChart.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/pie_chart/simple.dart b/web/charts/example/lib/pie_chart/simple.dart new file mode 100644 index 000000000..6dd6908a4 --- /dev/null +++ b/web/charts/example/lib/pie_chart/simple.dart @@ -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. + +/// Simple pie chart example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SimplePieChart extends StatelessWidget { + final List seriesList; + final bool animate; + + SimplePieChart(this.seriesList, {this.animate}); + + /// Creates a [PieChart] with sample data and no transition. + factory SimplePieChart.withSampleData() { + return new SimplePieChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimplePieChart.withRandomData() { + return new SimplePieChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/web/charts/example/lib/scatter_plot_chart/animation_zoom.dart b/web/charts/example/lib/scatter_plot_chart/animation_zoom.dart new file mode 100644 index 000000000..a5bf95d5b --- /dev/null +++ b/web/charts/example/lib/scatter_plot_chart/animation_zoom.dart @@ -0,0 +1,144 @@ +// 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. + +/// Example of a line chart with pan and zoom enabled via +/// [Charts.PanAndZoomBehavior]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class ScatterPlotAnimationZoomChart extends StatelessWidget { + final List seriesList; + final bool animate; + + ScatterPlotAnimationZoomChart(this.seriesList, {this.animate}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory ScatterPlotAnimationZoomChart.withSampleData() { + return new ScatterPlotAnimationZoomChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ScatterPlotAnimationZoomChart.withRandomData() { + return new ScatterPlotAnimationZoomChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = []; + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + for (var i = 0; i < 100; i++) { + data.add(new LinearSales(i, random.nextInt(100), makeRadius(4))); + } + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + behaviors: [ + new charts.PanAndZoomBehavior(), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5, 3.0), + new LinearSales(10, 25, 5.0), + new LinearSales(12, 75, 4.0), + new LinearSales(13, 225, 5.0), + new LinearSales(16, 50, 4.0), + new LinearSales(24, 75, 3.0), + new LinearSales(25, 100, 3.0), + new LinearSales(34, 150, 5.0), + new LinearSales(37, 10, 4.5), + new LinearSales(45, 300, 8.0), + new LinearSales(52, 15, 4.0), + new LinearSales(56, 200, 7.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + + LinearSales(this.year, this.sales, this.radius); +} diff --git a/web/charts/example/lib/scatter_plot_chart/bucketing_axis.dart b/web/charts/example/lib/scatter_plot_chart/bucketing_axis.dart new file mode 100644 index 000000000..acfe587e0 --- /dev/null +++ b/web/charts/example/lib/scatter_plot_chart/bucketing_axis.dart @@ -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. + +/// Example of a scatter plot chart with a bucketing measure axis and a legend. +/// +/// A bucketing measure axis 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. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class BucketingAxisScatterPlotChart extends StatelessWidget { + final List seriesList; + final bool animate; + + BucketingAxisScatterPlotChart(this.seriesList, {this.animate}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory BucketingAxisScatterPlotChart.withSampleData() { + return new BucketingAxisScatterPlotChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory BucketingAxisScatterPlotChart.withRandomData() { + return new BucketingAxisScatterPlotChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 6).toDouble(); + + // Make sure that the measure values for the first five series are well + // above the threshold. This simulates the grouping of the small values into + // the "Other" series. + final myFakeDesktopData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + final myFakeTabletData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + final myFakeMobileData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + final myFakeChromebookData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + final myFakeHomeData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + // Make sure that the "Other" series values are smaller. + final myFakeOtherData = [ + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeDesktopData), + new charts.Series( + id: 'Tablet', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeTabletData), + new charts.Series( + id: 'Mobile', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeMobileData), + new charts.Series( + id: 'Chromebook', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeChromebookData), + new charts.Series( + id: 'Home', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.indigo.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeHomeData), + new charts.Series( + id: 'Other', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.gray.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeOtherData), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + // Set up a bucketing axis that will place all values below 0.1 (10%) + // into a bucket at the bottom of the chart. + // + // Configure a tick count of 3 so that we get 100%, 50%, and the + // threshold. + primaryMeasureAxis: new charts.BucketingAxisSpec( + threshold: 0.1, + tickProviderSpec: new charts.BucketingNumericTickProviderSpec( + desiredTickCount: 3)), + // Add a series legend to display the series names. + behaviors: [ + new charts.SeriesLegend(position: charts.BehaviorPosition.end), + ], + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(52, 0.75, 14.0), + ]; + + final myFakeTabletData = [ + new LinearSales(45, 0.3, 18.0), + ]; + + final myFakeMobileData = [ + new LinearSales(56, 0.8, 17.0), + ]; + + final myFakeChromebookData = [ + new LinearSales(25, 0.6, 13.0), + ]; + + final myFakeHomeData = [ + new LinearSales(34, 0.5, 15.0), + ]; + + final myFakeOtherData = [ + new LinearSales(10, 0.25, 15.0), + new LinearSales(12, 0.075, 14.0), + new LinearSales(13, 0.225, 15.0), + new LinearSales(16, 0.03, 14.0), + new LinearSales(24, 0.04, 13.0), + new LinearSales(37, 0.1, 14.5), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeDesktopData), + new charts.Series( + id: 'Tablet', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeTabletData), + new charts.Series( + id: 'Mobile', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeMobileData), + new charts.Series( + id: 'Chromebook', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeChromebookData), + new charts.Series( + id: 'Home', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.indigo.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeHomeData), + new charts.Series( + id: 'Other', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.gray.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeOtherData), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final double revenueShare; + final double radius; + + LinearSales(this.year, this.revenueShare, this.radius); +} diff --git a/web/charts/example/lib/scatter_plot_chart/comparison_points.dart b/web/charts/example/lib/scatter_plot_chart/comparison_points.dart new file mode 100644 index 000000000..1e0e5d07b --- /dev/null +++ b/web/charts/example/lib/scatter_plot_chart/comparison_points.dart @@ -0,0 +1,169 @@ +// 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. + +/// Line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class ComparisonPointsScatterPlotChart extends StatelessWidget { + final List seriesList; + final bool animate; + + ComparisonPointsScatterPlotChart(this.seriesList, {this.animate}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory ComparisonPointsScatterPlotChart.withSampleData() { + return new ComparisonPointsScatterPlotChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ComparisonPointsScatterPlotChart.withRandomData() { + return new ComparisonPointsScatterPlotChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final maxMeasure = 100; + + final data = [ + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + domainLowerBoundFn: (LinearSales sales, _) => sales.yearLower, + domainUpperBoundFn: (LinearSales sales, _) => sales.yearUpper, + measureFn: (LinearSales sales, _) => sales.sales, + measureLowerBoundFn: (LinearSales sales, _) => sales.salesLower, + measureUpperBoundFn: (LinearSales sales, _) => sales.salesUpper, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } + + static LinearSales _makeRandomDatum(int max, Random random) { + final makeRadius = (int value) => (random.nextInt(value) + 6).toDouble(); + + final year = random.nextInt(max); + final yearLower = (year * 0.8).round(); + final yearUpper = year; + final sales = random.nextInt(max); + final salesLower = (sales * 0.8).round(); + final salesUpper = sales; + + return new LinearSales(year, yearLower, yearUpper, sales, salesLower, + salesUpper, makeRadius(4)); + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + defaultRenderer: + new charts.PointRendererConfig(pointRendererDecorators: [ + new charts.ComparisonPointsDecorator( + symbolRenderer: new charts.CylinderSymbolRenderer()) + ])); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(10, 7, 10, 25, 20, 25, 5.0), + new LinearSales(13, 11, 13, 225, 205, 225, 5.0), + new LinearSales(34, 34, 24, 150, 150, 130, 5.0), + new LinearSales(37, 37, 57, 10, 10, 12, 6.5), + new LinearSales(45, 35, 45, 260, 300, 260, 8.0), + new LinearSales(56, 46, 56, 200, 170, 200, 7.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + domainLowerBoundFn: (LinearSales sales, _) => sales.yearLower, + domainUpperBoundFn: (LinearSales sales, _) => sales.yearUpper, + measureFn: (LinearSales sales, _) => sales.sales, + measureLowerBoundFn: (LinearSales sales, _) => sales.salesLower, + measureUpperBoundFn: (LinearSales sales, _) => sales.salesUpper, + // Providing a radius function is optional. + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int yearLower; + final int yearUpper; + final int sales; + final int salesLower; + final int salesUpper; + final double radius; + + LinearSales(this.year, this.yearLower, this.yearUpper, this.sales, + this.salesLower, this.salesUpper, this.radius); +} diff --git a/web/charts/example/lib/scatter_plot_chart/scatter_plot_gallery.dart b/web/charts/example/lib/scatter_plot_chart/scatter_plot_gallery.dart new file mode 100644 index 000000000..be022a565 --- /dev/null +++ b/web/charts/example/lib/scatter_plot_chart/scatter_plot_gallery.dart @@ -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:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'animation_zoom.dart'; +import 'bucketing_axis.dart'; +import 'comparison_points.dart'; +import 'shapes.dart'; +import 'simple.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Simple Scatter Plot Chart', + subtitle: 'With a single series', + childBuilder: () => new SimpleScatterPlotChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Shapes Scatter Plot Chart', + subtitle: 'With custom shapes', + childBuilder: () => new ShapesScatterPlotChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Comparison Points Scatter Plot Chart', + subtitle: 'Scatter plot chart with comparison points', + childBuilder: () => new ComparisonPointsScatterPlotChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Pan and Zoom Scatter Plot Chart', + subtitle: 'Simple scatter plot chart pan and zoom behaviors enabled', + childBuilder: () => new ScatterPlotAnimationZoomChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Bucketing Axis Scatter Plot Chart', + subtitle: 'Scatter plot with a measure axis that buckets values less ' + + 'than 10% into a single region below the draw area', + childBuilder: () => new BucketingAxisScatterPlotChart.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/scatter_plot_chart/shapes.dart b/web/charts/example/lib/scatter_plot_chart/shapes.dart new file mode 100644 index 000000000..f6824f4db --- /dev/null +++ b/web/charts/example/lib/scatter_plot_chart/shapes.dart @@ -0,0 +1,206 @@ +// 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. + +/// Example of a scatter plot chart using custom symbols for the points. +/// +/// The series has been configured to draw each point as a square by default. +/// +/// Some data will be drawn as a circle, indicated by defining a custom "circle" +/// value referenced by [pointSymbolRendererFnKey]. +/// +/// Some other data have will be drawn as a hollow circle. In addition to the +/// custom renderer key, these data also have stroke and fillColor values +/// defined. Configuring a separate fillColor will cause the center of the shape +/// to be filled in, with white in these examples. The border of the shape will +/// be color with the color of the data. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class ShapesScatterPlotChart extends StatelessWidget { + final List seriesList; + final bool animate; + + ShapesScatterPlotChart(this.seriesList, {this.animate}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory ShapesScatterPlotChart.withSampleData() { + return new ShapesScatterPlotChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ShapesScatterPlotChart.withRandomData() { + return new ShapesScatterPlotChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + final data = [ + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + 'circle', null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + 'circle', null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow square, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, null, charts.MaterialPalette.white, 2.0), + ]; + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + fillColorFn: (LinearSales row, _) => row.fillColor, + strokeWidthPxFn: (LinearSales row, _) => row.strokeWidth, + data: data, + ) + // Accessor function that associates each datum with a symbol renderer. + ..setAttribute( + charts.pointSymbolRendererFnKey, (int index) => data[index].shape) + // Default symbol renderer ID for data that have no defined shape. + ..setAttribute(charts.pointSymbolRendererIdKey, 'rect') + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + // Configure the point renderer to have a map of custom symbol + // renderers. + defaultRenderer: + new charts.PointRendererConfig(customSymbolRenderers: { + 'circle': new charts.CircleSymbolRenderer(), + 'rect': new charts.RectSymbolRenderer(), + })); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5, 3.0, 'circle', null, null), + new LinearSales(10, 25, 5.0, null, null, null), + new LinearSales(12, 75, 4.0, null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales( + 13, 225, 5.0, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(16, 50, 4.0, null, null, null), + new LinearSales(24, 75, 3.0, null, null, null), + new LinearSales(25, 100, 3.0, 'circle', null, null), + new LinearSales(34, 150, 5.0, null, null, null), + new LinearSales(37, 10, 4.5, null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales( + 45, 300, 8.0, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(52, 15, 4.0, null, null, null), + // Render a hollow square, filled in with white. + new LinearSales(56, 200, 7.0, null, charts.MaterialPalette.white, 2.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + fillColorFn: (LinearSales row, _) => row.fillColor, + strokeWidthPxFn: (LinearSales row, _) => row.strokeWidth, + data: data, + ) + // Accessor function that associates each datum with a symbol renderer. + ..setAttribute( + charts.pointSymbolRendererFnKey, (int index) => data[index].shape) + // Default symbol renderer ID for data that have no defined shape. + ..setAttribute(charts.pointSymbolRendererIdKey, 'rect') + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + final String shape; + final charts.Color fillColor; + final double strokeWidth; + + LinearSales(this.year, this.sales, this.radius, this.shape, this.fillColor, + this.strokeWidth); +} diff --git a/web/charts/example/lib/scatter_plot_chart/simple.dart b/web/charts/example/lib/scatter_plot_chart/simple.dart new file mode 100644 index 000000000..e7b4a0df0 --- /dev/null +++ b/web/charts/example/lib/scatter_plot_chart/simple.dart @@ -0,0 +1,150 @@ +// 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. + +/// Scatter plot chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SimpleScatterPlotChart extends StatelessWidget { + final List seriesList; + final bool animate; + + SimpleScatterPlotChart(this.seriesList, {this.animate}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory SimpleScatterPlotChart.withSampleData() { + return new SimpleScatterPlotChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleScatterPlotChart.withRandomData() { + return new SimpleScatterPlotChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + final data = [ + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + ]; + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5, 3.0), + new LinearSales(10, 25, 5.0), + new LinearSales(12, 75, 4.0), + new LinearSales(13, 225, 5.0), + new LinearSales(16, 50, 4.0), + new LinearSales(24, 75, 3.0), + new LinearSales(25, 100, 3.0), + new LinearSales(34, 150, 5.0), + new LinearSales(37, 10, 4.5), + new LinearSales(45, 300, 8.0), + new LinearSales(52, 15, 4.0), + new LinearSales(56, 200, 7.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + // Providing a radius function is optional. + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + + LinearSales(this.year, this.sales, this.radius); +} diff --git a/web/charts/example/lib/time_series_chart/confidence_interval.dart b/web/charts/example/lib/time_series_chart/confidence_interval.dart new file mode 100644 index 000000000..d421b3e05 --- /dev/null +++ b/web/charts/example/lib/time_series_chart/confidence_interval.dart @@ -0,0 +1,119 @@ +// 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. + +/// Example of a time series chart with a confidence interval. +/// +/// Confidence interval is defined by specifying the upper and lower measure +/// bounds in the series. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class TimeSeriesConfidenceInterval extends StatelessWidget { + final List seriesList; + final bool animate; + + TimeSeriesConfidenceInterval(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesConfidenceInterval.withSampleData() { + return new TimeSeriesConfidenceInterval( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesConfidenceInterval.withRandomData() { + return new TimeSeriesConfidenceInterval(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + // When the measureLowerBoundFn and measureUpperBoundFn is defined, + // the line renderer will render the area around the bounds. + measureLowerBoundFn: (TimeSeriesSales sales, _) => sales.sales - 5, + measureUpperBoundFn: (TimeSeriesSales sales, _) => sales.sales + 5, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + // When the measureLowerBoundFn and measureUpperBoundFn is defined, + // the line renderer will render the area around the bounds. + measureLowerBoundFn: (TimeSeriesSales sales, _) => sales.sales - 5, + measureUpperBoundFn: (TimeSeriesSales sales, _) => sales.sales + 5, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/web/charts/example/lib/time_series_chart/end_points_axis.dart b/web/charts/example/lib/time_series_chart/end_points_axis.dart new file mode 100644 index 000000000..9793c62d3 --- /dev/null +++ b/web/charts/example/lib/time_series_chart/end_points_axis.dart @@ -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. + +/// Example of a time series chart with an end points domain axis. +/// +/// An end points axis generates two ticks, one at each end of the axis range. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class EndPointsAxisTimeSeriesChart extends StatelessWidget { + final List seriesList; + final bool animate; + + EndPointsAxisTimeSeriesChart(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory EndPointsAxisTimeSeriesChart.withSampleData() { + return new EndPointsAxisTimeSeriesChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory EndPointsAxisTimeSeriesChart.withRandomData() { + return new EndPointsAxisTimeSeriesChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Configures an axis spec that is configured to render one tick at each + // end of the axis range, anchored "inside" the axis. The start tick label + // will be left-aligned with its tick mark, and the end tick label will be + // right-aligned with its tick mark. + domainAxis: new charts.EndPointsTimeAxisSpec(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/web/charts/example/lib/time_series_chart/line_annotation.dart b/web/charts/example/lib/time_series_chart/line_annotation.dart new file mode 100644 index 000000000..22e8c02a0 --- /dev/null +++ b/web/charts/example/lib/time_series_chart/line_annotation.dart @@ -0,0 +1,115 @@ +// 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. + +/// Time series chart with line annotation example +/// +/// The example future range annotation extends beyond the range of the series +/// data, demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] +/// flag. This can be set to false to disable range extension. +/// +/// Additional annotations may be added simply by adding additional +/// [Charts.RangeAnnotationSegment] items to the list. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class TimeSeriesLineAnnotationChart extends StatelessWidget { + final List seriesList; + final bool animate; + + TimeSeriesLineAnnotationChart(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesLineAnnotationChart.withSampleData() { + return new TimeSeriesLineAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesLineAnnotationChart.withRandomData() { + return new TimeSeriesLineAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, animate: animate, behaviors: [ + new charts.RangeAnnotation([ + new charts.LineAnnotationSegment( + new DateTime(2017, 10, 4), charts.RangeAnnotationAxisType.domain, + startLabel: 'Oct 4'), + new charts.LineAnnotationSegment( + new DateTime(2017, 10, 15), charts.RangeAnnotationAxisType.domain, + endLabel: 'Oct 15'), + ]), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/web/charts/example/lib/time_series_chart/range_annotation.dart b/web/charts/example/lib/time_series_chart/range_annotation.dart new file mode 100644 index 000000000..b98e22ec5 --- /dev/null +++ b/web/charts/example/lib/time_series_chart/range_annotation.dart @@ -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. + +/// Time series chart with range annotation example +/// +/// The example future range annotation extends beyond the range of the series +/// data, demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] +/// flag. This can be set to false to disable range extension. +/// +/// Additional annotations may be added simply by adding additional +/// [Charts.RangeAnnotationSegment] items to the list. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class TimeSeriesRangeAnnotationChart extends StatelessWidget { + final List seriesList; + final bool animate; + + TimeSeriesRangeAnnotationChart(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesRangeAnnotationChart.withSampleData() { + return new TimeSeriesRangeAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesRangeAnnotationChart.withRandomData() { + return new TimeSeriesRangeAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, animate: animate, behaviors: [ + new charts.RangeAnnotation([ + new charts.RangeAnnotationSegment(new DateTime(2017, 10, 4), + new DateTime(2017, 10, 15), charts.RangeAnnotationAxisType.domain), + ]), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/web/charts/example/lib/time_series_chart/range_annotation_margin.dart b/web/charts/example/lib/time_series_chart/range_annotation_margin.dart new file mode 100644 index 000000000..3c8c0b763 --- /dev/null +++ b/web/charts/example/lib/time_series_chart/range_annotation_margin.dart @@ -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. + +/// Example of a time series chart with range annotations configured to render +/// labels in the chart margin area. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class TimeSeriesRangeAnnotationMarginChart extends StatelessWidget { + final List seriesList; + final bool animate; + + TimeSeriesRangeAnnotationMarginChart(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesRangeAnnotationMarginChart.withSampleData() { + return new TimeSeriesRangeAnnotationMarginChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesRangeAnnotationMarginChart.withRandomData() { + return new TimeSeriesRangeAnnotationMarginChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + // Fix one of the points to 100 so that the annotations are consistently + // placed. + new TimeSeriesSales(new DateTime(2017, 10, 10), 100), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, + animate: animate, + + // Allow enough space in the left and right chart margins for the + // annotations. + layoutConfig: new charts.LayoutConfig( + leftMarginSpec: new charts.MarginSpec.fixedPixel(60), + topMarginSpec: new charts.MarginSpec.fixedPixel(20), + rightMarginSpec: new charts.MarginSpec.fixedPixel(60), + bottomMarginSpec: new charts.MarginSpec.fixedPixel(20)), + behaviors: [ + // Define one domain and two measure annotations configured to render + // labels in the chart margins. + new charts.RangeAnnotation([ + new charts.RangeAnnotationSegment( + new DateTime(2017, 10, 4), + new DateTime(2017, 10, 15), + charts.RangeAnnotationAxisType.domain, + startLabel: 'D1 Start', + endLabel: 'D1 End', + labelAnchor: charts.AnnotationLabelAnchor.end, + color: charts.MaterialPalette.gray.shade200, + // Override the default vertical direction for domain labels. + labelDirection: charts.AnnotationLabelDirection.horizontal), + new charts.RangeAnnotationSegment( + 15, 20, charts.RangeAnnotationAxisType.measure, + startLabel: 'M1 Start', + endLabel: 'M1 End', + labelAnchor: charts.AnnotationLabelAnchor.end, + color: charts.MaterialPalette.gray.shade300), + new charts.RangeAnnotationSegment( + 35, 65, charts.RangeAnnotationAxisType.measure, + startLabel: 'M2 Start', + endLabel: 'M2 End', + labelAnchor: charts.AnnotationLabelAnchor.start, + color: charts.MaterialPalette.gray.shade300), + ], defaultLabelPosition: charts.AnnotationLabelPosition.margin), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/web/charts/example/lib/time_series_chart/simple.dart b/web/charts/example/lib/time_series_chart/simple.dart new file mode 100644 index 000000000..b286f7c96 --- /dev/null +++ b/web/charts/example/lib/time_series_chart/simple.dart @@ -0,0 +1,108 @@ +// 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. + +/// Timeseries chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class SimpleTimeSeriesChart extends StatelessWidget { + final List seriesList; + final bool animate; + + SimpleTimeSeriesChart(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory SimpleTimeSeriesChart.withSampleData() { + return new SimpleTimeSeriesChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleTimeSeriesChart.withRandomData() { + return new SimpleTimeSeriesChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/web/charts/example/lib/time_series_chart/symbol_annotation.dart b/web/charts/example/lib/time_series_chart/symbol_annotation.dart new file mode 100644 index 000000000..ca28bd504 --- /dev/null +++ b/web/charts/example/lib/time_series_chart/symbol_annotation.dart @@ -0,0 +1,294 @@ +// 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. + +/// Example of timeseries chart with annotation rows between the chart draw area +/// and the domain axis. +/// +/// The symbol annotation renderer draws a row of symbols for each series below +/// the drawArea but above the bottom axis. +/// +/// This renderer can draw point annotations and range annotations. Point +/// annotations are drawn at the location of the domain along the chart's domain +/// axis, in the row for its series. Range annotations are drawn as a range +/// shape between the domainLowerBound and domainUpperBound positions along the +/// chart's domain axis. Point annotations are drawn on top of range +/// annotations. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class TimeSeriesSymbolAnnotationChart extends StatelessWidget { + final List seriesList; + final bool animate; + + TimeSeriesSymbolAnnotationChart(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesSymbolAnnotationChart.withSampleData() { + return new TimeSeriesSymbolAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesSymbolAnnotationChart.withRandomData() { + return new TimeSeriesSymbolAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myDesktopData = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 19), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 26), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 10, 3), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 10, 10), sales: random.nextInt(100)), + ]; + + final myTabletData = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 19), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 26), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 10, 3), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 10, 10), sales: random.nextInt(100)), + ]; + + // Example of a series with two range annotations. A regular point shape + // will be drawn at the current domain value, and a range shape will be + // drawn between the previous and target domain values. + // + // Note that these series do not contain any measure values. They are + // positioned automatically in rows. + final myAnnotationDataTop = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 24), + timePrevious: new DateTime(2017, 9, 19), + timeTarget: new DateTime(2017, 9, 24), + ), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 29), + timePrevious: new DateTime(2017, 9, 29), + timeTarget: new DateTime(2017, 10, 4), + ), + ]; + + // Example of a series with one range annotation and two single point + // annotations. Omitting the previous and target domain values causes that + // datum to be drawn as a single point. + final myAnnotationDataBottom = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 25), + timePrevious: new DateTime(2017, 9, 21), + timeTarget: new DateTime(2017, 9, 25), + ), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 31)), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 5)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: myDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: myTabletData, + ), + new charts.Series( + id: 'Annotation Series 1', + colorFn: (_, __) => charts.MaterialPalette.gray.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + domainLowerBoundFn: (TimeSeriesSales row, _) => row.timePrevious, + domainUpperBoundFn: (TimeSeriesSales row, _) => row.timeTarget, + // No measure values are needed for symbol annotations. + measureFn: (_, __) => null, + data: myAnnotationDataTop, + ) + // Configure our custom symbol annotation renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customSymbolAnnotation') + // Optional radius for the annotation shape. If not specified, this will + // default to the same radius as the points. + ..setAttribute(charts.boundsLineRadiusPxKey, 3.5), + new charts.Series( + id: 'Annotation Series 2', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + domainLowerBoundFn: (TimeSeriesSales row, _) => row.timePrevious, + domainUpperBoundFn: (TimeSeriesSales row, _) => row.timeTarget, + // No measure values are needed for symbol annotations. + measureFn: (_, __) => null, + data: myAnnotationDataBottom, + ) + // Configure our custom symbol annotation renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customSymbolAnnotation') + // Optional radius for the annotation shape. If not specified, this will + // default to the same radius as the points. + ..setAttribute(charts.boundsLineRadiusPxKey, 3.5), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Custom renderer configuration for the point series. + customSeriesRenderers: [ + new charts.SymbolAnnotationRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customSymbolAnnotation') + ], + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myDesktopData = [ + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 19), sales: 5), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 26), sales: 25), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 3), sales: 100), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 10), sales: 75), + ]; + + final myTabletData = [ + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 19), sales: 10), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 26), sales: 50), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 3), sales: 200), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 10), sales: 150), + ]; + + // Example of a series with two range annotations. A regular point shape + // will be drawn at the current domain value, and a range shape will be + // drawn between the previous and target domain values. + // + // Note that these series do not contain any measure values. They are + // positioned automatically in rows. + final myAnnotationDataTop = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 24), + timePrevious: new DateTime(2017, 9, 19), + timeTarget: new DateTime(2017, 9, 24), + ), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 29), + timePrevious: new DateTime(2017, 9, 29), + timeTarget: new DateTime(2017, 10, 4), + ), + ]; + + // Example of a series with one range annotation and two single point + // annotations. Omitting the previous and target domain values causes that + // datum to be drawn as a single point. + final myAnnotationDataBottom = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 25), + timePrevious: new DateTime(2017, 9, 21), + timeTarget: new DateTime(2017, 9, 25), + ), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 31)), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 5)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: myDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: myTabletData, + ), + new charts.Series( + id: 'Annotation Series 1', + colorFn: (_, __) => charts.MaterialPalette.gray.shadeDefault, + // A point shape will be drawn at the location of the domain. + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + // A range shape will be drawn between the lower and upper domain + // bounds. The range will be drawn underneath the domain point. + domainLowerBoundFn: (TimeSeriesSales row, _) => row.timePrevious, + domainUpperBoundFn: (TimeSeriesSales row, _) => row.timeTarget, + // No measure values are needed for symbol annotations. + measureFn: (_, __) => null, + data: myAnnotationDataTop, + ) + // Configure our custom symbol annotation renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customSymbolAnnotation') + // Optional radius for the annotation range. If not specified, this will + // default to the same radius as the domain point. + ..setAttribute(charts.boundsLineRadiusPxKey, 3.5), + new charts.Series( + id: 'Annotation Series 2', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + // A point shape will be drawn at the location of the domain. + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + // A range shape will be drawn between the lower and upper domain + // bounds. The range will be drawn underneath the domain point. + domainLowerBoundFn: (TimeSeriesSales row, _) => row.timePrevious, + domainUpperBoundFn: (TimeSeriesSales row, _) => row.timeTarget, + // No measure values are needed for symbol annotations. + measureFn: (_, __) => null, + data: myAnnotationDataBottom, + ) + // Configure our custom symbol annotation renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customSymbolAnnotation') + // Optional radius for the annotation range. If not specified, this will + // default to the same radius as the domain point. + ..setAttribute(charts.boundsLineRadiusPxKey, 3.5), + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime timeCurrent; + final DateTime timePrevious; + final DateTime timeTarget; + final int sales; + + TimeSeriesSales( + {this.timeCurrent, this.timePrevious, this.timeTarget, this.sales}); +} diff --git a/web/charts/example/lib/time_series_chart/time_series_gallery.dart b/web/charts/example/lib/time_series_chart/time_series_gallery.dart new file mode 100644 index 000000000..96ce0f1fa --- /dev/null +++ b/web/charts/example/lib/time_series_chart/time_series_gallery.dart @@ -0,0 +1,80 @@ +// 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:flutter_web/material.dart'; +import '../gallery_scaffold.dart'; +import 'confidence_interval.dart'; +import 'end_points_axis.dart'; +import 'line_annotation.dart'; +import 'range_annotation.dart'; +import 'range_annotation_margin.dart'; +import 'simple.dart'; +import 'symbol_annotation.dart'; +import 'with_bar_renderer.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Time Series Chart', + subtitle: 'Simple single time series chart', + childBuilder: () => new SimpleTimeSeriesChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'End Points Axis Time Series Chart', + subtitle: 'Time series chart with an end points axis', + childBuilder: () => new EndPointsAxisTimeSeriesChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Line Annotation on Time Series Chart', + subtitle: 'Time series chart with future line annotation', + childBuilder: () => new TimeSeriesLineAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Range Annotation on Time Series Chart', + subtitle: 'Time series chart with future range annotation', + childBuilder: () => new TimeSeriesRangeAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Range Annotation Margin Labels on Time Series Chart', + subtitle: + 'Time series chart with range annotations with labels in margins', + childBuilder: () => + new TimeSeriesRangeAnnotationMarginChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Symbol Annotation Time Series Chart', + subtitle: 'Time series chart with annotation data below the draw area', + childBuilder: () => new TimeSeriesSymbolAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Time Series Chart with Bars', + subtitle: 'Time series chart using the bar renderer', + childBuilder: () => new TimeSeriesBar.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Time Series Chart with Confidence Interval', + subtitle: 'Draws area around the confidence interval', + childBuilder: () => new TimeSeriesConfidenceInterval.withRandomData(), + ), + ]; +} diff --git a/web/charts/example/lib/time_series_chart/with_bar_renderer.dart b/web/charts/example/lib/time_series_chart/with_bar_renderer.dart new file mode 100644 index 000000000..fcd67f83a --- /dev/null +++ b/web/charts/example/lib/time_series_chart/with_bar_renderer.dart @@ -0,0 +1,148 @@ +// 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. + +/// Example of a time series chart using a bar renderer. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter_web/material.dart'; + +class TimeSeriesBar extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesBar(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesBar.withSampleData() { + return new TimeSeriesBar( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesBar.withRandomData() { + return new TimeSeriesBar(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 1), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 2), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 4), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 5), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 6), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 7), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 8), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 9), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 10), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 11), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 12), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 13), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 14), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 15), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 16), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 17), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 18), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 20), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 21), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Set the default renderer to a bar renderer. + // This can also be one of the custom renderers of the time series chart. + defaultRenderer: new charts.BarRendererConfig(), + // It is recommended that default interactions be turned off if using bar + // renderer, because the line point highlighter is the default for time + // series chart. + defaultInteractions: false, + // If default interactions were removed, optionally add select nearest + // and the domain highlighter that are typical for bar charts. + behaviors: [new charts.SelectNearest(), new charts.DomainHighlighter()], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 1), 5), + new TimeSeriesSales(new DateTime(2017, 9, 2), 5), + new TimeSeriesSales(new DateTime(2017, 9, 3), 25), + new TimeSeriesSales(new DateTime(2017, 9, 4), 100), + new TimeSeriesSales(new DateTime(2017, 9, 5), 75), + new TimeSeriesSales(new DateTime(2017, 9, 6), 88), + new TimeSeriesSales(new DateTime(2017, 9, 7), 65), + new TimeSeriesSales(new DateTime(2017, 9, 8), 91), + new TimeSeriesSales(new DateTime(2017, 9, 9), 100), + new TimeSeriesSales(new DateTime(2017, 9, 10), 111), + new TimeSeriesSales(new DateTime(2017, 9, 11), 90), + new TimeSeriesSales(new DateTime(2017, 9, 12), 50), + new TimeSeriesSales(new DateTime(2017, 9, 13), 40), + new TimeSeriesSales(new DateTime(2017, 9, 14), 30), + new TimeSeriesSales(new DateTime(2017, 9, 15), 40), + new TimeSeriesSales(new DateTime(2017, 9, 16), 50), + new TimeSeriesSales(new DateTime(2017, 9, 17), 30), + new TimeSeriesSales(new DateTime(2017, 9, 18), 35), + new TimeSeriesSales(new DateTime(2017, 9, 19), 40), + new TimeSeriesSales(new DateTime(2017, 9, 20), 32), + new TimeSeriesSales(new DateTime(2017, 9, 21), 31), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/web/charts/example/pubspec.lock b/web/charts/example/pubspec.lock new file mode 100644 index 000000000..2cc415e66 --- /dev/null +++ b/web/charts/example/pubspec.lock @@ -0,0 +1,485 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + charts_common: + dependency: transitive + description: + path: "../common" + relative: true + source: path + version: "0.6.0" + charts_flutter: + dependency: "direct main" + description: + path: "../flutter" + relative: true + source: path + version: "0.6.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct overridden" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: "direct main" + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/charts/example/pubspec.yaml b/web/charts/example/pubspec.yaml new file mode 100644 index 000000000..32987efdf --- /dev/null +++ b/web/charts/example/pubspec.yaml @@ -0,0 +1,24 @@ +name: example +description: Charts-Flutter Demo +dependencies: + charts_flutter: + path: ../flutter + flutter_web: any + meta: ^1.1.1 + intl: ^0.15.2 + +dev_dependencies: + build_runner: any + build_web_compilers: any + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/charts/example/web/assets/FontManifest.json b/web/charts/example/web/assets/FontManifest.json new file mode 100644 index 000000000..43fa68900 --- /dev/null +++ b/web/charts/example/web/assets/FontManifest.json @@ -0,0 +1,10 @@ +[ + { + "family": "MaterialIcons", + "fonts": [ + { + "asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" + } + ] + } +] diff --git a/web/charts/example/web/index.html b/web/charts/example/web/index.html new file mode 100644 index 000000000..b54ed98d8 --- /dev/null +++ b/web/charts/example/web/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/charts/example/web/main.dart b/web/charts/example/web/main.dart new file mode 100644 index 000000000..f12316cdf --- /dev/null +++ b/web/charts/example/web/main.dart @@ -0,0 +1,10 @@ +// 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 'package:flutter_web_ui/ui.dart' as ui; +import 'package:example/main.dart' as app; + +main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/charts/example/web/preview.png b/web/charts/example/web/preview.png new file mode 100644 index 000000000..45399d470 Binary files /dev/null and b/web/charts/example/web/preview.png differ diff --git a/web/charts/flutter/CHANGELOG.md b/web/charts/flutter/CHANGELOG.md new file mode 100644 index 000000000..331518adc --- /dev/null +++ b/web/charts/flutter/CHANGELOG.md @@ -0,0 +1,53 @@ +# 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 +* Fixed export file to export ChartsBehavior in the Flutter library instead of the one that resides +in charts_common. The charts_common behavior should not be used except internally in the +charts_flutter library. This is a breaking change if you are using charts_common behavior. +* 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. This is a breaking change. +* BarTargetLineRendererConfig is no longer default of type String, please change current usage to +BarTargetLineRendererConfig. 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()'. +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. diff --git a/web/charts/flutter/LICENSE b/web/charts/flutter/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/web/charts/flutter/LICENSE @@ -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. diff --git a/web/charts/flutter/README.md b/web/charts/flutter/README.md new file mode 100644 index 000000000..83ea081ff --- /dev/null +++ b/web/charts/flutter/README.md @@ -0,0 +1,14 @@ +# Flutter Charting library + +[![pub package](https://img.shields.io/pub/v/charts_flutter.svg)](https://pub.dartlang.org/packages/charts_flutter) + +Material Design data visualization library written natively in Dart. + +## Supported charts + +See the [online gallery](https://google.github.io/charts/flutter/gallery.html). + +## Using the library + +The `/example/` folder inside `charts_flutter` in the [GitHub repo](https://github.com/google/charts) +contains a full Flutter app with many demo examples. diff --git a/web/charts/flutter/lib/flutter.dart b/web/charts/flutter/lib/flutter.dart new file mode 100644 index 000000000..84047edd8 --- /dev/null +++ b/web/charts/flutter/lib/flutter.dart @@ -0,0 +1,191 @@ +// 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 'package:charts_common/common.dart' + show + boundsLineRadiusPxFnKey, + boundsLineRadiusPxKey, + measureAxisIdKey, + pointSymbolRendererFnKey, + pointSymbolRendererIdKey, + rendererIdKey, + AnnotationLabelAnchor, + AnnotationLabelDirection, + AnnotationLabelPosition, + ArcLabelDecorator, + ArcLabelLeaderLineStyleSpec, + ArcLabelPosition, + ArcRenderer, + ArcRendererConfig, + AutoDateTimeTickFormatterSpec, + AutoDateTimeTickProviderSpec, + Axis, + AxisDirection, + AxisSpec, + BarGroupingType, + BarLabelAnchor, + BarLabelDecorator, + BarLabelPosition, + BarLaneRendererConfig, + BarRenderer, + BarRendererConfig, + BarTargetLineRenderer, + BarTargetLineRendererConfig, + BaseCartesianRenderer, + BasicNumericTickFormatterSpec, + BasicNumericTickProviderSpec, + BasicOrdinalTickProviderSpec, + BasicOrdinalTickFormatterSpec, + BehaviorPosition, + BucketingAxisSpec, + BucketingNumericTickProviderSpec, + CartesianChart, + ChartCanvas, + ChartContext, + ChartTitleDirection, + CircleSymbolRenderer, + Color, + ComparisonPointsDecorator, + ConstCornerStrategy, + CornerStrategy, + CylinderSymbolRenderer, + DateTimeAxisSpec, + DateTimeEndPointsTickProviderSpec, + DateTimeExtents, + DateTimeFactory, + DateTimeTickFormatter, + DateTimeTickFormatterSpec, + DateTimeTickProviderSpec, + DayTickProviderSpec, + DomainFormatter, + EndPointsTimeAxisSpec, + ExploreModeTrigger, + FillPatternType, + GestureListener, + GraphicsFactory, + GridlineRendererSpec, + ImmutableSeries, + InsideJustification, + LayoutPosition, + LayoutViewPaintOrder, + LayoutViewPositionOrder, + LegendDefaultMeasure, + LegendTapHandling, + LineAnnotationSegment, + LinePointHighlighterFollowLineType, + LineRenderer, + LineRendererConfig, + LineStyleSpec, + LocalDateTimeFactory, + LockSelection, + MarginSpec, + MaterialPalette, + MaterialStyle, + MaxWidthStrategy, + MeasureFormatter, + NoCornerStrategy, + NoneRenderSpec, + NumericAxis, + NumericAxisSpec, + NumericCartesianChart, + NumericEndPointsTickProviderSpec, + NumericExtents, + NumericTickFormatterSpec, + NumericTickProviderSpec, + OrdinalAxis, + OrdinalAxisSpec, + OrdinalCartesianChart, + OrdinalTickFormatterSpec, + OrdinalTickProviderSpec, + OrdinalViewport, + OutsideJustification, + PanningCompletedCallback, + PercentAxisSpec, + PercentInjectorTotalType, + Performance, + PointRenderer, + PointRendererConfig, + PointRendererDecorator, + PointSymbolRenderer, + RangeAnnotationAxisType, + RangeAnnotationSegment, + RectSymbolRenderer, + RenderSpec, + RTLSpec, + SelectionModel, + SelectionModelListener, + SelectionModelType, + SelectionTrigger, + Series, + SeriesDatum, + SeriesDatumConfig, + SeriesRenderer, + SeriesRendererConfig, + SimpleTickFormatterBase, + SliderListenerCallback, + SliderListenerDragState, + SliderStyle, + SmallTickRendererSpec, + StaticDateTimeTickProviderSpec, + StaticNumericTickProviderSpec, + StaticOrdinalTickProviderSpec, + StyleFactory, + SymbolAnnotationRenderer, + SymbolAnnotationRendererConfig, + TextStyleSpec, + TickFormatter, + TickFormatterSpec, + TickLabelAnchor, + TickLabelJustification, + TickSpec, + TimeFormatterSpec, + TypedAccessorFn, + UTCDateTimeFactory, + ViewMargin, + VocalizationCallback; + +export 'src/bar_chart.dart'; +export 'src/base_chart.dart' show BaseChart, LayoutConfig; +export 'src/behaviors/a11y/domain_a11y_explore_behavior.dart' + show DomainA11yExploreBehavior; +export 'src/behaviors/chart_behavior.dart' show ChartBehavior; +export 'src/behaviors/domain_highlighter.dart' show DomainHighlighter; +export 'src/behaviors/initial_selection.dart' show InitialSelection; +export 'src/behaviors/calculation/percent_injector.dart' show PercentInjector; +export 'src/behaviors/chart_title/chart_title.dart' show ChartTitle; +export 'src/behaviors/legend/datum_legend.dart' show DatumLegend; +export 'src/behaviors/legend/legend_content_builder.dart' + show LegendContentBuilder, TabularLegendContentBuilder; +export 'src/behaviors/legend/legend_layout.dart' + show LegendLayout, TabularLegendLayout; +export 'src/behaviors/legend/series_legend.dart' show SeriesLegend; +export 'src/behaviors/line_point_highlighter.dart' show LinePointHighlighter; +export 'src/behaviors/range_annotation.dart' show RangeAnnotation; +export 'src/behaviors/select_nearest.dart' show SelectNearest; +export 'src/behaviors/sliding_viewport.dart' show SlidingViewport; +export 'src/behaviors/slider/slider.dart' show Slider; +export 'src/behaviors/zoom/initial_hint_behavior.dart' show InitialHintBehavior; +export 'src/behaviors/zoom/pan_and_zoom_behavior.dart' show PanAndZoomBehavior; +export 'src/behaviors/zoom/pan_behavior.dart' show PanBehavior; +export 'src/combo_chart/combo_chart.dart'; +export 'src/line_chart.dart'; +export 'src/pie_chart.dart'; +export 'src/scatter_plot_chart.dart'; +export 'src/selection_model_config.dart' show SelectionModelConfig; +export 'src/symbol_renderer.dart' show CustomSymbolRenderer; +export 'src/time_series_chart.dart'; +export 'src/user_managed_state.dart' + show UserManagedState, UserManagedSelectionModel; +export 'src/util/color.dart' show ColorUtil; diff --git a/web/charts/flutter/lib/src/bar_chart.dart b/web/charts/flutter/lib/src/bar_chart.dart new file mode 100644 index 000000000..f8777916c --- /dev/null +++ b/web/charts/flutter/lib/src/bar_chart.dart @@ -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 'dart:collection' show LinkedHashMap; + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + BarChart, + BarGroupingType, + BarRendererConfig, + BarRendererDecorator, + NumericAxisSpec, + RTLSpec, + Series, + SeriesRendererConfig; +import 'behaviors/domain_highlighter.dart' show DomainHighlighter; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'package:meta/meta.dart' show immutable; +import 'base_chart.dart' show LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'cartesian_chart.dart' show CartesianChart; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +@immutable +class BarChart extends CartesianChart { + final bool vertical; + final common.BarRendererDecorator barRendererDecorator; + + BarChart( + List> seriesList, { + bool animate, + Duration animationDuration, + common.AxisSpec domainAxis, + common.AxisSpec primaryMeasureAxis, + common.AxisSpec secondaryMeasureAxis, + LinkedHashMap disjointMeasureAxes, + common.BarGroupingType barGroupingType, + common.BarRendererConfig defaultRenderer, + List> customSeriesRenderers, + List behaviors, + List> selectionModels, + common.RTLSpec rtlSpec, + this.vertical: true, + bool defaultInteractions: true, + LayoutConfig layoutConfig, + UserManagedState userManagedState, + this.barRendererDecorator, + bool flipVerticalAxis, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes, + defaultRenderer: defaultRenderer ?? + new common.BarRendererConfig( + groupingType: barGroupingType, + barRendererDecorator: barRendererDecorator), + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + defaultInteractions: defaultInteractions, + layoutConfig: layoutConfig, + userManagedState: userManagedState, + flipVerticalAxis: flipVerticalAxis, + ); + + @override + common.BarChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.BarChart( + vertical: vertical, + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(), + disjointMeasureAxes: createDisjointMeasureAxes()); + } + + @override + void addDefaultInteractions(List behaviors) { + super.addDefaultInteractions(behaviors); + + behaviors.add(new DomainHighlighter()); + } +} diff --git a/web/charts/flutter/lib/src/base_chart.dart b/web/charts/flutter/lib/src/base_chart.dart new file mode 100644 index 000000000..d240e2d89 --- /dev/null +++ b/web/charts/flutter/lib/src/base_chart.dart @@ -0,0 +1,279 @@ +// 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/common.dart' as common + show + BaseChart, + LayoutConfig, + MarginSpec, + Performance, + RTLSpec, + Series, + SeriesRendererConfig, + SelectionModelType, + SelectionTrigger; +import 'behaviors/select_nearest.dart' show SelectNearest; +import 'package:meta/meta.dart' show immutable, required; +import 'behaviors/chart_behavior.dart' + show ChartBehavior, ChartStateBehavior, GestureType; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'package:flutter_web/material.dart' show StatefulWidget; +import 'base_chart_state.dart' show BaseChartState; +import 'user_managed_state.dart' show UserManagedState; + +@immutable +abstract class BaseChart extends StatefulWidget { + /// Series list to draw. + final List> seriesList; + + /// Animation transitions. + final bool animate; + final Duration animationDuration; + + /// Used to configure the margin sizes around the drawArea that the axis and + /// other things render into. + final LayoutConfig layoutConfig; + + // Default renderer used to draw series data on the chart. + final common.SeriesRendererConfig defaultRenderer; + + /// Include the default interactions or not. + final bool defaultInteractions; + + final List behaviors; + + final List> selectionModels; + + // List of custom series renderers used to draw series data on the chart. + // + // Series assigned a rendererIdKey will be drawn with the matching renderer in + // this list. Series without a rendererIdKey will be drawn by the default + // renderer. + final List> customSeriesRenderers; + + /// The spec to use if RTL is enabled. + final common.RTLSpec rtlSpec; + + /// Optional state that overrides internally kept state, such as selection. + final UserManagedState userManagedState; + + BaseChart(this.seriesList, + {bool animate, + Duration animationDuration, + this.defaultRenderer, + this.customSeriesRenderers, + this.behaviors, + this.selectionModels, + this.rtlSpec, + this.defaultInteractions = true, + this.layoutConfig, + this.userManagedState}) + : this.animate = animate ?? true, + this.animationDuration = + animationDuration ?? const Duration(milliseconds: 300); + + @override + BaseChartState createState() => new BaseChartState(); + + /// Creates and returns a [common.BaseChart]. + common.BaseChart createCommonChart(BaseChartState chartState); + + /// Updates the [common.BaseChart]. + void updateCommonChart(common.BaseChart chart, BaseChart oldWidget, + BaseChartState chartState) { + common.Performance.time('chartsUpdateRenderers'); + // Set default renderer if one was provided. + if (defaultRenderer != null && + defaultRenderer != oldWidget?.defaultRenderer) { + chart.defaultRenderer = defaultRenderer.build(); + chartState.markChartDirty(); + } + + // Add custom series renderers if any were provided. + if (customSeriesRenderers != null) { + // TODO: This logic does not remove old renderers and + // shouldn't require the series configs to remain in the same order. + for (var i = 0; i < customSeriesRenderers.length; i++) { + if (oldWidget == null || + (oldWidget.customSeriesRenderers != null && + i > oldWidget.customSeriesRenderers.length) || + customSeriesRenderers[i] != oldWidget.customSeriesRenderers[i]) { + chart.addSeriesRenderer(customSeriesRenderers[i].build()); + chartState.markChartDirty(); + } + } + } + common.Performance.timeEnd('chartsUpdateRenderers'); + + common.Performance.time('chartsUpdateBehaviors'); + _updateBehaviors(chart, chartState); + common.Performance.timeEnd('chartsUpdateBehaviors'); + + _updateSelectionModel(chart, chartState); + + chart.transition = animate ? animationDuration : Duration.zero; + } + + void _updateBehaviors(common.BaseChart chart, BaseChartState chartState) { + final behaviorList = behaviors != null + ? new List.from(behaviors) + : []; + + // Insert automatic behaviors to the front of the behavior list. + if (defaultInteractions) { + if (chartState.autoBehaviorWidgets.isEmpty) { + addDefaultInteractions(chartState.autoBehaviorWidgets); + } + + // Add default interaction behaviors to the front of the list if they + // don't conflict with user behaviors by role. + chartState.autoBehaviorWidgets.reversed + .where(_notACustomBehavior) + .forEach((ChartBehavior behavior) { + behaviorList.insert(0, behavior); + }); + } + + // Remove any behaviors from the chart that are not in the incoming list. + // Walk in reverse order they were added. + // Also, remove any persisting behaviors from incoming list. + for (int i = chartState.addedBehaviorWidgets.length - 1; i >= 0; i--) { + final addedBehavior = chartState.addedBehaviorWidgets[i]; + if (!behaviorList.remove(addedBehavior)) { + final role = addedBehavior.role; + chartState.addedBehaviorWidgets.remove(addedBehavior); + chartState.addedCommonBehaviorsByRole.remove(role); + chart.removeBehavior(chartState.addedCommonBehaviorsByRole[role]); + chartState.markChartDirty(); + } + } + + // Add any remaining/new behaviors. + behaviorList.forEach((ChartBehavior behaviorWidget) { + final commonBehavior = chart + .createBehavior(() => behaviorWidget.createCommonBehavior()); + + // Assign the chart state to any behavior that needs it. + if (commonBehavior is ChartStateBehavior) { + (commonBehavior as ChartStateBehavior).chartState = chartState; + } + + chart.addBehavior(commonBehavior); + chartState.addedBehaviorWidgets.add(behaviorWidget); + chartState.addedCommonBehaviorsByRole[behaviorWidget.role] = + commonBehavior; + chartState.markChartDirty(); + }); + } + + /// Create the list of default interaction behaviors. + void addDefaultInteractions(List behaviors) { + // Update selection model + behaviors.add(new SelectNearest( + eventTrigger: common.SelectionTrigger.tap, + selectionModelType: common.SelectionModelType.info, + expandToDomain: true, + selectClosestSeries: true)); + } + + bool _notACustomBehavior(ChartBehavior behavior) { + return this.behaviors == null || + !this.behaviors.any( + (ChartBehavior userBehavior) => userBehavior.role == behavior.role); + } + + void _updateSelectionModel( + common.BaseChart chart, BaseChartState chartState) { + final prevTypes = new List.from( + chartState.addedSelectionChangedListenersByType.keys); + + // Update any listeners for each type. + selectionModels?.forEach((SelectionModelConfig model) { + final selectionModel = chart.getSelectionModel(model.type); + + final prevChangedListener = + chartState.addedSelectionChangedListenersByType[model.type]; + if (!identical(model.changedListener, prevChangedListener)) { + selectionModel.removeSelectionChangedListener(prevChangedListener); + selectionModel.addSelectionChangedListener(model.changedListener); + chartState.addedSelectionChangedListenersByType[model.type] = + model.changedListener; + } + + final prevUpdatedListener = + chartState.addedSelectionUpdatedListenersByType[model.type]; + if (!identical(model.updatedListener, prevUpdatedListener)) { + selectionModel.removeSelectionUpdatedListener(prevUpdatedListener); + selectionModel.addSelectionUpdatedListener(model.updatedListener); + chartState.addedSelectionUpdatedListenersByType[model.type] = + model.updatedListener; + } + + prevTypes.remove(model.type); + }); + + // Remove any lingering listeners. + prevTypes.forEach((common.SelectionModelType type) { + chart.getSelectionModel(type) + ..removeSelectionChangedListener( + chartState.addedSelectionChangedListenersByType[type]) + ..removeSelectionUpdatedListener( + chartState.addedSelectionUpdatedListenersByType[type]); + }); + } + + /// Gets distinct set of gestures this chart will subscribe to. + /// + /// This is needed to allow setup of the [GestureDetector] widget with only + /// gestures we need to listen to and it must wrap [ChartContainer] widget. + /// Gestures are then setup to be proxied in [common.BaseChart] and that is + /// held by [ChartContainerRenderObject]. + Set getDesiredGestures(BaseChartState chartState) { + final types = new Set(); + behaviors?.forEach((ChartBehavior behavior) { + types.addAll(behavior.desiredGestures); + }); + + if (defaultInteractions && chartState.autoBehaviorWidgets.isEmpty) { + addDefaultInteractions(chartState.autoBehaviorWidgets); + } + + chartState.autoBehaviorWidgets.forEach((ChartBehavior behavior) { + types.addAll(behavior.desiredGestures); + }); + return types; + } +} + +@immutable +class LayoutConfig { + final common.MarginSpec leftMarginSpec; + final common.MarginSpec topMarginSpec; + final common.MarginSpec rightMarginSpec; + final common.MarginSpec bottomMarginSpec; + + LayoutConfig({ + @required this.leftMarginSpec, + @required this.topMarginSpec, + @required this.rightMarginSpec, + @required this.bottomMarginSpec, + }); + + common.LayoutConfig get commonLayoutConfig => new common.LayoutConfig( + leftSpec: leftMarginSpec, + topSpec: topMarginSpec, + rightSpec: rightMarginSpec, + bottomSpec: bottomMarginSpec); +} diff --git a/web/charts/flutter/lib/src/base_chart_state.dart b/web/charts/flutter/lib/src/base_chart_state.dart new file mode 100644 index 000000000..82b15957b --- /dev/null +++ b/web/charts/flutter/lib/src/base_chart_state.dart @@ -0,0 +1,179 @@ +// 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:flutter_web_ui/ui.dart' show TextDirection; +import 'package:flutter_web/material.dart' + show + AnimationController, + BuildContext, + State, + TickerProviderStateMixin, + Widget; +import 'package:charts_common/common.dart' as common; +import 'package:flutter_web/widgets.dart' + show Directionality, LayoutId, CustomMultiChildLayout; +import 'behaviors/chart_behavior.dart' + show BuildableBehavior, ChartBehavior, ChartStateBehavior; +import 'base_chart.dart' show BaseChart; +import 'chart_container.dart' show ChartContainer; +import 'chart_state.dart' show ChartState; +import 'chart_gesture_detector.dart' show ChartGestureDetector; +import 'widget_layout_delegate.dart'; + +class BaseChartState extends State> + with TickerProviderStateMixin + implements ChartState { + // Animation + AnimationController _animationController; + double _animationValue = 0.0; + + Widget _oldWidget; + + ChartGestureDetector _chartGestureDetector; + + bool _configurationChanged = false; + + final autoBehaviorWidgets = []; + final addedBehaviorWidgets = []; + final addedCommonBehaviorsByRole = {}; + + final addedSelectionChangedListenersByType = + >{}; + final addedSelectionUpdatedListenersByType = + >{}; + + final _behaviorAnimationControllers = + {}; + + static const chartContainerLayoutID = 'chartContainer'; + + @override + void initState() { + super.initState(); + _animationController = new AnimationController(vsync: this) + ..addListener(_animationTick); + } + + @override + void requestRebuild() { + setState(() {}); + } + + @override + void markChartDirty() { + _configurationChanged = true; + } + + @override + void resetChartDirtyFlag() { + _configurationChanged = false; + } + + @override + bool get chartIsDirty => _configurationChanged; + + /// Builds the common chart canvas widget. + Widget _buildChartContainer() { + final chartContainer = new ChartContainer( + oldChartWidget: _oldWidget, + chartWidget: widget, + chartState: this, + animationValue: _animationValue, + rtl: Directionality.of(context) == TextDirection.rtl, + rtlSpec: widget.rtlSpec, + userManagedState: widget.userManagedState, + ); + _oldWidget = widget; + + final desiredGestures = widget.getDesiredGestures(this); + if (desiredGestures.isNotEmpty) { + _chartGestureDetector ??= new ChartGestureDetector(); + return _chartGestureDetector.makeWidget( + context, chartContainer, desiredGestures); + } else { + return chartContainer; + } + } + + @override + Widget build(BuildContext context) { + final chartWidgets = []; + final idAndBehaviorMap = {}; + + // Add the common chart canvas widget. + chartWidgets.add(new LayoutId( + id: chartContainerLayoutID, child: _buildChartContainer())); + + // Add widget for each behavior that can build widgets + addedCommonBehaviorsByRole.forEach((id, behavior) { + if (behavior is BuildableBehavior) { + assert(id != chartContainerLayoutID); + + final buildableBehavior = behavior as BuildableBehavior; + idAndBehaviorMap[id] = buildableBehavior; + + final widget = buildableBehavior.build(context); + chartWidgets.add(new LayoutId(id: id, child: widget)); + } + }); + + final isRTL = Directionality.of(context) == TextDirection.rtl; + + return new CustomMultiChildLayout( + delegate: new WidgetLayoutDelegate( + chartContainerLayoutID, idAndBehaviorMap, isRTL), + children: chartWidgets); + } + + @override + void dispose() { + _animationController.dispose(); + _behaviorAnimationControllers + .forEach((_, controller) => controller?.dispose()); + _behaviorAnimationControllers.clear(); + super.dispose(); + } + + @override + void setAnimation(Duration transition) { + _playAnimation(transition); + } + + void _playAnimation(Duration duration) { + _animationController.duration = duration; + _animationController.forward(from: (duration == Duration.zero) ? 1.0 : 0.0); + _animationValue = _animationController.value; + } + + void _animationTick() { + setState(() { + _animationValue = _animationController.value; + }); + } + + /// Get animation controller to be used by [behavior]. + AnimationController getAnimationController(ChartStateBehavior behavior) { + _behaviorAnimationControllers[behavior] ??= + new AnimationController(vsync: this); + + return _behaviorAnimationControllers[behavior]; + } + + /// Dispose of animation controller used by [behavior]. + void disposeAnimationController(ChartStateBehavior behavior) { + final controller = _behaviorAnimationControllers.remove(behavior); + controller?.dispose(); + } +} diff --git a/web/charts/flutter/lib/src/behaviors/a11y/domain_a11y_explore_behavior.dart b/web/charts/flutter/lib/src/behaviors/a11y/domain_a11y_explore_behavior.dart new file mode 100644 index 000000000..bc5e22adc --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/a11y/domain_a11y_explore_behavior.dart @@ -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 'package:charts_common/common.dart' as common + show DomainA11yExploreBehavior, VocalizationCallback, ExploreModeTrigger; +import 'package:flutter_web/widgets.dart' show hashValues; +import '../chart_behavior.dart' show ChartBehavior, GestureType; + +/// Behavior that generates semantic nodes for each domain. +class DomainA11yExploreBehavior + extends ChartBehavior { + /// Returns a string for a11y vocalization from a list of series datum. + final common.VocalizationCallback vocalizationCallback; + + final Set desiredGestures; + + /// The gesture that activates explore mode. Defaults to long press. + /// + /// Turning on explore mode asks this [A11yBehavior] to generate nodes within + /// this chart. + final common.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; + + DomainA11yExploreBehavior._internal( + {this.vocalizationCallback, + this.exploreModeTrigger, + this.desiredGestures, + this.minimumWidth, + this.exploreModeEnabledAnnouncement, + this.exploreModeDisabledAnnouncement}); + + factory DomainA11yExploreBehavior( + {common.VocalizationCallback vocalizationCallback, + common.ExploreModeTrigger exploreModeTrigger, + double minimumWidth, + String exploreModeEnabledAnnouncement, + String exploreModeDisabledAnnouncement}) { + final desiredGestures = new Set(); + exploreModeTrigger ??= common.ExploreModeTrigger.pressHold; + + switch (exploreModeTrigger) { + case common.ExploreModeTrigger.pressHold: + desiredGestures..add(GestureType.onLongPress); + break; + case common.ExploreModeTrigger.tap: + desiredGestures..add(GestureType.onTap); + break; + } + + return new DomainA11yExploreBehavior._internal( + vocalizationCallback: vocalizationCallback, + desiredGestures: desiredGestures, + exploreModeTrigger: exploreModeTrigger, + minimumWidth: minimumWidth, + exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement, + exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement, + ); + } + + @override + common.DomainA11yExploreBehavior createCommonBehavior() { + return new common.DomainA11yExploreBehavior( + vocalizationCallback: vocalizationCallback, + exploreModeTrigger: exploreModeTrigger, + minimumWidth: minimumWidth, + exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement, + exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement); + } + + @override + void updateCommonBehavior(common.DomainA11yExploreBehavior commonBehavior) {} + + @override + String get role => 'DomainA11yExplore-${exploreModeTrigger}'; + + @override + bool operator ==(Object o) => + o is DomainA11yExploreBehavior && + vocalizationCallback == o.vocalizationCallback && + exploreModeTrigger == o.exploreModeTrigger && + minimumWidth == o.minimumWidth && + exploreModeEnabledAnnouncement == o.exploreModeEnabledAnnouncement && + exploreModeDisabledAnnouncement == o.exploreModeDisabledAnnouncement; + + @override + int get hashCode { + return hashValues(minimumWidth, vocalizationCallback, exploreModeTrigger, + exploreModeEnabledAnnouncement, exploreModeDisabledAnnouncement); + } +} diff --git a/web/charts/flutter/lib/src/behaviors/calculation/percent_injector.dart b/web/charts/flutter/lib/src/behaviors/calculation/percent_injector.dart new file mode 100644 index 000000000..35a982c10 --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/calculation/percent_injector.dart @@ -0,0 +1,72 @@ +// 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/common.dart' as common + show PercentInjector, PercentInjectorTotalType; +import 'package:meta/meta.dart' show immutable; + +import '../chart_behavior.dart' show ChartBehavior, GestureType; + +/// 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. +@immutable +class PercentInjector extends ChartBehavior { + final desiredGestures = new Set(); + + /// The type of data total to be calculated. + final common.PercentInjectorTotalType totalType; + + PercentInjector._internal({this.totalType}); + + /// Constructs a [PercentInjector]. + /// + /// [totalType] configures the type of data total to be calculated. + factory PercentInjector({common.PercentInjectorTotalType totalType}) { + totalType ??= common.PercentInjectorTotalType.domain; + return new PercentInjector._internal(totalType: totalType); + } + + @override + common.PercentInjector createCommonBehavior() => + new common.PercentInjector(totalType: totalType); + + @override + void updateCommonBehavior(common.PercentInjector commonBehavior) {} + + @override + String get role => 'PercentInjector'; + + @override + bool operator ==(Object o) { + return o is PercentInjector && totalType == o.totalType; + } + + @override + int get hashCode => totalType.hashCode; +} diff --git a/web/charts/flutter/lib/src/behaviors/chart_behavior.dart b/web/charts/flutter/lib/src/behaviors/chart_behavior.dart new file mode 100644 index 000000000..98f702111 --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/chart_behavior.dart @@ -0,0 +1,72 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Rectangle; +import 'package:charts_common/common.dart' as common + show + BehaviorPosition, + InsideJustification, + OutsideJustification, + ChartBehavior; +import 'package:meta/meta.dart' show immutable; +import 'package:flutter_web/widgets.dart' show BuildContext, Widget; + +import '../base_chart_state.dart' show BaseChartState; + +/// Flutter wrapper for chart behaviors. +@immutable +abstract class ChartBehavior { + Set get desiredGestures; + + B createCommonBehavior(); + + void updateCommonBehavior(B commonBehavior); + + String get role; +} + +/// A chart behavior that depends on Flutter [State]. +abstract class ChartStateBehavior { + set chartState(BaseChartState chartState); +} + +/// A chart behavior that can build a Flutter [Widget]. +abstract class BuildableBehavior { + /// Builds a [Widget] based on the information passed in. + /// + /// [context] Flutter build context for extracting inherited properties such + /// as Directionality. + Widget build(BuildContext context); + + /// The position on the widget. + common.BehaviorPosition get position; + + /// Justification of the widget, if [position] is top, bottom, start, or end. + common.OutsideJustification get outsideJustification; + + /// Justification of the widget if [position] is [common.BehaviorPosition.inside]. + common.InsideJustification get insideJustification; + + /// Chart's draw area bounds are used for positioning. + Rectangle get drawAreaBounds; +} + +/// Types of gestures accepted by a chart. +enum GestureType { + onLongPress, + onTap, + onHover, + onDrag, +} diff --git a/web/charts/flutter/lib/src/behaviors/chart_title/chart_title.dart b/web/charts/flutter/lib/src/behaviors/chart_title/chart_title.dart new file mode 100644 index 000000000..47d606cda --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/chart_title/chart_title.dart @@ -0,0 +1,200 @@ +// 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/common.dart' as common + show + BehaviorPosition, + ChartTitle, + ChartTitleDirection, + MaxWidthStrategy, + OutsideJustification, + TextStyleSpec; +import 'package:flutter_web/widgets.dart' show hashValues; +import 'package:meta/meta.dart' show immutable; + +import '../chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that adds a ChartTitle widget to a chart. +@immutable +class ChartTitle extends ChartBehavior { + final desiredGestures = new Set(); + + final common.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. + final int 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. + final int layoutPreferredSize; + + /// Strategy for handling title text that is too large to fit. Defaults to + /// truncating the text with ellipses. + final common.MaxWidthStrategy maxWidthStrategy; + + /// Primary text for the title. + final String 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]. + final common.ChartTitleDirection titleDirection; + + /// Justification of the title text if it is positioned outside of the draw + /// area. + final common.OutsideJustification titleOutsideJustification; + + /// Space between the title and sub-title text, if defined. + /// + /// This padding is not used if no sub-title is provided. + final int titlePadding; + + /// Style of the [title] text. + final common.TextStyleSpec titleStyleSpec; + + /// Secondary text for the sub-title. + /// + /// [subTitle] is rendered on a second line below the [title], and may be + /// styled differently. + final String subTitle; + + /// Style of the [subTitle] text. + final common.TextStyleSpec 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. + final int 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. + final int outerPadding; + + /// Constructs a [ChartTitle]. + /// + /// [title] primary text for the title. + /// + /// [behaviorPosition] layout position for the title. Defaults to the top of + /// the chart. + /// + /// [innerPadding] space between the "inside" of the chart, and the title + /// behavior itself. + /// + /// [maxWidthStrategy] strategy for handling title text that is too large to + /// fit. Defaults to truncating the text with ellipses. + /// + /// [titleDirection] direction of the chart title text. + /// + /// [titleOutsideJustification] Justification of the title text if it is + /// positioned outside of the draw. Defaults to the middle of the margin area. + /// + /// [titlePadding] space between the title and sub-title text, if defined. + /// + /// [titleStyleSpec] style of the [title] text. + /// + /// [subTitle] secondary text for the sub-title. Optional. + /// + /// [subTitleStyleSpec] style of the [subTitle] text. + ChartTitle(this.title, + {this.behaviorPosition, + this.innerPadding, + this.layoutMinSize, + this.layoutPreferredSize, + this.outerPadding, + this.maxWidthStrategy, + this.titleDirection, + this.titleOutsideJustification, + this.titlePadding, + this.titleStyleSpec, + this.subTitle, + this.subTitleStyleSpec}); + + @override + common.ChartTitle createCommonBehavior() => + new common.ChartTitle(title, + behaviorPosition: behaviorPosition, + innerPadding: innerPadding, + layoutMinSize: layoutMinSize, + layoutPreferredSize: layoutPreferredSize, + outerPadding: outerPadding, + maxWidthStrategy: maxWidthStrategy, + titleDirection: titleDirection, + titleOutsideJustification: titleOutsideJustification, + titlePadding: titlePadding, + titleStyleSpec: titleStyleSpec, + subTitle: subTitle, + subTitleStyleSpec: subTitleStyleSpec); + + @override + void updateCommonBehavior(common.ChartTitle commonBehavior) {} + + @override + String get role => 'ChartTitle-${behaviorPosition.toString()}'; + + @override + bool operator ==(Object o) { + return o is ChartTitle && + behaviorPosition == o.behaviorPosition && + layoutMinSize == o.layoutMinSize && + layoutPreferredSize == o.layoutPreferredSize && + maxWidthStrategy == o.maxWidthStrategy && + title == o.title && + titleDirection == o.titleDirection && + titleOutsideJustification == o.titleOutsideJustification && + titleStyleSpec == o.titleStyleSpec && + subTitle == o.subTitle && + subTitleStyleSpec == o.subTitleStyleSpec && + innerPadding == o.innerPadding && + titlePadding == o.titlePadding && + outerPadding == o.outerPadding; + } + + @override + int get hashCode { + return hashValues( + behaviorPosition, + layoutMinSize, + layoutPreferredSize, + maxWidthStrategy, + title, + titleDirection, + titleOutsideJustification, + titleStyleSpec, + subTitle, + subTitleStyleSpec, + innerPadding, + titlePadding, + outerPadding); + } +} diff --git a/web/charts/flutter/lib/src/behaviors/domain_highlighter.dart b/web/charts/flutter/lib/src/behaviors/domain_highlighter.dart new file mode 100644 index 000000000..896a6bf6e --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/domain_highlighter.dart @@ -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:charts_common/common.dart' as common + show DomainHighlighter, SelectionModelType; + +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// 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. +@immutable +class DomainHighlighter extends ChartBehavior { + final desiredGestures = new Set(); + + final common.SelectionModelType selectionModelType; + + DomainHighlighter([this.selectionModelType = common.SelectionModelType.info]); + + @override + common.DomainHighlighter createCommonBehavior() => + new common.DomainHighlighter(selectionModelType); + + @override + void updateCommonBehavior(common.DomainHighlighter commonBehavior) {} + + @override + String get role => 'domainHighlight-${selectionModelType.toString()}'; + + @override + bool operator ==(Object o) => + o is DomainHighlighter && selectionModelType == o.selectionModelType; + + @override + int get hashCode => selectionModelType.hashCode; +} diff --git a/web/charts/flutter/lib/src/behaviors/initial_selection.dart b/web/charts/flutter/lib/src/behaviors/initial_selection.dart new file mode 100644 index 000000000..f7086b564 --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/initial_selection.dart @@ -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 'package:collection/collection.dart' show ListEquality; + +import 'package:charts_common/common.dart' as common + show InitialSelection, SeriesDatumConfig, SelectionModelType; + +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that sets the initial selection for a [selectionModelType]. +@immutable +class InitialSelection extends ChartBehavior { + final desiredGestures = new Set(); + + final common.SelectionModelType selectionModelType; + final List selectedSeriesConfig; + final List selectedDataConfig; + + InitialSelection( + {this.selectionModelType = common.SelectionModelType.info, + this.selectedSeriesConfig, + this.selectedDataConfig}); + + @override + common.InitialSelection createCommonBehavior() => + new common.InitialSelection( + selectionModelType: selectionModelType, + selectedDataConfig: selectedDataConfig, + selectedSeriesConfig: selectedSeriesConfig); + + @override + void updateCommonBehavior(common.InitialSelection commonBehavior) {} + + @override + String get role => 'InitialSelection-${selectionModelType.toString()}'; + + @override + bool operator ==(Object o) { + return o is InitialSelection && + selectionModelType == o.selectionModelType && + new ListEquality() + .equals(selectedSeriesConfig, o.selectedSeriesConfig) && + new ListEquality().equals(selectedDataConfig, o.selectedDataConfig); + } + + @override + int get hashCode { + int hashcode = selectionModelType.hashCode; + hashcode = hashcode * 37 + (selectedSeriesConfig?.hashCode ?? 0); + hashcode = hashcode * 37 + (selectedDataConfig?.hashCode ?? 0); + return hashcode; + } +} diff --git a/web/charts/flutter/lib/src/behaviors/legend/datum_legend.dart b/web/charts/flutter/lib/src/behaviors/legend/datum_legend.dart new file mode 100644 index 000000000..38645e58a --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/legend/datum_legend.dart @@ -0,0 +1,340 @@ +// 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/common.dart' as common + show + BehaviorPosition, + DatumLegend, + InsideJustification, + LegendEntry, + MeasureFormatter, + LegendDefaultMeasure, + OutsideJustification, + SelectionModelType, + TextStyleSpec; +import 'package:flutter_web/widgets.dart' + show BuildContext, EdgeInsets, Widget, hashValues; +import 'package:meta/meta.dart' show immutable; +import '../../chart_container.dart' show ChartContainerRenderObject; +import '../chart_behavior.dart' + show BuildableBehavior, ChartBehavior, GestureType; +import 'legend.dart' show TappableLegend; +import 'legend_content_builder.dart' + show LegendContentBuilder, TabularLegendContentBuilder; +import 'legend_layout.dart' show TabularLegendLayout; + +/// Datum legend behavior for charts. +/// +/// By default this behavior creates one legend entry per datum in the first +/// series rendered on the chart. +@immutable +class DatumLegend extends ChartBehavior { + static const defaultBehaviorPosition = common.BehaviorPosition.top; + static const defaultOutsideJustification = + common.OutsideJustification.startDrawArea; + static const defaultInsideJustification = common.InsideJustification.topStart; + + final desiredGestures = new Set(); + + final common.SelectionModelType selectionModelType; + + /// Builder for creating custom legend content. + final LegendContentBuilder contentBuilder; + + /// Position of the legend relative to the chart. + final common.BehaviorPosition position; + + /// Justification of the legend relative to the chart + final common.OutsideJustification outsideJustification; + final common.InsideJustification insideJustification; + + /// 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. + /// + /// This flag is used by the [contentBuilder], so a custom content builder + /// has to choose if it wants to use this flag. + final bool showMeasures; + + /// 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. + final common.LegendDefaultMeasure legendDefaultMeasure; + + /// Formatter for measure value(s) if the measures are shown on the legend. + final common.MeasureFormatter measureFormatter; + + /// Formatter for secondary measure value(s) if the measures are shown on the + /// legend and the series uses the secondary axis. + final common.MeasureFormatter secondaryMeasureFormatter; + + /// Styles for legend entry label text. + final common.TextStyleSpec entryTextStyle; + + static const defaultCellPadding = const EdgeInsets.all(8.0); + + /// Create a new tabular layout legend. + /// + /// By default, the legend is place above the chart and horizontally aligned + /// to the start of the draw area. + /// + /// [position] the legend will be positioned relative to the chart. Default + /// position is top. + /// + /// [outsideJustification] justification of the legend relative to the chart + /// if the position is top, bottom, left, right. Default to start of the draw + /// area. + /// + /// [insideJustification] justification of the legend relative to the chart if + /// the position is inside. Default to top of the chart, start of draw area. + /// Start of draw area means left for LTR directionality, and right for RTL. + /// + /// [horizontalFirst] if true, legend entries will grow horizontally first + /// instead of vertically first. If the position is top, bottom, or inside, + /// this defaults to true. Otherwise false. + /// + /// [desiredMaxRows] the max rows to use before layout out items in a new + /// column. By default there is no limit. The max columns created is the + /// smaller of desiredMaxRows and number of legend entries. + /// + /// [desiredMaxColumns] the max columns to use before laying out items in a + /// new row. By default there is no limit. The max columns created is the + /// smaller of desiredMaxColumns and number of legend entries. + /// + /// [showMeasures] show measure values for each series. + /// + /// [legendDefaultMeasure] if measure should show when there is no selection. + /// This is set to none by default (only shows measure for selected data). + /// + /// [measureFormatter] formats measure value if measures are shown. + /// + /// [secondaryMeasureFormatter] formats measures if measures are shown for the + /// series that uses secondary measure axis. + factory DatumLegend({ + common.BehaviorPosition position, + common.OutsideJustification outsideJustification, + common.InsideJustification insideJustification, + bool horizontalFirst, + int desiredMaxRows, + int desiredMaxColumns, + EdgeInsets cellPadding, + bool showMeasures, + common.LegendDefaultMeasure legendDefaultMeasure, + common.MeasureFormatter measureFormatter, + common.MeasureFormatter secondaryMeasureFormatter, + common.TextStyleSpec entryTextStyle, + }) { + // Set defaults if empty. + position ??= defaultBehaviorPosition; + outsideJustification ??= defaultOutsideJustification; + insideJustification ??= defaultInsideJustification; + cellPadding ??= defaultCellPadding; + + // Set the tabular layout settings to match the position if it is not + // specified. + horizontalFirst ??= (position == common.BehaviorPosition.top || + position == common.BehaviorPosition.bottom || + position == common.BehaviorPosition.inside); + final layoutBuilder = horizontalFirst + ? new TabularLegendLayout.horizontalFirst( + desiredMaxColumns: desiredMaxColumns, cellPadding: cellPadding) + : new TabularLegendLayout.verticalFirst( + desiredMaxRows: desiredMaxRows, cellPadding: cellPadding); + + return new DatumLegend._internal( + contentBuilder: + new TabularLegendContentBuilder(legendLayout: layoutBuilder), + selectionModelType: common.SelectionModelType.info, + position: position, + outsideJustification: outsideJustification, + insideJustification: insideJustification, + showMeasures: showMeasures ?? false, + legendDefaultMeasure: + legendDefaultMeasure ?? common.LegendDefaultMeasure.none, + measureFormatter: measureFormatter, + secondaryMeasureFormatter: secondaryMeasureFormatter, + entryTextStyle: entryTextStyle); + } + + /// Create a legend with custom layout. + /// + /// By default, the legend is place above the chart and horizontally aligned + /// to the start of the draw area. + /// + /// [contentBuilder] builder for the custom layout. + /// + /// [position] the legend will be positioned relative to the chart. Default + /// position is top. + /// + /// [outsideJustification] justification of the legend relative to the chart + /// if the position is top, bottom, left, right. Default to start of the draw + /// area. + /// + /// [insideJustification] justification of the legend relative to the chart if + /// the position is inside. Default to top of the chart, start of draw area. + /// Start of draw area means left for LTR directionality, and right for RTL. + /// + /// [showMeasures] show measure values for each series. + /// + /// [legendDefaultMeasure] if measure should show when there is no selection. + /// This is set to none by default (only shows measure for selected data). + /// + /// [measureFormatter] formats measure value if measures are shown. + /// + /// [secondaryMeasureFormatter] formats measures if measures are shown for the + /// series that uses secondary measure axis. + factory DatumLegend.customLayout( + LegendContentBuilder contentBuilder, { + common.BehaviorPosition position, + common.OutsideJustification outsideJustification, + common.InsideJustification insideJustification, + bool showMeasures, + common.LegendDefaultMeasure legendDefaultMeasure, + common.MeasureFormatter measureFormatter, + common.MeasureFormatter secondaryMeasureFormatter, + common.TextStyleSpec entryTextStyle, + }) { + // Set defaults if empty. + position ??= defaultBehaviorPosition; + outsideJustification ??= defaultOutsideJustification; + insideJustification ??= defaultInsideJustification; + + return new DatumLegend._internal( + contentBuilder: contentBuilder, + selectionModelType: common.SelectionModelType.info, + position: position, + outsideJustification: outsideJustification, + insideJustification: insideJustification, + showMeasures: showMeasures ?? false, + legendDefaultMeasure: + legendDefaultMeasure ?? common.LegendDefaultMeasure.none, + measureFormatter: measureFormatter, + secondaryMeasureFormatter: secondaryMeasureFormatter, + entryTextStyle: entryTextStyle, + ); + } + + DatumLegend._internal({ + this.contentBuilder, + this.selectionModelType, + this.position, + this.outsideJustification, + this.insideJustification, + this.showMeasures, + this.legendDefaultMeasure, + this.measureFormatter, + this.secondaryMeasureFormatter, + this.entryTextStyle, + }); + + @override + common.DatumLegend createCommonBehavior() => + new _FlutterDatumLegend(this); + + @override + void updateCommonBehavior(common.DatumLegend commonBehavior) { + (commonBehavior as _FlutterDatumLegend).config = this; + } + + /// All Legend behaviors get the same role ID, because you should only have + /// one legend on a chart. + @override + String get role => 'legend'; + + @override + bool operator ==(Object o) { + return o is DatumLegend && + selectionModelType == o.selectionModelType && + contentBuilder == o.contentBuilder && + position == o.position && + outsideJustification == o.outsideJustification && + insideJustification == o.insideJustification && + showMeasures == o.showMeasures && + legendDefaultMeasure == o.legendDefaultMeasure && + measureFormatter == o.measureFormatter && + secondaryMeasureFormatter == o.secondaryMeasureFormatter && + entryTextStyle == o.entryTextStyle; + } + + @override + int get hashCode { + return hashValues( + selectionModelType, + contentBuilder, + position, + outsideJustification, + insideJustification, + showMeasures, + legendDefaultMeasure, + measureFormatter, + secondaryMeasureFormatter, + entryTextStyle); + } +} + +/// Flutter specific wrapper on the common Legend for building content. +class _FlutterDatumLegend extends common.DatumLegend + implements BuildableBehavior, TappableLegend { + DatumLegend config; + + _FlutterDatumLegend(this.config) + : super( + selectionModelType: config.selectionModelType, + measureFormatter: config.measureFormatter, + secondaryMeasureFormatter: config.secondaryMeasureFormatter, + legendDefaultMeasure: config.legendDefaultMeasure, + ) { + super.entryTextStyle = config.entryTextStyle; + } + + @override + void updateLegend() { + (chartContext as ChartContainerRenderObject).requestRebuild(); + } + + @override + common.BehaviorPosition get position => config.position; + + @override + common.OutsideJustification get outsideJustification => + config.outsideJustification; + + @override + common.InsideJustification get insideJustification => + config.insideJustification; + + @override + Widget build(BuildContext context) { + final hasSelection = + legendState.legendEntries.any((entry) => entry.isSelected); + + // Show measures if [showMeasures] is true and there is a selection or if + // showing measures when there is no selection. + final showMeasures = config.showMeasures && + (hasSelection || + legendDefaultMeasure != common.LegendDefaultMeasure.none); + + return config.contentBuilder + .build(context, legendState, this, showMeasures: showMeasures); + } + + /// TODO: Maybe highlight the pie wedge. + @override + onLegendEntryTapUp(common.LegendEntry detail) {} +} diff --git a/web/charts/flutter/lib/src/behaviors/legend/legend.dart b/web/charts/flutter/lib/src/behaviors/legend/legend.dart new file mode 100644 index 000000000..5cd347b2e --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/legend/legend.dart @@ -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. + +import 'package:charts_common/common.dart' show LegendEntry, LegendTapHandling; + +abstract class TappableLegend { + /// Delegates handling of legend entry clicks according to the configured + /// [LegendTapHandling] strategy. + onLegendEntryTapUp(LegendEntry detail); +} diff --git a/web/charts/flutter/lib/src/behaviors/legend/legend_content_builder.dart b/web/charts/flutter/lib/src/behaviors/legend/legend_content_builder.dart new file mode 100644 index 000000000..22f35a11e --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/legend/legend_content_builder.dart @@ -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 'package:charts_common/common.dart' as common + show Legend, LegendState, SeriesLegend; +import 'package:flutter_web/widgets.dart' show BuildContext, hashValues, Widget; +import 'legend.dart'; +import 'legend_entry_layout.dart'; +import 'legend_layout.dart'; + +/// Strategy for building a legend content widget. +abstract class LegendContentBuilder { + const LegendContentBuilder(); + + Widget build(BuildContext context, common.LegendState legendState, + common.Legend legend, + {bool showMeasures}); +} + +/// Base strategy for building a legend content widget. +/// +/// Each legend entry is passed to a [LegendLayout] strategy to create a widget +/// for each legend entry. These widgets are then passed to a +/// [LegendEntryLayout] strategy to create the legend widget. +abstract class BaseLegendContentBuilder implements LegendContentBuilder { + /// Strategy for creating one widget or each legend entry. + LegendEntryLayout get legendEntryLayout; + + /// Strategy for creating the legend content widget from a list of widgets. + /// + /// This is typically the list of widgets from legend entries. + LegendLayout get legendLayout; + + @override + Widget build(BuildContext context, common.LegendState legendState, + common.Legend legend, + {bool showMeasures}) { + final entryWidgets = legendState.legendEntries.map((entry) { + var isHidden = false; + if (legend is common.SeriesLegend) { + isHidden = legend.isSeriesHidden(entry.series.id); + } + + return legendEntryLayout.build( + context, entry, legend as TappableLegend, isHidden, + showMeasures: showMeasures); + }).toList(); + + return legendLayout.build(context, entryWidgets); + } +} + +// TODO: Expose settings for tabular layout. +/// Strategy that builds a tabular legend. +/// +/// [legendEntryLayout] custom strategy for creating widgets for each legend +/// entry. +/// [legendLayout] custom strategy for creating legend widget from list of +/// widgets that represent a legend entry. +class TabularLegendContentBuilder extends BaseLegendContentBuilder { + final LegendEntryLayout legendEntryLayout; + final LegendLayout legendLayout; + + TabularLegendContentBuilder( + {LegendEntryLayout legendEntryLayout, LegendLayout legendLayout}) + : this.legendEntryLayout = + legendEntryLayout ?? const SimpleLegendEntryLayout(), + this.legendLayout = + legendLayout ?? new TabularLegendLayout.horizontalFirst(); + + @override + bool operator ==(Object o) { + return o is TabularLegendContentBuilder && + legendEntryLayout == o.legendEntryLayout && + legendLayout == o.legendLayout; + } + + @override + int get hashCode => hashValues(legendEntryLayout, legendLayout); +} diff --git a/web/charts/flutter/lib/src/behaviors/legend/legend_entry_layout.dart b/web/charts/flutter/lib/src/behaviors/legend/legend_entry_layout.dart new file mode 100644 index 000000000..16882f792 --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/legend/legend_entry_layout.dart @@ -0,0 +1,144 @@ +// 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/common.dart' as common; +import 'package:charts_flutter/src/util/color.dart'; +import 'package:flutter_web/widgets.dart'; +import 'package:flutter_web/material.dart' + show GestureDetector, GestureTapUpCallback, TapUpDetails, Theme; + +import '../../symbol_renderer.dart'; +import 'legend.dart' show TappableLegend; + +/// Strategy for building one widget from one [common.LegendEntry]. +abstract class LegendEntryLayout { + Widget build(BuildContext context, common.LegendEntry legendEntry, + TappableLegend legend, bool isHidden, + {bool showMeasures}); +} + +/// Builds one legend entry as a row with symbol and label from the series. +/// +/// If directionality from the chart context indicates RTL, the symbol is placed +/// to the right of the text instead of the left of the text. +class SimpleLegendEntryLayout implements LegendEntryLayout { + const SimpleLegendEntryLayout(); + + Widget createSymbol(BuildContext context, common.LegendEntry legendEntry, + TappableLegend legend, bool isHidden) { + // TODO: Consider allowing scaling the size for the symbol. + // A custom symbol renderer can ignore this size and use their own. + final materialSymbolSize = new Size(12.0, 12.0); + + final entryColor = legendEntry.color; + var color = ColorUtil.toDartColor(entryColor); + + // Get the SymbolRendererBuilder wrapping a common.SymbolRenderer if needed. + final SymbolRendererBuilder symbolRendererBuilder = + legendEntry.symbolRenderer is SymbolRendererBuilder + ? legendEntry.symbolRenderer + : new SymbolRendererCanvas(legendEntry.symbolRenderer); + + return new GestureDetector( + child: symbolRendererBuilder.build( + context, + size: materialSymbolSize, + color: color, + enabled: !isHidden, + ), + onTapUp: makeTapUpCallback(context, legendEntry, legend)); + } + + Widget createLabel(BuildContext context, common.LegendEntry legendEntry, + TappableLegend legend, bool isHidden) { + TextStyle style = + _convertTextStyle(isHidden, context, legendEntry.textStyle); + + return new GestureDetector( + child: new Text(legendEntry.label, style: style), + onTapUp: makeTapUpCallback(context, legendEntry, legend)); + } + + Widget createMeasureValue(BuildContext context, + common.LegendEntry legendEntry, TappableLegend legend, bool isHidden) { + return new GestureDetector( + child: new Text(legendEntry.formattedValue), + onTapUp: makeTapUpCallback(context, legendEntry, legend)); + } + + @override + Widget build(BuildContext context, common.LegendEntry legendEntry, + TappableLegend legend, bool isHidden, + {bool showMeasures}) { + final rowChildren = []; + + // TODO: Allow setting to configure the padding. + final padding = new EdgeInsets.only(right: 8.0); // Material default. + final symbol = createSymbol(context, legendEntry, legend, isHidden); + final label = createLabel(context, legendEntry, legend, isHidden); + + final measure = showMeasures + ? createMeasureValue(context, legendEntry, legend, isHidden) + : null; + + rowChildren.add(symbol); + rowChildren.add(new Container(padding: padding)); + rowChildren.add(label); + if (measure != null) { + rowChildren.add(new Container(padding: padding)); + rowChildren.add(measure); + } + + // Row automatically reverses the content if Directionality is rtl. + return new Row(children: rowChildren); + } + + GestureTapUpCallback makeTapUpCallback(BuildContext context, + common.LegendEntry legendEntry, TappableLegend legend) { + return (TapUpDetails d) { + legend.onLegendEntryTapUp(legendEntry); + }; + } + + bool operator ==(Object other) => other is SimpleLegendEntryLayout; + + int get hashCode { + return this.runtimeType.hashCode; + } + + /// Convert the charts common TextStlyeSpec into a standard TextStyle, while + /// reducing the color opacity to 26% if the entry is hidden. + /// + /// For non-specified values, override the hidden text color to use the body 1 + /// theme, but allow other properties of [Text] to be inherited. + TextStyle _convertTextStyle( + bool isHidden, BuildContext context, common.TextStyleSpec textStyle) { + Color color = textStyle?.color != null + ? ColorUtil.toDartColor(textStyle.color) + : null; + if (isHidden) { + // Use a default color for hidden legend entries if none is provided. + color ??= Theme.of(context).textTheme.body1.color; + color = color.withOpacity(0.26); + } + + return new TextStyle( + inherit: true, + fontFamily: textStyle?.fontFamily, + fontSize: + textStyle?.fontSize != null ? textStyle.fontSize.toDouble() : null, + color: color); + } +} diff --git a/web/charts/flutter/lib/src/behaviors/legend/legend_layout.dart b/web/charts/flutter/lib/src/behaviors/legend/legend_layout.dart new file mode 100644 index 000000000..76bf7c875 --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/legend/legend_layout.dart @@ -0,0 +1,158 @@ +// 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; +import 'package:flutter_web/rendering.dart'; +import 'package:flutter_web/widgets.dart'; + +/// Strategy for building legend from legend entry widgets. +abstract class LegendLayout { + Widget build(BuildContext context, List legendEntryWidgets); +} + +/// Layout legend entries in tabular format. +class TabularLegendLayout implements LegendLayout { + /// No limit for max rows or max columns. + static const _noLimit = -1; + + final bool isHorizontalFirst; + final int desiredMaxRows; + final int desiredMaxColumns; + final EdgeInsets cellPadding; + + TabularLegendLayout._internal( + {this.isHorizontalFirst, + this.desiredMaxRows, + this.desiredMaxColumns, + this.cellPadding}); + + /// Layout horizontally until columns exceed [desiredMaxColumns]. + /// + /// [desiredMaxColumns] the max columns to use before laying out items in a + /// new row. By default there is no limit. The max columns created is the + /// smaller of desiredMaxColumns and number of legend entries. + /// + /// [cellPadding] the [EdgeInsets] for each widget. + factory TabularLegendLayout.horizontalFirst({ + int desiredMaxColumns, + EdgeInsets cellPadding, + }) { + return new TabularLegendLayout._internal( + isHorizontalFirst: true, + desiredMaxRows: _noLimit, + desiredMaxColumns: desiredMaxColumns ?? _noLimit, + cellPadding: cellPadding, + ); + } + + /// Layout vertically, until rows exceed [desiredMaxRows]. + /// + /// [desiredMaxRows] the max rows to use before layout out items in a new + /// column. By default there is no limit. The max columns created is the + /// smaller of desiredMaxRows and number of legend entries. + /// + /// [cellPadding] the [EdgeInsets] for each widget. + factory TabularLegendLayout.verticalFirst({ + int desiredMaxRows, + EdgeInsets cellPadding, + }) { + return new TabularLegendLayout._internal( + isHorizontalFirst: false, + desiredMaxRows: desiredMaxRows ?? _noLimit, + desiredMaxColumns: _noLimit, + cellPadding: cellPadding, + ); + } + + @override + Widget build(BuildContext context, List legendEntries) { + final paddedLegendEntries = ((cellPadding == null) + ? legendEntries + : legendEntries + .map((entry) => new Padding(padding: cellPadding, child: entry)) + .toList()); + + return isHorizontalFirst + ? _buildHorizontalFirst(paddedLegendEntries) + : _buildVerticalFirst(paddedLegendEntries); + } + + @override + bool operator ==(o) => + o is TabularLegendLayout && + desiredMaxRows == o.desiredMaxRows && + desiredMaxColumns == o.desiredMaxColumns && + isHorizontalFirst == o.isHorizontalFirst && + cellPadding == o.cellPadding; + + @override + int get hashCode => hashValues( + desiredMaxRows, desiredMaxColumns, isHorizontalFirst, cellPadding); + + Widget _buildHorizontalFirst(List legendEntries) { + final maxColumns = (desiredMaxColumns == _noLimit) + ? legendEntries.length + : min(legendEntries.length, desiredMaxColumns); + + final rows = []; + for (var i = 0; i < legendEntries.length; i += maxColumns) { + rows.add(new TableRow( + children: legendEntries + .sublist(i, min(i + maxColumns, legendEntries.length)) + .toList())); + } + + return _buildTableFromRows(rows); + } + + Widget _buildVerticalFirst(List legendEntries) { + final maxRows = (desiredMaxRows == _noLimit) + ? legendEntries.length + : min(legendEntries.length, desiredMaxRows); + + final rows = + new List.generate(maxRows, (_) => new TableRow(children: [])); + for (var i = 0; i < legendEntries.length; i++) { + rows[i % maxRows].children.add(legendEntries[i]); + } + + return _buildTableFromRows(rows); + } + + Table _buildTableFromRows(List rows) { + final padWidget = new Row(); + + // Pad rows to the max column count, because each TableRow in a table is + // required to have the same number of children. + final columnCount = rows + .map((r) => r.children.length) + .fold(0, (max, current) => (current > max) ? current : max); + + for (var i = 0; i < rows.length; i++) { + final rowChildren = rows[i].children; + final padCount = columnCount - rowChildren.length; + if (padCount > 0) { + rowChildren.addAll(new Iterable.generate(padCount, (_) => padWidget)); + } + } + + // TODO: Investigate other means of creating the tabular legend + // Sizing the column width using [IntrinsicColumnWidth] is expensive per + // Flutter's documentation, but has to be used if the table is desired to + // have a width that is tight on each column. + return new Table( + children: rows, defaultColumnWidth: new IntrinsicColumnWidth()); + } +} diff --git a/web/charts/flutter/lib/src/behaviors/legend/series_legend.dart b/web/charts/flutter/lib/src/behaviors/legend/series_legend.dart new file mode 100644 index 000000000..e49b3c9ea --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/legend/series_legend.dart @@ -0,0 +1,382 @@ +// 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/common.dart' as common + show + BehaviorPosition, + InsideJustification, + LegendEntry, + LegendTapHandling, + MeasureFormatter, + LegendDefaultMeasure, + OutsideJustification, + SeriesLegend, + SelectionModelType, + TextStyleSpec; +import 'package:collection/collection.dart' show ListEquality; +import 'package:flutter_web/widgets.dart' + show BuildContext, EdgeInsets, Widget, hashValues; +import 'package:meta/meta.dart' show immutable; +import '../../chart_container.dart' show ChartContainerRenderObject; +import '../chart_behavior.dart' + show BuildableBehavior, ChartBehavior, GestureType; +import 'legend.dart' show TappableLegend; +import 'legend_content_builder.dart' + show LegendContentBuilder, TabularLegendContentBuilder; +import 'legend_layout.dart' show TabularLegendLayout; + +/// Series legend behavior for charts. +@immutable +class SeriesLegend extends ChartBehavior { + static const defaultBehaviorPosition = common.BehaviorPosition.top; + static const defaultOutsideJustification = + common.OutsideJustification.startDrawArea; + static const defaultInsideJustification = common.InsideJustification.topStart; + + final desiredGestures = new Set(); + + final common.SelectionModelType selectionModelType; + + /// Builder for creating custom legend content. + final LegendContentBuilder contentBuilder; + + /// Position of the legend relative to the chart. + final common.BehaviorPosition position; + + /// Justification of the legend relative to the chart + final common.OutsideJustification outsideJustification; + final common.InsideJustification insideJustification; + + /// 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. + /// + /// This flag is used by the [contentBuilder], so a custom content builder + /// has to choose if it wants to use this flag. + final bool showMeasures; + + /// 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. + final common.LegendDefaultMeasure legendDefaultMeasure; + + /// Formatter for measure value(s) if the measures are shown on the legend. + final common.MeasureFormatter measureFormatter; + + /// Formatter for secondary measure value(s) if the measures are shown on the + /// legend and the series uses the secondary axis. + final common.MeasureFormatter secondaryMeasureFormatter; + + /// Styles for legend entry label text. + final common.TextStyleSpec entryTextStyle; + + static const defaultCellPadding = const EdgeInsets.all(8.0); + + final List defaultHiddenSeries; + + /// Create a new tabular layout legend. + /// + /// By default, the legend is place above the chart and horizontally aligned + /// to the start of the draw area. + /// + /// [position] the legend will be positioned relative to the chart. Default + /// position is top. + /// + /// [outsideJustification] justification of the legend relative to the chart + /// if the position is top, bottom, left, right. Default to start of the draw + /// area. + /// + /// [insideJustification] justification of the legend relative to the chart if + /// the position is inside. Default to top of the chart, start of draw area. + /// Start of draw area means left for LTR directionality, and right for RTL. + /// + /// [horizontalFirst] if true, legend entries will grow horizontally first + /// instead of vertically first. If the position is top, bottom, or inside, + /// this defaults to true. Otherwise false. + /// + /// [desiredMaxRows] the max rows to use before layout out items in a new + /// column. By default there is no limit. The max columns created is the + /// smaller of desiredMaxRows and number of legend entries. + /// + /// [desiredMaxColumns] the max columns to use before laying out items in a + /// new row. By default there is no limit. The max columns created is the + /// smaller of desiredMaxColumns and number of legend entries. + /// + /// [defaultHiddenSeries] lists the IDs of series that should be hidden on + /// first chart draw. + /// + /// [showMeasures] show measure values for each series. + /// + /// [legendDefaultMeasure] if measure should show when there is no selection. + /// This is set to none by default (only shows measure for selected data). + /// + /// [measureFormatter] formats measure value if measures are shown. + /// + /// [secondaryMeasureFormatter] formats measures if measures are shown for the + /// series that uses secondary measure axis. + factory SeriesLegend({ + common.BehaviorPosition position, + common.OutsideJustification outsideJustification, + common.InsideJustification insideJustification, + bool horizontalFirst, + int desiredMaxRows, + int desiredMaxColumns, + EdgeInsets cellPadding, + List defaultHiddenSeries, + bool showMeasures, + common.LegendDefaultMeasure legendDefaultMeasure, + common.MeasureFormatter measureFormatter, + common.MeasureFormatter secondaryMeasureFormatter, + common.TextStyleSpec entryTextStyle, + }) { + // Set defaults if empty. + position ??= defaultBehaviorPosition; + outsideJustification ??= defaultOutsideJustification; + insideJustification ??= defaultInsideJustification; + cellPadding ??= defaultCellPadding; + + // Set the tabular layout settings to match the position if it is not + // specified. + horizontalFirst ??= (position == common.BehaviorPosition.top || + position == common.BehaviorPosition.bottom || + position == common.BehaviorPosition.inside); + final layoutBuilder = horizontalFirst + ? new TabularLegendLayout.horizontalFirst( + desiredMaxColumns: desiredMaxColumns, cellPadding: cellPadding) + : new TabularLegendLayout.verticalFirst( + desiredMaxRows: desiredMaxRows, cellPadding: cellPadding); + + return new SeriesLegend._internal( + contentBuilder: + new TabularLegendContentBuilder(legendLayout: layoutBuilder), + selectionModelType: common.SelectionModelType.info, + position: position, + outsideJustification: outsideJustification, + insideJustification: insideJustification, + defaultHiddenSeries: defaultHiddenSeries, + showMeasures: showMeasures ?? false, + legendDefaultMeasure: + legendDefaultMeasure ?? common.LegendDefaultMeasure.none, + measureFormatter: measureFormatter, + secondaryMeasureFormatter: secondaryMeasureFormatter, + entryTextStyle: entryTextStyle); + } + + /// Create a legend with custom layout. + /// + /// By default, the legend is place above the chart and horizontally aligned + /// to the start of the draw area. + /// + /// [contentBuilder] builder for the custom layout. + /// + /// [position] the legend will be positioned relative to the chart. Default + /// position is top. + /// + /// [outsideJustification] justification of the legend relative to the chart + /// if the position is top, bottom, left, right. Default to start of the draw + /// area. + /// + /// [insideJustification] justification of the legend relative to the chart if + /// the position is inside. Default to top of the chart, start of draw area. + /// Start of draw area means left for LTR directionality, and right for RTL. + /// + /// [defaultHiddenSeries] lists the IDs of series that should be hidden on + /// first chart draw. + /// + /// [showMeasures] show measure values for each series. + /// + /// [legendDefaultMeasure] if measure should show when there is no selection. + /// This is set to none by default (only shows measure for selected data). + /// + /// [measureFormatter] formats measure value if measures are shown. + /// + /// [secondaryMeasureFormatter] formats measures if measures are shown for the + /// series that uses secondary measure axis. + factory SeriesLegend.customLayout( + LegendContentBuilder contentBuilder, { + common.BehaviorPosition position, + common.OutsideJustification outsideJustification, + common.InsideJustification insideJustification, + List defaultHiddenSeries, + bool showMeasures, + common.LegendDefaultMeasure legendDefaultMeasure, + common.MeasureFormatter measureFormatter, + common.MeasureFormatter secondaryMeasureFormatter, + common.TextStyleSpec entryTextStyle, + }) { + // Set defaults if empty. + position ??= defaultBehaviorPosition; + outsideJustification ??= defaultOutsideJustification; + insideJustification ??= defaultInsideJustification; + + return new SeriesLegend._internal( + contentBuilder: contentBuilder, + selectionModelType: common.SelectionModelType.info, + position: position, + outsideJustification: outsideJustification, + insideJustification: insideJustification, + defaultHiddenSeries: defaultHiddenSeries, + showMeasures: showMeasures ?? false, + legendDefaultMeasure: + legendDefaultMeasure ?? common.LegendDefaultMeasure.none, + measureFormatter: measureFormatter, + secondaryMeasureFormatter: secondaryMeasureFormatter, + entryTextStyle: entryTextStyle, + ); + } + + SeriesLegend._internal({ + this.contentBuilder, + this.selectionModelType, + this.position, + this.outsideJustification, + this.insideJustification, + this.defaultHiddenSeries, + this.showMeasures, + this.legendDefaultMeasure, + this.measureFormatter, + this.secondaryMeasureFormatter, + this.entryTextStyle, + }); + + @override + common.SeriesLegend createCommonBehavior() => + new _FlutterSeriesLegend(this); + + @override + void updateCommonBehavior(common.SeriesLegend commonBehavior) { + (commonBehavior as _FlutterSeriesLegend).config = this; + } + + /// All Legend behaviors get the same role ID, because you should only have + /// one legend on a chart. + @override + String get role => 'legend'; + + @override + bool operator ==(Object o) { + return o is SeriesLegend && + selectionModelType == o.selectionModelType && + contentBuilder == o.contentBuilder && + position == o.position && + outsideJustification == o.outsideJustification && + insideJustification == o.insideJustification && + new ListEquality().equals(defaultHiddenSeries, o.defaultHiddenSeries) && + showMeasures == o.showMeasures && + legendDefaultMeasure == o.legendDefaultMeasure && + measureFormatter == o.measureFormatter && + secondaryMeasureFormatter == o.secondaryMeasureFormatter && + entryTextStyle == o.entryTextStyle; + } + + @override + int get hashCode { + return hashValues( + selectionModelType, + contentBuilder, + position, + outsideJustification, + insideJustification, + defaultHiddenSeries, + showMeasures, + legendDefaultMeasure, + measureFormatter, + secondaryMeasureFormatter, + entryTextStyle); + } +} + +/// Flutter specific wrapper on the common Legend for building content. +class _FlutterSeriesLegend extends common.SeriesLegend + implements BuildableBehavior, TappableLegend { + SeriesLegend config; + + _FlutterSeriesLegend(this.config) + : super( + selectionModelType: config.selectionModelType, + measureFormatter: config.measureFormatter, + secondaryMeasureFormatter: config.secondaryMeasureFormatter, + legendDefaultMeasure: config.legendDefaultMeasure, + ) { + super.defaultHiddenSeries = config.defaultHiddenSeries; + super.entryTextStyle = config.entryTextStyle; + } + + @override + void updateLegend() { + (chartContext as ChartContainerRenderObject).requestRebuild(); + } + + @override + common.BehaviorPosition get position => config.position; + + @override + common.OutsideJustification get outsideJustification => + config.outsideJustification; + + @override + common.InsideJustification get insideJustification => + config.insideJustification; + + @override + Widget build(BuildContext context) { + final hasSelection = + legendState.legendEntries.any((entry) => entry.isSelected); + + // Show measures if [showMeasures] is true and there is a selection or if + // showing measures when there is no selection. + final showMeasures = config.showMeasures && + (hasSelection || + legendDefaultMeasure != common.LegendDefaultMeasure.none); + + return config.contentBuilder + .build(context, legendState, this, showMeasures: showMeasures); + } + + @override + onLegendEntryTapUp(common.LegendEntry detail) { + switch (legendTapHandling) { + case common.LegendTapHandling.hide: + _hideSeries(detail); + break; + + case common.LegendTapHandling.none: + default: + break; + } + } + + /// Handles tap events by hiding or un-hiding entries tapped in the legend. + /// + /// Tapping on a visible series in the legend will hide it. Tapping on a + /// hidden series will make it visible again. + void _hideSeries(common.LegendEntry detail) { + final seriesId = detail.series.id; + + // Handle the event by toggling the hidden state of the target. + if (isSeriesHidden(seriesId)) { + showSeries(seriesId); + } else { + hideSeries(seriesId); + } + + // Redraw the chart to actually hide hidden series. + chart.redraw(skipLayout: true, skipAnimation: false); + } +} diff --git a/web/charts/flutter/lib/src/behaviors/line_point_highlighter.dart b/web/charts/flutter/lib/src/behaviors/line_point_highlighter.dart new file mode 100644 index 000000000..424f445d5 --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/line_point_highlighter.dart @@ -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 'package:collection/collection.dart' show ListEquality; +import 'package:charts_common/common.dart' as common + show + LinePointHighlighter, + LinePointHighlighterFollowLineType, + SelectionModelType, + SymbolRenderer; +import 'package:flutter_web/widgets.dart' show hashValues; +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// 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. +@immutable +class LinePointHighlighter extends ChartBehavior { + final desiredGestures = new Set(); + + final common.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; + + final common.LinePointHighlighterFollowLineType showHorizontalFollowLine; + + final common.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 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 common.SymbolRenderer symbolRenderer; + + LinePointHighlighter( + {this.selectionModelType, + this.defaultRadiusPx, + this.radiusPaddingPx, + this.showHorizontalFollowLine, + this.showVerticalFollowLine, + this.dashPattern, + this.drawFollowLinesAcrossChart, + this.symbolRenderer}); + + @override + common.LinePointHighlighter createCommonBehavior() => + new common.LinePointHighlighter( + selectionModelType: selectionModelType, + defaultRadiusPx: defaultRadiusPx, + radiusPaddingPx: radiusPaddingPx, + showHorizontalFollowLine: showHorizontalFollowLine, + showVerticalFollowLine: showVerticalFollowLine, + dashPattern: dashPattern, + drawFollowLinesAcrossChart: drawFollowLinesAcrossChart, + symbolRenderer: symbolRenderer, + ); + + @override + void updateCommonBehavior(common.LinePointHighlighter commonBehavior) {} + + @override + String get role => 'LinePointHighlighter-${selectionModelType.toString()}'; + + @override + bool operator ==(Object o) { + return o is LinePointHighlighter && + defaultRadiusPx == o.defaultRadiusPx && + radiusPaddingPx == o.radiusPaddingPx && + showHorizontalFollowLine == o.showHorizontalFollowLine && + showVerticalFollowLine == o.showVerticalFollowLine && + selectionModelType == o.selectionModelType && + new ListEquality().equals(dashPattern, o.dashPattern) && + drawFollowLinesAcrossChart == o.drawFollowLinesAcrossChart; + } + + @override + int get hashCode { + return hashValues( + selectionModelType, + defaultRadiusPx, + radiusPaddingPx, + showHorizontalFollowLine, + showVerticalFollowLine, + dashPattern, + drawFollowLinesAcrossChart, + ); + } +} diff --git a/web/charts/flutter/lib/src/behaviors/range_annotation.dart b/web/charts/flutter/lib/src/behaviors/range_annotation.dart new file mode 100644 index 000000000..c094e9547 --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/range_annotation.dart @@ -0,0 +1,117 @@ +// 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/common.dart' as common + show + AnnotationLabelAnchor, + AnnotationLabelDirection, + AnnotationLabelPosition, + AnnotationSegment, + Color, + MaterialPalette, + RangeAnnotation, + TextStyleSpec; +import 'package:collection/collection.dart' show ListEquality; +import 'package:flutter_web/widgets.dart' show hashValues; +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that annotations domain ranges with a solid fill color. +/// +/// The annotations will be drawn underneath series data and chart axes. +/// +/// This is typically used for line charts to call out sections of the data +/// range. +@immutable +class RangeAnnotation extends ChartBehavior { + final desiredGestures = new Set(); + + /// List of annotations to render on the chart. + final List annotations; + + /// Configures where to anchor annotation label text. + final common.AnnotationLabelAnchor defaultLabelAnchor; + + /// Direction of label text on the annotations. + final common.AnnotationLabelDirection defaultLabelDirection; + + /// Configures where to place labels relative to the annotation. + final common.AnnotationLabelPosition defaultLabelPosition; + + /// Configures the style of label text. + final common.TextStyleSpec defaultLabelStyleSpec; + + /// Default color for annotations. + final common.Color defaultColor; + + /// Whether or not the range of the axis should be extended to include the + /// annotation start and end values. + final bool extendAxis; + + /// Space before and after label text. + final int labelPadding; + + RangeAnnotation(this.annotations, + {common.Color defaultColor, + this.defaultLabelAnchor, + this.defaultLabelDirection, + this.defaultLabelPosition, + this.defaultLabelStyleSpec, + this.extendAxis, + this.labelPadding}) + : defaultColor = common.MaterialPalette.gray.shade100; + + @override + common.RangeAnnotation createCommonBehavior() => + new common.RangeAnnotation(annotations, + defaultColor: defaultColor, + defaultLabelAnchor: defaultLabelAnchor, + defaultLabelDirection: defaultLabelDirection, + defaultLabelPosition: defaultLabelPosition, + defaultLabelStyleSpec: defaultLabelStyleSpec, + extendAxis: extendAxis, + labelPadding: labelPadding); + + @override + void updateCommonBehavior(common.RangeAnnotation commonBehavior) {} + + @override + String get role => 'RangeAnnotation'; + + @override + bool operator ==(Object o) { + return o is RangeAnnotation && + new ListEquality().equals(annotations, o.annotations) && + defaultColor == o.defaultColor && + extendAxis == o.extendAxis && + defaultLabelAnchor == o.defaultLabelAnchor && + defaultLabelDirection == o.defaultLabelDirection && + defaultLabelPosition == o.defaultLabelPosition && + defaultLabelStyleSpec == o.defaultLabelStyleSpec && + labelPadding == o.labelPadding; + } + + @override + int get hashCode => hashValues( + annotations, + defaultColor, + extendAxis, + defaultLabelAnchor, + defaultLabelDirection, + defaultLabelPosition, + defaultLabelStyleSpec, + labelPadding); +} diff --git a/web/charts/flutter/lib/src/behaviors/select_nearest.dart b/web/charts/flutter/lib/src/behaviors/select_nearest.dart new file mode 100644 index 000000000..ae1e7d5f5 --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/select_nearest.dart @@ -0,0 +1,147 @@ +// 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/common.dart' as common + show ChartBehavior, SelectNearest, SelectionModelType, SelectionTrigger; + +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// 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 will be included in the selection. (Default: true) +/// selectClosestSeries - mark the series for the closest data point as +/// selected. (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. +@immutable +class SelectNearest extends ChartBehavior { + final Set desiredGestures; + + final common.SelectionModelType selectionModelType; + final common.SelectionTrigger eventTrigger; + final bool expandToDomain; + final bool selectAcrossAllDrawAreaComponents; + final bool selectClosestSeries; + final int maximumDomainDistancePx; + + SelectNearest._internal( + {this.selectionModelType, + this.expandToDomain = true, + this.selectAcrossAllDrawAreaComponents = false, + this.selectClosestSeries = true, + this.eventTrigger, + this.desiredGestures, + this.maximumDomainDistancePx}); + + factory SelectNearest( + {common.SelectionModelType selectionModelType = + common.SelectionModelType.info, + bool expandToDomain = true, + bool selectAcrossAllDrawAreaComponents = false, + bool selectClosestSeries = true, + common.SelectionTrigger eventTrigger = common.SelectionTrigger.tap, + int maximumDomainDistancePx}) { + return new SelectNearest._internal( + selectionModelType: selectionModelType, + expandToDomain: expandToDomain, + selectAcrossAllDrawAreaComponents: selectAcrossAllDrawAreaComponents, + selectClosestSeries: selectClosestSeries, + eventTrigger: eventTrigger, + desiredGestures: SelectNearest._getDesiredGestures(eventTrigger), + maximumDomainDistancePx: maximumDomainDistancePx); + } + + static Set _getDesiredGestures( + common.SelectionTrigger eventTrigger) { + final desiredGestures = new Set(); + switch (eventTrigger) { + case common.SelectionTrigger.tap: + desiredGestures..add(GestureType.onTap); + break; + case common.SelectionTrigger.tapAndDrag: + desiredGestures..add(GestureType.onTap)..add(GestureType.onDrag); + break; + case common.SelectionTrigger.pressHold: + case common.SelectionTrigger.longPressHold: + desiredGestures + ..add(GestureType.onTap) + ..add(GestureType.onLongPress) + ..add(GestureType.onDrag); + break; + case common.SelectionTrigger.hover: + default: + desiredGestures..add(GestureType.onHover); + break; + } + return desiredGestures; + } + + @override + common.SelectNearest createCommonBehavior() { + return new common.SelectNearest( + selectionModelType: selectionModelType, + eventTrigger: eventTrigger, + expandToDomain: expandToDomain, + selectClosestSeries: selectClosestSeries, + maximumDomainDistancePx: maximumDomainDistancePx); + } + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + // TODO: Explore the performance impact of calculating this once + // at the constructor for this and common ChartBehaviors. + @override + String get role => 'SelectNearest-${selectionModelType.toString()}}'; + + bool operator ==(Object other) { + if (other is SelectNearest) { + return (selectionModelType == other.selectionModelType) && + (eventTrigger == other.eventTrigger) && + (expandToDomain == other.expandToDomain) && + (selectClosestSeries == other.selectClosestSeries) && + (maximumDomainDistancePx == other.maximumDomainDistancePx); + } else { + return false; + } + } + + int get hashCode { + int hashcode = selectionModelType.hashCode; + hashcode = hashcode * 37 + eventTrigger.hashCode; + hashcode = hashcode * 37 + expandToDomain.hashCode; + hashcode = hashcode * 37 + selectClosestSeries.hashCode; + hashcode = hashcode * 37 + maximumDomainDistancePx.hashCode; + return hashcode; + } +} diff --git a/web/charts/flutter/lib/src/behaviors/slider/slider.dart b/web/charts/flutter/lib/src/behaviors/slider/slider.dart new file mode 100644 index 000000000..30c93dc8a --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/slider/slider.dart @@ -0,0 +1,196 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Rectangle; +import 'package:charts_common/common.dart' as common + show + LayoutViewPaintOrder, + RectSymbolRenderer, + SelectionTrigger, + Slider, + SliderListenerCallback, + SliderStyle, + SymbolRenderer; +import 'package:flutter_web/widgets.dart' show hashValues; +import 'package:meta/meta.dart' show immutable; + +import '../chart_behavior.dart' show ChartBehavior, GestureType; + +/// 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. +@immutable +class Slider extends ChartBehavior { + final Set desiredGestures; + + /// 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 common.SelectionTrigger eventTrigger; + + /// 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). + final int layoutPaintOrder; + + /// Initial domain position of the slider, in domain units. + final dynamic initialDomainValue; + + /// Callback function that will be called when the position of the slider + /// changes during a drag event. + /// + /// The callback will be given the current domain position of the slider. + final common.SliderListenerCallback onChangeCallback; + + /// Custom role ID for this slider + final 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. + final common.SliderStyle style; + + /// Renderer for the handle. Defaults to a rectangle. + final common.SymbolRenderer handleRenderer; + + Slider._internal( + {this.eventTrigger, + this.onChangeCallback, + this.initialDomainValue, + this.roleId, + this.snapToDatum, + this.style, + this.handleRenderer, + this.desiredGestures, + this.layoutPaintOrder}); + + /// 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. + /// + /// [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). + factory Slider( + {common.SelectionTrigger eventTrigger, + common.SymbolRenderer handleRenderer, + dynamic initialDomainValue, + String roleId, + common.SliderListenerCallback onChangeCallback, + bool snapToDatum = false, + common.SliderStyle style, + int layoutPaintOrder = common.LayoutViewPaintOrder.slider}) { + eventTrigger ??= common.SelectionTrigger.tapAndDrag; + handleRenderer ??= new common.RectSymbolRenderer(); + // Default the handle size large enough to tap on a mobile device. + style ??= new common.SliderStyle(handleSize: Rectangle(0, 0, 20, 30)); + return new Slider._internal( + eventTrigger: eventTrigger, + handleRenderer: handleRenderer, + initialDomainValue: initialDomainValue, + onChangeCallback: onChangeCallback, + roleId: roleId, + snapToDatum: snapToDatum, + style: style, + desiredGestures: Slider._getDesiredGestures(eventTrigger), + layoutPaintOrder: layoutPaintOrder); + } + + static Set _getDesiredGestures( + common.SelectionTrigger eventTrigger) { + final desiredGestures = new Set(); + switch (eventTrigger) { + case common.SelectionTrigger.tapAndDrag: + desiredGestures..add(GestureType.onTap)..add(GestureType.onDrag); + break; + case common.SelectionTrigger.pressHold: + case common.SelectionTrigger.longPressHold: + desiredGestures + ..add(GestureType.onTap) + ..add(GestureType.onLongPress) + ..add(GestureType.onDrag); + break; + default: + throw new ArgumentError( + 'Slider does not support the event trigger ' + '"${eventTrigger}"'); + break; + } + return desiredGestures; + } + + @override + common.Slider createCommonBehavior() => new common.Slider( + eventTrigger: eventTrigger, + handleRenderer: handleRenderer, + initialDomainValue: initialDomainValue as D, + onChangeCallback: onChangeCallback, + roleId: roleId, + snapToDatum: snapToDatum, + style: style); + + @override + void updateCommonBehavior(common.Slider commonBehavior) {} + + @override + String get role => 'Slider-${eventTrigger.toString()}'; + + @override + bool operator ==(Object o) { + return o is Slider && + eventTrigger == o.eventTrigger && + handleRenderer == o.handleRenderer && + initialDomainValue == o.initialDomainValue && + onChangeCallback == o.onChangeCallback && + roleId == o.roleId && + snapToDatum == o.snapToDatum && + style == o.style && + layoutPaintOrder == o.layoutPaintOrder; + } + + @override + int get hashCode { + return hashValues(eventTrigger, handleRenderer, initialDomainValue, roleId, + snapToDatum, style, layoutPaintOrder); + } +} diff --git a/web/charts/flutter/lib/src/behaviors/sliding_viewport.dart b/web/charts/flutter/lib/src/behaviors/sliding_viewport.dart new file mode 100644 index 000000000..d8241af49 --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/sliding_viewport.dart @@ -0,0 +1,53 @@ +// 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/common.dart' as common + show SelectionModelType, SlidingViewport; + +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// 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]. +@immutable +class SlidingViewport extends ChartBehavior { + final desiredGestures = new Set(); + + final common.SelectionModelType selectionModelType; + + SlidingViewport([this.selectionModelType = common.SelectionModelType.info]); + + @override + common.SlidingViewport createCommonBehavior() => + new common.SlidingViewport(selectionModelType); + + @override + void updateCommonBehavior(common.SlidingViewport commonBehavior) {} + + @override + String get role => 'slidingViewport-${selectionModelType.toString()}'; + + @override + bool operator ==(Object o) => + o is SlidingViewport && selectionModelType == o.selectionModelType; + + @override + int get hashCode => selectionModelType.hashCode; +} diff --git a/web/charts/flutter/lib/src/behaviors/zoom/initial_hint_behavior.dart b/web/charts/flutter/lib/src/behaviors/zoom/initial_hint_behavior.dart new file mode 100644 index 000000000..2477ef48f --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/zoom/initial_hint_behavior.dart @@ -0,0 +1,131 @@ +// 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:flutter_web/widgets.dart' show AnimationController; + +import 'package:charts_common/common.dart' as common + show BaseChart, ChartBehavior, InitialHintBehavior; +import 'package:meta/meta.dart' show immutable; + +import '../../base_chart_state.dart' show BaseChartState; +import '../chart_behavior.dart' + show ChartBehavior, ChartStateBehavior, GestureType; + +@immutable +class InitialHintBehavior extends ChartBehavior { + final desiredGestures = new Set(); + + final Duration hintDuration; + final double maxHintTranslate; + final double maxHintScaleFactor; + + InitialHintBehavior( + {this.hintDuration, this.maxHintTranslate, this.maxHintScaleFactor}); + + @override + common.InitialHintBehavior createCommonBehavior() { + final behavior = new FlutterInitialHintBehavior(); + + if (hintDuration != null) { + behavior.hintDuration = hintDuration; + } + + if (maxHintTranslate != null) { + behavior.maxHintTranslate = maxHintTranslate; + } + + if (maxHintScaleFactor != null) { + behavior.maxHintScaleFactor = maxHintScaleFactor; + } + + return behavior; + } + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'InitialHint'; + + bool operator ==(Object other) { + return other is InitialHintBehavior && other.hintDuration == hintDuration; + } + + int get hashCode { + return hintDuration.hashCode; + } +} + +/// Adds a native animation controller required for [common.InitialHintBehavior] +/// to function. +class FlutterInitialHintBehavior extends common.InitialHintBehavior + implements ChartStateBehavior { + AnimationController _hintAnimator; + + BaseChartState _chartState; + + set chartState(BaseChartState chartState) { + assert(chartState != null); + + _chartState = chartState; + + _hintAnimator = _chartState.getAnimationController(this); + _hintAnimator?.addListener(onHintTick); + } + + @override + void startHintAnimation() { + super.startHintAnimation(); + + _hintAnimator + ..duration = hintDuration + ..forward(from: 0.0); + } + + @override + void stopHintAnimation() { + super.stopHintAnimation(); + + _hintAnimator?.stop(); + // Hint animation occurs only on the first draw. The hint animator is no + // longer needed after the hint animation stops and is removed. + _chartState.disposeAnimationController(this); + _hintAnimator = null; + } + + @override + double get hintAnimationPercent => _hintAnimator.value; + + bool _skippedFirstTick = true; + + @override + void onHintTick() { + // Skip the first tick on Flutter because the widget rebuild scheduled + // during onAnimation fails on an assert on render object in the framework. + if (_skippedFirstTick) { + _skippedFirstTick = false; + return; + } + + super.onHintTick(); + } + + @override + removeFrom(common.BaseChart chart) { + _chartState.disposeAnimationController(this); + _hintAnimator = null; + super.removeFrom(chart); + } +} diff --git a/web/charts/flutter/lib/src/behaviors/zoom/pan_and_zoom_behavior.dart b/web/charts/flutter/lib/src/behaviors/zoom/pan_and_zoom_behavior.dart new file mode 100644 index 000000000..d0826be9c --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/zoom/pan_and_zoom_behavior.dart @@ -0,0 +1,64 @@ +// 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/common.dart' as common + show ChartBehavior, PanAndZoomBehavior, PanningCompletedCallback; +import 'package:meta/meta.dart' show immutable; + +import '../chart_behavior.dart' show ChartBehavior, GestureType; +import 'pan_behavior.dart' show FlutterPanBehaviorMixin; + +@immutable +class PanAndZoomBehavior extends ChartBehavior { + final _desiredGestures = new Set.from([ + GestureType.onDrag, + ]); + + Set get desiredGestures => _desiredGestures; + + /// Optional callback that is called when pan / zoom is completed. + /// + /// When flinging this callback is called after the fling is completed. + /// This is because panning is only completed when the flinging stops. + final common.PanningCompletedCallback panningCompletedCallback; + + PanAndZoomBehavior({this.panningCompletedCallback}); + + @override + common.PanAndZoomBehavior createCommonBehavior() { + return new FlutterPanAndZoomBehavior() + ..panningCompletedCallback = panningCompletedCallback; + } + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'PanAndZoom'; + + bool operator ==(Object other) { + return other is PanAndZoomBehavior && + other.panningCompletedCallback == panningCompletedCallback; + } + + int get hashCode { + return panningCompletedCallback.hashCode; + } +} + +/// Adds fling gesture support to [common.PanAndZoomBehavior], by way of +/// [FlutterPanBehaviorMixin]. +class FlutterPanAndZoomBehavior extends common.PanAndZoomBehavior + with FlutterPanBehaviorMixin {} diff --git a/web/charts/flutter/lib/src/behaviors/zoom/pan_behavior.dart b/web/charts/flutter/lib/src/behaviors/zoom/pan_behavior.dart new file mode 100644 index 000000000..38c2a671a --- /dev/null +++ b/web/charts/flutter/lib/src/behaviors/zoom/pan_behavior.dart @@ -0,0 +1,186 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show max, pow, Point; +import 'package:flutter_web_ui/ui.dart' hide Point; + +import 'package:flutter_web/widgets.dart' show AnimationController; + +import 'package:charts_common/common.dart' as common + show BaseChart, ChartBehavior, PanBehavior, PanningCompletedCallback; +import 'package:meta/meta.dart' show immutable; + +import '../../base_chart_state.dart' show BaseChartState; +import '../chart_behavior.dart' + show ChartBehavior, ChartStateBehavior, GestureType; + +@immutable +class PanBehavior extends ChartBehavior { + final _desiredGestures = new Set.from([ + GestureType.onDrag, + ]); + + /// Optional callback that is called when panning is completed. + /// + /// When flinging this callback is called after the fling is completed. + /// This is because panning is only completed when the flinging stops. + final common.PanningCompletedCallback panningCompletedCallback; + + PanBehavior({this.panningCompletedCallback}); + + Set get desiredGestures => _desiredGestures; + + @override + common.PanBehavior createCommonBehavior() { + return new FlutterPanBehavior() + ..panningCompletedCallback = panningCompletedCallback; + } + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'Pan'; + + bool operator ==(Object other) { + return other is PanBehavior && + other.panningCompletedCallback == panningCompletedCallback; + } + + int get hashCode { + return panningCompletedCallback.hashCode; + } +} + +/// Class extending [common.PanBehavior] with fling gesture support. +class FlutterPanBehavior = common.PanBehavior + with FlutterPanBehaviorMixin; + +/// Mixin that adds fling gesture support to [common.PanBehavior] or subclasses +/// thereof. +mixin FlutterPanBehaviorMixin on common.PanBehavior + implements ChartStateBehavior { + BaseChartState _chartState; + + set chartState(BaseChartState chartState) { + assert(chartState != null); + + _chartState = chartState; + _flingAnimator = _chartState.getAnimationController(this); + _flingAnimator?.addListener(_onFlingTick); + } + + AnimationController _flingAnimator; + + double _flingAnimationInitialTranslatePx; + double _flingAnimationTargetTranslatePx; + + bool _isFlinging = false; + + static const flingDistanceMultiplier = 0.15; + static const flingDeceleratorFactor = 1.0; + static const flingDurationMultiplier = 0.15; + static const minimumFlingVelocity = 300.0; + + @override + removeFrom(common.BaseChart chart) { + stopFlingAnimation(); + _chartState.disposeAnimationController(this); + _flingAnimator = null; + super.removeFrom(chart); + } + + @override + bool onTapTest(Point chartPoint) { + super.onTapTest(chartPoint); + + stopFlingAnimation(); + + return true; + } + + @override + bool onDragEnd( + Point localPosition, double scale, double pixelsPerSec) { + if (isPanning) { + // Ignore slow drag gestures to avoid jitter. + if (pixelsPerSec.abs() < minimumFlingVelocity) { + onPanEnd(); + return true; + } + + _startFling(pixelsPerSec); + } + + return super.onDragEnd(localPosition, scale, pixelsPerSec); + } + + /// Starts a 'fling' in the direction and speed given by [pixelsPerSec]. + void _startFling(double pixelsPerSec) { + final domainAxis = chart.domainAxis; + + _flingAnimationInitialTranslatePx = domainAxis.viewportTranslatePx; + _flingAnimationTargetTranslatePx = _flingAnimationInitialTranslatePx + + pixelsPerSec * flingDistanceMultiplier; + + final flingDuration = new Duration( + milliseconds: + max(200, (pixelsPerSec * flingDurationMultiplier).abs().round())); + + _flingAnimator + ..duration = flingDuration + ..forward(from: 0.0); + _isFlinging = true; + } + + /// Decelerates a fling event. + double _decelerate(double value) => flingDeceleratorFactor == 1.0 + ? 1.0 - (1.0 - value) * (1.0 - value) + : 1.0 - pow(1.0 - value, 2 * flingDeceleratorFactor); + + /// Updates the chart axis state on each tick of the [AnimationController]. + void _onFlingTick() { + if (!_isFlinging) { + return; + } + + final percent = _flingAnimator.value; + final deceleratedPercent = _decelerate(percent); + final translation = lerpDouble(_flingAnimationInitialTranslatePx, + _flingAnimationTargetTranslatePx, deceleratedPercent); + + final domainAxis = chart.domainAxis; + + domainAxis.setViewportSettings( + domainAxis.viewportScalingFactor, translation, + drawAreaWidth: chart.drawAreaBounds.width); + + if (percent >= 1.0) { + stopFlingAnimation(); + onPanEnd(); + chart.redraw(); + } else { + chart.redraw(skipAnimation: true, skipLayout: true); + } + } + + /// Stops any current fling animations that may be executing. + void stopFlingAnimation() { + if (_isFlinging) { + _isFlinging = false; + _flingAnimator?.stop(); + } + } +} diff --git a/web/charts/flutter/lib/src/canvas/circle_sector_painter.dart b/web/charts/flutter/lib/src/canvas/circle_sector_painter.dart new file mode 100644 index 000000000..d7f98a588 --- /dev/null +++ b/web/charts/flutter/lib/src/canvas/circle_sector_painter.dart @@ -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 'dart:math' show cos, pi, sin, Point; +import 'package:flutter_web/material.dart'; +import 'package:charts_common/common.dart' as common show Color; + +/// Draws a sector of a circle, with an optional hole in the center. +class CircleSectorPainter { + /// Draws a sector of a circle, with an optional hole in the center. + /// + /// [center] The x, y coordinates of the circle's center. + /// [radius] The radius of the circle. + /// [innerRadius] Optional radius of a hole in the center of the circle that + /// should not be filled in as part of the sector. + /// [startAngle] The angle at which the arc starts, measured clockwise from + /// the positive x axis and expressed in radians. + /// [endAngle] The angle at which the arc ends, measured clockwise from the + /// positive x axis and expressed in radians. + /// [fill] Fill color for the sector. + /// [stroke] Stroke color of the arc and radius lines. + /// [strokeWidthPx] Stroke width of the arc and radius lines. + void draw( + {Canvas canvas, + Paint paint, + Point center, + double radius, + double innerRadius, + double startAngle, + double endAngle, + common.Color fill, + common.Color stroke, + double strokeWidthPx}) { + paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b); + paint.style = PaintingStyle.fill; + + final innerRadiusStartPoint = new Point( + innerRadius * cos(startAngle) + center.x, + innerRadius * sin(startAngle) + center.y); + + final innerRadiusEndPoint = new Point( + innerRadius * cos(endAngle) + center.x, + innerRadius * sin(endAngle) + center.y); + + final radiusStartPoint = new Point( + radius * cos(startAngle) + center.x, + radius * sin(startAngle) + center.y); + + final centerOffset = new Offset(center.x, center.y); + + final isFullCircle = startAngle != null && + endAngle != null && + endAngle - startAngle == 2 * pi; + + final midpointAngle = (endAngle + startAngle) / 2; + + final path = new Path() + ..moveTo(innerRadiusStartPoint.x, innerRadiusStartPoint.y); + + path.lineTo(radiusStartPoint.x, radiusStartPoint.y); + + // For full circles, draw the arc in two parts. + if (isFullCircle) { + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius), + startAngle, midpointAngle - startAngle, true); + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius), + midpointAngle, endAngle - midpointAngle, true); + } else { + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius), + startAngle, endAngle - startAngle, true); + } + + path.lineTo(innerRadiusEndPoint.x, innerRadiusEndPoint.y); + + // For full circles, draw the arc in two parts. + if (isFullCircle) { + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius), + endAngle, midpointAngle - endAngle, true); + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius), + midpointAngle, startAngle - midpointAngle, true); + } else { + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius), + endAngle, startAngle - endAngle, true); + } + + // Drawing two copies of this line segment, before and after the arcs, + // ensures that the path actually gets closed correctly. + path.lineTo(radiusStartPoint.x, radiusStartPoint.y); + + canvas.drawPath(path, paint); + } +} diff --git a/web/charts/flutter/lib/src/canvas/line_painter.dart b/web/charts/flutter/lib/src/canvas/line_painter.dart new file mode 100644 index 000000000..44d2d3bcc --- /dev/null +++ b/web/charts/flutter/lib/src/canvas/line_painter.dart @@ -0,0 +1,242 @@ +// 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:flutter_web_ui/ui.dart' as ui show Shader; +import 'dart:math' show Point, Rectangle; +import 'package:flutter_web/material.dart'; +import 'package:charts_common/common.dart' as common show Color; + +/// Draws a simple line. +/// +/// Lines may be styled with dash patterns similar to stroke-dasharray in SVG +/// path elements. Dash patterns are currently only supported between vertical +/// or horizontal line segments at this time. +class LinePainter { + /// Draws a simple line. + /// + /// [dashPattern] controls the pattern of dashes and gaps in a line. It is a + /// list of lengths of alternating dashes and gaps. The rendering is similar + /// to stroke-dasharray in SVG path elements. An odd number of values in the + /// pattern will be repeated to derive an even number of values. "1,2,3" is + /// equivalent to "1,2,3,1,2,3." + void draw( + {Canvas canvas, + Paint paint, + List points, + Rectangle clipBounds, + common.Color fill, + common.Color stroke, + bool roundEndCaps, + double strokeWidthPx, + List dashPattern, + ui.Shader shader}) { + if (points.isEmpty) { + return; + } + + // Apply clip bounds as a clip region. + if (clipBounds != null) { + canvas + ..save() + ..clipRect(new Rect.fromLTWH( + clipBounds.left.toDouble(), + clipBounds.top.toDouble(), + clipBounds.width.toDouble(), + clipBounds.height.toDouble())); + } + + paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b); + if (shader != null) { + paint.shader = shader; + } + + // If the line has a single point, draw a circle. + if (points.length == 1) { + final point = points.first; + paint.style = PaintingStyle.fill; + canvas.drawCircle(new Offset(point.x, point.y), strokeWidthPx, paint); + } else { + if (strokeWidthPx != null) { + paint.strokeWidth = strokeWidthPx; + } + paint.strokeJoin = StrokeJoin.round; + paint.style = PaintingStyle.stroke; + + if (dashPattern == null || dashPattern.isEmpty) { + if (roundEndCaps == true) { + paint.strokeCap = StrokeCap.round; + } + + _drawSolidLine(canvas, paint, points); + } else { + _drawDashedLine(canvas, paint, points, dashPattern); + } + } + + if (clipBounds != null) { + canvas.restore(); + } + } + + /// Draws solid lines between each point. + void _drawSolidLine(Canvas canvas, Paint paint, List points) { + // TODO: Extract a native line component which constructs the + // appropriate underlying data structures to avoid conversion. + final path = new Path() + ..moveTo(points.first.x.toDouble(), points.first.y.toDouble()); + + for (var point in points) { + path.lineTo(point.x.toDouble(), point.y.toDouble()); + } + + canvas.drawPath(path, paint); + } + + /// Draws dashed lines lines between each point. + void _drawDashedLine( + Canvas canvas, Paint paint, List points, List dashPattern) { + final localDashPattern = new List.from(dashPattern); + + // If an odd number of parts are defined, repeat the pattern to get an even + // number. + if (dashPattern.length % 2 == 1) { + localDashPattern.addAll(dashPattern); + } + + // Stores the previous point in the series. + var previousSeriesPoint = _getOffset(points.first); + + var remainder = 0; + var solid = true; + var dashPatternIndex = 0; + + // Gets the next segment in the dash pattern, looping back to the + // beginning once the end has been reached. + var getNextDashPatternSegment = () { + final dashSegment = localDashPattern[dashPatternIndex]; + dashPatternIndex = (dashPatternIndex + 1) % localDashPattern.length; + return dashSegment; + }; + + // Array of points that is used to draw a connecting path when only a + // partial dash pattern segment can be drawn in the remaining length of a + // line segment (between two defined points in the shape). + var remainderPoints; + + // Draw the path through all the rest of the points in the series. + for (var pointIndex = 1; pointIndex < points.length; pointIndex++) { + // Stores the current point in the series. + final seriesPoint = _getOffset(points[pointIndex]); + + if (previousSeriesPoint == seriesPoint) { + // Bypass dash pattern handling if the points are the same. + } else { + // Stores the previous point along the current series line segment where + // we rendered a dash (or left a gap). + var previousPoint = previousSeriesPoint; + + var d = _getOffsetDistance(previousSeriesPoint, seriesPoint); + + while (d > 0) { + var dashSegment = + remainder > 0 ? remainder : getNextDashPatternSegment(); + remainder = 0; + + // Create a unit vector in the direction from previous to next point. + final v = seriesPoint - previousPoint; + final u = new Offset(v.dx / v.distance, v.dy / v.distance); + + // If the remaining distance is less than the length of the dash + // pattern segment, then cut off the pattern segment for this portion + // of the overall line. + final distance = d < dashSegment ? d : dashSegment.toDouble(); + + // Compute a vector representing the length of dash pattern segment to + // be drawn. + final nextPoint = previousPoint + (u * distance); + + // If we are in a solid portion of the dash pattern, draw a line. + // Else, move on. + if (solid) { + if (remainderPoints != null) { + // If we had a partial un-drawn dash from the previous point along + // the line, draw a path that includes it and the end of the dash + // pattern segment in the current line segment. + remainderPoints.add(new Offset(nextPoint.dx, nextPoint.dy)); + + final path = new Path() + ..moveTo(remainderPoints.first.dx, remainderPoints.first.dy); + + for (var p in remainderPoints) { + path.lineTo(p.dx, p.dy); + } + + canvas.drawPath(path, paint); + + remainderPoints = null; + } else { + if (d < dashSegment && pointIndex < points.length - 1) { + // If the remaining distance d is too small to fit this dash, + // and we have more points in the line, save off a series of + // remainder points so that we can draw a path segment moving in + // the direction of the next point. + // + // Note that we don't need to save anything off for the "blank" + // portions of the pattern because we still take the remaining + // distance into account before starting the next dash in the + // next line segment. + remainderPoints = [ + new Offset(previousPoint.dx, previousPoint.dy), + new Offset(nextPoint.dx, nextPoint.dy) + ]; + } else { + // Otherwise, draw a simple line segment for this dash. + canvas.drawLine(previousPoint, nextPoint, paint); + } + } + } + + solid = !solid; + previousPoint = nextPoint; + d = d - dashSegment; + } + + // Save off the remaining distance so that we can continue the dash (or + // gap) into the next line segment. + remainder = -d.round(); + + // If we have a remaining un-drawn distance for the current dash (or + // gap), revert the last change to "solid" so that we will continue + // either drawing a dash or leaving a gap. + if (remainder > 0) { + solid = !solid; + } + } + + previousSeriesPoint = seriesPoint; + } + } + + /// Converts a [Point] into an [Offset]. + Offset _getOffset(Point point) => + new Offset(point.x.toDouble(), point.y.toDouble()); + + /// Computes the distance between two [Offset]s, as if they were [Point]s. + num _getOffsetDistance(Offset o1, Offset o2) { + final p1 = new Point(o1.dx, o1.dy); + final p2 = new Point(o2.dx, o2.dy); + return p1.distanceTo(p2); + } +} diff --git a/web/charts/flutter/lib/src/canvas/pie_painter.dart b/web/charts/flutter/lib/src/canvas/pie_painter.dart new file mode 100644 index 000000000..7d75f551d --- /dev/null +++ b/web/charts/flutter/lib/src/canvas/pie_painter.dart @@ -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 'dart:math' show cos, sin, Point; +import 'package:flutter_web/material.dart'; +import 'package:charts_common/common.dart' as common show CanvasPie; +import 'circle_sector_painter.dart' show CircleSectorPainter; + +/// Draws a pie chart, with an optional hole in the center. +class PiePainter { + CircleSectorPainter _circleSectorPainter; + + /// Draws a pie chart, with an optional hole in the center. + void draw(Canvas canvas, Paint paint, common.CanvasPie canvasPie) { + _circleSectorPainter ??= new CircleSectorPainter(); + + final center = canvasPie.center; + final radius = canvasPie.radius; + final innerRadius = canvasPie.innerRadius; + + for (var slice in canvasPie.slices) { + _circleSectorPainter.draw( + canvas: canvas, + paint: paint, + center: center, + radius: radius, + innerRadius: innerRadius, + startAngle: slice.startAngle, + endAngle: slice.endAngle, + fill: slice.fill); + } + + // Draw stroke lines between pie slices. This is done after the slices are + // drawn to ensure that they appear on top. + if (canvasPie.stroke != null && + canvasPie.strokeWidthPx != null && + canvasPie.slices.length > 1) { + paint.color = new Color.fromARGB(canvasPie.stroke.a, canvasPie.stroke.r, + canvasPie.stroke.g, canvasPie.stroke.b); + + paint.strokeWidth = canvasPie.strokeWidthPx; + paint.strokeJoin = StrokeJoin.bevel; + paint.style = PaintingStyle.stroke; + + final path = new Path(); + + for (var slice in canvasPie.slices) { + final innerRadiusStartPoint = new Point( + innerRadius * cos(slice.startAngle) + center.x, + innerRadius * sin(slice.startAngle) + center.y); + + final innerRadiusEndPoint = new Point( + innerRadius * cos(slice.endAngle) + center.x, + innerRadius * sin(slice.endAngle) + center.y); + + final radiusStartPoint = new Point( + radius * cos(slice.startAngle) + center.x, + radius * sin(slice.startAngle) + center.y); + + final radiusEndPoint = new Point( + radius * cos(slice.endAngle) + center.x, + radius * sin(slice.endAngle) + center.y); + + path.moveTo(innerRadiusStartPoint.x, innerRadiusStartPoint.y); + + path.lineTo(radiusStartPoint.x, radiusStartPoint.y); + + path.moveTo(innerRadiusEndPoint.x, innerRadiusEndPoint.y); + + path.lineTo(radiusEndPoint.x, radiusEndPoint.y); + } + + canvas.drawPath(path, paint); + } + } +} diff --git a/web/charts/flutter/lib/src/canvas/point_painter.dart b/web/charts/flutter/lib/src/canvas/point_painter.dart new file mode 100644 index 000000000..efaa95e9e --- /dev/null +++ b/web/charts/flutter/lib/src/canvas/point_painter.dart @@ -0,0 +1,56 @@ +// 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:flutter_web/material.dart'; +import 'package:charts_common/common.dart' as common show Color; + +/// Draws a simple point. +/// +/// TODO: Support for more shapes than circles? +class PointPainter { + void draw( + {Canvas canvas, + Paint paint, + Point point, + double radius, + common.Color fill, + common.Color stroke, + double strokeWidthPx}) { + if (point == null) { + return; + } + + if (fill != null) { + paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b); + paint.style = PaintingStyle.fill; + + canvas.drawCircle( + new Offset(point.x.toDouble(), point.y.toDouble()), radius, paint); + } + + // [Canvas.drawCircle] does not support drawing a circle with both a fill + // and a stroke at this time. Use a separate circle for the stroke. + if (stroke != null && strokeWidthPx != null && strokeWidthPx > 0.0) { + paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b); + paint.strokeWidth = strokeWidthPx; + paint.strokeJoin = StrokeJoin.bevel; + paint.style = PaintingStyle.stroke; + + canvas.drawCircle( + new Offset(point.x.toDouble(), point.y.toDouble()), radius, paint); + } + } +} diff --git a/web/charts/flutter/lib/src/canvas/polygon_painter.dart b/web/charts/flutter/lib/src/canvas/polygon_painter.dart new file mode 100644 index 000000000..600b0f6cd --- /dev/null +++ b/web/charts/flutter/lib/src/canvas/polygon_painter.dart @@ -0,0 +1,96 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point, Rectangle; +import 'package:flutter_web/material.dart'; +import 'package:charts_common/common.dart' as common show Color; + +/// Draws a simple line. +/// +/// Lines may be styled with dash patterns similar to stroke-dasharray in SVG +/// path elements. Dash patterns are currently only supported between vertical +/// or horizontal line segments at this time. +class PolygonPainter { + /// Draws a simple line. + /// + /// [dashPattern] controls the pattern of dashes and gaps in a line. It is a + /// list of lengths of alternating dashes and gaps. The rendering is similar + /// to stroke-dasharray in SVG path elements. An odd number of values in the + /// pattern will be repeated to derive an even number of values. "1,2,3" is + /// equivalent to "1,2,3,1,2,3." + void draw( + {Canvas canvas, + Paint paint, + List points, + Rectangle clipBounds, + common.Color fill, + common.Color stroke, + double strokeWidthPx}) { + if (points.isEmpty) { + return; + } + + // Apply clip bounds as a clip region. + if (clipBounds != null) { + canvas + ..save() + ..clipRect(new Rect.fromLTWH( + clipBounds.left.toDouble(), + clipBounds.top.toDouble(), + clipBounds.width.toDouble(), + clipBounds.height.toDouble())); + } + + final strokeColor = stroke != null + ? new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b) + : null; + + final fillColor = fill != null + ? new Color.fromARGB(fill.a, fill.r, fill.g, fill.b) + : null; + + // If the line has a single point, draw a circle. + if (points.length == 1) { + final point = points.first; + paint.color = fillColor; + paint.style = PaintingStyle.fill; + canvas.drawCircle(new Offset(point.x, point.y), strokeWidthPx, paint); + } else { + if (strokeColor != null && strokeWidthPx != null) { + paint.strokeWidth = strokeWidthPx; + paint.strokeJoin = StrokeJoin.bevel; + paint.style = PaintingStyle.stroke; + } + + if (fillColor != null) { + paint.color = fillColor; + paint.style = PaintingStyle.fill; + } + + final path = new Path() + ..moveTo(points.first.x.toDouble(), points.first.y.toDouble()); + + for (var point in points) { + path.lineTo(point.x.toDouble(), point.y.toDouble()); + } + + canvas.drawPath(path, paint); + } + + if (clipBounds != null) { + canvas.restore(); + } + } +} diff --git a/web/charts/flutter/lib/src/cartesian_chart.dart b/web/charts/flutter/lib/src/cartesian_chart.dart new file mode 100644 index 000000000..959871d6e --- /dev/null +++ b/web/charts/flutter/lib/src/cartesian_chart.dart @@ -0,0 +1,125 @@ +// 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 'package:meta/meta.dart' show immutable, protected; + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + BaseChart, + CartesianChart, + NumericAxis, + NumericAxisSpec, + RTLSpec, + Series, + SeriesRendererConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'base_chart.dart' show BaseChart, LayoutConfig; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +@immutable +abstract class CartesianChart extends BaseChart { + final common.AxisSpec domainAxis; + final common.AxisSpec primaryMeasureAxis; + final common.AxisSpec secondaryMeasureAxis; + final LinkedHashMap disjointMeasureAxes; + final bool flipVerticalAxis; + + CartesianChart( + List> seriesList, { + bool animate, + Duration animationDuration, + this.domainAxis, + this.primaryMeasureAxis, + this.secondaryMeasureAxis, + this.disjointMeasureAxes, + common.SeriesRendererConfig defaultRenderer, + List> customSeriesRenderers, + List behaviors, + List> selectionModels, + common.RTLSpec rtlSpec, + bool defaultInteractions: true, + LayoutConfig layoutConfig, + UserManagedState userManagedState, + this.flipVerticalAxis, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + defaultInteractions: defaultInteractions, + layoutConfig: layoutConfig, + userManagedState: userManagedState, + ); + + @override + void updateCommonChart(common.BaseChart baseChart, BaseChart oldWidget, + BaseChartState chartState) { + super.updateCommonChart(baseChart, oldWidget, chartState); + + final prev = oldWidget as CartesianChart; + final chart = baseChart as common.CartesianChart; + + if (flipVerticalAxis != null) { + chart.flipVerticalAxisOutput = flipVerticalAxis; + } + + if (domainAxis != null && domainAxis != prev?.domainAxis) { + chart.domainAxisSpec = domainAxis; + chartState.markChartDirty(); + } + + if (primaryMeasureAxis != null && + primaryMeasureAxis != prev?.primaryMeasureAxis) { + chart.primaryMeasureAxisSpec = primaryMeasureAxis; + chartState.markChartDirty(); + } + + if (secondaryMeasureAxis != null && + secondaryMeasureAxis != prev?.secondaryMeasureAxis) { + chart.secondaryMeasureAxisSpec = secondaryMeasureAxis; + chartState.markChartDirty(); + } + + if (disjointMeasureAxes != null && + disjointMeasureAxes != prev?.disjointMeasureAxes) { + chart.disjointMeasureAxisSpecs = disjointMeasureAxes; + chartState.markChartDirty(); + } + } + + @protected + LinkedHashMap createDisjointMeasureAxes() { + if (disjointMeasureAxes != null) { + final disjointAxes = new LinkedHashMap(); + + disjointMeasureAxes + .forEach((String axisId, common.NumericAxisSpec axisSpec) { + disjointAxes[axisId] = axisSpec.createAxis(); + }); + + return disjointAxes; + } else { + return null; + } + } +} diff --git a/web/charts/flutter/lib/src/chart_canvas.dart b/web/charts/flutter/lib/src/chart_canvas.dart new file mode 100644 index 000000000..19409ffcc --- /dev/null +++ b/web/charts/flutter/lib/src/chart_canvas.dart @@ -0,0 +1,442 @@ +// 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:flutter_web_ui/ui.dart' as ui show Gradient, Shader; +import 'dart:math' show Point, Rectangle, max; +import 'package:charts_common/common.dart' as common + show + ChartCanvas, + CanvasBarStack, + CanvasPie, + Color, + FillPatternType, + GraphicsFactory, + StyleFactory, + TextElement, + TextDirection; +import 'package:flutter_web/material.dart'; +import 'text_element.dart' show TextElement; +import 'canvas/circle_sector_painter.dart' show CircleSectorPainter; +import 'canvas/line_painter.dart' show LinePainter; +import 'canvas/pie_painter.dart' show PiePainter; +import 'canvas/point_painter.dart' show PointPainter; +import 'canvas/polygon_painter.dart' show PolygonPainter; + +class ChartCanvas implements common.ChartCanvas { + /// Pixels to allow to overdraw above the draw area that fades to transparent. + static const double rect_top_gradient_pixels = 5; + + final Canvas canvas; + final common.GraphicsFactory graphicsFactory; + final _paint = new Paint(); + + CircleSectorPainter _circleSectorPainter; + LinePainter _linePainter; + PiePainter _piePainter; + PointPainter _pointPainter; + PolygonPainter _polygonPainter; + + ChartCanvas(this.canvas, this.graphicsFactory); + + @override + void drawCircleSector(Point center, double radius, double innerRadius, + double startAngle, double endAngle, + {common.Color fill, common.Color stroke, double strokeWidthPx}) { + _circleSectorPainter ??= new CircleSectorPainter(); + _circleSectorPainter.draw( + canvas: canvas, + paint: _paint, + center: center, + radius: radius, + innerRadius: innerRadius, + startAngle: startAngle, + endAngle: endAngle, + fill: fill, + stroke: stroke, + strokeWidthPx: strokeWidthPx); + } + + @override + void drawLine( + {List points, + Rectangle clipBounds, + common.Color fill, + common.Color stroke, + bool roundEndCaps, + double strokeWidthPx, + List dashPattern}) { + _linePainter ??= new LinePainter(); + _linePainter.draw( + canvas: canvas, + paint: _paint, + points: points, + clipBounds: clipBounds, + fill: fill, + stroke: stroke, + roundEndCaps: roundEndCaps, + strokeWidthPx: strokeWidthPx, + dashPattern: dashPattern); + } + + @override + void drawPie(common.CanvasPie canvasPie) { + _piePainter ??= new PiePainter(); + _piePainter.draw(canvas, _paint, canvasPie); + } + + @override + void drawPoint( + {Point point, + double radius, + common.Color fill, + common.Color stroke, + double strokeWidthPx}) { + _pointPainter ??= new PointPainter(); + _pointPainter.draw( + canvas: canvas, + paint: _paint, + point: point, + radius: radius, + fill: fill, + stroke: stroke, + strokeWidthPx: strokeWidthPx); + } + + @override + void drawPolygon( + {List points, + Rectangle clipBounds, + common.Color fill, + common.Color stroke, + double strokeWidthPx}) { + _polygonPainter ??= new PolygonPainter(); + _polygonPainter.draw( + canvas: canvas, + paint: _paint, + points: points, + clipBounds: clipBounds, + fill: fill, + stroke: stroke, + strokeWidthPx: strokeWidthPx); + } + + /// Creates a bottom to top gradient that transitions [fill] to transparent. + ui.Gradient _createHintGradient(double left, double top, common.Color fill) { + return new ui.Gradient.linear( + new Offset(left, top), + new Offset(left, top - rect_top_gradient_pixels), + [ + new Color.fromARGB(fill.a, fill.r, fill.g, fill.b), + new Color.fromARGB(0, fill.r, fill.g, fill.b) + ], + ); + } + + @override + void drawRect(Rectangle bounds, + {common.Color fill, + common.FillPatternType pattern, + common.Color stroke, + double strokeWidthPx, + Rectangle drawAreaBounds}) { + final drawStroke = + (strokeWidthPx != null && strokeWidthPx > 0.0 && stroke != null); + + final strokeWidthOffset = (drawStroke ? strokeWidthPx : 0); + + // Factor out stroke width, if a stroke is enabled. + final fillRectBounds = new Rectangle( + bounds.left + strokeWidthOffset / 2, + bounds.top + strokeWidthOffset / 2, + bounds.width - strokeWidthOffset, + bounds.height - strokeWidthOffset); + + switch (pattern) { + case common.FillPatternType.forwardHatch: + _drawForwardHatchPattern(fillRectBounds, canvas, + fill: fill, drawAreaBounds: drawAreaBounds); + break; + + case common.FillPatternType.solid: + default: + // Use separate rect for drawing stroke + _paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b); + _paint.style = PaintingStyle.fill; + + // Apply a gradient to the top [rect_top_gradient_pixels] to transparent + // if the rectangle is higher than the [drawAreaBounds] top. + if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) { + _paint.shader = _createHintGradient(drawAreaBounds.left.toDouble(), + drawAreaBounds.top.toDouble(), fill); + } + + canvas.drawRect(_getRect(fillRectBounds), _paint); + break; + } + + // [Canvas.drawRect] does not support drawing a rectangle with both a fill + // and a stroke at this time. Use a separate rect for the stroke. + if (drawStroke) { + _paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b); + // Set shader to null if no draw area bounds so it can use the color + // instead. + _paint.shader = drawAreaBounds != null + ? _createHintGradient(drawAreaBounds.left.toDouble(), + drawAreaBounds.top.toDouble(), stroke) + : null; + _paint.strokeJoin = StrokeJoin.round; + _paint.strokeWidth = strokeWidthPx; + _paint.style = PaintingStyle.stroke; + + canvas.drawRect(_getRect(bounds), _paint); + } + + // Reset the shader. + _paint.shader = null; + } + + @override + void drawRRect(Rectangle bounds, + {common.Color fill, + common.Color stroke, + num radius, + bool roundTopLeft, + bool roundTopRight, + bool roundBottomLeft, + bool roundBottomRight}) { + // Use separate rect for drawing stroke + _paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b); + _paint.style = PaintingStyle.fill; + + canvas.drawRRect( + _getRRect(bounds, + radius: radius, + roundTopLeft: roundTopLeft, + roundTopRight: roundTopRight, + roundBottomLeft: roundBottomLeft, + roundBottomRight: roundBottomRight), + _paint); + } + + @override + void drawBarStack(common.CanvasBarStack barStack, + {Rectangle drawAreaBounds}) { + // only clip if rounded rect. + + // Clip a rounded rect for the whole region if rounded bars. + final roundedCorners = 0 < barStack.radius; + + if (roundedCorners) { + canvas + ..save() + ..clipRRect(_getRRect( + barStack.fullStackRect, + radius: barStack.radius.toDouble(), + roundTopLeft: barStack.roundTopLeft, + roundTopRight: barStack.roundTopRight, + roundBottomLeft: barStack.roundBottomLeft, + roundBottomRight: barStack.roundBottomRight, + )); + } + + // Draw each bar. + for (var barIndex = 0; barIndex < barStack.segments.length; barIndex++) { + // TODO: Add configuration for hiding stack line. + // TODO: Don't draw stroke on bottom of bars. + final segment = barStack.segments[barIndex]; + drawRect(segment.bounds, + fill: segment.fill, + pattern: segment.pattern, + stroke: segment.stroke, + strokeWidthPx: segment.strokeWidthPx, + drawAreaBounds: drawAreaBounds); + } + + if (roundedCorners) { + canvas.restore(); + } + } + + @override + void drawText(common.TextElement textElement, int offsetX, int offsetY, + {double rotation = 0.0}) { + // Must be Flutter TextElement. + assert(textElement is TextElement); + + final flutterTextElement = textElement as TextElement; + final textDirection = flutterTextElement.textDirection; + final measurement = flutterTextElement.measurement; + + if (rotation != 0) { + // TODO: Remove once textAnchor works. + if (textDirection == common.TextDirection.rtl) { + offsetY += measurement.horizontalSliceWidth.toInt(); + } + + offsetX -= flutterTextElement.verticalFontShift; + + canvas.save(); + canvas.translate(offsetX.toDouble(), offsetY.toDouble()); + canvas.rotate(rotation); + + (textElement as TextElement) + .textPainter + .paint(canvas, new Offset(0.0, 0.0)); + + canvas.restore(); + } else { + // TODO: Remove once textAnchor works. + if (textDirection == common.TextDirection.rtl) { + offsetX -= measurement.horizontalSliceWidth.toInt(); + } + + // Account for missing center alignment. + if (textDirection == common.TextDirection.center) { + offsetX -= (measurement.horizontalSliceWidth / 2).ceil(); + } + + offsetY -= flutterTextElement.verticalFontShift; + + (textElement as TextElement) + .textPainter + .paint(canvas, new Offset(offsetX.toDouble(), offsetY.toDouble())); + } + } + + @override + void setClipBounds(Rectangle clipBounds) { + canvas + ..save() + ..clipRect(_getRect(clipBounds)); + } + + @override + void resetClipBounds() { + canvas.restore(); + } + + /// Convert dart:math [Rectangle] to Flutter [Rect]. + Rect _getRect(Rectangle rectangle) { + return new Rect.fromLTWH( + rectangle.left.toDouble(), + rectangle.top.toDouble(), + rectangle.width.toDouble(), + rectangle.height.toDouble()); + } + + /// Convert dart:math [Rectangle] and to Flutter [RRect]. + RRect _getRRect( + Rectangle rectangle, { + double radius, + bool roundTopLeft = false, + bool roundTopRight = false, + bool roundBottomLeft = false, + bool roundBottomRight = false, + }) { + final cornerRadius = + radius == 0 ? Radius.zero : new Radius.circular(radius); + + return new RRect.fromLTRBAndCorners( + rectangle.left.toDouble(), + rectangle.top.toDouble(), + rectangle.right.toDouble(), + rectangle.bottom.toDouble(), + topLeft: roundTopLeft ? cornerRadius : Radius.zero, + topRight: roundTopRight ? cornerRadius : Radius.zero, + bottomLeft: roundBottomLeft ? cornerRadius : Radius.zero, + bottomRight: roundBottomRight ? cornerRadius : Radius.zero); + } + + /// Draws a forward hatch pattern in the given bounds. + _drawForwardHatchPattern( + Rectangle bounds, + Canvas canvas, { + common.Color background, + common.Color fill, + double fillWidthPx = 4.0, + Rectangle drawAreaBounds, + }) { + background ??= common.StyleFactory.style.white; + fill ??= common.StyleFactory.style.black; + + // Fill in the shape with a solid background color. + _paint.color = new Color.fromARGB( + background.a, background.r, background.g, background.b); + _paint.style = PaintingStyle.fill; + + // Apply a gradient the background if bounds exceed the draw area. + if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) { + _paint.shader = _createHintGradient(drawAreaBounds.left.toDouble(), + drawAreaBounds.top.toDouble(), background); + } + + canvas.drawRect(_getRect(bounds), _paint); + + // As a simplification, we will treat the bounds as a large square and fill + // it up with lines from the bottom-left corner to the top-right corner. + // Get the longer side of the bounds here for the size of this square. + final size = max(bounds.width, bounds.height); + + final x0 = bounds.left + size + fillWidthPx; + final x1 = bounds.left - fillWidthPx; + final y0 = bounds.bottom - size - fillWidthPx; + final y1 = bounds.bottom + fillWidthPx; + final offset = 8; + + final isVertical = bounds.height >= bounds.width; + + _linePainter ??= new LinePainter(); + + // The "first" line segment will be drawn from the bottom left corner of the + // bounds, up and towards the right. Start the loop N iterations "back" to + // draw partial line segments beneath (or to the left) of this segment, + // where N is the number of offsets that fit inside the smaller dimension of + // the bounds. + final smallSide = isVertical ? bounds.width : bounds.height; + final start = -(smallSide / offset).round() * offset; + + // Keep going until we reach the top or right of the bounds, depending on + // whether the rectangle is oriented vertically or horizontally. + final end = size + offset; + + // Create gradient for line painter if top bounds exceeded. + ui.Shader lineShader; + if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) { + lineShader = _createHintGradient( + drawAreaBounds.left.toDouble(), drawAreaBounds.top.toDouble(), fill); + } + + for (int i = start; i < end; i = i + offset) { + // For vertical bounds, we need to draw lines from top to bottom. For + // bounds, we need to draw lines from left to right. + final modifier = isVertical ? -1 * i : i; + + // Draw a line segment in the bottom right corner of the pattern. + _linePainter.draw( + canvas: canvas, + paint: _paint, + points: [ + new Point(x0 + modifier, y0), + new Point(x1 + modifier, y1), + ], + stroke: fill, + strokeWidthPx: fillWidthPx, + shader: lineShader); + } + } + + @override + set drawingView(String viewName) {} +} diff --git a/web/charts/flutter/lib/src/chart_container.dart b/web/charts/flutter/lib/src/chart_container.dart new file mode 100644 index 000000000..62f89e24c --- /dev/null +++ b/web/charts/flutter/lib/src/chart_container.dart @@ -0,0 +1,419 @@ +// 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/common.dart' as common + show + A11yNode, + AxisDirection, + BaseChart, + ChartContext, + DateTimeFactory, + LocalDateTimeFactory, + ProxyGestureListener, + RTLSpec, + SelectionModelType, + Series, + Performance; +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; +import 'package:flutter_web/scheduler.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart' show required; +import 'chart_canvas.dart' show ChartCanvas; +import 'chart_state.dart' show ChartState; +import 'base_chart.dart' show BaseChart; +import 'graphics_factory.dart' show GraphicsFactory; +import 'time_series_chart.dart' show TimeSeriesChart; +import 'user_managed_state.dart' show UserManagedState; + +/// Widget that inflates to a [CustomPaint] that implements common [ChartContext]. +class ChartContainer extends CustomPaint { + final BaseChart chartWidget; + final BaseChart oldChartWidget; + final ChartState chartState; + final double animationValue; + final bool rtl; + final common.RTLSpec rtlSpec; + final UserManagedState userManagedState; + + ChartContainer( + {@required this.oldChartWidget, + @required this.chartWidget, + @required this.chartState, + @required this.animationValue, + @required this.rtl, + @required this.rtlSpec, + this.userManagedState}); + + @override + RenderCustomPaint createRenderObject(BuildContext context) { + return new ChartContainerRenderObject()..reconfigure(this, context); + } + + @override + void updateRenderObject( + BuildContext context, ChartContainerRenderObject renderObject) { + renderObject.reconfigure(this, context); + } +} + +/// [RenderCustomPaint] that implements common [ChartContext]. +class ChartContainerRenderObject extends RenderCustomPaint + implements common.ChartContext { + common.BaseChart _chart; + List> _seriesList; + ChartState _chartState; + bool _chartContainerIsRtl = false; + common.RTLSpec _rtlSpec; + common.DateTimeFactory _dateTimeFactory; + bool _exploreMode = false; + List _a11yNodes; + + final Logger _log = new Logger('charts_flutter.charts_container'); + + /// Keeps the last time the configuration was changed and chart draw on the + /// common chart is called. + /// + /// An assert uses this value to check if the configuration changes more + /// frequently than a threshold. This is to notify developers of something + /// wrong in the configuration of their charts if it keeps changes (usually + /// due to equality checks not being implemented and when a new object is + /// created inside a new chart widget, a change is detected even if nothing + /// has changed). + DateTime _lastConfigurationChangeTime; + + /// The minimum time required before the next configuration change. + static const configurationChangeThresholdMs = 500; + + void reconfigure(ChartContainer config, BuildContext context) { + _chartState = config.chartState; + + _dateTimeFactory = (config.chartWidget is TimeSeriesChart) + ? (config.chartWidget as TimeSeriesChart).dateTimeFactory + : null; + _dateTimeFactory ??= new common.LocalDateTimeFactory(); + + if (_chart == null) { + common.Performance.time('chartsCreate'); + _chart = config.chartWidget.createCommonChart(_chartState); + _chart.init(this, new GraphicsFactory(context)); + common.Performance.timeEnd('chartsCreate'); + } + common.Performance.time('chartsConfig'); + config.chartWidget + .updateCommonChart(_chart, config.oldChartWidget, _chartState); + + _rtlSpec = config.rtlSpec ?? const common.RTLSpec(); + _chartContainerIsRtl = config.rtl ?? false; + + common.Performance.timeEnd('chartsConfig'); + + // If the configuration is changed more frequently than the threshold, + // log the occurrence and reset the configurationChanged flag to false + // to skip calling chart draw and avoid getting into an infinite rebuild + // cycle. + // + // One common cause for the configuration changing on every chart build + // is because a behavior is detected to have changed when it has not. + // A common case is when a setting is passed to a behavior is an object + // and doesn't override the equality checks. + if (_chartState.chartIsDirty) { + final currentTime = DateTime.now(); + final lastConfigurationBelowThreshold = _lastConfigurationChangeTime != + null && + currentTime.difference(_lastConfigurationChangeTime).inMilliseconds < + configurationChangeThresholdMs; + + _lastConfigurationChangeTime = currentTime; + + if (lastConfigurationBelowThreshold) { + _chartState.resetChartDirtyFlag(); + _log.warning( + 'Chart configuration is changing more frequent than threshold' + ' of $configurationChangeThresholdMs. Check if your behavior, axis,' + ' or renderer config is missing equality checks that may be causing' + ' configuration to be detected as changed. '); + } + } + + if (_chartState.chartIsDirty) { + _chart.configurationChanged(); + } + + // If series list changes or other configuration changed that triggered the + // _chartState.configurationChanged flag to be set (such as axis, behavior, + // and renderer changes). Otherwise, the chart only requests repainting and + // does not reprocess the series. + // + // Series list is considered "changed" based on the instance. + if (_seriesList != config.chartWidget.seriesList || + _chartState.chartIsDirty) { + _chartState.resetChartDirtyFlag(); + _seriesList = config.chartWidget.seriesList; + + // Clear out the a11y nodes generated. + _a11yNodes = null; + + common.Performance.time('chartsDraw'); + _chart.draw(_seriesList); + common.Performance.timeEnd('chartsDraw'); + + // This is needed because when a series changes we need to reset flutter's + // animation value from 1.0 back to 0.0. + _chart.animationPercent = 0.0; + markNeedsLayout(); + } else { + _chart.animationPercent = config.animationValue; + markNeedsPaint(); + } + + _updateUserManagedState(config.userManagedState); + + // Set the painter used for calling common chart for paint. + // This painter is also used to generate semantic nodes for a11y. + _setNewPainter(); + } + + /// If user managed state is set, check each setting to see if it is different + /// than internal chart state and only update if different. + _updateUserManagedState(UserManagedState newState) { + if (newState == null) { + return; + } + + // Only override the selection model if it is different than the existing + // selection model so update listeners are not unnecessarily triggered. + for (common.SelectionModelType type in newState.selectionModels.keys) { + final model = _chart.getSelectionModel(type); + + final userModel = + newState.selectionModels[type].getModel(_chart.currentSeriesList); + + if (model != userModel) { + model.updateSelection( + userModel.selectedDatum, userModel.selectedSeries); + } + } + } + + @override + void performLayout() { + common.Performance.time('chartsLayout'); + _chart.measure(constraints.maxWidth.toInt(), constraints.maxHeight.toInt()); + _chart.layout(constraints.maxWidth.toInt(), constraints.maxHeight.toInt()); + common.Performance.timeEnd('chartsLayout'); + size = constraints.biggest; + + // Check if the gestures registered in gesture registry matches what the + // common chart is listening to. + // TODO: Still need a test for this for sanity sake. +// assert(_desiredGestures +// .difference(_chart.gestureProxy.listenedGestures) +// .isEmpty); + } + + @override + void markNeedsLayout() { + super.markNeedsLayout(); + if (parent != null) { + markParentNeedsLayout(); + } + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + void requestRedraw() {} + + @override + void requestAnimation(Duration transition) { + void startAnimationController(_) { + _chartState.setAnimation(transition); + } + + // Sometimes chart behaviors try to draw the chart outside of a Flutter draw + // cycle. Schedule a frame manually to handle these cases. + if (!SchedulerBinding.instance.hasScheduledFrame) { + SchedulerBinding.instance.scheduleFrame(); + } + + SchedulerBinding.instance.addPostFrameCallback(startAnimationController); + } + + /// Request Flutter to rebuild the widget/container of chart. + /// + /// This is different than requesting redraw and paint because those only + /// affect the chart widget. This is for requesting rebuild of the Flutter + /// widget that contains the chart widget. This is necessary for supporting + /// Flutter widgets that are layout with the chart. + /// + /// Example is legends, a legend widget can be layout on top of the chart + /// widget or along the sides of the chart. Requesting a rebuild allows + /// the legend to layout and redraw itself. + void requestRebuild() { + void doRebuild(_) { + _chartState.requestRebuild(); + } + + // Flutter does not allow requesting rebuild during the build cycle, this + // schedules rebuild request to happen after the current build cycle. + // This is needed to request rebuild after the legend has been added in the + // post process phase of the chart, which happens during the chart widget's + // build cycle. + SchedulerBinding.instance.addPostFrameCallback(doRebuild); + } + + /// When Flutter's markNeedsLayout is called, layout and paint are both + /// called. If animations are off, Flutter's paint call after layout will + /// paint the chart. If animations are on, Flutter's paint is called with the + /// initial animation value and then the animation controller is started after + /// this first build cycle. + @override + void requestPaint() { + markNeedsPaint(); + } + + @override + double get pixelsPerDp => 1.0; + + @override + bool get chartContainerIsRtl => _chartContainerIsRtl; + + @override + common.RTLSpec get rtlSpec => _rtlSpec; + + @override + bool get isRtl => + _chartContainerIsRtl && + _rtlSpec?.axisDirection == common.AxisDirection.reversed; + + @override + bool get isTappable => _chart.isTappable; + + @override + common.DateTimeFactory get dateTimeFactory => _dateTimeFactory; + + /// Gets the chart's gesture listener. + common.ProxyGestureListener get gestureProxy => _chart.gestureProxy; + + TextDirection get textDirection => + _chartContainerIsRtl ? TextDirection.rtl : TextDirection.ltr; + + @override + void enableA11yExploreMode(List nodes, + {String announcement}) { + _a11yNodes = nodes; + _exploreMode = true; + _setNewPainter(); + requestRebuild(); + if (announcement != null) { + SemanticsService.announce(announcement, textDirection); + } + } + + @override + void disableA11yExploreMode({String announcement}) { + _a11yNodes = []; + _exploreMode = false; + _setNewPainter(); + requestRebuild(); + if (announcement != null) { + SemanticsService.announce(announcement, textDirection); + } + } + + void _setNewPainter() { + painter = new ChartContainerCustomPaint( + oldPainter: painter, + chart: _chart, + exploreMode: _exploreMode, + a11yNodes: _a11yNodes, + textDirection: textDirection); + } +} + +class ChartContainerCustomPaint extends CustomPainter { + final common.BaseChart chart; + final bool exploreMode; + final List a11yNodes; + final TextDirection textDirection; + + factory ChartContainerCustomPaint( + {ChartContainerCustomPaint oldPainter, + common.BaseChart chart, + bool exploreMode, + List a11yNodes, + TextDirection textDirection}) { + if (oldPainter != null && + oldPainter.exploreMode == exploreMode && + oldPainter.a11yNodes == a11yNodes && + oldPainter.textDirection == textDirection) { + return oldPainter; + } else { + return new ChartContainerCustomPaint._internal( + chart: chart, + exploreMode: exploreMode ?? false, + a11yNodes: a11yNodes ?? [], + textDirection: textDirection ?? TextDirection.ltr); + } + } + + ChartContainerCustomPaint._internal( + {this.chart, this.exploreMode, this.a11yNodes, this.textDirection}); + + @override + void paint(Canvas canvas, Size size) { + common.Performance.time('chartsPaint'); + final chartsCanvas = new ChartCanvas(canvas, chart.graphicsFactory); + chart.paint(chartsCanvas); + common.Performance.timeEnd('chartsPaint'); + } + + /// Common chart requests rebuild that handle repaint requests. + @override + bool shouldRepaint(ChartContainerCustomPaint oldPainter) => false; + + /// Rebuild semantics when explore mode is toggled semantic properties change. + @override + bool shouldRebuildSemantics(ChartContainerCustomPaint oldDelegate) { + return exploreMode != oldDelegate.exploreMode || + a11yNodes != oldDelegate.a11yNodes || + textDirection != textDirection; + } + + @override + SemanticsBuilderCallback get semanticsBuilder => _buildSemantics; + + List _buildSemantics(Size size) { + final nodes = []; + + for (common.A11yNode node in a11yNodes) { + final rect = new Rect.fromLTWH( + node.boundingBox.left.toDouble(), + node.boundingBox.top.toDouble(), + node.boundingBox.width.toDouble(), + node.boundingBox.height.toDouble()); + nodes.add(new CustomPainterSemantics( + rect: rect, + properties: new SemanticsProperties( + value: node.label, + textDirection: textDirection, + onDidGainAccessibilityFocus: node.onFocus))); + } + + return nodes; + } +} diff --git a/web/charts/flutter/lib/src/chart_gesture_detector.dart b/web/charts/flutter/lib/src/chart_gesture_detector.dart new file mode 100644 index 000000000..f24e6117a --- /dev/null +++ b/web/charts/flutter/lib/src/chart_gesture_detector.dart @@ -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:async' show Timer; +import 'dart:math' show Point; +import 'package:flutter_web/material.dart' + show + BuildContext, + GestureDetector, + ScaleEndDetails, + ScaleStartDetails, + ScaleUpdateDetails, + TapDownDetails, + TapUpDetails; + +import 'behaviors/chart_behavior.dart' show GestureType; +import 'chart_container.dart' show ChartContainer, ChartContainerRenderObject; +import 'util.dart' show getChartContainerRenderObject; + +// From https://docs.flutter.io/flutter/gestures/kLongPressTimeout-constant.html +const Duration _kLongPressTimeout = const Duration(milliseconds: 500); + +class ChartGestureDetector { + bool _listeningForLongPress; + + bool _isDragging = false; + + Timer _longPressTimer; + Point _lastTapPoint; + double _lastScale; + + _ContainerResolver _containerResolver; + + makeWidget(BuildContext context, ChartContainer chartContainer, + Set desiredGestures) { + _containerResolver = + () => getChartContainerRenderObject(context.findRenderObject()); + + final wantTapDown = desiredGestures.isNotEmpty; + final wantTap = desiredGestures.contains(GestureType.onTap); + final wantDrag = desiredGestures.contains(GestureType.onDrag); + + // LongPress is special, we'd like to be able to trigger long press before + // Drag/Press to trigger tooltips then explore with them. This means we + // can't rely on gesture detection since it will block out the scale + // gestures. + _listeningForLongPress = desiredGestures.contains(GestureType.onLongPress); + + return new GestureDetector( + child: chartContainer, + onTapDown: wantTapDown ? onTapDown : null, + onTapUp: wantTap ? onTapUp : null, + onScaleStart: wantDrag ? onScaleStart : null, + onScaleUpdate: wantDrag ? onScaleUpdate : null, + onScaleEnd: wantDrag ? onScaleEnd : null, + ); + } + + void onTapDown(TapDownDetails d) { + final container = _containerResolver(); + final localPosition = container.globalToLocal(d.globalPosition); + _lastTapPoint = new Point(localPosition.dx, localPosition.dy); + container.gestureProxy.onTapTest(_lastTapPoint); + + // Kick off a timer to see if this is a LongPress. + if (_listeningForLongPress) { + _longPressTimer = new Timer(_kLongPressTimeout, () { + onLongPress(); + _longPressTimer = null; + }); + } + } + + void onTapUp(TapUpDetails d) { + _longPressTimer?.cancel(); + + final container = _containerResolver(); + final localPosition = container.globalToLocal(d.globalPosition); + _lastTapPoint = new Point(localPosition.dx, localPosition.dy); + container.gestureProxy.onTap(_lastTapPoint); + } + + void onLongPress() { + final container = _containerResolver(); + container.gestureProxy.onLongPress(_lastTapPoint); + } + + void onScaleStart(ScaleStartDetails d) { + _longPressTimer?.cancel(); + + final container = _containerResolver(); + final localPosition = container.globalToLocal(d.focalPoint); + _lastTapPoint = new Point(localPosition.dx, localPosition.dy); + + _isDragging = container.gestureProxy.onDragStart(_lastTapPoint); + } + + void onScaleUpdate(ScaleUpdateDetails d) { + if (!_isDragging) { + return; + } + + final container = _containerResolver(); + final localPosition = container.globalToLocal(d.focalPoint); + _lastTapPoint = new Point(localPosition.dx, localPosition.dy); + _lastScale = d.scale; + + container.gestureProxy.onDragUpdate(_lastTapPoint, d.scale); + } + + void onScaleEnd(ScaleEndDetails d) { + if (!_isDragging) { + return; + } + + final container = _containerResolver(); + + container.gestureProxy + .onDragEnd(_lastTapPoint, _lastScale, d.velocity.pixelsPerSecond.dx); + } +} + +// Exposed for testing. +typedef ChartContainerRenderObject _ContainerResolver(); diff --git a/web/charts/flutter/lib/src/chart_state.dart b/web/charts/flutter/lib/src/chart_state.dart new file mode 100644 index 000000000..6103968bf --- /dev/null +++ b/web/charts/flutter/lib/src/chart_state.dart @@ -0,0 +1,36 @@ +// 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. + +abstract class ChartState { + void setAnimation(Duration transition); + + /// Request to the native platform to rebuild the chart. + void requestRebuild(); + + /// Informs the chart that the configuration has changed. + /// + /// This flag is set by checks that detect if a configuration has changed, + /// such as behaviors, axis, and renderers. + /// + /// This flag is read on chart rebuild, if chart is marked as dirty, then the + /// chart will call a base chart draw. + void markChartDirty(); + + /// Reset the chart dirty flag. + void resetChartDirtyFlag(); + + /// Gets if the chart is dirty. + bool get chartIsDirty; +} diff --git a/web/charts/flutter/lib/src/combo_chart/combo_chart.dart b/web/charts/flutter/lib/src/combo_chart/combo_chart.dart new file mode 100644 index 000000000..a9ef033b8 --- /dev/null +++ b/web/charts/flutter/lib/src/combo_chart/combo_chart.dart @@ -0,0 +1,122 @@ +// 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/common.dart' as common + show + AxisSpec, + NumericCartesianChart, + OrdinalCartesianChart, + RTLSpec, + Series, + SeriesRendererConfig; +import '../behaviors/chart_behavior.dart' show ChartBehavior; +import '../base_chart.dart' show LayoutConfig; +import '../base_chart_state.dart' show BaseChartState; +import '../cartesian_chart.dart' show CartesianChart; +import '../selection_model_config.dart' show SelectionModelConfig; + +/// A numeric combo chart supports rendering each series of data with different +/// series renderers. +/// +/// Note that if you have DateTime data, you should use [TimeSeriesChart]. We do +/// not expose a separate DateTimeComboChart because it would just be a copy of +/// that chart. +class NumericComboChart extends CartesianChart { + NumericComboChart( + List seriesList, { + bool animate, + Duration animationDuration, + common.AxisSpec domainAxis, + common.AxisSpec primaryMeasureAxis, + common.AxisSpec secondaryMeasureAxis, + common.SeriesRendererConfig defaultRenderer, + List> customSeriesRenderers, + List behaviors, + List> selectionModels, + common.RTLSpec rtlSpec, + LayoutConfig layoutConfig, + bool defaultInteractions: true, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + ); + + @override + common.NumericCartesianChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.NumericCartesianChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis()); + } +} + +/// An ordinal combo chart supports rendering each series of data with different +/// series renderers. +class OrdinalComboChart extends CartesianChart { + OrdinalComboChart( + List seriesList, { + bool animate, + Duration animationDuration, + common.AxisSpec domainAxis, + common.AxisSpec primaryMeasureAxis, + common.AxisSpec secondaryMeasureAxis, + common.SeriesRendererConfig defaultRenderer, + List> customSeriesRenderers, + List behaviors, + List> selectionModels, + common.RTLSpec rtlSpec, + LayoutConfig layoutConfig, + bool defaultInteractions: true, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + ); + + @override + common.OrdinalCartesianChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.OrdinalCartesianChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis()); + } +} diff --git a/web/charts/flutter/lib/src/graphics_factory.dart b/web/charts/flutter/lib/src/graphics_factory.dart new file mode 100644 index 000000000..b1027c9d4 --- /dev/null +++ b/web/charts/flutter/lib/src/graphics_factory.dart @@ -0,0 +1,50 @@ +// 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/common.dart' as common + show GraphicsFactory, LineStyle, TextElement, TextStyle; +import 'package:flutter_web/widgets.dart' show BuildContext, MediaQuery; +import 'line_style.dart' show LineStyle; +import 'text_element.dart' show TextElement; +import 'text_style.dart' show TextStyle; + +class GraphicsFactory implements common.GraphicsFactory { + final double textScaleFactor; + + GraphicsFactory(BuildContext context, + {GraphicsFactoryHelper helper = const GraphicsFactoryHelper()}) + : textScaleFactor = helper.getTextScaleFactorOf(context); + + /// Returns a [TextStyle] object. + @override + common.TextStyle createTextPaint() => new TextStyle(); + + /// Returns a text element from [text] and [style]. + @override + common.TextElement createTextElement(String text) { + return new TextElement(text, textScaleFactor: textScaleFactor); + } + + @override + common.LineStyle createLinePaint() => new LineStyle(); +} + +/// Wraps the MediaQuery function to allow for testing. +class GraphicsFactoryHelper { + const GraphicsFactoryHelper(); + + double getTextScaleFactorOf(BuildContext context) => + MediaQuery.textScaleFactorOf(context); +} diff --git a/web/charts/flutter/lib/src/line_chart.dart b/web/charts/flutter/lib/src/line_chart.dart new file mode 100644 index 000000000..f0407cb3f --- /dev/null +++ b/web/charts/flutter/lib/src/line_chart.dart @@ -0,0 +1,90 @@ +// 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 'package:charts_common/common.dart' as common + show + AxisSpec, + LineChart, + NumericAxisSpec, + RTLSpec, + Series, + LineRendererConfig, + SeriesRendererConfig; +import 'behaviors/line_point_highlighter.dart' show LinePointHighlighter; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'base_chart.dart' show LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'cartesian_chart.dart' show CartesianChart; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +class LineChart extends CartesianChart { + LineChart( + List seriesList, { + bool animate, + Duration animationDuration, + common.AxisSpec domainAxis, + common.AxisSpec primaryMeasureAxis, + common.AxisSpec secondaryMeasureAxis, + LinkedHashMap disjointMeasureAxes, + common.LineRendererConfig defaultRenderer, + List> customSeriesRenderers, + List behaviors, + List> selectionModels, + common.RTLSpec rtlSpec, + LayoutConfig layoutConfig, + bool defaultInteractions: true, + bool flipVerticalAxis, + UserManagedState userManagedState, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + flipVerticalAxis: flipVerticalAxis, + userManagedState: userManagedState, + ); + + @override + common.LineChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.LineChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(), + disjointMeasureAxes: createDisjointMeasureAxes()); + } + + @override + void addDefaultInteractions(List behaviors) { + super.addDefaultInteractions(behaviors); + + behaviors.add(new LinePointHighlighter()); + } +} diff --git a/web/charts/flutter/lib/src/line_style.dart b/web/charts/flutter/lib/src/line_style.dart new file mode 100644 index 000000000..2353dafb8 --- /dev/null +++ b/web/charts/flutter/lib/src/line_style.dart @@ -0,0 +1,25 @@ +// 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/common.dart' as common show Color, LineStyle; + +class LineStyle implements common.LineStyle { + @override + common.Color color; + @override + List dashPattern; + @override + int strokeWidth; +} diff --git a/web/charts/flutter/lib/src/pie_chart.dart b/web/charts/flutter/lib/src/pie_chart.dart new file mode 100644 index 000000000..13ce35c7a --- /dev/null +++ b/web/charts/flutter/lib/src/pie_chart.dart @@ -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:charts_common/common.dart' as common + show ArcRendererConfig, PieChart, RTLSpec, Series; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'base_chart.dart' show BaseChart, LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'selection_model_config.dart' show SelectionModelConfig; + +class PieChart extends BaseChart { + PieChart( + List seriesList, { + bool animate, + Duration animationDuration, + common.ArcRendererConfig defaultRenderer, + List behaviors, + List> selectionModels, + common.RTLSpec rtlSpec, + LayoutConfig layoutConfig, + bool defaultInteractions: true, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + defaultRenderer: defaultRenderer, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + ); + + @override + common.PieChart createCommonChart(BaseChartState chartState) => + new common.PieChart(layoutConfig: layoutConfig?.commonLayoutConfig); + + @override + void addDefaultInteractions(List behaviors) { + super.addDefaultInteractions(behaviors); + } +} diff --git a/web/charts/flutter/lib/src/scatter_plot_chart.dart b/web/charts/flutter/lib/src/scatter_plot_chart.dart new file mode 100644 index 000000000..0347aa66c --- /dev/null +++ b/web/charts/flutter/lib/src/scatter_plot_chart.dart @@ -0,0 +1,82 @@ +// 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 'package:charts_common/common.dart' as common + show + AxisSpec, + NumericAxisSpec, + PointRendererConfig, + RTLSpec, + ScatterPlotChart, + SeriesRendererConfig, + Series; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'base_chart.dart' show LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'cartesian_chart.dart' show CartesianChart; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +class ScatterPlotChart extends CartesianChart { + ScatterPlotChart( + List seriesList, { + bool animate, + Duration animationDuration, + common.AxisSpec domainAxis, + common.AxisSpec primaryMeasureAxis, + common.AxisSpec secondaryMeasureAxis, + LinkedHashMap disjointMeasureAxes, + common.PointRendererConfig defaultRenderer, + List> customSeriesRenderers, + List behaviors, + List> selectionModels, + common.RTLSpec rtlSpec, + LayoutConfig layoutConfig, + bool defaultInteractions: true, + bool flipVerticalAxis, + UserManagedState userManagedState, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + flipVerticalAxis: flipVerticalAxis, + userManagedState: userManagedState, + ); + + @override + common.ScatterPlotChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.ScatterPlotChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(), + disjointMeasureAxes: createDisjointMeasureAxes()); + } +} diff --git a/web/charts/flutter/lib/src/selection_model_config.dart b/web/charts/flutter/lib/src/selection_model_config.dart new file mode 100644 index 000000000..5ecac3901 --- /dev/null +++ b/web/charts/flutter/lib/src/selection_model_config.dart @@ -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 'package:meta/meta.dart' show immutable; + +import 'package:charts_common/common.dart' as common; + +@immutable +class SelectionModelConfig { + final common.SelectionModelType type; + + /// Listens for change in selection. + final common.SelectionModelListener changedListener; + + /// Listens anytime update selection is called. + final common.SelectionModelListener updatedListener; + + SelectionModelConfig( + {this.type = common.SelectionModelType.info, + this.changedListener, + this.updatedListener}); +} diff --git a/web/charts/flutter/lib/src/symbol_renderer.dart b/web/charts/flutter/lib/src/symbol_renderer.dart new file mode 100644 index 000000000..036749299 --- /dev/null +++ b/web/charts/flutter/lib/src/symbol_renderer.dart @@ -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 'dart:math' show Rectangle; +import 'package:charts_common/common.dart' as common + show ChartCanvas, Color, SymbolRenderer; +import 'package:flutter_web/widgets.dart'; +import 'chart_canvas.dart' show ChartCanvas; +import 'graphics_factory.dart' show GraphicsFactory; + +/// Flutter widget responsible for painting a common SymbolRenderer from the +/// chart. +/// +/// If you want to customize the symbol, then use [CustomSymbolRenderer]. +class SymbolRendererCanvas implements SymbolRendererBuilder { + final common.SymbolRenderer commonSymbolRenderer; + + SymbolRendererCanvas(this.commonSymbolRenderer); + + @override + Widget build(BuildContext context, + {Color color, Size size, bool enabled = true}) { + if (!enabled) { + color = color.withOpacity(0.26); + } + + return new SizedBox.fromSize( + size: size, + child: new CustomPaint( + painter: + new _SymbolCustomPaint(context, commonSymbolRenderer, color))); + } +} + +/// Convenience class allowing you to pass your Widget builder through the +/// common chart so that it is created for you by the Legend. +/// +/// This allows a custom SymbolRenderer in Flutter without having to create +/// a completely custom legend. +abstract class CustomSymbolRenderer extends common.SymbolRenderer + implements SymbolRendererBuilder { + /// Must override this method to build the custom Widget with the given color + /// as + @override + Widget build(BuildContext context, {Color color, Size size, bool enabled}); + + @override + void paint(common.ChartCanvas canvas, Rectangle bounds, + {List dashPattern, + common.Color fillColor, + common.Color strokeColor, + double strokeWidthPx}) { + // Intentionally ignored (never called). + } + + @override + bool shouldRepaint(common.SymbolRenderer oldRenderer) { + return false; // Repainting is handled directly in Flutter. + } +} + +/// Common interface for [CustomSymbolRenderer] & [SymbolRendererCanvas] for +/// convenience for [LegendEntryLayout]. +abstract class SymbolRendererBuilder { + Widget build(BuildContext context, {Color color, Size size, bool enabled}); +} + +/// The Widget which fulfills the guts of [SymbolRendererCanvas] actually +/// painting the symbol to a canvas using [CustomPainter]. +class _SymbolCustomPaint extends CustomPainter { + final BuildContext context; + final common.SymbolRenderer symbolRenderer; + final Color color; + + _SymbolCustomPaint(this.context, this.symbolRenderer, this.color); + + @override + void paint(Canvas canvas, Size size) { + final bounds = + new Rectangle(0, 0, size.width.toInt(), size.height.toInt()); + final commonColor = new common.Color( + r: color.red, g: color.green, b: color.blue, a: color.alpha); + symbolRenderer.paint( + new ChartCanvas(canvas, GraphicsFactory(context)), bounds, + fillColor: commonColor, strokeColor: commonColor); + } + + @override + bool shouldRepaint(_SymbolCustomPaint oldDelegate) { + return symbolRenderer.shouldRepaint(oldDelegate.symbolRenderer); + } +} diff --git a/web/charts/flutter/lib/src/text_element.dart b/web/charts/flutter/lib/src/text_element.dart new file mode 100644 index 000000000..65b49171a --- /dev/null +++ b/web/charts/flutter/lib/src/text_element.dart @@ -0,0 +1,183 @@ +// 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:flutter_web_ui/ui.dart' show TextAlign, TextDirection; +import 'package:charts_common/common.dart' as common + show + MaxWidthStrategy, + TextElement, + TextDirection, + TextMeasurement, + TextStyle; +import 'package:flutter_web/rendering.dart' + show Color, TextBaseline, TextPainter, TextSpan, TextStyle; + +/// Flutter implementation for text measurement and painter. +class TextElement implements common.TextElement { + static const ellipsis = '\u{2026}'; + + @override + final String text; + + final double textScaleFactor; + + var _painterReady = false; + common.TextStyle _textStyle; + common.TextDirection _textDirection = common.TextDirection.ltr; + + int _maxWidth; + common.MaxWidthStrategy _maxWidthStrategy; + + TextPainter _textPainter; + + common.TextMeasurement _measurement; + + double _opacity; + + TextElement(this.text, {common.TextStyle style, this.textScaleFactor}) + : _textStyle = style; + + @override + common.TextStyle get textStyle => _textStyle; + + @override + set textStyle(common.TextStyle value) { + if (_textStyle == value) { + return; + } + _textStyle = value; + _painterReady = false; + } + + @override + set textDirection(common.TextDirection direction) { + if (_textDirection == direction) { + return; + } + _textDirection = direction; + _painterReady = false; + } + + @override + common.TextDirection get textDirection => _textDirection; + + @override + int get maxWidth => _maxWidth; + + @override + set maxWidth(int value) { + if (_maxWidth == value) { + return; + } + _maxWidth = value; + _painterReady = false; + } + + @override + common.MaxWidthStrategy get maxWidthStrategy => _maxWidthStrategy; + + @override + set maxWidthStrategy(common.MaxWidthStrategy maxWidthStrategy) { + if (_maxWidthStrategy == maxWidthStrategy) { + return; + } + _maxWidthStrategy = maxWidthStrategy; + _painterReady = false; + } + + @override + set opacity(double opacity) { + if (opacity != _opacity) { + _painterReady = false; + _opacity = opacity; + } + } + + @override + common.TextMeasurement get measurement { + if (!_painterReady) { + _refreshPainter(); + } + + return _measurement; + } + + /// The estimated distance between where we asked to draw the text (top, left) + /// and where it visually started (top + verticalFontShift, left). + /// + /// 10% of reported font height seems to be about right. + int get verticalFontShift { + if (!_painterReady) { + _refreshPainter(); + } + + return (_textPainter.height * 0.1).ceil(); + } + + TextPainter get textPainter { + if (!_painterReady) { + _refreshPainter(); + } + return _textPainter; + } + + /// Create text painter and measure based on current settings + void _refreshPainter() { + _opacity ??= 1.0; + var color = new Color.fromARGB( + (textStyle.color.a * _opacity).round(), + textStyle.color.r, + textStyle.color.g, + textStyle.color.b, + ); + + _textPainter = new TextPainter( + text: new TextSpan( + text: text, + style: new TextStyle( + color: color, + fontSize: textStyle.fontSize.toDouble(), + fontFamily: textStyle.fontFamily))) + ..textDirection = TextDirection.ltr + // TODO Flip once textAlign works + ..textAlign = TextAlign.left + // ..textAlign = _textDirection == common.TextDirection.rtl ? + // TextAlign.right : TextAlign.left + ..ellipsis = maxWidthStrategy == common.MaxWidthStrategy.ellipsize + ? ellipsis + : null; + + if (textScaleFactor != null) { + _textPainter.textScaleFactor = textScaleFactor; + } + + _textPainter.layout(maxWidth: maxWidth?.toDouble() ?? double.infinity); + + final baseline = + _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic); + + // Estimating the actual draw height to 70% of measures size. + // + // The font reports a size larger than the drawn size, which makes it + // difficult to shift the text around to get it to visually line up + // vertically with other components. + _measurement = new common.TextMeasurement( + horizontalSliceWidth: _textPainter.width, + verticalSliceWidth: _textPainter.height * 0.70, + baseline: baseline); + + _painterReady = true; + } +} diff --git a/web/charts/flutter/lib/src/text_style.dart b/web/charts/flutter/lib/src/text_style.dart new file mode 100644 index 000000000..8508a3bd9 --- /dev/null +++ b/web/charts/flutter/lib/src/text_style.dart @@ -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:flutter_web_ui/ui.dart' show hashValues; +import 'package:charts_common/common.dart' as common show Color, TextStyle; + +class TextStyle implements common.TextStyle { + int fontSize; + String fontFamily; + common.Color color; + + @override + bool operator ==(Object other) => + other is TextStyle && + fontSize == other.fontSize && + fontFamily == other.fontFamily && + color == other.color; + + @override + int get hashCode => hashValues(fontSize, fontFamily, color); +} diff --git a/web/charts/flutter/lib/src/time_series_chart.dart b/web/charts/flutter/lib/src/time_series_chart.dart new file mode 100644 index 000000000..9ea31db0e --- /dev/null +++ b/web/charts/flutter/lib/src/time_series_chart.dart @@ -0,0 +1,94 @@ +// 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 'package:charts_common/common.dart' as common + show + AxisSpec, + DateTimeFactory, + NumericAxisSpec, + Series, + SeriesRendererConfig, + TimeSeriesChart; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'behaviors/line_point_highlighter.dart' show LinePointHighlighter; +import 'cartesian_chart.dart' show CartesianChart; +import 'base_chart.dart' show LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +class TimeSeriesChart extends CartesianChart { + final common.DateTimeFactory dateTimeFactory; + + /// Create a [TimeSeriesChart]. + /// + /// [dateTimeFactory] allows specifying a factory that creates [DateTime] to + /// be used for the time axis. If none specified, local date time is used. + TimeSeriesChart( + List> seriesList, { + bool animate, + Duration animationDuration, + common.AxisSpec domainAxis, + common.AxisSpec primaryMeasureAxis, + common.AxisSpec secondaryMeasureAxis, + LinkedHashMap disjointMeasureAxes, + common.SeriesRendererConfig defaultRenderer, + List> customSeriesRenderers, + List behaviors, + List> selectionModels, + LayoutConfig layoutConfig, + this.dateTimeFactory, + bool defaultInteractions: true, + bool flipVerticalAxis, + UserManagedState userManagedState, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + flipVerticalAxis: flipVerticalAxis, + userManagedState: userManagedState, + ); + + @override + common.TimeSeriesChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.TimeSeriesChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(), + disjointMeasureAxes: createDisjointMeasureAxes()); + } + + @override + void addDefaultInteractions(List behaviors) { + super.addDefaultInteractions(behaviors); + + behaviors.add(new LinePointHighlighter()); + } +} diff --git a/web/charts/flutter/lib/src/user_managed_state.dart b/web/charts/flutter/lib/src/user_managed_state.dart new file mode 100644 index 000000000..0fc4caab3 --- /dev/null +++ b/web/charts/flutter/lib/src/user_managed_state.dart @@ -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 'package:charts_common/common.dart' as common + show ImmutableSeries, SelectionModel, SelectionModelType, SeriesDatumConfig; + +/// Contains override settings for the internal chart state. +/// +/// The chart will check non null settings and apply them if they differ from +/// the internal chart state and trigger the appropriate level of redrawing. +class UserManagedState { + /// The expected selection(s) on the chart. + /// + /// If this is set and the model for the selection model type differs from + /// what is in the internal chart state, the selection will be applied and + /// repainting will occur such that behaviors that draw differently on + /// selection change can update, such as the line point highlighter. + /// + /// If more than one type of selection model is used, only the one(s) + /// specified in this list will override what is kept in the internally. + /// + /// To clear the selection, add an empty selection model. + final selectionModels = + >{}; +} + +/// Container for the user managed selection model. +/// +/// This container is needed because the selection model generated by selection +/// events is a [SelectionModel], while any user defined selection has to be +/// specified by passing in [selectedSeriesConfig] and [selectedDataConfig]. +/// The configuration is converted to a selection model after the series data +/// has been processed. +class UserManagedSelectionModel { + final List selectedSeriesConfig; + final List selectedDataConfig; + common.SelectionModel _model; + + /// Creates a [UserManagedSelectionModel] that holds [SelectionModel]. + /// + /// [selectedSeriesConfig] and [selectedDataConfig] is set to null because the + /// [_model] is returned when [getModel] is called. + UserManagedSelectionModel({common.SelectionModel model}) + : _model = model ?? new common.SelectionModel(), + selectedSeriesConfig = null, + selectedDataConfig = null; + + /// Creates a [UserManagedSelectionModel] with configuration that is converted + /// to a [SelectionModel] when [getModel] provides a processed series list. + UserManagedSelectionModel.fromConfig( + {List selectedSeriesConfig, + List selectedDataConfig}) + : this.selectedSeriesConfig = selectedSeriesConfig ?? [], + this.selectedDataConfig = + selectedDataConfig ?? []; + + /// Gets the selection model. If the model is null, create one from + /// configuration and the processed [seriesList] passed in. + common.SelectionModel getModel( + List> seriesList) { + _model ??= new common.SelectionModel.fromConfig( + selectedDataConfig, selectedSeriesConfig, seriesList); + + return _model; + } +} diff --git a/web/charts/flutter/lib/src/util.dart b/web/charts/flutter/lib/src/util.dart new file mode 100644 index 000000000..1742ee32b --- /dev/null +++ b/web/charts/flutter/lib/src/util.dart @@ -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:flutter_web/rendering.dart' + show + RenderBox, + RenderSemanticsGestureHandler, + RenderPointerListener, + RenderCustomMultiChildLayoutBox; +import 'chart_container.dart' show ChartContainerRenderObject; + +/// Get the [ChartContainerRenderObject] from a [RenderBox]. +/// +/// [RenderBox] is expected to be a [RenderSemanticsGestureHandler] with child +/// of [RenderPointerListener] with child of [ChartContainerRenderObject]. +ChartContainerRenderObject getChartContainerRenderObject(RenderBox box) { + assert(box is RenderCustomMultiChildLayoutBox); + final semanticHandler = (box as RenderCustomMultiChildLayoutBox) + .getChildrenAsList() + .firstWhere((child) => child is RenderSemanticsGestureHandler); + + assert(semanticHandler is RenderSemanticsGestureHandler); + final renderPointerListener = + (semanticHandler as RenderSemanticsGestureHandler).child; + + assert(renderPointerListener is RenderPointerListener); + final chartContainerRenderObject = + (renderPointerListener as RenderPointerListener).child; + + assert(chartContainerRenderObject is ChartContainerRenderObject); + + return chartContainerRenderObject as ChartContainerRenderObject; +} diff --git a/web/charts/flutter/lib/src/util/color.dart b/web/charts/flutter/lib/src/util/color.dart new file mode 100644 index 000000000..af5440832 --- /dev/null +++ b/web/charts/flutter/lib/src/util/color.dart @@ -0,0 +1,28 @@ +// 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/common.dart' as common show Color; +import 'package:flutter_web_ui/ui.dart' as ui; + +class ColorUtil { + static ui.Color toDartColor(common.Color color) { + return ui.Color.fromARGB(color.a, color.r, color.g, color.b); + } + + static common.Color fromDartColor(ui.Color color) { + return common.Color( + r: color.red, g: color.green, b: color.blue, a: color.alpha); + } +} diff --git a/web/charts/flutter/lib/src/widget_layout_delegate.dart b/web/charts/flutter/lib/src/widget_layout_delegate.dart new file mode 100644 index 000000000..baecb07ce --- /dev/null +++ b/web/charts/flutter/lib/src/widget_layout_delegate.dart @@ -0,0 +1,219 @@ +// 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:flutter_web_ui/ui.dart' show Offset; +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; +import 'package:flutter_web/widgets.dart'; +import 'package:charts_common/common.dart' as common + show BehaviorPosition, InsideJustification, OutsideJustification; + +import 'behaviors/chart_behavior.dart' show BuildableBehavior; + +/// Layout delegate that layout chart widget with [BuildableBehavior] widgets. +class WidgetLayoutDelegate extends MultiChildLayoutDelegate { + /// ID of the common chart widget. + final String chartID; + + /// Directionality of the widget. + final isRTL; + + /// ID and [BuildableBehavior] of the widgets for calculating offset. + final Map idAndBehavior; + + WidgetLayoutDelegate(this.chartID, this.idAndBehavior, this.isRTL); + + @override + void performLayout(Size size) { + // TODO: Change this to a layout manager that supports more + // than one buildable behavior that changes chart size. Remove assert when + // this is possible. + assert(idAndBehavior.keys.isEmpty || idAndBehavior.keys.length == 1); + + // Size available for the chart widget. + var availableWidth = size.width; + var availableHeight = size.height; + var chartOffset = Offset.zero; + + // Measure the first buildable behavior. + final behaviorID = + idAndBehavior.keys.isNotEmpty ? idAndBehavior.keys.first : null; + var behaviorSize = Size.zero; + if (behaviorID != null) { + if (hasChild(behaviorID)) { + final leftPosition = + isRTL ? common.BehaviorPosition.end : common.BehaviorPosition.start; + final rightPosition = + isRTL ? common.BehaviorPosition.start : common.BehaviorPosition.end; + final behaviorPosition = idAndBehavior[behaviorID].position; + + behaviorSize = layoutChild(behaviorID, new BoxConstraints.loose(size)); + if (behaviorPosition == common.BehaviorPosition.top) { + chartOffset = new Offset(0.0, behaviorSize.height); + availableHeight -= behaviorSize.height; + } else if (behaviorPosition == common.BehaviorPosition.bottom) { + availableHeight -= behaviorSize.height; + } else if (behaviorPosition == leftPosition) { + chartOffset = new Offset(behaviorSize.width, 0.0); + availableWidth -= behaviorSize.width; + } else if (behaviorPosition == rightPosition) { + availableWidth -= behaviorSize.width; + } + } + } + + // Layout chart. + final chartSize = new Size(availableWidth, availableHeight); + if (hasChild(chartID)) { + layoutChild(chartID, new BoxConstraints.tight(chartSize)); + positionChild(chartID, chartOffset); + } + + // Position buildable behavior. + if (behaviorID != null) { + // TODO: Unable to relayout with new smaller width. + // In the delegate, all children are required to have layout called + // exactly once. + final behaviorOffset = _getBehaviorOffset(idAndBehavior[behaviorID], + behaviorSize: behaviorSize, chartSize: chartSize, isRTL: isRTL); + + positionChild(behaviorID, behaviorOffset); + } + } + + @override + bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) { + // TODO: Deep equality check because the instance will not be + // the same on each build, even if the buildable behavior has not changed. + return idAndBehavior != (oldDelegate as WidgetLayoutDelegate).idAndBehavior; + } + + // Calculate buildable behavior's offset. + Offset _getBehaviorOffset(BuildableBehavior behavior, + {Size behaviorSize, Size chartSize, bool isRTL}) { + Offset behaviorOffset; + + final behaviorPosition = behavior.position; + final outsideJustification = behavior.outsideJustification; + final insideJustification = behavior.insideJustification; + + if (behaviorPosition == common.BehaviorPosition.top || + behaviorPosition == common.BehaviorPosition.bottom) { + final heightOffset = behaviorPosition == common.BehaviorPosition.bottom + ? chartSize.height + : 0.0; + + final horizontalJustification = + getOutsideJustification(outsideJustification, isRTL); + + switch (horizontalJustification) { + case _HorizontalJustification.leftDrawArea: + behaviorOffset = + new Offset(behavior.drawAreaBounds.left.toDouble(), heightOffset); + break; + case _HorizontalJustification.left: + behaviorOffset = new Offset(0.0, heightOffset); + break; + case _HorizontalJustification.rightDrawArea: + behaviorOffset = new Offset( + behavior.drawAreaBounds.right - behaviorSize.width, heightOffset); + break; + case _HorizontalJustification.right: + behaviorOffset = + new Offset(chartSize.width - behaviorSize.width, heightOffset); + break; + } + } else if (behaviorPosition == common.BehaviorPosition.start || + behaviorPosition == common.BehaviorPosition.end) { + final widthOffset = + (isRTL && behaviorPosition == common.BehaviorPosition.start) || + (!isRTL && behaviorPosition == common.BehaviorPosition.end) + ? chartSize.width + : 0.0; + + switch (outsideJustification) { + case common.OutsideJustification.startDrawArea: + case common.OutsideJustification.middleDrawArea: + behaviorOffset = + new Offset(widthOffset, behavior.drawAreaBounds.top.toDouble()); + break; + case common.OutsideJustification.start: + case common.OutsideJustification.middle: + behaviorOffset = new Offset(widthOffset, 0.0); + break; + case common.OutsideJustification.endDrawArea: + behaviorOffset = new Offset(widthOffset, + behavior.drawAreaBounds.bottom - behaviorSize.height); + break; + case common.OutsideJustification.end: + behaviorOffset = + new Offset(widthOffset, chartSize.height - behaviorSize.height); + break; + } + } else if (behaviorPosition == common.BehaviorPosition.inside) { + var rightOffset = new Offset(chartSize.width - behaviorSize.width, 0.0); + + switch (insideJustification) { + case common.InsideJustification.topStart: + behaviorOffset = isRTL ? rightOffset : Offset.zero; + break; + case common.InsideJustification.topEnd: + behaviorOffset = isRTL ? Offset.zero : rightOffset; + break; + } + } + + return behaviorOffset; + } + + _HorizontalJustification getOutsideJustification( + common.OutsideJustification justification, bool isRTL) { + _HorizontalJustification mappedJustification; + + switch (justification) { + case common.OutsideJustification.startDrawArea: + case common.OutsideJustification.middleDrawArea: + mappedJustification = isRTL + ? _HorizontalJustification.rightDrawArea + : _HorizontalJustification.leftDrawArea; + break; + case common.OutsideJustification.start: + case common.OutsideJustification.middle: + mappedJustification = isRTL + ? _HorizontalJustification.right + : _HorizontalJustification.left; + break; + case common.OutsideJustification.endDrawArea: + mappedJustification = isRTL + ? _HorizontalJustification.leftDrawArea + : _HorizontalJustification.rightDrawArea; + break; + case common.OutsideJustification.end: + mappedJustification = isRTL + ? _HorizontalJustification.left + : _HorizontalJustification.right; + break; + } + + return mappedJustification; + } +} + +enum _HorizontalJustification { + leftDrawArea, + left, + rightDrawArea, + right, +} diff --git a/web/charts/flutter/pubspec.lock b/web/charts/flutter/pubspec.lock new file mode 100644 index 000000000..0193fea28 --- /dev/null +++ b/web/charts/flutter/pubspec.lock @@ -0,0 +1,417 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + charts_common: + dependency: "direct main" + description: + path: "../common" + relative: true + source: path + version: "0.6.0" + collection: + dependency: "direct main" + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_test: + dependency: "direct dev" + description: + path: "packages/flutter_web_test" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct overridden" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: "direct main" + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: "direct main" + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + multi_server_socket: + dependency: transitive + description: + name: multi_server_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.5" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.3" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + vm_service_client: + dependency: transitive + description: + name: vm_service_client + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6+2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.2.0 <3.0.0" diff --git a/web/charts/flutter/pubspec.yaml b/web/charts/flutter/pubspec.yaml new file mode 100644 index 000000000..0c10b957c --- /dev/null +++ b/web/charts/flutter/pubspec.yaml @@ -0,0 +1,45 @@ +name: charts_flutter +version: 0.6.0 +description: Material Design charting library for flutter. +author: Charts Team +homepage: https://github.com/google/charts + +environment: + sdk: '>=2.0.0 <3.0.0' + +dependencies: + # Pointing this to a local path allows for pointing to the latest code + # in Github for open source development. + # + # The pub version of charts_flutter will point to the pub version of charts_common. + # The latest pub version is commented out and shown below as an example. + # charts_common: 0.6.0 + charts_common: + path: ../common/ + collection: ^1.14.5 + flutter_web: any + intl: ^0.15.2 + logging: any + meta: ^1.1.1 + + +dev_dependencies: + mockito: + flutter_web_test: any + test: ^1.3.0 + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_test: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_test + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/charts/flutter/test/behaviors/legend/legend_layout_test.dart b/web/charts/flutter/test/behaviors/legend/legend_layout_test.dart new file mode 100644 index 000000000..8fbc80522 --- /dev/null +++ b/web/charts/flutter/test/behaviors/legend/legend_layout_test.dart @@ -0,0 +1,116 @@ +// 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:flutter_web/material.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'package:charts_flutter/src/behaviors/legend/legend_layout.dart'; + +class MockContext extends Mock implements BuildContext {} + +void main() { + BuildContext context; + + setUp(() { + context = new MockContext(); + }); + + group('TabularLegendLayoutBuilder', () { + test('builds horizontally', () { + final builder = new TabularLegendLayout.horizontalFirst(); + final widgets = [new Text('1'), new Text('2'), new Text('3')]; + + final Table layout = builder.build(context, widgets); + expect(layout.children.length, 1); + expect(layout.children.first.children.length, 3); + }); + + test('does not build extra columns if max columns exceed widget count', () { + final builder = + new TabularLegendLayout.horizontalFirst(desiredMaxColumns: 10); + final widgets = [new Text('1'), new Text('2'), new Text('3')]; + + final Table layout = builder.build(context, widgets); + expect(layout.children.length, 1); + expect(layout.children.first.children.length, 3); + }); + + test('builds horizontally until max column exceeded', () { + final builder = + new TabularLegendLayout.horizontalFirst(desiredMaxColumns: 2); + + final widgets = new List.generate( + 7, (int index) => new Text(index.toString())); + + final Table layout = builder.build(context, widgets); + expect(layout.children.length, 4); + + expect(layout.children[0].children[0], equals(widgets[0])); + expect(layout.children[0].children[1], equals(widgets[1])); + + expect(layout.children[1].children[0], equals(widgets[2])); + expect(layout.children[1].children[1], equals(widgets[3])); + + expect(layout.children[2].children[0], equals(widgets[4])); + expect(layout.children[2].children[1], equals(widgets[5])); + + expect(layout.children[3].children[0], equals(widgets[6])); + }); + + test('builds vertically', () { + final builder = new TabularLegendLayout.verticalFirst(); + final widgets = [new Text('1'), new Text('2'), new Text('3')]; + + final Table layout = builder.build(context, widgets); + expect(layout.children.length, 3); + expect(layout.children[0].children.length, 1); + expect(layout.children[1].children.length, 1); + expect(layout.children[2].children.length, 1); + }); + + test('does not build extra rows if max rows exceed widget count', () { + final builder = new TabularLegendLayout.verticalFirst(desiredMaxRows: 10); + final widgets = [new Text('1'), new Text('2'), new Text('3')]; + + final Table layout = builder.build(context, widgets); + expect(layout.children.length, 3); + expect(layout.children[0].children.length, 1); + expect(layout.children[1].children.length, 1); + expect(layout.children[2].children.length, 1); + }); + + test('builds vertically until max column exceeded', () { + final builder = new TabularLegendLayout.verticalFirst(desiredMaxRows: 2); + + final widgets = new List.generate( + 7, (int index) => new Text(index.toString())); + + final Table layout = builder.build(context, widgets); + expect(layout.children.length, 2); + + expect(layout.children[0].children[0], equals(widgets[0])); + expect(layout.children[1].children[0], equals(widgets[1])); + + expect(layout.children[0].children[1], equals(widgets[2])); + expect(layout.children[1].children[1], equals(widgets[3])); + + expect(layout.children[0].children[2], equals(widgets[4])); + expect(layout.children[1].children[2], equals(widgets[5])); + + expect(layout.children[0].children[3], equals(widgets[6])); + }); + }); +} diff --git a/web/charts/flutter/test/text_element_test.dart b/web/charts/flutter/test/text_element_test.dart new file mode 100644 index 000000000..4cdff2c75 --- /dev/null +++ b/web/charts/flutter/test/text_element_test.dart @@ -0,0 +1,39 @@ +// 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:flutter_web/material.dart' show BuildContext; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; +import 'package:charts_flutter/src/graphics_factory.dart'; +import 'package:charts_flutter/src/text_element.dart'; + +class MockContext extends Mock implements BuildContext {} + +class MockGraphicsFactoryHelper extends Mock implements GraphicsFactoryHelper {} + +void main() { + test('Text element gets assigned scale factor', () { + final helper = new MockGraphicsFactoryHelper(); + when(helper.getTextScaleFactorOf(any)).thenReturn(3.0); + final graphicsFactory = + new GraphicsFactory(new MockContext(), helper: helper); + + final textElement = + graphicsFactory.createTextElement('test') as TextElement; + + expect(textElement.text, equals('test')); + expect(textElement.textScaleFactor, equals(3.0)); + }); +} diff --git a/web/charts/flutter/test/user_managed_state_test.dart b/web/charts/flutter/test/user_managed_state_test.dart new file mode 100644 index 000000000..a2eb2c737 --- /dev/null +++ b/web/charts/flutter/test/user_managed_state_test.dart @@ -0,0 +1,128 @@ +// 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:flutter_web/widgets.dart'; +import 'package:flutter_web_test/flutter_web_test.dart'; + +import 'package:charts_flutter/flutter.dart' as charts; + +void main() { + testWidgets('selection can be set programmatically', + (WidgetTester tester) async { + final onTapSelection = + new charts.UserManagedSelectionModel.fromConfig( + selectedDataConfig: [ + new charts.SeriesDatumConfig('Sales', '2016') + ]); + + charts.SelectionModel currentSelectionModel; + + void selectionChangedListener(charts.SelectionModel model) { + currentSelectionModel = model; + } + + final testChart = new TestChart(selectionChangedListener, onTapSelection); + + await tester.pumpWidget(testChart); + + expect(currentSelectionModel, isNull); + + await tester.tap(find.byType(charts.BarChart)); + + await tester.pump(); + + expect(currentSelectionModel.selectedDatum, hasLength(1)); + final selectedDatum = + currentSelectionModel.selectedDatum.first.datum as OrdinalSales; + expect(selectedDatum.year, equals('2016')); + expect(selectedDatum.sales, equals(100)); + expect(currentSelectionModel.selectedSeries, hasLength(1)); + expect(currentSelectionModel.selectedSeries.first.id, equals('Sales')); + }); +} + +class TestChart extends StatefulWidget { + final charts.SelectionModelListener selectionChangedListener; + final charts.UserManagedSelectionModel onTapSelection; + + TestChart(this.selectionChangedListener, this.onTapSelection); + + @override + TestChartState createState() { + return new TestChartState(selectionChangedListener, onTapSelection); + } +} + +class TestChartState extends State { + final charts.SelectionModelListener selectionChangedListener; + final charts.UserManagedSelectionModel onTapSelection; + + final seriesList = _createSampleData(); + final myState = new charts.UserManagedState(); + + TestChartState(this.selectionChangedListener, this.onTapSelection); + + @override + Widget build(BuildContext context) { + final chart = new charts.BarChart( + seriesList, + userManagedState: myState, + selectionModels: [ + new charts.SelectionModelConfig( + type: charts.SelectionModelType.info, + changedListener: widget.selectionChangedListener) + ], + // Disable animation and gesture for testing. + animate: false, //widget.animate, + defaultInteractions: false, + ); + + return new GestureDetector(child: chart, onTap: handleOnTap); + } + + void handleOnTap() { + setState(() { + myState.selectionModels[charts.SelectionModelType.info] = onTapSelection; + }); + } +} + +/// Create one series with sample hard coded data. +List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/web/charts/flutter/test/widget_layout_delegate_test.dart b/web/charts/flutter/test/widget_layout_delegate_test.dart new file mode 100644 index 000000000..149da451b --- /dev/null +++ b/web/charts/flutter/test/widget_layout_delegate_test.dart @@ -0,0 +1,548 @@ +// 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:flutter_web/material.dart'; +import 'package:mockito/mockito.dart'; +import 'package:flutter_web_test/flutter_web_test.dart'; + +import 'package:charts_common/common.dart' as common + show BehaviorPosition, InsideJustification, OutsideJustification; +import 'package:charts_flutter/src/behaviors/chart_behavior.dart'; +import 'package:charts_flutter/src/widget_layout_delegate.dart'; + +const chartContainerLayoutID = 'chartContainer'; + +class MockBuildableBehavior extends Mock implements BuildableBehavior {} + +void main() { + group('widget layout test', () { + final chartKey = new UniqueKey(); + final behaviorKey = new UniqueKey(); + final behaviorID = 'behavior'; + final totalSize = const Size(200.0, 100.0); + final behaviorSize = const Size(50.0, 50.0); + + /// Creates widget for testing. + Widget createWidget( + Size chartSize, Size behaviorSize, common.BehaviorPosition position, + {common.OutsideJustification outsideJustification, + common.InsideJustification insideJustification, + Rectangle drawAreaBounds, + bool isRTL: false}) { + // Create a mock buildable behavior that returns information about the + // position and justification desired. + final behavior = new MockBuildableBehavior(); + when(behavior.position).thenReturn(position); + when(behavior.outsideJustification).thenReturn(outsideJustification); + when(behavior.insideJustification).thenReturn(insideJustification); + when(behavior.drawAreaBounds).thenReturn(drawAreaBounds); + + // The 'chart' widget that expands to the full size allowed to test that + // the behavior widget's size affects the size given to the chart. + final chart = new LayoutId( + key: chartKey, id: chartContainerLayoutID, child: new Container()); + + // A behavior widget + final behaviorWidget = new LayoutId( + key: behaviorKey, + id: behaviorID, + child: new SizedBox.fromSize(size: behaviorSize)); + + // Create a the widget that uses the layout delegate that is being tested. + final layout = new CustomMultiChildLayout( + delegate: new WidgetLayoutDelegate( + chartContainerLayoutID, {behaviorID: behavior}, isRTL), + children: [chart, behaviorWidget]); + + final container = new Align( + alignment: Alignment.topLeft, + child: new Container( + width: chartSize.width, height: chartSize.height, child: layout)); + + return container; + } + + // Verifies the expected results. + void verifyResults(WidgetTester tester, Size expectedChartSize, + Offset expectedChartOffset, Offset expectedBehaviorOffset) { + final RenderBox chartBox = tester.firstRenderObject(find.byKey(chartKey)); + expect(chartBox.size, equals(expectedChartSize)); + + final chartOffset = chartBox.localToGlobal(Offset.zero); + expect(chartOffset, equals(expectedChartOffset)); + + final RenderBox behaviorBox = + tester.firstRenderObject(find.byKey(behaviorKey)); + final behaviorOffset = behaviorBox.localToGlobal(Offset.zero); + expect(behaviorOffset, equals(expectedBehaviorOffset)); + } + + testWidgets('Position top - start draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.startDrawArea; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to draw area + final expectedBehaviorOffset = const Offset(25.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position bottom - end draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.bottom; + final outsideJustification = common.OutsideJustification.endDrawArea; + final drawAreaBounds = const Rectangle(25, 0, 125, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the bottom, so the chart is offset by 0. + final expectedChartOffset = const Offset(0.0, 0.0); + // Behavior is aligned to draw area and offset to the bottom. + final expectedBehaviorOffset = const Offset(100.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position start - start draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.startDrawArea; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the start (left) since this is NOT a RTL + // so the chart is offset to the right by the behavior width of 50. + final expectedChartOffset = const Offset(50.0, 0.0); + // Behavior is aligned to draw area. + final expectedBehaviorOffset = const Offset(0.0, 25.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position end - end draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.end; + final outsideJustification = common.OutsideJustification.endDrawArea; + final drawAreaBounds = const Rectangle(25, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the right (left) since this is NOT a RTL + // so no offset for the chart. + final expectedChartOffset = const Offset(0.0, 0.0); + // Behavior is aligned to draw area and offset to the right of the + // chart. + final expectedBehaviorOffset = const Offset(150.0, 25.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position top - start justified', (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.start; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to the start, so no offset + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position top - end justified', (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.end; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to the end, so it is offset by total size minus + // the behavior size. + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position start - start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.start; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the start (left) since this is NOT a RTL + // so the chart is offset to the right by the behavior width of 50. + final expectedChartOffset = const Offset(50.0, 0.0); + // No offset because it is start justified. + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position start - end justified', (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.end; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the start (left) since this is NOT a RTL + // so the chart is offset to the right by the behavior width of 50. + final expectedChartOffset = const Offset(50.0, 0.0); + // End justified, total height minus behavior height + final expectedBehaviorOffset = const Offset(0.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position inside - top start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.inside; + final insideJustification = common.InsideJustification.topStart; + final drawAreaBounds = const Rectangle(25, 25, 175, 75); + + // Behavior is layered on top, chart uses the full size. + final expectedChartSize = const Size(200.0, 100.0); + // No offset since chart takes up full size. + final expectedChartOffset = const Offset(0.0, 0.0); + // Top start justified, no offset + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + insideJustification: insideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position inside - top end justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.inside; + final insideJustification = common.InsideJustification.topEnd; + final drawAreaBounds = const Rectangle(25, 25, 175, 75); + + // Behavior is layered on top, chart uses the full size. + final expectedChartSize = const Size(200.0, 100.0); + // No offset since chart takes up full size. + final expectedChartOffset = const Offset(0.0, 0.0); + // Offset to the top end + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + insideJustification: insideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position top - start draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.startDrawArea; + final drawAreaBounds = const Rectangle(0, 50, 175, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to start draw area, which is to the left in RTL + final expectedBehaviorOffset = const Offset(125.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position bottom - end draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.bottom; + final outsideJustification = common.OutsideJustification.endDrawArea; + final drawAreaBounds = const Rectangle(0, 0, 175, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the bottom, so the chart is offset by 0. + final expectedChartOffset = const Offset(0.0, 0.0); + // Behavior is aligned to end draw area (left) and offset to the bottom. + final expectedBehaviorOffset = const Offset(0.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position start - start draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.startDrawArea; + final drawAreaBounds = const Rectangle(0, 25, 125, 75); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Chart is on the left, so no offset. + final expectedChartOffset = const Offset(0.0, 0.0); + // Behavior is positioned at the start (right) and start draw area. + final expectedBehaviorOffset = const Offset(150.0, 25.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position end - end draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.end; + final outsideJustification = common.OutsideJustification.endDrawArea; + final drawAreaBounds = const Rectangle(75, 25, 125, 75); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Chart is to the left of the behavior because of RTL. + final expectedChartOffset = const Offset(50.0, 0.0); + // Behavior is aligned to end draw area. + final expectedBehaviorOffset = const Offset(0.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position top - start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.start; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to the end, offset by behavior size. + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position top - end justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.end; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to the end, no offset. + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position start - start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.start; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the right since this is RTL so the chart is + // has no offset. + final expectedChartOffset = const Offset(0.0, 0.0); + // No offset because it is start justified. + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position start - end justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.end; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the right since this is RTL so the chart is + // has no offset. + final expectedChartOffset = const Offset(0.0, 0.0); + // End justified, total height minus behavior height + final expectedBehaviorOffset = const Offset(150.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position inside - top start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.inside; + final insideJustification = common.InsideJustification.topStart; + final drawAreaBounds = const Rectangle(25, 25, 175, 75); + + // Behavior is layered on top, chart uses the full size. + final expectedChartSize = const Size(200.0, 100.0); + // No offset since chart takes up full size. + final expectedChartOffset = const Offset(0.0, 0.0); + // Offset to the right + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + insideJustification: insideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position inside - top end justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.inside; + final insideJustification = common.InsideJustification.topEnd; + final drawAreaBounds = const Rectangle(25, 25, 175, 75); + + // Behavior is layered on top, chart uses the full size. + final expectedChartSize = const Size(200.0, 100.0); + // No offset since chart takes up full size. + final expectedChartOffset = const Offset(0.0, 0.0); + // No offset, since end is to the left. + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + insideJustification: insideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + }); +} diff --git a/web/dad_jokes/LICENSE b/web/dad_jokes/LICENSE new file mode 100644 index 000000000..4eaf9c689 --- /dev/null +++ b/web/dad_jokes/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Tim Sneath + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/dad_jokes/README.md b/web/dad_jokes/README.md new file mode 100644 index 000000000..946161c92 --- /dev/null +++ b/web/dad_jokes/README.md @@ -0,0 +1 @@ +A fun little app for groan-worthy dad jokes. diff --git a/web/dad_jokes/analysis_options.yaml b/web/dad_jokes/analysis_options.yaml new file mode 100644 index 000000000..d7de72eb6 --- /dev/null +++ b/web/dad_jokes/analysis_options.yaml @@ -0,0 +1,93 @@ +include: package:pedantic/analysis_options.yaml +analyzer: + strong-mode: + implicit-casts: false +linter: + rules: + - always_declare_return_types + - annotate_overrides + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_empty_else + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_types_as_parameter_names + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - comment_references + - constant_identifier_names + - control_flow_in_finally + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - file_names + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - join_return_with_assignment + - library_names + - library_prefixes + - list_remove_unrelated_type + - literal_only_boolean_expressions + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_final_locals + - prefer_generic_function_type_aliases + - prefer_initializing_formals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_null_aware_operators + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_function_type_syntax_for_parameters + - use_rethrow_when_possible + - valid_regexps + - void_checks diff --git a/web/dad_jokes/lib/auto_size_text.dart b/web/dad_jokes/lib/auto_size_text.dart new file mode 100644 index 000000000..53675216e --- /dev/null +++ b/web/dad_jokes/lib/auto_size_text.dart @@ -0,0 +1,404 @@ +// Package auto_size_text: +// https://pub.dartlang.org/packages/auto_size_text + +import 'package:flutter_web/widgets.dart'; + +bool checkTextFits(TextSpan text, Locale locale, double scale, int maxLines, + double maxWidth, double maxHeight) { + final tp = TextPainter( + text: text, + textAlign: TextAlign.left, + textDirection: TextDirection.ltr, + textScaleFactor: scale ?? 1, + maxLines: maxLines, + locale: locale, + )..layout(maxWidth: maxWidth); + + return !(tp.didExceedMaxLines || + tp.height > maxHeight || + tp.width > maxWidth); +} + +/// Flutter widget that automatically resizes text to fit perfectly within its bounds. +/// +/// All size constraints as well as maxLines are taken into account. If the text +/// overflows anyway, you should check if the parent widget actually constraints +/// the size of this widget. +class AutoSizeText extends StatefulWidget { + /// Creates a [AutoSizeText] widget. + /// + /// If the [style] argument is null, the text will use the style from the + /// closest enclosing [DefaultTextStyle]. + const AutoSizeText( + this.data, { + Key key, + this.style, + this.minFontSize = 12.0, + this.maxFontSize, + this.stepGranularity = 1.0, + this.presetFontSizes, + this.group, + this.textAlign, + this.textDirection, + this.locale, + this.softWrap, + this.overflow, + this.textScaleFactor, + this.maxLines, + this.semanticsLabel, + }) : assert(data != null), + assert(stepGranularity >= 0.1), + textSpan = null, + super(key: key); + + /// Creates a [AutoSizeText] widget with a [TextSpan]. + const AutoSizeText.rich( + this.textSpan, { + Key key, + this.style, + this.minFontSize = 12.0, + this.maxFontSize, + this.stepGranularity = 1.0, + this.presetFontSizes, + this.group, + this.textAlign, + this.textDirection, + this.locale, + this.softWrap, + this.overflow, + this.textScaleFactor, + this.maxLines, + this.semanticsLabel, + }) : assert(textSpan != null), + assert(stepGranularity >= 0.1), + data = null, + super(key: key); + + /// The text to display. + /// + /// This will be null if a [textSpan] is provided instead. + final String data; + + /// The text to display as a [TextSpan]. + /// + /// This will be null if [data] is provided instead. + final TextSpan textSpan; + + /// If non-null, the style to use for this text. + /// + /// If the style's "inherit" property is true, the style will be merged with + /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will + /// replace the closest enclosing [DefaultTextStyle]. + final TextStyle style; + + /// The minimum text size constraint to be used when auto-sizing text. + /// + /// Is being ignored if [presetFontSizes] is set. + final double minFontSize; + + /// The maximum text size constraint to be used when auto-sizing text. + /// + /// Is being ignored if [presetFontSizes] is set. + final double maxFontSize; + + /// The steps in which the font size is being adapted to constraints. + /// + /// The Text scales uniformly in a range between [minFontSize] and + /// [maxFontSize]. + /// Each increment occurs as per the step size set in stepGranularity. + /// + /// Most of the time you don't want a stepGranularity below 1.0. + /// + /// Is being ignored if [presetFontSizes] is set. + final double stepGranularity; + + /// Lets you specify all the possible font sizes. + /// + /// **Important:** The presetFontSizes are used the order they are given in. + /// If the first fontSize matches, all others are being ignored. + final List presetFontSizes; + + /// Synchronizes the size of multiple [AutoSizeText]s. + /// + /// If you want multiple [AutoSizeText]s to have the same text size, give all + /// of them the same [AutoSizeGroup] instance. All of them will have the + /// size of the smallest [AutoSizeText] + final AutoSizeGroup group; + + /// How the text should be aligned horizontally. + final TextAlign textAlign; + + /// The directionality of the text. + /// + /// This decides how [textAlign] values like [TextAlign.start] and + /// [TextAlign.end] are interpreted. + /// + /// This is also used to disambiguate how to render bidirectional text. For + /// example, if the [data] is an English phrase followed by a Hebrew phrase, + /// in a [TextDirection.ltr] context the English phrase will be on the left + /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] + /// context, the English phrase will be on the right and the Hebrew phrase on + /// its left. + /// + /// Defaults to the ambient [Directionality], if any. + final TextDirection textDirection; + + /// Used to select a font when the same Unicode character can + /// be rendered differently, depending on the locale. + /// + /// It's rarely necessary to set this property. By default its value + /// is inherited from the enclosing app with `Localizations.localeOf(context)`. + final Locale locale; + + /// Whether the text should break at soft line breaks. + /// + /// If false, the glyphs in the text will be positioned as if there was + /// unlimited horizontal space. + final bool softWrap; + + /// How visual overflow should be handled. + final TextOverflow overflow; + + /// The number of font pixels for each logical pixel. + /// + /// For example, if the text scale factor is 1.5, text will be 50% larger than + /// the specified font size. + /// + /// This property also affects [minFontSize], [maxFontSize] and [presetFontSizes]. + /// + /// The value given to the constructor as textScaleFactor. If null, will + /// use the [MediaQueryData.textScaleFactor] obtained from the ambient + /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. + final double textScaleFactor; + + /// An optional maximum number of lines for the text to span, wrapping if necessary. + /// If the text exceeds the given number of lines, it will be resized according + /// to the specified bounds and if necessary truncated according to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. + /// + /// If this is null, but there is an ambient [DefaultTextStyle] that specifies + /// an explicit number for its [DefaultTextStyle.maxLines], then the + /// [DefaultTextStyle] value will take precedence. You can use a [RichText] + /// widget directly to entirely override the [DefaultTextStyle]. + final int maxLines; + + /// An alternative semantics label for this text. + /// + /// If present, the semantics of this widget will contain this value instead + /// of the actual text. + final String semanticsLabel; + + @override + _AutoSizeTextState createState() => _AutoSizeTextState(); +} + +class _AutoSizeTextState extends State { + double _previousFontSize; + + Text _cachedText; + double _cachedFontSize; + + @override + void initState() { + super.initState(); + + if (widget.group != null) { + widget.group._register(this); + } + } + + @override + void didUpdateWidget(AutoSizeText oldWidget) { + _cachedText = null; + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, size) { + final defaultTextStyle = DefaultTextStyle.of(context); + + var style = widget.style; + if (widget.style == null || widget.style.inherit) { + style = defaultTextStyle.style.merge(widget.style); + } + + final fontSize = _calculateFontSize(size, style, defaultTextStyle); + + Widget text; + + if (widget.group != null) { + if (fontSize != _previousFontSize) { + widget.group._updateFontSize(this, fontSize); + } + text = _buildText(widget.group._fontSize, style); + } else { + text = _buildText(fontSize, style); + } + + _previousFontSize = fontSize; + + return text; + }); + } + + double _calculateFontSize( + BoxConstraints size, TextStyle style, DefaultTextStyle defaultStyle) { + final userScale = + widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context); + + final minFontSize = widget.minFontSize ?? 0; + assert( + minFontSize >= 0, 'MinFontSize has to be greater than or equal to 0.'); + + final maxFontSize = widget.maxFontSize ?? double.infinity; + assert(maxFontSize > 0, 'MaxFontSize has to be greater than 0.'); + + assert(minFontSize <= maxFontSize, + 'MinFontSize has to be smaller or equal than maxFontSize.'); + + final maxLines = widget.maxLines ?? defaultStyle.maxLines; + + var presetIndex = 0; + if (widget.presetFontSizes != null) { + assert(widget.presetFontSizes.isNotEmpty, 'PresetFontSizes is empty.'); + } + + double initialFontSize; + if (widget.presetFontSizes == null) { + final current = style.fontSize; + initialFontSize = current.clamp(minFontSize, maxFontSize).toDouble(); + } else { + initialFontSize = widget.presetFontSizes[presetIndex++]; + } + + var fontSize = initialFontSize * userScale; + + final span = TextSpan( + style: widget.textSpan?.style ?? style, + text: widget.textSpan?.text ?? widget.data, + children: widget.textSpan?.children, + recognizer: widget.textSpan?.recognizer, + ); + while (!checkTextFits(span, widget.locale, fontSize / style.fontSize, + maxLines, size.maxWidth, size.maxHeight)) { + if (widget.presetFontSizes == null) { + final newFontSize = fontSize - widget.stepGranularity; + if (newFontSize < (minFontSize * userScale)) break; + fontSize = newFontSize; + } else if (presetIndex < widget.presetFontSizes.length) { + fontSize = widget.presetFontSizes[presetIndex++] * userScale; + } else { + break; + } + } + + return fontSize; + } + + Widget _buildText(double fontSize, TextStyle style) { + if (_cachedText != null && _cachedFontSize == fontSize) { + return _cachedText; + } + + Text text; + if (widget.data != null) { + text = Text( + widget.data, + style: style.copyWith(fontSize: fontSize), + textAlign: widget.textAlign, + textDirection: widget.textDirection, + locale: widget.locale, + softWrap: widget.softWrap, + overflow: widget.overflow, + textScaleFactor: 1, + maxLines: widget.maxLines, + semanticsLabel: widget.semanticsLabel, + ); + } else { + text = Text.rich( + widget.textSpan, + style: style, + textAlign: widget.textAlign, + textDirection: widget.textDirection, + locale: widget.locale, + softWrap: widget.softWrap, + overflow: widget.overflow, + textScaleFactor: fontSize / style.fontSize, + maxLines: widget.maxLines, + semanticsLabel: widget.semanticsLabel, + ); + } + + _cachedText = text; + _cachedFontSize = fontSize; + return text; + } + + void _notifySync() { + setState(() {}); + } + + @override + void dispose() { + if (widget.group != null) { + widget.group._remove(this); + } + super.dispose(); + } +} + +class AutoSizeGroup { + final _listeners = <_AutoSizeTextState, double>{}; + var _widgetsNotified = false; + double _fontSize = double.infinity; + + void _register(_AutoSizeTextState text) { + _listeners[text] = double.infinity; + } + + void _updateFontSize(_AutoSizeTextState text, double maxFontSize) { + final oldFontSize = _fontSize; + if (maxFontSize <= _fontSize) { + _fontSize = maxFontSize; + _listeners[text] = maxFontSize; + } else if (_listeners[text] == _fontSize) { + _listeners[text] = maxFontSize; + _fontSize = double.infinity; + for (var size in _listeners.values) { + if (size < _fontSize) _fontSize = size; + } + } else { + _listeners[text] = maxFontSize; + } + + if (oldFontSize != _fontSize) { + _widgetsNotified = false; + // Timer.run(_notifyListeners); + _notifyListeners(); + } + } + + void _notifyListeners() { + if (_widgetsNotified) { + return; + } else { + _widgetsNotified = true; + } + + for (var text in _listeners.keys.toList()) { + if (text.mounted) { + text._notifySync(); + } else { + _listeners.remove(text); + } + } + } + + void _remove(_AutoSizeTextState text) { + _updateFontSize(text, double.infinity); + _listeners.remove(text); + } +} diff --git a/web/dad_jokes/lib/main.dart b/web/dad_jokes/lib/main.dart new file mode 100644 index 000000000..32a82a4f9 --- /dev/null +++ b/web/dad_jokes/lib/main.dart @@ -0,0 +1,17 @@ +import 'package:flutter_web/material.dart'; +import 'main_page.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Dad Jokes', + theme: ThemeData( + primarySwatch: Colors.deepOrange, + ), + home: MainPage(title: 'Dad Jokes'), + ); + } +} diff --git a/web/dad_jokes/lib/main_page.dart b/web/dad_jokes/lib/main_page.dart new file mode 100644 index 000000000..b440a6427 --- /dev/null +++ b/web/dad_jokes/lib/main_page.dart @@ -0,0 +1,136 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_web/material.dart'; +import 'package:http/http.dart' as http; + +import 'auto_size_text.dart'; + +const _dadJokeApi = 'https://icanhazdadjoke.com/'; +const _httpHeaders = { + 'Accept': 'application/json', +}; + +const jokeTextStyle = TextStyle( + fontFamily: 'Patrick Hand', + fontSize: 36, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal); + +class MainPage extends StatefulWidget { + MainPage({Key key, this.title}) : super(key: key); + + final String title; + + @override + MainPageState createState() => MainPageState(); +} + +class MainPageState extends State { + Future _response; + String _displayedJoke = ''; + + @override + void initState() { + super.initState(); + _refreshAction(); + } + + void _refreshAction() { + setState(() { + _response = http.read(_dadJokeApi, headers: _httpHeaders); + }); + } + + void _aboutAction() { + showDialog( + context: context, + builder: (BuildContext context) { + return const AlertDialog( + title: Text('About Dad Jokes'), + content: Text( + 'Dad jokes is brought to you by Tim Sneath (@timsneath), ' + 'proud dad of Naomi, Esther, and Silas. May your children ' + 'groan like mine will.\n\nDad jokes come from ' + 'https://icanhazdadjoke.com with thanks.')); + }); + } + + FutureBuilder _jokeBody() { + return FutureBuilder( + future: _response, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + return const ListTile( + leading: Icon(Icons.sync_problem), + title: Text('No connection'), + ); + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + default: + if (snapshot.hasError) { + return const Center( + child: ListTile( + leading: Icon(Icons.error), + title: Text('Network error'), + subtitle: Text( + 'Sorry - this isn\'t funny, we know, but something went ' + 'wrong when connecting to the Internet. Check your ' + 'network connection and try again.'), + ), + ); + } else { + final decoded = json.decode(snapshot.data); + if (decoded['status'] == 200) { + _displayedJoke = decoded['joke'] as String; + return Padding( + padding: const EdgeInsets.all(16), + child: Dismissible( + key: const Key('joke'), + direction: DismissDirection.horizontal, + onDismissed: (direction) { + _refreshAction(); + }, + child: AutoSizeText(_displayedJoke, style: jokeTextStyle), + )); + } else { + return ListTile( + leading: const Icon(Icons.sync_problem), + title: Text('Unexpected error: ${snapshot.data}'), + ); + } + } + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: Image.asset( + 'icon.png', + fit: BoxFit.scaleDown, + ), + title: Text(widget.title), + actions: [ + IconButton( + icon: const Icon(Icons.info), + tooltip: 'About Dad Jokes', + onPressed: _aboutAction, + ), + ], + ), + body: Center( + child: SafeArea(child: _jokeBody()), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: _refreshAction, + icon: const Icon(Icons.mood), + label: const Text('NEW JOKE'), + ), + ); + } +} diff --git a/web/dad_jokes/pubspec.lock b/web/dad_jokes/pubspec.lock new file mode 100644 index 000000000..d1288b168 --- /dev/null +++ b/web/dad_jokes/pubspec.lock @@ -0,0 +1,471 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct main" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/dad_jokes/pubspec.yaml b/web/dad_jokes/pubspec.yaml new file mode 100644 index 000000000..e75405e65 --- /dev/null +++ b/web/dad_jokes/pubspec.yaml @@ -0,0 +1,26 @@ +name: dad_jokes + +environment: + sdk: ">=2.2.0 <3.0.0" + +dependencies: + flutter_web: any + flutter_web_ui: any + +dev_dependencies: + pedantic: ^1.3.0 + + build_runner: any + build_web_compilers: any + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/dad_jokes/web/assets/FontManifest.json b/web/dad_jokes/web/assets/FontManifest.json new file mode 100644 index 000000000..536054ad8 --- /dev/null +++ b/web/dad_jokes/web/assets/FontManifest.json @@ -0,0 +1,18 @@ +[ + { + "family": "MaterialIcons", + "fonts": [ + { + "asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" + } + ] + }, + { + "family": "Patrick Hand", + "fonts": [ + { + "asset": "fonts/PatrickHand-Regular.ttf" + } + ] + } +] \ No newline at end of file diff --git a/web/dad_jokes/web/assets/fonts/PatrickHand-Regular.ttf b/web/dad_jokes/web/assets/fonts/PatrickHand-Regular.ttf new file mode 100755 index 000000000..fb45ccdbd Binary files /dev/null and b/web/dad_jokes/web/assets/fonts/PatrickHand-Regular.ttf differ diff --git a/web/dad_jokes/web/assets/icon.png b/web/dad_jokes/web/assets/icon.png new file mode 100644 index 000000000..3c463b0c8 Binary files /dev/null and b/web/dad_jokes/web/assets/icon.png differ diff --git a/web/dad_jokes/web/index.html b/web/dad_jokes/web/index.html new file mode 100644 index 000000000..b54ed98d8 --- /dev/null +++ b/web/dad_jokes/web/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/dad_jokes/web/main.dart b/web/dad_jokes/web/main.dart new file mode 100644 index 000000000..848590a19 --- /dev/null +++ b/web/dad_jokes/web/main.dart @@ -0,0 +1,10 @@ +// 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 'package:dad_jokes/main.dart' as app; +import 'package:flutter_web_ui/ui.dart' as ui; + +Future main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/dad_jokes/web/preview.png b/web/dad_jokes/web/preview.png new file mode 100644 index 000000000..78a030bfe Binary files /dev/null and b/web/dad_jokes/web/preview.png differ diff --git a/web/filipino_cuisine/LICENSE b/web/filipino_cuisine/LICENSE new file mode 100644 index 000000000..33177f0ea --- /dev/null +++ b/web/filipino_cuisine/LICENSE @@ -0,0 +1,201 @@ + 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 2019 John Mark Grancapal + + 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. diff --git a/web/filipino_cuisine/README.md b/web/filipino_cuisine/README.md new file mode 100644 index 000000000..9dfd1e0cd --- /dev/null +++ b/web/filipino_cuisine/README.md @@ -0,0 +1,28 @@ +A sample that shows a visual recipe catalog for Filipino food. + +Contributed as part of the Flutter Create 5K challenge by John Mark Grancapal. + +**Asset License**: All images used on this app are under the "CC0 - Creative Commons" license + + +## Developed By + +John Mark Grancapal +Homepage: https://github.com/markgrancapal + + +## License + + Copyright 2019 John Mark Grancapal + + 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. diff --git a/web/filipino_cuisine/lib/cook.dart b/web/filipino_cuisine/lib/cook.dart new file mode 100644 index 000000000..c2c53aec9 --- /dev/null +++ b/web/filipino_cuisine/lib/cook.dart @@ -0,0 +1,56 @@ +import 'package:flutter_web/material.dart'; + +class Cook extends StatefulWidget { + final List dr; + final img; + final nme; + + Cook(this.dr, this.img, this.nme); + CState createState() => CState(); +} + +class CState extends State { + List cb; + + initState() { + super.initState(); + cb = List(); + } + + Widget build(ct) { + List dr = widget.dr; + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.red, + title: Text("INSTRUCTIONS"), + centerTitle: true), + body: Column(children: [ + Container( + child: ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: Hero( + tag: widget.nme, + child: Image.asset(widget.img, + fit: BoxFit.cover, width: 100, height: 100))), + title: Text(widget.nme, + style: Theme.of(ct) + .textTheme + .display2 + .copyWith(fontFamily: 'ark', color: Colors.black))), + margin: EdgeInsets.only(top: 40, bottom: 30, left: 20)), + Expanded( + child: ListView.builder( + itemCount: dr.length, + padding: EdgeInsets.all(10), + itemBuilder: (ct, i) { + cb.add(false); + return ListTile( + title: Text(dr[i]), + trailing: Checkbox( + value: cb[i], + onChanged: (v) => setState(() => cb[i] = v))); + })), + ])); + } +} diff --git a/web/filipino_cuisine/lib/flutter_page_indicator.dart b/web/filipino_cuisine/lib/flutter_page_indicator.dart new file mode 100644 index 000000000..35fe702d2 --- /dev/null +++ b/web/filipino_cuisine/lib/flutter_page_indicator.dart @@ -0,0 +1,353 @@ +// Package flutter_page_indicator: +// https://pub.dartlang.org/packages/flutter_page_indicator + +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/widgets.dart'; + +class WarmPainter extends BasePainter { + WarmPainter(PageIndicator widget, double page, int index, Paint paint) + : super(widget, page, index, paint); + + void draw(Canvas canvas, double space, double size, double radius) { + double progress = page - index; + double distance = size + space; + double start = index * (size + space); + + if (progress > 0.5) { + double right = start + size + distance; + //progress=>0.5-1.0 + //left:0.0=>distance + + double left = index * distance + distance * (progress - 0.5) * 2; + canvas.drawRRect( + new RRect.fromLTRBR( + left, 0.0, right, size, new Radius.circular(radius)), + _paint); + } else { + double right = start + size + distance * progress * 2; + + canvas.drawRRect( + new RRect.fromLTRBR( + start, 0.0, right, size, new Radius.circular(radius)), + _paint); + } + } +} + +class DropPainter extends BasePainter { + DropPainter(PageIndicator widget, double page, int index, Paint paint) + : super(widget, page, index, paint); + + @override + void draw(Canvas canvas, double space, double size, double radius) { + double progress = page - index; + double dropHeight = widget.dropHeight; + double rate = (0.5 - progress).abs() * 2; + double scale = widget.scale; + + //lerp(begin, end, progress) + + canvas.drawCircle( + new Offset(radius + ((page) * (size + space)), + radius - dropHeight * (1 - rate)), + radius * (scale + rate * (1.0 - scale)), + _paint); + } +} + +class NonePainter extends BasePainter { + NonePainter(PageIndicator widget, double page, int index, Paint paint) + : super(widget, page, index, paint); + + @override + void draw(Canvas canvas, double space, double size, double radius) { + double progress = page - index; + double secondOffset = index == widget.count - 1 + ? radius + : radius + ((index + 1) * (size + space)); + + if (progress > 0.5) { + canvas.drawCircle(new Offset(secondOffset, radius), radius, _paint); + } else { + canvas.drawCircle(new Offset(radius + (index * (size + space)), radius), + radius, _paint); + } + } +} + +class SlidePainter extends BasePainter { + SlidePainter(PageIndicator widget, double page, int index, Paint paint) + : super(widget, page, index, paint); + + @override + void draw(Canvas canvas, double space, double size, double radius) { + canvas.drawCircle( + new Offset(radius + (page * (size + space)), radius), radius, _paint); + } +} + +class ScalePainter extends BasePainter { + ScalePainter(PageIndicator widget, double page, int index, Paint paint) + : super(widget, page, index, paint); + + // 连续的两个点,含有最后一个和第一个 + @override + bool _shouldSkip(int i) { + if (index == widget.count - 1) { + return i == 0 || i == index; + } + return (i == index || i == index + 1); + } + + @override + void paint(Canvas canvas, Size size) { + _paint.color = widget.color; + double space = widget.space; + double size = widget.size; + double radius = size / 2; + for (int i = 0, c = widget.count; i < c; ++i) { + if (_shouldSkip(i)) { + continue; + } + canvas.drawCircle(new Offset(i * (size + space) + radius, radius), + radius * widget.scale, _paint); + } + + _paint.color = widget.activeColor; + draw(canvas, space, size, radius); + } + + @override + void draw(Canvas canvas, double space, double size, double radius) { + double secondOffset = index == widget.count - 1 + ? radius + : radius + ((index + 1) * (size + space)); + + double progress = page - index; + _paint.color = Color.lerp(widget.activeColor, widget.color, progress); + //last + canvas.drawCircle(new Offset(radius + (index * (size + space)), radius), + lerp(radius, radius * widget.scale, progress), _paint); + //first + _paint.color = Color.lerp(widget.color, widget.activeColor, progress); + canvas.drawCircle(new Offset(secondOffset, radius), + lerp(radius * widget.scale, radius, progress), _paint); + } +} + +class ColorPainter extends BasePainter { + ColorPainter(PageIndicator widget, double page, int index, Paint paint) + : super(widget, page, index, paint); + + // 连续的两个点,含有最后一个和第一个 + @override + bool _shouldSkip(int i) { + if (index == widget.count - 1) { + return i == 0 || i == index; + } + return (i == index || i == index + 1); + } + + @override + void draw(Canvas canvas, double space, double size, double radius) { + double progress = page - index; + double secondOffset = index == widget.count - 1 + ? radius + : radius + ((index + 1) * (size + space)); + + _paint.color = Color.lerp(widget.activeColor, widget.color, progress); + //left + canvas.drawCircle( + new Offset(radius + (index * (size + space)), radius), radius, _paint); + //right + _paint.color = Color.lerp(widget.color, widget.activeColor, progress); + canvas.drawCircle(new Offset(secondOffset, radius), radius, _paint); + } +} + +abstract class BasePainter extends CustomPainter { + final PageIndicator widget; + final double page; + final int index; + final Paint _paint; + + double lerp(double begin, double end, double progress) { + return begin + (end - begin) * progress; + } + + BasePainter(this.widget, this.page, this.index, this._paint); + + void draw(Canvas canvas, double space, double size, double radius); + + bool _shouldSkip(int index) { + return false; + } + //double secondOffset = index == widget.count-1 ? radius : radius + ((index + 1) * (size + space)); + + @override + void paint(Canvas canvas, Size size) { + _paint.color = widget.color; + double space = widget.space; + double size = widget.size; + double radius = size / 2; + for (int i = 0, c = widget.count; i < c; ++i) { + if (_shouldSkip(i)) { + continue; + } + canvas.drawCircle( + new Offset(i * (size + space) + radius, radius), radius, _paint); + } + + double page = this.page; + if (page < index) { + page = 0.0; + } + _paint.color = widget.activeColor; + draw(canvas, space, size, radius); + } + + @override + bool shouldRepaint(BasePainter oldDelegate) { + return oldDelegate.page != page; + } +} + +class _PageIndicatorState extends State { + int index = 0; + Paint _paint = new Paint(); + + BasePainter _createPainer() { + switch (widget.layout) { + case PageIndicatorLayout.NONE: + return new NonePainter( + widget, widget.controller.page ?? 0.0, index, _paint); + case PageIndicatorLayout.SLIDE: + return new SlidePainter( + widget, widget.controller.page ?? 0.0, index, _paint); + case PageIndicatorLayout.WARM: + return new WarmPainter( + widget, widget.controller.page ?? 0.0, index, _paint); + case PageIndicatorLayout.COLOR: + return new ColorPainter( + widget, widget.controller.page ?? 0.0, index, _paint); + case PageIndicatorLayout.SCALE: + return new ScalePainter( + widget, widget.controller.page ?? 0.0, index, _paint); + case PageIndicatorLayout.DROP: + return new DropPainter( + widget, widget.controller.page ?? 0.0, index, _paint); + default: + throw new Exception("Not a valid layout"); + } + } + + @override + Widget build(BuildContext context) { + Widget child = new SizedBox( + width: widget.count * widget.size + (widget.count - 1) * widget.space, + height: widget.size, + child: new CustomPaint( + painter: _createPainer(), + ), + ); + + if (widget.layout == PageIndicatorLayout.SCALE || + widget.layout == PageIndicatorLayout.COLOR) { + child = new ClipRect( + child: child, + ); + } + + return new IgnorePointer( + child: child, + ); + } + + void _onController() { + double page = widget.controller.page ?? 0.0; + index = page.floor(); + + setState(() {}); + } + + @override + void initState() { + widget.controller.addListener(_onController); + super.initState(); + } + + @override + void didUpdateWidget(PageIndicator oldWidget) { + if (widget.controller != oldWidget.controller) { + oldWidget.controller.removeListener(_onController); + widget.controller.addListener(_onController); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + widget.controller.removeListener(_onController); + super.dispose(); + } +} + +enum PageIndicatorLayout { + NONE, + SLIDE, + WARM, + COLOR, + SCALE, + DROP, +} + +class PageIndicator extends StatefulWidget { + /// size of the dots + final double size; + + /// space between dots. + final double space; + + /// count of dots + final int count; + + /// active color + final Color activeColor; + + /// normal color + final Color color; + + /// layout of the dots,default is [PageIndicatorLayout.SLIDE] + final PageIndicatorLayout layout; + + // Only valid when layout==PageIndicatorLayout.scale + final double scale; + + // Only valid when layout==PageIndicatorLayout.drop + final double dropHeight; + + final PageController controller; + + final double activeSize; + + PageIndicator( + {Key key, + this.size: 20.0, + this.space: 5.0, + this.count, + this.activeSize: 20.0, + this.controller, + this.color: Colors.white30, + this.layout: PageIndicatorLayout.SLIDE, + this.activeColor: Colors.white, + this.scale: 0.6, + this.dropHeight: 20.0}) + : assert(count != null), + assert(controller != null), + super(key: key); + + @override + State createState() { + return new _PageIndicatorState(); + } +} diff --git a/web/filipino_cuisine/lib/flutter_swiper.dart b/web/filipino_cuisine/lib/flutter_swiper.dart new file mode 100644 index 000000000..f31458f8d --- /dev/null +++ b/web/filipino_cuisine/lib/flutter_swiper.dart @@ -0,0 +1,1870 @@ +// Package flutter_swiper: +// https://pub.dartlang.org/packages/flutter_swiper + +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/foundation.dart'; + +import 'dart:async'; + +import 'flutter_page_indicator.dart'; +import 'transformer_page_view.dart'; + +typedef void SwiperOnTap(int index); + +typedef Widget SwiperDataBuilder(BuildContext context, dynamic data, int index); + +/// default auto play delay +const int kDefaultAutoplayDelayMs = 3000; + +/// Default auto play transition duration (in millisecond) +const int kDefaultAutoplayTransactionDuration = 300; + +const int kMaxValue = 2000000000; +const int kMiddleValue = 1000000000; + +enum SwiperLayout { DEFAULT, STACK, TINDER, CUSTOM } + +class Swiper extends StatefulWidget { + /// If set true , the pagination will display 'outer' of the 'content' container. + final bool outer; + + /// Inner item height, this property is valid if layout=STACK or layout=TINDER or LAYOUT=CUSTOM, + final double itemHeight; + + /// Inner item width, this property is valid if layout=STACK or layout=TINDER or LAYOUT=CUSTOM, + final double itemWidth; + + // height of the inside container,this property is valid when outer=true,otherwise the inside container size is controlled by parent widget + final double containerHeight; + // width of the inside container,this property is valid when outer=true,otherwise the inside container size is controlled by parent widget + final double containerWidth; + + /// Build item on index + final IndexedWidgetBuilder itemBuilder; + + /// Support transform like Android PageView did + /// `itemBuilder` and `transformItemBuilder` must have one not null + final PageTransformer transformer; + + /// count of the display items + final int itemCount; + + final ValueChanged onIndexChanged; + + ///auto play config + final bool autoplay; + + ///Duration of the animation between transactions (in millisecond). + final int autoplayDelay; + + ///disable auto play when interaction + final bool autoplayDisableOnInteraction; + + ///auto play transition duration (in millisecond) + final int duration; + + ///horizontal/vertical + final Axis scrollDirection; + + ///transition curve + final Curve curve; + + /// Set to false to disable continuous loop mode. + final bool loop; + + ///Index number of initial slide. + ///If not set , the `Swiper` is 'uncontrolled', which means manage index by itself + ///If set , the `Swiper` is 'controlled', which means the index is fully managed by parent widget. + final int index; + + ///Called when tap + final SwiperOnTap onTap; + + ///The swiper pagination plugin + final SwiperPlugin pagination; + + ///the swiper control button plugin + final SwiperPlugin control; + + ///other plugins, you can custom your own plugin + final List plugins; + + /// + final SwiperController controller; + + final ScrollPhysics physics; + + /// + final double viewportFraction; + + /// Build in layouts + final SwiperLayout layout; + + /// this value is valid when layout == SwiperLayout.CUSTOM + final CustomLayoutOption customLayoutOption; + + // This value is valid when viewportFraction is set and < 1.0 + final double scale; + + // This value is valid when viewportFraction is set and < 1.0 + final double fade; + + final PageIndicatorLayout indicatorLayout; + + Swiper({ + this.itemBuilder, + this.indicatorLayout: PageIndicatorLayout.NONE, + + /// + this.transformer, + @required this.itemCount, + this.autoplay: false, + this.layout: SwiperLayout.DEFAULT, + this.autoplayDelay: kDefaultAutoplayDelayMs, + this.autoplayDisableOnInteraction: true, + this.duration: kDefaultAutoplayTransactionDuration, + this.onIndexChanged, + this.index, + this.onTap, + this.control, + this.loop: true, + this.curve: Curves.ease, + this.scrollDirection: Axis.horizontal, + this.pagination, + this.plugins, + this.physics, + Key key, + this.controller, + this.customLayoutOption, + + /// since v1.0.0 + this.containerHeight, + this.containerWidth, + this.viewportFraction: 1.0, + this.itemHeight, + this.itemWidth, + this.outer: false, + this.scale, + this.fade, + }) : assert(itemBuilder != null || transformer != null, + "itemBuilder and transformItemBuilder must not be both null"), + assert( + !loop || + ((loop && + layout == SwiperLayout.DEFAULT && + (indicatorLayout == PageIndicatorLayout.SCALE || + indicatorLayout == PageIndicatorLayout.COLOR || + indicatorLayout == PageIndicatorLayout.NONE)) || + (loop && layout != SwiperLayout.DEFAULT)), + "Only support `PageIndicatorLayout.SCALE` and `PageIndicatorLayout.COLOR`when layout==SwiperLayout.DEFAULT in loop mode"), + super(key: key); + + factory Swiper.children({ + List children, + bool autoplay: false, + PageTransformer transformer, + int autoplayDelay: kDefaultAutoplayDelayMs, + bool reverse: false, + bool autoplayDisableOnInteraction: true, + int duration: kDefaultAutoplayTransactionDuration, + ValueChanged onIndexChanged, + int index, + SwiperOnTap onTap, + bool loop: true, + Curve curve: Curves.ease, + Axis scrollDirection: Axis.horizontal, + SwiperPlugin pagination, + SwiperPlugin control, + List plugins, + SwiperController controller, + Key key, + CustomLayoutOption customLayoutOption, + ScrollPhysics physics, + double containerHeight, + double containerWidth, + double viewportFraction: 1.0, + double itemHeight, + double itemWidth, + bool outer: false, + double scale: 1.0, + }) { + assert(children != null, "children must not be null"); + + return new Swiper( + transformer: transformer, + customLayoutOption: customLayoutOption, + containerHeight: containerHeight, + containerWidth: containerWidth, + viewportFraction: viewportFraction, + itemHeight: itemHeight, + itemWidth: itemWidth, + outer: outer, + scale: scale, + autoplay: autoplay, + autoplayDelay: autoplayDelay, + autoplayDisableOnInteraction: autoplayDisableOnInteraction, + duration: duration, + onIndexChanged: onIndexChanged, + index: index, + onTap: onTap, + curve: curve, + scrollDirection: scrollDirection, + pagination: pagination, + control: control, + controller: controller, + loop: loop, + plugins: plugins, + physics: physics, + key: key, + itemBuilder: (BuildContext context, int index) { + return children[index]; + }, + itemCount: children.length); + } + + factory Swiper.list({ + PageTransformer transformer, + List list, + CustomLayoutOption customLayoutOption, + SwiperDataBuilder builder, + bool autoplay: false, + int autoplayDelay: kDefaultAutoplayDelayMs, + bool reverse: false, + bool autoplayDisableOnInteraction: true, + int duration: kDefaultAutoplayTransactionDuration, + ValueChanged onIndexChanged, + int index, + SwiperOnTap onTap, + bool loop: true, + Curve curve: Curves.ease, + Axis scrollDirection: Axis.horizontal, + SwiperPlugin pagination, + SwiperPlugin control, + List plugins, + SwiperController controller, + Key key, + ScrollPhysics physics, + double containerHeight, + double containerWidth, + double viewportFraction: 1.0, + double itemHeight, + double itemWidth, + bool outer: false, + double scale: 1.0, + }) { + return new Swiper( + transformer: transformer, + customLayoutOption: customLayoutOption, + containerHeight: containerHeight, + containerWidth: containerWidth, + viewportFraction: viewportFraction, + itemHeight: itemHeight, + itemWidth: itemWidth, + outer: outer, + scale: scale, + autoplay: autoplay, + autoplayDelay: autoplayDelay, + autoplayDisableOnInteraction: autoplayDisableOnInteraction, + duration: duration, + onIndexChanged: onIndexChanged, + index: index, + onTap: onTap, + curve: curve, + key: key, + scrollDirection: scrollDirection, + pagination: pagination, + control: control, + controller: controller, + loop: loop, + plugins: plugins, + physics: physics, + itemBuilder: (BuildContext context, int index) { + return builder(context, list[index], index); + }, + itemCount: list.length); + } + + @override + State createState() { + return new _SwiperState(); + } +} + +abstract class _SwiperTimerMixin extends State { + Timer _timer; + + SwiperController _controller; + + @override + void initState() { + _controller = widget.controller; + if (_controller == null) { + _controller = new SwiperController(); + } + _controller.addListener(_onController); + _handleAutoplay(); + super.initState(); + } + + void _onController() { + switch (_controller.event) { + case SwiperController.START_AUTOPLAY: + { + if (_timer == null) { + _startAutoplay(); + } + } + break; + case SwiperController.STOP_AUTOPLAY: + { + if (_timer != null) { + _stopAutoplay(); + } + } + break; + } + } + + @override + void didUpdateWidget(Swiper oldWidget) { + if (_controller != oldWidget.controller) { + if (oldWidget.controller != null) { + oldWidget.controller.removeListener(_onController); + _controller = oldWidget.controller; + _controller.addListener(_onController); + } + } + _handleAutoplay(); + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + if (_controller != null) { + _controller.removeListener(_onController); + // _controller.dispose(); + } + + _stopAutoplay(); + super.dispose(); + } + + bool _autoplayEnabled() { + return _controller.autoplay ?? widget.autoplay; + } + + void _handleAutoplay() { + if (_autoplayEnabled() && _timer != null) return; + _stopAutoplay(); + if (_autoplayEnabled()) { + _startAutoplay(); + } + } + + void _startAutoplay() { + assert(_timer == null, "Timer must be stopped before start!"); + _timer = + Timer.periodic(Duration(milliseconds: widget.autoplayDelay), _onTimer); + } + + void _onTimer(Timer timer) { + _controller.next(animation: true); + } + + void _stopAutoplay() { + if (_timer != null) { + _timer.cancel(); + _timer = null; + } + } +} + +class _SwiperState extends _SwiperTimerMixin { + int _activeIndex; + + TransformerPageController _pageController; + + Widget _wrapTap(BuildContext context, int index) { + return new GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + this.widget.onTap(index); + }, + child: widget.itemBuilder(context, index), + ); + } + + @override + void initState() { + _activeIndex = widget.index ?? 0; + if (_isPageViewLayout()) { + _pageController = new TransformerPageController( + initialPage: widget.index, + loop: widget.loop, + itemCount: widget.itemCount, + reverse: + widget.transformer == null ? false : widget.transformer.reverse, + viewportFraction: widget.viewportFraction); + } + super.initState(); + } + + bool _isPageViewLayout() { + return widget.layout == null || widget.layout == SwiperLayout.DEFAULT; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + bool _getReverse(Swiper widget) => + widget.transformer == null ? false : widget.transformer.reverse; + + @override + void didUpdateWidget(Swiper oldWidget) { + super.didUpdateWidget(oldWidget); + if (_isPageViewLayout()) { + if (_pageController == null || + (widget.index != oldWidget.index || + widget.loop != oldWidget.loop || + widget.itemCount != oldWidget.itemCount || + widget.viewportFraction != oldWidget.viewportFraction || + _getReverse(widget) != _getReverse(oldWidget))) { + _pageController = new TransformerPageController( + initialPage: widget.index, + loop: widget.loop, + itemCount: widget.itemCount, + reverse: _getReverse(widget), + viewportFraction: widget.viewportFraction); + } + } else { + scheduleMicrotask(() { + // So that we have a chance to do `removeListener` in child widgets. + if (_pageController != null) { + _pageController.dispose(); + _pageController = null; + } + }); + } + if (widget.index != null && widget.index != _activeIndex) { + _activeIndex = widget.index; + } + } + + void _onIndexChanged(int index) { + setState(() { + _activeIndex = index; + }); + if (widget.onIndexChanged != null) { + widget.onIndexChanged(index); + } + } + + Widget _buildSwiper() { + IndexedWidgetBuilder itemBuilder; + if (widget.onTap != null) { + itemBuilder = _wrapTap; + } else { + itemBuilder = widget.itemBuilder; + } + + if (widget.layout == SwiperLayout.STACK) { + return new _StackSwiper( + loop: widget.loop, + itemWidth: widget.itemWidth, + itemHeight: widget.itemHeight, + itemCount: widget.itemCount, + itemBuilder: itemBuilder, + index: _activeIndex, + curve: widget.curve, + duration: widget.duration, + onIndexChanged: _onIndexChanged, + controller: _controller, + scrollDirection: widget.scrollDirection, + ); + } else if (_isPageViewLayout()) { + PageTransformer transformer = widget.transformer; + if (widget.scale != null || widget.fade != null) { + transformer = + new ScaleAndFadeTransformer(scale: widget.scale, fade: widget.fade); + } + + Widget child = new TransformerPageView( + pageController: _pageController, + loop: widget.loop, + itemCount: widget.itemCount, + itemBuilder: itemBuilder, + transformer: transformer, + viewportFraction: widget.viewportFraction, + index: _activeIndex, + duration: new Duration(milliseconds: widget.duration), + scrollDirection: widget.scrollDirection, + onPageChanged: _onIndexChanged, + curve: widget.curve, + physics: widget.physics, + controller: _controller, + ); + if (widget.autoplayDisableOnInteraction && widget.autoplay) { + return new NotificationListener( + child: child, + onNotification: (ScrollNotification notification) { + if (notification is ScrollStartNotification) { + if (notification.dragDetails != null) { + //by human + if (_timer != null) _stopAutoplay(); + } + } else if (notification is ScrollEndNotification) { + if (_timer == null) _startAutoplay(); + } + + return false; + }, + ); + } + + return child; + } else if (widget.layout == SwiperLayout.TINDER) { + return new _TinderSwiper( + loop: widget.loop, + itemWidth: widget.itemWidth, + itemHeight: widget.itemHeight, + itemCount: widget.itemCount, + itemBuilder: itemBuilder, + index: _activeIndex, + curve: widget.curve, + duration: widget.duration, + onIndexChanged: _onIndexChanged, + controller: _controller, + scrollDirection: widget.scrollDirection, + ); + } else if (widget.layout == SwiperLayout.CUSTOM) { + return new _CustomLayoutSwiper( + loop: widget.loop, + option: widget.customLayoutOption, + itemWidth: widget.itemWidth, + itemHeight: widget.itemHeight, + itemCount: widget.itemCount, + itemBuilder: itemBuilder, + index: _activeIndex, + curve: widget.curve, + duration: widget.duration, + onIndexChanged: _onIndexChanged, + controller: _controller, + scrollDirection: widget.scrollDirection, + ); + } else { + return new Container(); + } + } + + SwiperPluginConfig _ensureConfig(SwiperPluginConfig config) { + if (config == null) { + config = new SwiperPluginConfig( + outer: widget.outer, + itemCount: widget.itemCount, + layout: widget.layout, + indicatorLayout: widget.indicatorLayout, + pageController: _pageController, + activeIndex: _activeIndex, + scrollDirection: widget.scrollDirection, + controller: _controller, + loop: widget.loop); + } + return config; + } + + List _ensureListForStack( + Widget swiper, List listForStack, Widget widget) { + if (listForStack == null) { + listForStack = [swiper, widget]; + } else { + listForStack.add(widget); + } + return listForStack; + } + + @override + Widget build(BuildContext context) { + Widget swiper = _buildSwiper(); + List listForStack; + SwiperPluginConfig config; + if (widget.control != null) { + //Stack + config = _ensureConfig(config); + listForStack = _ensureListForStack( + swiper, listForStack, widget.control.build(context, config)); + } + + if (widget.plugins != null) { + config = _ensureConfig(config); + for (SwiperPlugin plugin in widget.plugins) { + listForStack = _ensureListForStack( + swiper, listForStack, plugin.build(context, config)); + } + } + if (widget.pagination != null) { + config = _ensureConfig(config); + if (widget.outer) { + return _buildOuterPagination( + widget.pagination, + listForStack == null ? swiper : new Stack(children: listForStack), + config); + } else { + listForStack = _ensureListForStack( + swiper, listForStack, widget.pagination.build(context, config)); + } + } + + if (listForStack != null) { + return new Stack( + children: listForStack, + ); + } + + return swiper; + } + + Widget _buildOuterPagination( + SwiperPagination pagination, Widget swiper, SwiperPluginConfig config) { + List list = []; + //Only support bottom yet! + if (widget.containerHeight != null || widget.containerWidth != null) { + list.add(swiper); + } else { + list.add(new Expanded(child: swiper)); + } + + list.add(new Align( + alignment: Alignment.center, + child: pagination.build(context, config), + )); + + return new Column( + children: list, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + ); + } +} + +abstract class _SubSwiper extends StatefulWidget { + final IndexedWidgetBuilder itemBuilder; + final int itemCount; + final int index; + final ValueChanged onIndexChanged; + final SwiperController controller; + final int duration; + final Curve curve; + final double itemWidth; + final double itemHeight; + final bool loop; + final Axis scrollDirection; + + _SubSwiper( + {Key key, + this.loop, + this.itemHeight, + this.itemWidth, + this.duration, + this.curve, + this.itemBuilder, + this.controller, + this.index, + this.itemCount, + this.scrollDirection: Axis.horizontal, + this.onIndexChanged}) + : super(key: key); + + @override + State createState(); + + int getCorrectIndex(int indexNeedsFix) { + if (itemCount == 0) return 0; + int value = indexNeedsFix % itemCount; + if (value < 0) { + value += itemCount; + } + return value; + } +} + +class _TinderSwiper extends _SubSwiper { + _TinderSwiper({ + Key key, + Curve curve, + int duration, + SwiperController controller, + ValueChanged onIndexChanged, + double itemHeight, + double itemWidth, + IndexedWidgetBuilder itemBuilder, + int index, + bool loop, + int itemCount, + Axis scrollDirection, + }) : assert(itemWidth != null && itemHeight != null), + super( + loop: loop, + key: key, + itemWidth: itemWidth, + itemHeight: itemHeight, + itemBuilder: itemBuilder, + curve: curve, + duration: duration, + controller: controller, + index: index, + onIndexChanged: onIndexChanged, + itemCount: itemCount, + scrollDirection: scrollDirection); + + @override + State createState() { + return new _TinderState(); + } +} + +class _StackSwiper extends _SubSwiper { + _StackSwiper({ + Key key, + Curve curve, + int duration, + SwiperController controller, + ValueChanged onIndexChanged, + double itemHeight, + double itemWidth, + IndexedWidgetBuilder itemBuilder, + int index, + bool loop, + int itemCount, + Axis scrollDirection, + }) : super( + loop: loop, + key: key, + itemWidth: itemWidth, + itemHeight: itemHeight, + itemBuilder: itemBuilder, + curve: curve, + duration: duration, + controller: controller, + index: index, + onIndexChanged: onIndexChanged, + itemCount: itemCount, + scrollDirection: scrollDirection); + + @override + State createState() { + return new _StackViewState(); + } +} + +class _TinderState extends _CustomLayoutStateBase<_TinderSwiper> { + List scales; + List offsetsX; + List offsetsY; + List opacity; + List rotates; + + double getOffsetY(double scale) { + return widget.itemHeight - widget.itemHeight * scale; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + void didUpdateWidget(_TinderSwiper oldWidget) { + _updateValues(); + super.didUpdateWidget(oldWidget); + } + + @override + void afterRender() { + super.afterRender(); + + _startIndex = -3; + _animationCount = 5; + opacity = [0.0, 0.9, 0.9, 1.0, 0.0, 0.0]; + scales = [0.80, 0.80, 0.85, 0.90, 1.0, 1.0, 1.0]; + rotates = [0.0, 0.0, 0.0, 0.0, 20.0, 25.0]; + _updateValues(); + } + + void _updateValues() { + if (widget.scrollDirection == Axis.horizontal) { + offsetsX = [0.0, 0.0, 0.0, 0.0, _swiperWidth, _swiperWidth]; + offsetsY = [ + 0.0, + 0.0, + -5.0, + -10.0, + -15.0, + -20.0, + ]; + } else { + offsetsX = [ + 0.0, + 0.0, + 5.0, + 10.0, + 15.0, + 20.0, + ]; + + offsetsY = [0.0, 0.0, 0.0, 0.0, _swiperHeight, _swiperHeight]; + } + } + + @override + Widget _buildItem(int i, int realIndex, double animationValue) { + double s = _getValue(scales, animationValue, i); + double f = _getValue(offsetsX, animationValue, i); + double fy = _getValue(offsetsY, animationValue, i); + double o = _getValue(opacity, animationValue, i); + double a = _getValue(rotates, animationValue, i); + + Alignment alignment = widget.scrollDirection == Axis.horizontal + ? Alignment.bottomCenter + : Alignment.centerLeft; + + return new Opacity( + opacity: o, + child: new Transform.rotate( + angle: a / 180.0, + child: new Transform.translate( + key: new ValueKey(_currentIndex + i), + offset: new Offset(f, fy), + child: new Transform.scale( + scale: s, + alignment: alignment, + child: new SizedBox( + width: widget.itemWidth ?? double.infinity, + height: widget.itemHeight ?? double.infinity, + child: widget.itemBuilder(context, realIndex), + ), + ), + ), + ), + ); + } +} + +class _StackViewState extends _CustomLayoutStateBase<_StackSwiper> { + List scales; + List offsets; + List opacity; + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + void _updateValues() { + if (widget.scrollDirection == Axis.horizontal) { + double space = (_swiperWidth - widget.itemWidth) / 2; + offsets = [-space, -space / 3 * 2, -space / 3, 0.0, _swiperWidth]; + } else { + double space = (_swiperHeight - widget.itemHeight) / 2; + offsets = [-space, -space / 3 * 2, -space / 3, 0.0, _swiperHeight]; + } + } + + @override + void didUpdateWidget(_StackSwiper oldWidget) { + _updateValues(); + super.didUpdateWidget(oldWidget); + } + + @override + void afterRender() { + super.afterRender(); + + //length of the values array below + _animationCount = 5; + + //Array below this line, '0' index is 1.0 ,witch is the first item show in swiper. + _startIndex = -3; + scales = [0.7, 0.8, 0.9, 1.0, 1.0]; + opacity = [0.0, 0.5, 1.0, 1.0, 1.0]; + + _updateValues(); + } + + @override + Widget _buildItem(int i, int realIndex, double animationValue) { + double s = _getValue(scales, animationValue, i); + double f = _getValue(offsets, animationValue, i); + double o = _getValue(opacity, animationValue, i); + + Offset offset = widget.scrollDirection == Axis.horizontal + ? new Offset(f, 0.0) + : new Offset(0.0, f); + + Alignment alignment = widget.scrollDirection == Axis.horizontal + ? Alignment.centerLeft + : Alignment.topCenter; + + return new Opacity( + opacity: o, + child: new Transform.translate( + key: new ValueKey(_currentIndex + i), + offset: offset, + child: new Transform.scale( + scale: s, + alignment: alignment, + child: new SizedBox( + width: widget.itemWidth ?? double.infinity, + height: widget.itemHeight ?? double.infinity, + child: widget.itemBuilder(context, realIndex), + ), + ), + ), + ); + } +} + +class ScaleAndFadeTransformer extends PageTransformer { + final double _scale; + final double _fade; + + ScaleAndFadeTransformer({double fade: 0.3, double scale: 0.8}) + : _fade = fade, + _scale = scale; + + @override + Widget transform(Widget item, TransformInfo info) { + double position = info.position; + Widget child = item; + if (_scale != null) { + double scaleFactor = (1 - position.abs()) * (1 - _scale); + double scale = _scale + scaleFactor; + + child = new Transform.scale( + scale: scale, + child: item, + ); + } + + if (_fade != null) { + double fadeFactor = (1 - position.abs()) * (1 - _fade); + double opacity = _fade + fadeFactor; + child = new Opacity( + opacity: opacity, + child: child, + ); + } + + return child; + } +} + +class SwiperControl extends SwiperPlugin { + ///IconData for previous + final IconData iconPrevious; + + ///iconData fopr next + final IconData iconNext; + + ///icon size + final double size; + + ///Icon normal color, The theme's [ThemeData.primaryColor] by default. + final Color color; + + ///if set loop=false on Swiper, this color will be used when swiper goto the last slide. + ///The theme's [ThemeData.disabledColor] by default. + final Color disableColor; + + final EdgeInsetsGeometry padding; + + final Key key; + + const SwiperControl( + {this.iconPrevious: Icons.arrow_back_ios, + this.iconNext: Icons.arrow_forward_ios, + this.color, + this.disableColor, + this.key, + this.size: 30.0, + this.padding: const EdgeInsets.all(5.0)}); + + Widget buildButton(SwiperPluginConfig config, Color color, IconData iconDaga, + int quarterTurns, bool previous) { + return new GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (previous) { + config.controller.previous(animation: true); + } else { + config.controller.next(animation: true); + } + }, + child: Padding( + padding: padding, + child: RotatedBox( + quarterTurns: quarterTurns, + child: Icon( + iconDaga, + semanticLabel: previous ? "Previous" : "Next", + size: size, + color: color, + ))), + ); + } + + @override + Widget build(BuildContext context, SwiperPluginConfig config) { + ThemeData themeData = Theme.of(context); + + Color color = this.color ?? themeData.primaryColor; + Color disableColor = this.disableColor ?? themeData.disabledColor; + Color prevColor; + Color nextColor; + + if (config.loop) { + prevColor = nextColor = color; + } else { + bool next = config.activeIndex < config.itemCount - 1; + bool prev = config.activeIndex > 0; + prevColor = prev ? color : disableColor; + nextColor = next ? color : disableColor; + } + + Widget child; + if (config.scrollDirection == Axis.horizontal) { + child = Row( + key: key, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + buildButton(config, prevColor, iconPrevious, 0, true), + buildButton(config, nextColor, iconNext, 0, false) + ], + ); + } else { + child = Column( + key: key, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + buildButton(config, prevColor, iconPrevious, -3, true), + buildButton(config, nextColor, iconNext, -3, false) + ], + ); + } + + return new Container( + height: double.infinity, + child: child, + width: double.infinity, + ); + } +} + +class SwiperController extends IndexController { + // Autoplay is started + static const int START_AUTOPLAY = 2; + + // Autoplay is stopped. + static const int STOP_AUTOPLAY = 3; + + // Indicate that the user is swiping + static const int SWIPE = 4; + + // Indicate that the `Swiper` has changed it's index and is building it's ui ,so that the + // `SwiperPluginConfig` is available. + static const int BUILD = 5; + + // available when `event` == SwiperController.BUILD + SwiperPluginConfig config; + + // available when `event` == SwiperController.SWIPE + // this value is PageViewController.pos + double pos; + + int index; + bool animation; + bool autoplay; + + SwiperController(); + + void startAutoplay() { + event = SwiperController.START_AUTOPLAY; + this.autoplay = true; + notifyListeners(); + } + + void stopAutoplay() { + event = SwiperController.STOP_AUTOPLAY; + this.autoplay = false; + notifyListeners(); + } +} + +class FractionPaginationBuilder extends SwiperPlugin { + ///color ,if set null , will be Theme.of(context).scaffoldBackgroundColor + final Color color; + + ///color when active,if set null , will be Theme.of(context).primaryColor + final Color activeColor; + + ////font size + final double fontSize; + + ///font size when active + final double activeFontSize; + + final Key key; + + const FractionPaginationBuilder( + {this.color, + this.fontSize: 20.0, + this.key, + this.activeColor, + this.activeFontSize: 35.0}); + + @override + Widget build(BuildContext context, SwiperPluginConfig config) { + ThemeData themeData = Theme.of(context); + Color activeColor = this.activeColor ?? themeData.primaryColor; + Color color = this.color ?? themeData.scaffoldBackgroundColor; + + if (Axis.vertical == config.scrollDirection) { + return new Column( + key: key, + mainAxisSize: MainAxisSize.min, + children: [ + new Text( + "${config.activeIndex + 1}", + style: TextStyle(color: activeColor, fontSize: activeFontSize), + ), + new Text( + "/", + style: TextStyle(color: color, fontSize: fontSize), + ), + new Text( + "${config.itemCount}", + style: TextStyle(color: color, fontSize: fontSize), + ) + ], + ); + } else { + return new Row( + key: key, + mainAxisSize: MainAxisSize.min, + children: [ + new Text( + "${config.activeIndex + 1}", + style: TextStyle(color: activeColor, fontSize: activeFontSize), + ), + new Text( + " / ${config.itemCount}", + style: TextStyle(color: color, fontSize: fontSize), + ) + ], + ); + } + } +} + +class RectSwiperPaginationBuilder extends SwiperPlugin { + ///color when current index,if set null , will be Theme.of(context).primaryColor + final Color activeColor; + + ///,if set null , will be Theme.of(context).scaffoldBackgroundColor + final Color color; + + ///Size of the rect when activate + final Size activeSize; + + ///Size of the rect + final Size size; + + /// Space between rects + final double space; + + final Key key; + + const RectSwiperPaginationBuilder( + {this.activeColor, + this.color, + this.key, + this.size: const Size(10.0, 2.0), + this.activeSize: const Size(10.0, 2.0), + this.space: 3.0}); + + @override + Widget build(BuildContext context, SwiperPluginConfig config) { + ThemeData themeData = Theme.of(context); + Color activeColor = this.activeColor ?? themeData.primaryColor; + Color color = this.color ?? themeData.scaffoldBackgroundColor; + + List list = []; + + if (config.itemCount > 20) { + print( + "The itemCount is too big, we suggest use FractionPaginationBuilder instead of DotSwiperPaginationBuilder in this sitituation"); + } + + int itemCount = config.itemCount; + int activeIndex = config.activeIndex; + + for (int i = 0; i < itemCount; ++i) { + bool active = i == activeIndex; + Size size = active ? this.activeSize : this.size; + list.add(SizedBox( + width: size.width, + height: size.height, + child: Container( + color: active ? activeColor : color, + key: Key("pagination_$i"), + margin: EdgeInsets.all(space), + ), + )); + } + + if (config.scrollDirection == Axis.vertical) { + return new Column( + key: key, + mainAxisSize: MainAxisSize.min, + children: list, + ); + } else { + return new Row( + key: key, + mainAxisSize: MainAxisSize.min, + children: list, + ); + } + } +} + +class DotSwiperPaginationBuilder extends SwiperPlugin { + ///color when current index,if set null , will be Theme.of(context).primaryColor + final Color activeColor; + + ///,if set null , will be Theme.of(context).scaffoldBackgroundColor + final Color color; + + ///Size of the dot when activate + final double activeSize; + + ///Size of the dot + final double size; + + /// Space between dots + final double space; + + final Key key; + + const DotSwiperPaginationBuilder( + {this.activeColor, + this.color, + this.key, + this.size: 10.0, + this.activeSize: 10.0, + this.space: 3.0}); + + @override + Widget build(BuildContext context, SwiperPluginConfig config) { + if (config.itemCount > 20) { + print( + "The itemCount is too big, we suggest use FractionPaginationBuilder instead of DotSwiperPaginationBuilder in this sitituation"); + } + Color activeColor = this.activeColor; + Color color = this.color; + + if (activeColor == null || color == null) { + ThemeData themeData = Theme.of(context); + activeColor = this.activeColor ?? themeData.primaryColor; + color = this.color ?? themeData.scaffoldBackgroundColor; + } + + if (config.indicatorLayout != PageIndicatorLayout.NONE && + config.layout == SwiperLayout.DEFAULT) { + return new PageIndicator( + count: config.itemCount, + controller: config.pageController, + layout: config.indicatorLayout, + size: size, + activeColor: activeColor, + color: color, + space: space, + ); + } + + List list = []; + + int itemCount = config.itemCount; + int activeIndex = config.activeIndex; + + for (int i = 0; i < itemCount; ++i) { + bool active = i == activeIndex; + list.add(Container( + key: Key("pagination_$i"), + margin: EdgeInsets.all(space), + child: ClipOval( + child: Container( + color: active ? activeColor : color, + width: active ? activeSize : size, + height: active ? activeSize : size, + ), + ), + )); + } + + if (config.scrollDirection == Axis.vertical) { + return new Column( + key: key, + mainAxisSize: MainAxisSize.min, + children: list, + ); + } else { + return new Row( + key: key, + mainAxisSize: MainAxisSize.min, + children: list, + ); + } + } +} + +typedef Widget SwiperPaginationBuilder( + BuildContext context, SwiperPluginConfig config); + +class SwiperCustomPagination extends SwiperPlugin { + final SwiperPaginationBuilder builder; + + SwiperCustomPagination({@required this.builder}) : assert(builder != null); + + @override + Widget build(BuildContext context, SwiperPluginConfig config) { + return builder(context, config); + } +} + +class SwiperPagination extends SwiperPlugin { + /// dot style pagination + static const SwiperPlugin dots = const DotSwiperPaginationBuilder(); + + /// fraction style pagination + static const SwiperPlugin fraction = const FractionPaginationBuilder(); + + static const SwiperPlugin rect = const RectSwiperPaginationBuilder(); + + /// Alignment.bottomCenter by default when scrollDirection== Axis.horizontal + /// Alignment.centerRight by default when scrollDirection== Axis.vertical + final Alignment alignment; + + /// Distance between pagination and the container + final EdgeInsetsGeometry margin; + + /// Build the widet + final SwiperPlugin builder; + + final Key key; + + const SwiperPagination( + {this.alignment, + this.key, + this.margin: const EdgeInsets.all(10.0), + this.builder: SwiperPagination.dots}); + + Widget build(BuildContext context, SwiperPluginConfig config) { + Alignment alignment = this.alignment ?? + (config.scrollDirection == Axis.horizontal + ? Alignment.bottomCenter + : Alignment.centerRight); + Widget child = Container( + margin: margin, + child: this.builder.build(context, config), + ); + if (!config.outer) { + child = new Align( + key: key, + alignment: alignment, + child: child, + ); + } + return child; + } +} + +/// plugin to display swiper components +/// +abstract class SwiperPlugin { + const SwiperPlugin(); + + Widget build(BuildContext context, SwiperPluginConfig config); +} + +class SwiperPluginConfig { + final int activeIndex; + final int itemCount; + final PageIndicatorLayout indicatorLayout; + final Axis scrollDirection; + final bool loop; + final bool outer; + final PageController pageController; + final SwiperController controller; + final SwiperLayout layout; + + const SwiperPluginConfig( + {this.activeIndex, + this.itemCount, + this.indicatorLayout, + this.outer, + this.scrollDirection, + this.controller, + this.pageController, + this.layout, + this.loop}) + : assert(scrollDirection != null), + assert(controller != null); +} + +class SwiperPluginView extends StatelessWidget { + final SwiperPlugin plugin; + final SwiperPluginConfig config; + + const SwiperPluginView(this.plugin, this.config); + + @override + Widget build(BuildContext context) { + return plugin.build(context, config); + } +} + +abstract class _CustomLayoutStateBase extends State + with SingleTickerProviderStateMixin { + double _swiperWidth; + double _swiperHeight; + Animation _animation; + AnimationController _animationController; + int _startIndex; + int _animationCount; + + @override + void initState() { + if (widget.itemWidth == null) { + throw new Exception( + "==============\n\nwidget.itemWith must not be null when use stack layout.\n========\n"); + } + + _createAnimationController(); + widget.controller.addListener(_onController); + super.initState(); + } + + void _createAnimationController() { + _animationController = new AnimationController(vsync: this, value: 0.5); + Tween tween = new Tween(begin: 0.0, end: 1.0); + _animation = tween.animate(_animationController); + } + + @override + void didChangeDependencies() { + WidgetsBinding.instance.addPostFrameCallback(_getSize); + super.didChangeDependencies(); + } + + void _getSize(_) { + afterRender(); + } + + @mustCallSuper + void afterRender() { + RenderObject renderObject = context.findRenderObject(); + Size size = renderObject.paintBounds.size; + _swiperWidth = size.width; + _swiperHeight = size.height; + setState(() {}); + } + + @override + void didUpdateWidget(T oldWidget) { + if (widget.controller != oldWidget.controller) { + oldWidget.controller.removeListener(_onController); + widget.controller.addListener(_onController); + } + + if (widget.loop != oldWidget.loop) { + if (!widget.loop) { + _currentIndex = _ensureIndex(_currentIndex); + } + } + + super.didUpdateWidget(oldWidget); + } + + int _ensureIndex(int index) { + index = index % widget.itemCount; + if (index < 0) { + index += widget.itemCount; + } + return index; + } + + @override + void dispose() { + widget.controller.removeListener(_onController); + _animationController?.dispose(); + super.dispose(); + } + + Widget _buildItem(int i, int realIndex, double animationValue); + + Widget _buildContainer(List list) { + return new Stack( + children: list, + ); + } + + Widget _buildAnimation(BuildContext context, Widget w) { + List list = []; + + double animationValue = _animation.value; + + for (int i = 0; i < _animationCount; ++i) { + int realIndex = _currentIndex + i + _startIndex; + realIndex = realIndex % widget.itemCount; + if (realIndex < 0) { + realIndex += widget.itemCount; + } + + list.add(_buildItem(i, realIndex, animationValue)); + } + + return new GestureDetector( + behavior: HitTestBehavior.opaque, + onPanStart: _onPanStart, + onPanEnd: _onPanEnd, + onPanUpdate: _onPanUpdate, + child: new ClipRect( + child: new Center( + child: _buildContainer(list), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + if (_animationCount == null) { + return new Container(); + } + return new AnimatedBuilder( + animation: _animationController, builder: _buildAnimation); + } + + double _currentValue; + double _currentPos; + + bool _lockScroll = false; + + void _move(double position, {int nextIndex}) async { + if (_lockScroll) return; + try { + _lockScroll = true; + await _animationController.animateTo(position, + duration: new Duration(milliseconds: widget.duration), + curve: widget.curve); + if (nextIndex != null) { + widget.onIndexChanged(widget.getCorrectIndex(nextIndex)); + } + } catch (e) { + print(e); + } finally { + if (nextIndex != null) { + try { + _animationController.value = 0.5; + } catch (e) { + print(e); + } + + _currentIndex = nextIndex; + } + _lockScroll = false; + } + } + + int _nextIndex() { + int index = _currentIndex + 1; + if (!widget.loop && index >= widget.itemCount - 1) { + return widget.itemCount - 1; + } + return index; + } + + int _prevIndex() { + int index = _currentIndex - 1; + if (!widget.loop && index < 0) { + return 0; + } + return index; + } + + void _onController() { + switch (widget.controller.event) { + case IndexController.PREVIOUS: + int prevIndex = _prevIndex(); + if (prevIndex == _currentIndex) return; + _move(1.0, nextIndex: prevIndex); + break; + case IndexController.NEXT: + int nextIndex = _nextIndex(); + if (nextIndex == _currentIndex) return; + _move(0.0, nextIndex: nextIndex); + break; + case IndexController.MOVE: + throw new Exception( + "Custom layout does not support SwiperControllerEvent.MOVE_INDEX yet!"); + case SwiperController.STOP_AUTOPLAY: + case SwiperController.START_AUTOPLAY: + break; + } + } + + void _onPanEnd(DragEndDetails details) { + if (_lockScroll) return; + + double velocity = widget.scrollDirection == Axis.horizontal + ? details.velocity.pixelsPerSecond.dx + : details.velocity.pixelsPerSecond.dy; + + if (_animationController.value >= 0.75 || velocity > 500.0) { + if (_currentIndex <= 0 && !widget.loop) { + return; + } + _move(1.0, nextIndex: _currentIndex - 1); + } else if (_animationController.value < 0.25 || velocity < -500.0) { + if (_currentIndex >= widget.itemCount - 1 && !widget.loop) { + return; + } + _move(0.0, nextIndex: _currentIndex + 1); + } else { + _move(0.5); + } + } + + void _onPanStart(DragStartDetails details) { + if (_lockScroll) return; + _currentValue = _animationController.value; + _currentPos = widget.scrollDirection == Axis.horizontal + ? details.globalPosition.dx + : details.globalPosition.dy; + } + + void _onPanUpdate(DragUpdateDetails details) { + if (_lockScroll) return; + double value = _currentValue + + ((widget.scrollDirection == Axis.horizontal + ? details.globalPosition.dx + : details.globalPosition.dy) - + _currentPos) / + _swiperWidth / + 2; + // no loop ? + if (!widget.loop) { + if (_currentIndex >= widget.itemCount - 1) { + if (value < 0.5) { + value = 0.5; + } + } else if (_currentIndex <= 0) { + if (value > 0.5) { + value = 0.5; + } + } + } + + _animationController.value = value; + } + + int _currentIndex = 0; +} + +double _getValue(List values, double animationValue, int index) { + double s = values[index]; + if (animationValue >= 0.5) { + if (index < values.length - 1) { + s = s + (values[index + 1] - s) * (animationValue - 0.5) * 2.0; + } + } else { + if (index != 0) { + s = s - (s - values[index - 1]) * (0.5 - animationValue) * 2.0; + } + } + return s; +} + +Offset _getOffsetValue(List values, double animationValue, int index) { + Offset s = values[index]; + double dx = s.dx; + double dy = s.dy; + if (animationValue >= 0.5) { + if (index < values.length - 1) { + dx = dx + (values[index + 1].dx - dx) * (animationValue - 0.5) * 2.0; + dy = dy + (values[index + 1].dy - dy) * (animationValue - 0.5) * 2.0; + } + } else { + if (index != 0) { + dx = dx - (dx - values[index - 1].dx) * (0.5 - animationValue) * 2.0; + dy = dy - (dy - values[index - 1].dy) * (0.5 - animationValue) * 2.0; + } + } + return new Offset(dx, dy); +} + +abstract class TransformBuilder { + List values; + TransformBuilder({this.values}); + Widget build(int i, double animationValue, Widget widget); +} + +class ScaleTransformBuilder extends TransformBuilder { + final Alignment alignment; + ScaleTransformBuilder({List values, this.alignment: Alignment.center}) + : super(values: values); + + Widget build(int i, double animationValue, Widget widget) { + double s = _getValue(values, animationValue, i); + return new Transform.scale(scale: s, child: widget); + } +} + +class OpacityTransformBuilder extends TransformBuilder { + OpacityTransformBuilder({List values}) : super(values: values); + + Widget build(int i, double animationValue, Widget widget) { + double v = _getValue(values, animationValue, i); + return new Opacity( + opacity: v, + child: widget, + ); + } +} + +class RotateTransformBuilder extends TransformBuilder { + RotateTransformBuilder({List values}) : super(values: values); + + Widget build(int i, double animationValue, Widget widget) { + double v = _getValue(values, animationValue, i); + return new Transform.rotate( + angle: v, + child: widget, + ); + } +} + +class TranslateTransformBuilder extends TransformBuilder { + TranslateTransformBuilder({List values}) : super(values: values); + + @override + Widget build(int i, double animationValue, Widget widget) { + Offset s = _getOffsetValue(values, animationValue, i); + return new Transform.translate( + offset: s, + child: widget, + ); + } +} + +class CustomLayoutOption { + final List builders = []; + final int startIndex; + final int stateCount; + + CustomLayoutOption({this.stateCount, this.startIndex}) + : assert(startIndex != null, stateCount != null); + + CustomLayoutOption addOpacity(List values) { + builders.add(new OpacityTransformBuilder(values: values)); + return this; + } + + CustomLayoutOption addTranslate(List values) { + builders.add(new TranslateTransformBuilder(values: values)); + return this; + } + + CustomLayoutOption addScale(List values, Alignment alignment) { + builders + .add(new ScaleTransformBuilder(values: values, alignment: alignment)); + return this; + } + + CustomLayoutOption addRotate(List values) { + builders.add(new RotateTransformBuilder(values: values)); + return this; + } +} + +class _CustomLayoutSwiper extends _SubSwiper { + final CustomLayoutOption option; + + _CustomLayoutSwiper( + {this.option, + double itemWidth, + bool loop, + double itemHeight, + ValueChanged onIndexChanged, + Key key, + IndexedWidgetBuilder itemBuilder, + Curve curve, + int duration, + int index, + int itemCount, + Axis scrollDirection, + SwiperController controller}) + : assert(option != null), + super( + loop: loop, + onIndexChanged: onIndexChanged, + itemWidth: itemWidth, + itemHeight: itemHeight, + key: key, + itemBuilder: itemBuilder, + curve: curve, + duration: duration, + index: index, + itemCount: itemCount, + controller: controller, + scrollDirection: scrollDirection); + + @override + State createState() { + return new _CustomLayoutState(); + } +} + +class _CustomLayoutState extends _CustomLayoutStateBase<_CustomLayoutSwiper> { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _startIndex = widget.option.startIndex; + _animationCount = widget.option.stateCount; + } + + @override + void didUpdateWidget(_CustomLayoutSwiper oldWidget) { + _startIndex = widget.option.startIndex; + _animationCount = widget.option.stateCount; + super.didUpdateWidget(oldWidget); + } + + @override + Widget _buildItem(int index, int realIndex, double animationValue) { + List builders = widget.option.builders; + + Widget child = new SizedBox( + width: widget.itemWidth ?? double.infinity, + height: widget.itemHeight ?? double.infinity, + child: widget.itemBuilder(context, realIndex)); + + for (int i = builders.length - 1; i >= 0; --i) { + TransformBuilder builder = builders[i]; + child = builder.build(index, animationValue, child); + } + + return child; + } +} diff --git a/web/filipino_cuisine/lib/main.dart b/web/filipino_cuisine/lib/main.dart new file mode 100644 index 000000000..9b386ec5d --- /dev/null +++ b/web/filipino_cuisine/lib/main.dart @@ -0,0 +1,135 @@ +import 'package:flutter_web/material.dart'; + +import 'package:http/http.dart' as http; +import 'dart:convert'; + +import 'cook.dart'; +import 'flutter_swiper.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + Widget build(ct) { + return MaterialApp( + theme: ThemeData( + brightness: Brightness.light, + accentColor: Colors.red, + iconTheme: IconThemeData(color: Colors.red)), + title: "Filipino Cuisine", + home: Home()); + } +} + +class Home extends StatefulWidget { + HState createState() => HState(); +} + +class HState extends State { + List fd; + Map fi; + + void initState() { + super.initState(); + getData(); + } + + getData() async { + http.Response r = + await http.get('https://filipino-cuisine-app.firebaseio.com/data.json'); + fd = json.decode(r.body); + setState(() => fi = fd[0]); + } + + Widget build(ct) { + if (fd == null) + return Container( + color: Colors.white, + child: Center( + child: CircularProgressIndicator(), + )); + var t = Theme.of(ct).textTheme; + return Scaffold( + body: Column( + children: [ + Expanded( + flex: 5, + child: Swiper( + onIndexChanged: (n) => setState(() => fi = fd[n]), + itemCount: fd.length, + itemBuilder: (cx, i) { + return Container( + margin: EdgeInsets.only(top: 40, bottom: 24), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Hero( + tag: fd[i]['fn'], + child: + Image.asset(fd[i]['pf'], fit: BoxFit.cover)), + )); + }, + viewportFraction: .85, + scale: .9)), + Text(fi['fn'], + style: + t.display3.copyWith(fontFamily: 'ark', color: Colors.black)), + Container( + child: Text(fi['cn'], + style: t.subhead.apply(color: Colors.red, fontFamily: 'opb')), + margin: EdgeInsets.only(top: 10, bottom: 30), + ), + Container( + child: Text(fi['dc'], + textAlign: TextAlign.center, + style: t.subhead.copyWith(fontFamily: 'opr')), + margin: EdgeInsets.only(left: 10, right: 10)), + Expanded( + flex: 2, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: fi['ig'].length, + itemBuilder: (cx, i) { + return Row(children: [ + Container( + margin: EdgeInsets.only(left: 10), + height: 60, + child: Image.asset(fi['ig'][i]['p'], + fit: BoxFit.contain)), + Container( + margin: EdgeInsets.only(left: 5, right: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(fi['ig'][i]['n'], + style: + t.subtitle.copyWith(fontFamily: 'opb')), + Text(fi['ig'][i]['c'], + style: + t.caption.copyWith(fontFamily: 'opr')) + ])) + ]); + })) + ], + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + floatingActionButton: FloatingActionButton( + child: Icon(Icons.restaurant_menu), + onPressed: () => Navigator.push( + ct, + MaterialPageRoute( + builder: (cx) => Cook(fi['in'], fi['pf'], fi['fn']))), + ), + bottomNavigationBar: BottomAppBar( + shape: CircularNotchedRectangle(), + notchMargin: 4.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + icon: + Icon(fi['fv'] ? Icons.favorite : Icons.favorite_border), + onPressed: () => setState(() => fi['fv'] = !fi['fv'])), + IconButton(icon: Icon(Icons.share), onPressed: () {}) + ])), + ); + } +} diff --git a/web/filipino_cuisine/lib/transformer_page_view.dart b/web/filipino_cuisine/lib/transformer_page_view.dart new file mode 100644 index 000000000..ff63234f8 --- /dev/null +++ b/web/filipino_cuisine/lib/transformer_page_view.dart @@ -0,0 +1,809 @@ +// Package transformer_page_view: +// https://pub.dartlang.org/packages/transformer_page_view + +import 'dart:async'; + +import 'package:flutter_web/foundation.dart'; +import 'package:flutter_web/widgets.dart'; + +class IndexController extends ChangeNotifier { + static const int NEXT = 1; + static const int PREVIOUS = -1; + static const int MOVE = 0; + + Completer _completer; + + int index; + bool animation; + int event; + + Future move(int index, {bool animation: true}) { + this.animation = animation ?? true; + this.index = index; + this.event = MOVE; + _completer = new Completer(); + notifyListeners(); + return _completer.future; + } + + Future next({bool animation: true}) { + this.event = NEXT; + this.animation = animation ?? true; + _completer = new Completer(); + notifyListeners(); + return _completer.future; + } + + Future previous({bool animation: true}) { + this.event = PREVIOUS; + this.animation = animation ?? true; + _completer = new Completer(); + notifyListeners(); + return _completer.future; + } + + void complete() { + if (!_completer.isCompleted) { + _completer.complete(); + } + } +} + +typedef void PaintCallback(Canvas canvas, Size siz); + +class ColorPainter extends CustomPainter { + final Paint _paint; + final TransformInfo info; + final List colors; + + ColorPainter(this._paint, this.info, this.colors); + + @override + void paint(Canvas canvas, Size size) { + int index = info.fromIndex; + _paint.color = colors[index]; + canvas.drawRect( + new Rect.fromLTWH(0.0, 0.0, size.width, size.height), _paint); + if (info.done) { + return; + } + int alpha; + int color; + double opacity; + double position = info.position; + if (info.forward) { + if (index < colors.length - 1) { + color = colors[index + 1].value & 0x00ffffff; + opacity = (position <= 0 + ? (-position / info.viewportFraction) + : 1 - position / info.viewportFraction); + if (opacity > 1) { + opacity -= 1.0; + } + if (opacity < 0) { + opacity += 1.0; + } + alpha = (0xff * opacity).toInt(); + + _paint.color = new Color((alpha << 24) | color); + canvas.drawRect( + new Rect.fromLTWH(0.0, 0.0, size.width, size.height), _paint); + } + } else { + if (index > 0) { + color = colors[index - 1].value & 0x00ffffff; + opacity = (position > 0 + ? position / info.viewportFraction + : (1 + position / info.viewportFraction)); + if (opacity > 1) { + opacity -= 1.0; + } + if (opacity < 0) { + opacity += 1.0; + } + alpha = (0xff * opacity).toInt(); + + _paint.color = new Color((alpha << 24) | color); + canvas.drawRect( + new Rect.fromLTWH(0.0, 0.0, size.width, size.height), _paint); + } + } + } + + @override + bool shouldRepaint(ColorPainter oldDelegate) { + return oldDelegate.info != info; + } +} + +class _ParallaxColorState extends State { + Paint paint = new Paint(); + + @override + Widget build(BuildContext context) { + return new CustomPaint( + painter: new ColorPainter(paint, widget.info, widget.colors), + child: widget.child, + ); + } +} + +class ParallaxColor extends StatefulWidget { + final Widget child; + + final List colors; + + final TransformInfo info; + + ParallaxColor({ + @required this.colors, + @required this.info, + @required this.child, + }); + + @override + State createState() { + return new _ParallaxColorState(); + } +} + +class ParallaxContainer extends StatelessWidget { + final Widget child; + final double position; + final double translationFactor; + final double opacityFactor; + + ParallaxContainer( + {@required this.child, + @required this.position, + this.translationFactor: 100.0, + this.opacityFactor: 1.0}) + : assert(position != null), + assert(translationFactor != null); + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: (1 - position.abs()).clamp(0.0, 1.0) * opacityFactor, + child: new Transform.translate( + offset: new Offset(position * translationFactor, 0.0), + child: child, + ), + ); + } +} + +class ParallaxImage extends StatelessWidget { + final Image image; + final double imageFactor; + + ParallaxImage.asset(String name, {double position, this.imageFactor: 0.3}) + : assert(imageFactor != null), + image = Image.asset(name, + fit: BoxFit.cover, + alignment: FractionalOffset( + 0.5 + position * imageFactor, + 0.5, + )); + + @override + Widget build(BuildContext context) { + return image; + } +} + +/// +/// NOTICE:: +/// +/// In order to make package smaller,currently we're not supporting any build-in page transformers +/// You can find build in transforms here: +/// +/// +/// + +const int kMaxValue = 2000000000; +const int kMiddleValue = 1000000000; + +/// Default auto play transition duration (in millisecond) +const int kDefaultTransactionDuration = 300; + +class TransformInfo { + /// The `width` of the `TransformerPageView` + final double width; + + /// The `height` of the `TransformerPageView` + final double height; + + /// The `position` of the widget pass to [PageTransformer.transform] + /// A `position` describes how visible the widget is. + /// The widget in the center of the screen' which is full visible, position is 0.0. + /// The widge in the left ,may be hidden, of the screen's position is less than 0.0, -1.0 when out of the screen. + /// The widge in the right ,may be hidden, of the screen's position is greater than 0.0, 1.0 when out of the screen + /// + /// + final double position; + + /// The `index` of the widget pass to [PageTransformer.transform] + final int index; + + /// The `activeIndex` of the PageView + final int activeIndex; + + /// The `activeIndex` of the PageView, from user start to swipe + /// It will change when user end drag + final int fromIndex; + + /// Next `index` is greater than this `index` + final bool forward; + + /// User drag is done. + final bool done; + + /// Same as [TransformerPageView.viewportFraction] + final double viewportFraction; + + /// Copy from [TransformerPageView.scrollDirection] + final Axis scrollDirection; + + TransformInfo( + {this.index, + this.position, + this.width, + this.height, + this.activeIndex, + this.fromIndex, + this.forward, + this.done, + this.viewportFraction, + this.scrollDirection}); +} + +abstract class PageTransformer { + /// + final bool reverse; + + PageTransformer({this.reverse: false}); + + /// Return a transformed widget, based on child and TransformInfo + Widget transform(Widget child, TransformInfo info); +} + +typedef Widget PageTransformerBuilderCallback(Widget child, TransformInfo info); + +class PageTransformerBuilder extends PageTransformer { + final PageTransformerBuilderCallback builder; + + PageTransformerBuilder({bool reverse: false, @required this.builder}) + : assert(builder != null), + super(reverse: reverse); + + @override + Widget transform(Widget child, TransformInfo info) { + return builder(child, info); + } +} + +class TransformerPageController extends PageController { + final bool loop; + final int itemCount; + final bool reverse; + + TransformerPageController({ + int initialPage = 0, + bool keepPage = true, + double viewportFraction = 1.0, + this.loop: false, + this.itemCount, + this.reverse: false, + }) : super( + initialPage: TransformerPageController._getRealIndexFromRenderIndex( + initialPage ?? 0, loop, itemCount, reverse), + keepPage: keepPage, + viewportFraction: viewportFraction); + + int getRenderIndexFromRealIndex(num index) { + return _getRenderIndexFromRealIndex(index, loop, itemCount, reverse); + } + + int getRealItemCount() { + if (itemCount == 0) return 0; + return loop ? itemCount + kMaxValue : itemCount; + } + + static _getRenderIndexFromRealIndex( + num index, bool loop, int itemCount, bool reverse) { + if (itemCount == 0) return 0; + int renderIndex; + if (loop) { + renderIndex = index - kMiddleValue; + renderIndex = renderIndex % itemCount; + if (renderIndex < 0) { + renderIndex += itemCount; + } + } else { + renderIndex = index; + } + if (reverse) { + renderIndex = itemCount - renderIndex - 1; + } + + return renderIndex; + } + + double get realPage { + double page; + if (position.maxScrollExtent == null || position.minScrollExtent == null) { + page = 0.0; + } else { + page = super.page; + } + + return page; + } + + static _getRenderPageFromRealPage( + double page, bool loop, int itemCount, bool reverse) { + double renderPage; + if (loop) { + renderPage = page - kMiddleValue; + renderPage = renderPage % itemCount; + if (renderPage < 0) { + renderPage += itemCount; + } + } else { + renderPage = page; + } + if (reverse) { + renderPage = itemCount - renderPage - 1; + } + + return renderPage; + } + + double get page { + return loop + ? _getRenderPageFromRealPage(realPage, loop, itemCount, reverse) + : realPage; + } + + int getRealIndexFromRenderIndex(num index) { + return _getRealIndexFromRenderIndex(index, loop, itemCount, reverse); + } + + static int _getRealIndexFromRenderIndex( + num index, bool loop, int itemCount, bool reverse) { + int result = reverse ? (itemCount - index - 1) : index; + if (loop) { + result += kMiddleValue; + } + return result; + } +} + +class TransformerPageView extends StatefulWidget { + /// Create a `transformed` widget base on the widget that has been passed to the [PageTransformer.transform]. + /// See [TransformInfo] + /// + final PageTransformer transformer; + + /// Same as [PageView.scrollDirection] + /// + /// Defaults to [Axis.horizontal]. + final Axis scrollDirection; + + /// Same as [PageView.physics] + final ScrollPhysics physics; + + /// Set to false to disable page snapping, useful for custom scroll behavior. + /// Same as [PageView.pageSnapping] + final bool pageSnapping; + + /// Called whenever the page in the center of the viewport changes. + /// Same as [PageView.onPageChanged] + final ValueChanged onPageChanged; + + final IndexedWidgetBuilder itemBuilder; + + // See [IndexController.mode],[IndexController.next],[IndexController.previous] + final IndexController controller; + + /// Animation duration + final Duration duration; + + /// Animation curve + final Curve curve; + + final TransformerPageController pageController; + + /// Set true to open infinity loop mode. + final bool loop; + + /// This value is only valid when `pageController` is not set, + final int itemCount; + + /// This value is only valid when `pageController` is not set, + final double viewportFraction; + + /// If not set, it is controlled by this widget. + final int index; + + /// Creates a scrollable list that works page by page using widgets that are + /// created on demand. + /// + /// This constructor is appropriate for page views with a large (or infinite) + /// number of children because the builder is called only for those children + /// that are actually visible. + /// + /// Providing a non-null [itemCount] lets the [PageView] compute the maximum + /// scroll extent. + /// + /// [itemBuilder] will be called only with indices greater than or equal to + /// zero and less than [itemCount]. + TransformerPageView({ + Key key, + this.index, + Duration duration, + this.curve: Curves.ease, + this.viewportFraction: 1.0, + this.loop: false, + this.scrollDirection = Axis.horizontal, + this.physics, + this.pageSnapping = true, + this.onPageChanged, + this.controller, + this.transformer, + this.itemBuilder, + this.pageController, + @required this.itemCount, + }) : assert(itemCount != null), + assert(itemCount == 0 || itemBuilder != null || transformer != null), + this.duration = + duration ?? new Duration(milliseconds: kDefaultTransactionDuration), + super(key: key); + + factory TransformerPageView.children( + {Key key, + int index, + Duration duration, + Curve curve: Curves.ease, + double viewportFraction: 1.0, + bool loop: false, + Axis scrollDirection = Axis.horizontal, + ScrollPhysics physics, + bool pageSnapping = true, + ValueChanged onPageChanged, + IndexController controller, + PageTransformer transformer, + @required List children, + TransformerPageController pageController}) { + assert(children != null); + return new TransformerPageView( + itemCount: children.length, + itemBuilder: (BuildContext context, int index) { + return children[index]; + }, + pageController: pageController, + transformer: transformer, + pageSnapping: pageSnapping, + key: key, + index: index, + duration: duration, + curve: curve, + viewportFraction: viewportFraction, + scrollDirection: scrollDirection, + physics: physics, + onPageChanged: onPageChanged, + controller: controller, + ); + } + + @override + State createState() { + return new _TransformerPageViewState(); + } + + static int getRealIndexFromRenderIndex( + {bool reverse, int index, int itemCount, bool loop}) { + int initPage = reverse ? (itemCount - index - 1) : index; + if (loop) { + initPage += kMiddleValue; + } + return initPage; + } + + static PageController createPageController( + {bool reverse, + int index, + int itemCount, + bool loop, + double viewportFraction}) { + return new PageController( + initialPage: getRealIndexFromRenderIndex( + reverse: reverse, index: index, itemCount: itemCount, loop: loop), + viewportFraction: viewportFraction); + } +} + +class _TransformerPageViewState extends State { + Size _size; + int _activeIndex; + double _currentPixels; + bool _done = false; + + ///This value will not change until user end drag. + int _fromIndex; + + PageTransformer _transformer; + + TransformerPageController _pageController; + + Widget _buildItemNormal(BuildContext context, int index) { + int renderIndex = _pageController.getRenderIndexFromRealIndex(index); + Widget child = widget.itemBuilder(context, renderIndex); + return child; + } + + Widget _buildItem(BuildContext context, int index) { + return new AnimatedBuilder( + animation: _pageController, + builder: (BuildContext c, Widget w) { + int renderIndex = _pageController.getRenderIndexFromRealIndex(index); + Widget child; + if (widget.itemBuilder != null) { + child = widget.itemBuilder(context, renderIndex); + } + if (child == null) { + child = new Container(); + } + if (_size == null) { + return child ?? new Container(); + } + + double position; + + double page = _pageController.realPage; + + if (_transformer.reverse) { + position = page - index; + } else { + position = index - page; + } + position *= widget.viewportFraction; + + TransformInfo info = new TransformInfo( + index: renderIndex, + width: _size.width, + height: _size.height, + position: position.clamp(-1.0, 1.0), + activeIndex: + _pageController.getRenderIndexFromRealIndex(_activeIndex), + fromIndex: _fromIndex, + forward: _pageController.position.pixels - _currentPixels >= 0, + done: _done, + scrollDirection: widget.scrollDirection, + viewportFraction: widget.viewportFraction); + return _transformer.transform(child, info); + }); + } + + double _calcCurrentPixels() { + _currentPixels = _pageController.getRenderIndexFromRealIndex(_activeIndex) * + _pageController.position.viewportDimension * + widget.viewportFraction; + + // print("activeIndex:$_activeIndex , pix:$_currentPixels"); + + return _currentPixels; + } + + @override + Widget build(BuildContext context) { + IndexedWidgetBuilder builder = + _transformer == null ? _buildItemNormal : _buildItem; + Widget child = new PageView.builder( + itemBuilder: builder, + itemCount: _pageController.getRealItemCount(), + onPageChanged: _onIndexChanged, + controller: _pageController, + scrollDirection: widget.scrollDirection, + physics: widget.physics, + pageSnapping: widget.pageSnapping, + reverse: _pageController.reverse, + ); + if (_transformer == null) { + return child; + } + return new NotificationListener( + onNotification: (ScrollNotification notification) { + if (notification is ScrollStartNotification) { + _calcCurrentPixels(); + _done = false; + _fromIndex = _activeIndex; + } else if (notification is ScrollEndNotification) { + _calcCurrentPixels(); + _fromIndex = _activeIndex; + _done = true; + } + + return false; + }, + child: child); + } + + void _onIndexChanged(int index) { + _activeIndex = index; + if (widget.onPageChanged != null) { + widget.onPageChanged(_pageController.getRenderIndexFromRealIndex(index)); + } + } + + void _onGetSize(_) { + Size size; + if (context == null) { + onGetSize(size); + return; + } + RenderObject renderObject = context.findRenderObject(); + if (renderObject != null) { + Rect bounds = renderObject.paintBounds; + if (bounds != null) { + size = bounds.size; + } + } + _calcCurrentPixels(); + onGetSize(size); + } + + void onGetSize(Size size) { + if (mounted) { + setState(() { + _size = size; + }); + } + } + + @override + void initState() { + _transformer = widget.transformer; + // int index = widget.index ?? 0; + _pageController = widget.pageController; + if (_pageController == null) { + _pageController = new TransformerPageController( + initialPage: widget.index, + itemCount: widget.itemCount, + loop: widget.loop, + reverse: + widget.transformer == null ? false : widget.transformer.reverse); + } + // int initPage = _getRealIndexFromRenderIndex(index); + // _pageController = new PageController(initialPage: initPage,viewportFraction: widget.viewportFraction); + _fromIndex = _activeIndex = _pageController.initialPage; + + _controller = getNotifier(); + if (_controller != null) { + _controller.addListener(onChangeNotifier); + } + super.initState(); + } + + @override + void didUpdateWidget(TransformerPageView oldWidget) { + _transformer = widget.transformer; + int index = widget.index ?? 0; + bool created = false; + if (_pageController != widget.pageController) { + if (widget.pageController != null) { + _pageController = widget.pageController; + } else { + created = true; + _pageController = new TransformerPageController( + initialPage: widget.index, + itemCount: widget.itemCount, + loop: widget.loop, + reverse: widget.transformer == null + ? false + : widget.transformer.reverse); + } + } + + if (_pageController.getRenderIndexFromRealIndex(_activeIndex) != index) { + _fromIndex = _activeIndex = _pageController.initialPage; + if (!created) { + int initPage = _pageController.getRealIndexFromRenderIndex(index); + _pageController.animateToPage(initPage, + duration: widget.duration, curve: widget.curve); + } + } + if (_transformer != null) + WidgetsBinding.instance.addPostFrameCallback(_onGetSize); + + if (_controller != getNotifier()) { + if (_controller != null) { + _controller.removeListener(onChangeNotifier); + } + _controller = getNotifier(); + if (_controller != null) { + _controller.addListener(onChangeNotifier); + } + } + super.didUpdateWidget(oldWidget); + } + + @override + void didChangeDependencies() { + if (_transformer != null) + WidgetsBinding.instance.addPostFrameCallback(_onGetSize); + super.didChangeDependencies(); + } + + ChangeNotifier getNotifier() { + return widget.controller; + } + + int _calcNextIndex(bool next) { + int currentIndex = _activeIndex; + if (_pageController.reverse) { + if (next) { + currentIndex--; + } else { + currentIndex++; + } + } else { + if (next) { + currentIndex++; + } else { + currentIndex--; + } + } + + if (!_pageController.loop) { + if (currentIndex >= _pageController.itemCount) { + currentIndex = 0; + } else if (currentIndex < 0) { + currentIndex = _pageController.itemCount - 1; + } + } + + return currentIndex; + } + + void onChangeNotifier() { + int event = widget.controller.event; + int index; + switch (event) { + case IndexController.MOVE: + { + index = _pageController + .getRealIndexFromRenderIndex(widget.controller.index); + } + break; + case IndexController.PREVIOUS: + case IndexController.NEXT: + { + index = _calcNextIndex(event == IndexController.NEXT); + } + break; + default: + //ignore this event + return; + } + if (widget.controller.animation) { + _pageController + .animateToPage(index, + duration: widget.duration, curve: widget.curve ?? Curves.ease) + .whenComplete(widget.controller.complete); + } else { + _pageController.jumpToPage(index); + widget.controller.complete(); + } + } + + ChangeNotifier _controller; + + void dispose() { + super.dispose(); + if (_controller != null) { + _controller.removeListener(onChangeNotifier); + } + } +} diff --git a/web/filipino_cuisine/pubspec.lock b/web/filipino_cuisine/pubspec.lock new file mode 100644 index 000000000..d1288b168 --- /dev/null +++ b/web/filipino_cuisine/pubspec.lock @@ -0,0 +1,471 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct main" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/filipino_cuisine/pubspec.yaml b/web/filipino_cuisine/pubspec.yaml new file mode 100644 index 000000000..3edea254e --- /dev/null +++ b/web/filipino_cuisine/pubspec.yaml @@ -0,0 +1,26 @@ +name: filipino_cuisine + +environment: + sdk: ">=2.2.0 <3.0.0" + +dependencies: + flutter_web: any + flutter_web_ui: any + +dev_dependencies: + pedantic: ^1.3.0 + + build_runner: any + build_web_compilers: any + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/filipino_cuisine/web/assets/FontManifest.json b/web/filipino_cuisine/web/assets/FontManifest.json new file mode 100644 index 000000000..03b3631de --- /dev/null +++ b/web/filipino_cuisine/web/assets/FontManifest.json @@ -0,0 +1,34 @@ +[ + { + "family": "MaterialIcons", + "fonts": [ + { + "asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" + } + ] + }, + { + "family": "opr", + "fonts": [ + { + "asset": "fonts/OpenSans-Regular.ttf" + } + ] + }, + { + "family": "opb", + "fonts": [ + { + "asset": "fonts/OpenSans-Bold.ttf" + } + ] + }, + { + "family": "ark", + "fonts": [ + { + "asset": "fonts/Arkipelago.otf" + } + ] + } +] \ No newline at end of file diff --git a/web/filipino_cuisine/web/assets/banana.png b/web/filipino_cuisine/web/assets/banana.png new file mode 100644 index 000000000..4b382a4bd Binary files /dev/null and b/web/filipino_cuisine/web/assets/banana.png differ diff --git a/web/filipino_cuisine/web/assets/beef.png b/web/filipino_cuisine/web/assets/beef.png new file mode 100644 index 000000000..f7d66da5b Binary files /dev/null and b/web/filipino_cuisine/web/assets/beef.png differ diff --git a/web/filipino_cuisine/web/assets/beef_caldereta.jpg b/web/filipino_cuisine/web/assets/beef_caldereta.jpg new file mode 100644 index 000000000..2cc3a073f Binary files /dev/null and b/web/filipino_cuisine/web/assets/beef_caldereta.jpg differ diff --git a/web/filipino_cuisine/web/assets/black_pepper.png b/web/filipino_cuisine/web/assets/black_pepper.png new file mode 100644 index 000000000..d1504cac8 Binary files /dev/null and b/web/filipino_cuisine/web/assets/black_pepper.png differ diff --git a/web/filipino_cuisine/web/assets/bokchoy.png b/web/filipino_cuisine/web/assets/bokchoy.png new file mode 100644 index 000000000..ff4a6d259 Binary files /dev/null and b/web/filipino_cuisine/web/assets/bokchoy.png differ diff --git a/web/filipino_cuisine/web/assets/butter.png b/web/filipino_cuisine/web/assets/butter.png new file mode 100644 index 000000000..5aed09edf Binary files /dev/null and b/web/filipino_cuisine/web/assets/butter.png differ diff --git a/web/filipino_cuisine/web/assets/cabbage.png b/web/filipino_cuisine/web/assets/cabbage.png new file mode 100644 index 000000000..ffd40c976 Binary files /dev/null and b/web/filipino_cuisine/web/assets/cabbage.png differ diff --git a/web/filipino_cuisine/web/assets/calamares.jpg b/web/filipino_cuisine/web/assets/calamares.jpg new file mode 100644 index 000000000..52871020b Binary files /dev/null and b/web/filipino_cuisine/web/assets/calamares.jpg differ diff --git a/web/filipino_cuisine/web/assets/carrot.png b/web/filipino_cuisine/web/assets/carrot.png new file mode 100644 index 000000000..121fc38eb Binary files /dev/null and b/web/filipino_cuisine/web/assets/carrot.png differ diff --git a/web/filipino_cuisine/web/assets/cheese.png b/web/filipino_cuisine/web/assets/cheese.png new file mode 100644 index 000000000..904d7ab4d Binary files /dev/null and b/web/filipino_cuisine/web/assets/cheese.png differ diff --git a/web/filipino_cuisine/web/assets/chicken_adobo.jpg b/web/filipino_cuisine/web/assets/chicken_adobo.jpg new file mode 100644 index 000000000..b19a6dad1 Binary files /dev/null and b/web/filipino_cuisine/web/assets/chicken_adobo.jpg differ diff --git a/web/filipino_cuisine/web/assets/chili.png b/web/filipino_cuisine/web/assets/chili.png new file mode 100644 index 000000000..7297b5287 Binary files /dev/null and b/web/filipino_cuisine/web/assets/chili.png differ diff --git a/web/filipino_cuisine/web/assets/crispy_pata.jpg b/web/filipino_cuisine/web/assets/crispy_pata.jpg new file mode 100644 index 000000000..c9d516c0a Binary files /dev/null and b/web/filipino_cuisine/web/assets/crispy_pata.jpg differ diff --git a/web/filipino_cuisine/web/assets/egg.png b/web/filipino_cuisine/web/assets/egg.png new file mode 100644 index 000000000..ca26cad2a Binary files /dev/null and b/web/filipino_cuisine/web/assets/egg.png differ diff --git a/web/filipino_cuisine/web/assets/embutido.jpg b/web/filipino_cuisine/web/assets/embutido.jpg new file mode 100644 index 000000000..850a8a78d Binary files /dev/null and b/web/filipino_cuisine/web/assets/embutido.jpg differ diff --git a/web/filipino_cuisine/web/assets/flour.png b/web/filipino_cuisine/web/assets/flour.png new file mode 100644 index 000000000..c64e781b4 Binary files /dev/null and b/web/filipino_cuisine/web/assets/flour.png differ diff --git a/web/filipino_cuisine/web/assets/fonts/Arkipelago.otf b/web/filipino_cuisine/web/assets/fonts/Arkipelago.otf new file mode 100644 index 000000000..37afb8423 Binary files /dev/null and b/web/filipino_cuisine/web/assets/fonts/Arkipelago.otf differ diff --git a/web/filipino_cuisine/web/assets/fonts/OpenSans-Bold.ttf b/web/filipino_cuisine/web/assets/fonts/OpenSans-Bold.ttf new file mode 100644 index 000000000..fd79d43be Binary files /dev/null and b/web/filipino_cuisine/web/assets/fonts/OpenSans-Bold.ttf differ diff --git a/web/filipino_cuisine/web/assets/fonts/OpenSans-Regular.ttf b/web/filipino_cuisine/web/assets/fonts/OpenSans-Regular.ttf new file mode 100644 index 000000000..db433349b Binary files /dev/null and b/web/filipino_cuisine/web/assets/fonts/OpenSans-Regular.ttf differ diff --git a/web/filipino_cuisine/web/assets/garlic.png b/web/filipino_cuisine/web/assets/garlic.png new file mode 100644 index 000000000..c4e1c2f69 Binary files /dev/null and b/web/filipino_cuisine/web/assets/garlic.png differ diff --git a/web/filipino_cuisine/web/assets/green_beans.png b/web/filipino_cuisine/web/assets/green_beans.png new file mode 100644 index 000000000..9bf1ab5c7 Binary files /dev/null and b/web/filipino_cuisine/web/assets/green_beans.png differ diff --git a/web/filipino_cuisine/web/assets/green_bell.png b/web/filipino_cuisine/web/assets/green_bell.png new file mode 100644 index 000000000..5c1dd5912 Binary files /dev/null and b/web/filipino_cuisine/web/assets/green_bell.png differ diff --git a/web/filipino_cuisine/web/assets/grilled_pork_ribs.jpg b/web/filipino_cuisine/web/assets/grilled_pork_ribs.jpg new file mode 100644 index 000000000..713b5191d Binary files /dev/null and b/web/filipino_cuisine/web/assets/grilled_pork_ribs.jpg differ diff --git a/web/filipino_cuisine/web/assets/grilled_seafood.jpg b/web/filipino_cuisine/web/assets/grilled_seafood.jpg new file mode 100644 index 000000000..c33038d20 Binary files /dev/null and b/web/filipino_cuisine/web/assets/grilled_seafood.jpg differ diff --git a/web/filipino_cuisine/web/assets/ground_pork.png b/web/filipino_cuisine/web/assets/ground_pork.png new file mode 100644 index 000000000..0d3167116 Binary files /dev/null and b/web/filipino_cuisine/web/assets/ground_pork.png differ diff --git a/web/filipino_cuisine/web/assets/lemon.png b/web/filipino_cuisine/web/assets/lemon.png new file mode 100644 index 000000000..4f9e3ea76 Binary files /dev/null and b/web/filipino_cuisine/web/assets/lemon.png differ diff --git a/web/filipino_cuisine/web/assets/oil.png b/web/filipino_cuisine/web/assets/oil.png new file mode 100644 index 000000000..e4c733566 Binary files /dev/null and b/web/filipino_cuisine/web/assets/oil.png differ diff --git a/web/filipino_cuisine/web/assets/onion.png b/web/filipino_cuisine/web/assets/onion.png new file mode 100644 index 000000000..3c0a9db8f Binary files /dev/null and b/web/filipino_cuisine/web/assets/onion.png differ diff --git a/web/filipino_cuisine/web/assets/pancit_canton.jpg b/web/filipino_cuisine/web/assets/pancit_canton.jpg new file mode 100644 index 000000000..31ddf6a3e Binary files /dev/null and b/web/filipino_cuisine/web/assets/pancit_canton.jpg differ diff --git a/web/filipino_cuisine/web/assets/pochero.jpg b/web/filipino_cuisine/web/assets/pochero.jpg new file mode 100644 index 000000000..2107e1dcc Binary files /dev/null and b/web/filipino_cuisine/web/assets/pochero.jpg differ diff --git a/web/filipino_cuisine/web/assets/pork.png b/web/filipino_cuisine/web/assets/pork.png new file mode 100644 index 000000000..38f94bc12 Binary files /dev/null and b/web/filipino_cuisine/web/assets/pork.png differ diff --git a/web/filipino_cuisine/web/assets/pork_sisig.jpg b/web/filipino_cuisine/web/assets/pork_sisig.jpg new file mode 100644 index 000000000..ed8eff4f0 Binary files /dev/null and b/web/filipino_cuisine/web/assets/pork_sisig.jpg differ diff --git a/web/filipino_cuisine/web/assets/potato.png b/web/filipino_cuisine/web/assets/potato.png new file mode 100644 index 000000000..2efa6e483 Binary files /dev/null and b/web/filipino_cuisine/web/assets/potato.png differ diff --git a/web/filipino_cuisine/web/assets/raisins.png b/web/filipino_cuisine/web/assets/raisins.png new file mode 100644 index 000000000..ae3514922 Binary files /dev/null and b/web/filipino_cuisine/web/assets/raisins.png differ diff --git a/web/filipino_cuisine/web/assets/recipes.json b/web/filipino_cuisine/web/assets/recipes.json new file mode 100644 index 000000000..80131bb10 --- /dev/null +++ b/web/filipino_cuisine/web/assets/recipes.json @@ -0,0 +1,366 @@ +[ + { + "cn": "120 Gr 85 Kcal 13 Min", + "ct": "3 minutes", + "dc": "Calamares is the Filipino version of the Mediterranean breaded fried squid dish, Calamari. ", + "fn": "Calamares", + "fv": false, + "ig": [ + { + "c": "1/2 lb", + "n": "Squid", + "p": "squid.png" + }, + { + "c": "3/4 cup", + "n": "Flour", + "p": "flour.png" + }, + { + "c": "1 pc", + "n": "Egg", + "p": "egg.png" + }, + { + "c": "1 tsp", + "n": "Salt", + "p": "salt.png" + }, + { + "c": "1/2 tsp", + "n": "Black Pepper", + "p": "black_pepper.png" + }, + { + "c": "2 cups", + "n": "Cooking Oil", + "p": "oil.png" + } + ], + "in": [ + "Combine squid, salt, and ground black pepper then mix well. Let stand for 10 minutes", + "Heat a cooking pot the pour-in cooking oil", + "Dredge the squid in flour then dip in beaten egg and roll over", + "When the oil is hot enough, deep-fry the squid until the color of the coating turns brown", + "Remove the fried squid from the cooking pot and transfer in a plate lined with paper towels", + "Serve with sinamak or Asian dipping sauce" + ], + "pf": "calamares.jpg", + "pt": "10 minutes", + "sv": "3", + "tt": "13 MIN" + }, + { + "cn": "260 Gr 293 Kcal 1 Hour 42 Min", + "ct": "1 hour 30 minutes", + "dc": "Sisig is a popular Filipino dish. It is composed of minced pork, chopped onion, and mayonnaise.", + "fn": "Pork Sisig", + "fv": false, + "ig": [ + { + "c": "1 1/2 lb", + "n": "Pork Meat", + "p": "pork.png" + }, + { + "c": "1 pc", + "n": "Onion", + "p": "onion.png" + }, + { + "c": "3 tsp", + "n": "Chili", + "p": "chili.png" + }, + { + "c": "1/2 tsp", + "n": "Garlic", + "p": "garlic.png" + }, + { + "c": "1/4 tsp", + "n": "Black Pepper", + "p": "black_pepper.png" + }, + { + "c": "1 pc", + "n": "Lemon", + "p": "lemon.png" + }, + { + "c": "1/2 cup", + "n": "Butter", + "p": "butter.png" + }, + { + "c": "1/2 tsp", + "n": "Salt", + "p": "salt.png" + }, + { + "c": "1 pc", + "n": "Egg", + "p": "egg.png" + } + ], + "in": [ + "Pour the water in a pan and bring to a boil Add salt and pepper", + "Put-in the pork meat then simmer for 40 minutes to 1 hour (or until tender)", + "Grill the boiled pork meat until done", + "Chop the pork meat into fine pieces", + "In a wide pan, melt the butter. Add the onions. Cook until onions are soft.", + "Put-in the ginger and cook for 2 minutes", + "Add the pork meat. Cook for 10 to 12 minutes", + "Put-in the soy sauce, garlic powder, and chili. Mix well", + "Add salt and pepper to taste", + "Put-in the mayonnaise and mix with the other ingredients", + "Transfer to a serving plate. Top with chopped green onions and raw egg", + "Add the lemon before eating" + ], + "pf": "pork_sisig.jpg", + "pt": "12 minutes", + "sv": "6", + "tt": "1 hour 42 minutes" + }, + { + "cn": "120 Gr 168 Kcal 1 Hour 10 Min", + "ct": "3 minutes", + "dc": "Pochero or Puchero is a well-loved Filipino stew. Made with meat, tomatoes, and saba bananas. ", + "fn": "Pochero", + "fv": false, + "ig": [ + { + "c": "1 large", + "n": "Banana", + "p": "banana.png" + }, + { + "c": "2 pcs", + "n": "Tomato", + "p": "tomato.png" + }, + { + "c": "1 lb", + "n": "Pork Meat", + "p": "pork.png" + }, + { + "c": "1 pc", + "n": "Onion", + "p": "onion.png" + }, + { + "c": "1 tsp", + "n": "Garlic", + "p": "garlic.png" + }, + { + "c": "1 tbsp", + "n": "Peppercorn", + "p": "black_pepper.png" + }, + { + "c": "1 medium", + "n": "Potato", + "p": "potato.png" + }, + { + "c": "1 small", + "n": "Cabbage", + "p": "cabbage.png" + }, + { + "c": "1 bunch", + "n": "Bok Choy", + "p": "bokchoy.png" + }, + { + "c": "1/4 lb", + "n": "Green Beans", + "p": "green_beans.png" + }, + { + "c": "2 tbsp", + "n": "Cooking Oil", + "p": "oil.png" + } + ], + "in": [ + "Heat cooking oil in a cooking pot", + "Sauté garlic, onions, and tomatoes", + "Add pork and cook until the color turns light brown", + "Put-in fish sauce, whole pepper corn, and tomato sauce. Stir.", + "Add water and let boil. Simmer until pork is tender (about 30 to 40 minutes)", + "Put-in potato, plantain, and chick peas. Cook for 5 to 7 minutes.", + "Add cabbage and long green beans. Cook for 5 minutes", + "Stir-in the bok choy. Cover the pot and turn off the heat", + "Let the residual heat cook the bok choy (about 5 minutes)", + "Transfer to a serving plate and serve" + ], + "pf": "pochero.jpg", + "pt": "10 minutes", + "sv": "3", + "tt": "13 MIN" + }, + { + "cn": "140 Gr 250 Kcal 1 Hour 30 Min", + "ct": "3 minutes", + "dc": "A type of Filipino Beef Stew. This dish is cooked in a tomato-based sauce with vegetables such as potato, carrot, and bell pepper.", + "fn": "Beef Caldereta", + "fv": false, + "ig": [ + { + "c": "1/2 lb", + "n": "Beef", + "p": "beef.png" + }, + { + "c": "2 medium", + "n": "Carrot", + "p": "carrot.png" + }, + { + "c": "1 large", + "n": "Potato", + "p": "potato.png" + }, + { + "c": "1 small", + "n": "Green Bell Pepper", + "p": "green_bell.png" + }, + { + "c": "1 small", + "n": "Red Bell Pepper", + "p": "red_bell.png" + }, + { + "c": "2 Cloves", + "n": "Garlic", + "p": "garlic.png" + }, + { + "c": "1 medium", + "n": "Yellow Onion", + "p": "yellow_onion.png" + }, + { + "c": "1 tsp", + "n": "Salt", + "p": "salt.png" + }, + { + "c": "6 tbsp", + "n": "Cooking oil", + "p": "oil.png" + }, + { + "c": "1 tsp", + "n": "Peppercorn", + "p": "black_pepper.png" + }, + { + "c": "1 tsp", + "n": "Red Pepper", + "p": "chili.png" + } + ], + "in": [ + "Heat a pan or wok and then pour 3 tablespoons cooking oil. Stir-fry the bell peppers for 3 minutes. Remove the bell peppers and put in a plate. Set aside", + "Using the oil in the pan (add more if necessary), pan fry the carrots and potato for 3 to 5 minutes. Put these in a plate and then set aside", + "Heat the remaining 3 tablespoons of oil in a clean pot", + "Sauté the garlic and onion", + "Add the beef. Cook until it turns light brown", + "Pour –in tomato sauce and water. Let boil", + "Continue to cook in low heat for 60 minutes or until the beef gets tender. Add more water if needed", + "Stir-in the liver spread and then add some salt and pepper", + "Put the pan-fried potato and carrots in the pot. Stir. Add the bell peppers", + "Cover the pot. Continue to cook for 5 minutes", + "Add the red pepper flakes. Stir and cook for 3 minutes more", + "Transfer to a serving plate. Serve" + ], + "pf": "beef_caldereta.jpg", + "pt": "10 minutes", + "sv": "3", + "tt": "13 MIN" + }, + { + "cn": "90 Gr 130 Kcal 1 Hour 15 Min", + "ct": "3 minutes", + "dc": "Pork Embutido is a Filipino-style meatloaf made with a festive mixture of ground pork, carrots, and raisins wrapped around slices of eggs and sausage.", + "fn": "Embutido", + "fv": false, + "ig": [ + { + "c": "2 lbs", + "n": "Ground pork", + "p": "ground_pork.png" + }, + { + "c": "12 pcs", + "n": "Sausage", + "p": "sausage.png" + }, + { + "c": "3 pcs", + "n": "Egg", + "p": "egg.png" + }, + { + "c": "2 cups", + "n": "Cheese", + "p": "cheese.png" + }, + { + "c": "1 cup", + "n": "Red Bell Pepper", + "p": "red_bell.png" + }, + { + "c": "1 cup", + "n": "Green Bell Pepper", + "p": "green_bell.png" + }, + { + "c": "1/2 cup", + "n": "Raisins", + "p": "raisins.png" + }, + { + "c": "1/2 cup", + "n": "Carrot", + "p": "carrot.png" + }, + { + "c": "1/2 cup", + "n": "Onion", + "p": "onion.png" + }, + { + "c": "2 tbsp", + "n": "Salt", + "p": "salt.png" + }, + { + "c": "1 tbsp", + "n": "Peppercorn", + "p": "black_pepper.png" + } + ], + "in": [ + "Place the ground pork in a large container", + "Add the bread crumbs then break the raw eggs and add it in. Mix well", + "Put-in the carrots, bell pepper (red and green), onion, pickle relish, and cheddar cheese. Mix thoroughly", + "Add the raisins, tomato sauce, salt, and pepper then mix well.", + "Put in the sliced vienna sausage and sliced boiled eggs alternately on the middle of the flat meat mixture.", + "Roll the foil to form a cylinder — locking the sausage and egg in the middle if the meat mixture. Once done, lock the edges of the foil.", + "Place in a steamer and let cook for 1 hour.", + "Place inside the refrigerator until temperature turns cold", + "Slice and serve" + ], + "pf": "embutido.jpg", + "pt": "10 minutes", + "sv": "3", + "tt": "13 MIN" + } +] \ No newline at end of file diff --git a/web/filipino_cuisine/web/assets/red_bell.png b/web/filipino_cuisine/web/assets/red_bell.png new file mode 100644 index 000000000..c8147a1de Binary files /dev/null and b/web/filipino_cuisine/web/assets/red_bell.png differ diff --git a/web/filipino_cuisine/web/assets/red_pepper.png b/web/filipino_cuisine/web/assets/red_pepper.png new file mode 100644 index 000000000..2e6393cd5 Binary files /dev/null and b/web/filipino_cuisine/web/assets/red_pepper.png differ diff --git a/web/filipino_cuisine/web/assets/salt.png b/web/filipino_cuisine/web/assets/salt.png new file mode 100644 index 000000000..a151a5792 Binary files /dev/null and b/web/filipino_cuisine/web/assets/salt.png differ diff --git a/web/filipino_cuisine/web/assets/sausage.png b/web/filipino_cuisine/web/assets/sausage.png new file mode 100644 index 000000000..34b8e8d7b Binary files /dev/null and b/web/filipino_cuisine/web/assets/sausage.png differ diff --git a/web/filipino_cuisine/web/assets/smoked_salmon.jpg b/web/filipino_cuisine/web/assets/smoked_salmon.jpg new file mode 100644 index 000000000..fd6694b37 Binary files /dev/null and b/web/filipino_cuisine/web/assets/smoked_salmon.jpg differ diff --git a/web/filipino_cuisine/web/assets/squid.png b/web/filipino_cuisine/web/assets/squid.png new file mode 100644 index 000000000..beafe4a73 Binary files /dev/null and b/web/filipino_cuisine/web/assets/squid.png differ diff --git a/web/filipino_cuisine/web/assets/sweet_and_sour_chicken_poultry.jpg b/web/filipino_cuisine/web/assets/sweet_and_sour_chicken_poultry.jpg new file mode 100644 index 000000000..ad225dfc8 Binary files /dev/null and b/web/filipino_cuisine/web/assets/sweet_and_sour_chicken_poultry.jpg differ diff --git a/web/filipino_cuisine/web/assets/tomato.png b/web/filipino_cuisine/web/assets/tomato.png new file mode 100644 index 000000000..cb7316f41 Binary files /dev/null and b/web/filipino_cuisine/web/assets/tomato.png differ diff --git a/web/filipino_cuisine/web/assets/yellow_onion.png b/web/filipino_cuisine/web/assets/yellow_onion.png new file mode 100644 index 000000000..c03e83194 Binary files /dev/null and b/web/filipino_cuisine/web/assets/yellow_onion.png differ diff --git a/web/filipino_cuisine/web/index.html b/web/filipino_cuisine/web/index.html new file mode 100644 index 000000000..b54ed98d8 --- /dev/null +++ b/web/filipino_cuisine/web/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/filipino_cuisine/web/main.dart b/web/filipino_cuisine/web/main.dart new file mode 100644 index 000000000..95b5427ac --- /dev/null +++ b/web/filipino_cuisine/web/main.dart @@ -0,0 +1,10 @@ +// 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 'package:flutter_web_ui/ui.dart' as ui; +import 'package:filipino_cuisine/main.dart' as app; + +main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/filipino_cuisine/web/preview.png b/web/filipino_cuisine/web/preview.png new file mode 100644 index 000000000..c05c393b2 Binary files /dev/null and b/web/filipino_cuisine/web/preview.png differ diff --git a/web/gallery/README.md b/web/gallery/README.md new file mode 100644 index 000000000..b7a2de496 --- /dev/null +++ b/web/gallery/README.md @@ -0,0 +1 @@ +A [gallery](https://github.com/flutter/flutter/tree/master/examples/flutter_gallery) of Flutter widgets and UX studies. diff --git a/web/gallery/build.yaml b/web/gallery/build.yaml new file mode 100644 index 000000000..c897b2e6d --- /dev/null +++ b/web/gallery/build.yaml @@ -0,0 +1,7 @@ +# See https://github.com/dart-lang/build/tree/master/build_web_compilers#configuration +targets: + $default: + builders: + build_web_compilers|entrypoint: + # Avoid building the test directory. + generate_for: ['web/**.dart'] diff --git a/web/gallery/lib/demo/all.dart b/web/gallery/lib/demo/all.dart new file mode 100644 index 000000000..9ebcbc82c --- /dev/null +++ b/web/gallery/lib/demo/all.dart @@ -0,0 +1,15 @@ +// Copyright 2018 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. + +export 'animation_demo.dart'; +//export 'calculator_demo.dart'; +export 'colors_demo.dart'; +export 'contacts_demo.dart'; +//export 'cupertino/cupertino.dart'; +//export 'images_demo.dart'; +export 'material/material.dart'; +export 'pesto_demo.dart'; +export 'shrine_demo.dart'; +export 'typography_demo.dart'; +//export 'video_demo.dart'; diff --git a/web/gallery/lib/demo/animation/home.dart b/web/gallery/lib/demo/animation/home.dart new file mode 100644 index 000000000..5fa2bd416 --- /dev/null +++ b/web/gallery/lib/demo/animation/home.dart @@ -0,0 +1,660 @@ +// Copyright 2018 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:math' as math; + +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; + +import 'sections.dart'; +import 'widgets.dart'; + +const Color _kAppBackgroundColor = Color(0xFF353662); +const Duration _kScrollDuration = Duration(milliseconds: 400); +const Curve _kScrollCurve = Curves.fastOutSlowIn; + +// This app's contents start out at _kHeadingMaxHeight and they function like +// an appbar. Initially the appbar occupies most of the screen and its section +// headings are laid out in a column. By the time its height has been +// reduced to _kAppBarMidHeight, its layout is horizontal, only one section +// heading is visible, and the section's list of details is visible below the +// heading. The appbar's height can be reduced to no more than _kAppBarMinHeight. +const double _kAppBarMinHeight = 90.0; +const double _kAppBarMidHeight = 256.0; +// The AppBar's max height depends on the screen, see _AnimationDemoHomeState._buildBody() + +// Initially occupies the same space as the status bar and gets smaller as +// the primary scrollable scrolls upwards. +// TODO(hansmuller): it would be worth adding something like this to the framework. +class _RenderStatusBarPaddingSliver extends RenderSliver { + _RenderStatusBarPaddingSliver({ + @required double maxHeight, + @required double scrollFactor, + }) : assert(maxHeight != null && maxHeight >= 0.0), + assert(scrollFactor != null && scrollFactor >= 1.0), + _maxHeight = maxHeight, + _scrollFactor = scrollFactor; + + // The height of the status bar + double get maxHeight => _maxHeight; + double _maxHeight; + set maxHeight(double value) { + assert(maxHeight != null && maxHeight >= 0.0); + if (_maxHeight == value) return; + _maxHeight = value; + markNeedsLayout(); + } + + // That rate at which this renderer's height shrinks when the scroll + // offset changes. + double get scrollFactor => _scrollFactor; + double _scrollFactor; + set scrollFactor(double value) { + assert(scrollFactor != null && scrollFactor >= 1.0); + if (_scrollFactor == value) return; + _scrollFactor = value; + markNeedsLayout(); + } + + @override + void performLayout() { + final double height = (maxHeight - constraints.scrollOffset / scrollFactor) + .clamp(0.0, maxHeight); + geometry = SliverGeometry( + paintExtent: math.min(height, constraints.remainingPaintExtent), + scrollExtent: maxHeight, + maxPaintExtent: maxHeight, + ); + } +} + +class _StatusBarPaddingSliver extends SingleChildRenderObjectWidget { + const _StatusBarPaddingSliver({ + Key key, + @required this.maxHeight, + this.scrollFactor = 5.0, + }) : assert(maxHeight != null && maxHeight >= 0.0), + assert(scrollFactor != null && scrollFactor >= 1.0), + super(key: key); + + final double maxHeight; + final double scrollFactor; + + @override + _RenderStatusBarPaddingSliver createRenderObject(BuildContext context) { + return _RenderStatusBarPaddingSliver( + maxHeight: maxHeight, + scrollFactor: scrollFactor, + ); + } + + @override + void updateRenderObject( + BuildContext context, _RenderStatusBarPaddingSliver renderObject) { + renderObject + ..maxHeight = maxHeight + ..scrollFactor = scrollFactor; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(DoubleProperty('maxHeight', maxHeight)); + description.add(DoubleProperty('scrollFactor', scrollFactor)); + } +} + +class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { + _SliverAppBarDelegate({ + @required this.minHeight, + @required this.maxHeight, + @required this.child, + }); + + final double minHeight; + final double maxHeight; + final Widget child; + + @override + double get minExtent => minHeight; + @override + double get maxExtent => math.max(maxHeight, minHeight); + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return SizedBox.expand(child: child); + } + + @override + bool shouldRebuild(_SliverAppBarDelegate oldDelegate) { + return maxHeight != oldDelegate.maxHeight || + minHeight != oldDelegate.minHeight || + child != oldDelegate.child; + } + + @override + String toString() => '_SliverAppBarDelegate'; +} + +// Arrange the section titles, indicators, and cards. The cards are only included when +// the layout is transitioning between vertical and horizontal. Once the layout is +// horizontal the cards are laid out by a PageView. +// +// The layout of the section cards, titles, and indicators is defined by the +// two 0.0-1.0 "t" parameters, both of which are based on the layout's height: +// - tColumnToRow +// 0.0 when height is maxHeight and the layout is a column +// 1.0 when the height is midHeight and the layout is a row +// - tCollapsed +// 0.0 when height is midHeight and the layout is a row +// 1.0 when height is minHeight and the layout is a (still) row +// +// minHeight < midHeight < maxHeight +// +// The general approach here is to compute the column layout and row size +// and position of each element and then interpolate between them using +// tColumnToRow. Once tColumnToRow reaches 1.0, the layout changes are +// defined by tCollapsed. As tCollapsed increases the titles spread out +// until only one title is visible and the indicators cluster together +// until they're all visible. +class _AllSectionsLayout extends MultiChildLayoutDelegate { + _AllSectionsLayout({ + this.translation, + this.tColumnToRow, + this.tCollapsed, + this.cardCount, + this.selectedIndex, + }); + + final Alignment translation; + final double tColumnToRow; + final double tCollapsed; + final int cardCount; + final double selectedIndex; + + Rect _interpolateRect(Rect begin, Rect end) { + return Rect.lerp(begin, end, tColumnToRow); + } + + Offset _interpolatePoint(Offset begin, Offset end) { + return Offset.lerp(begin, end, tColumnToRow); + } + + @override + void performLayout(Size size) { + final double columnCardX = size.width / 5.0; + final double columnCardWidth = size.width - columnCardX; + final double columnCardHeight = size.height / cardCount; + final double rowCardWidth = size.width; + final Offset offset = translation.alongSize(size); + double columnCardY = 0.0; + double rowCardX = -(selectedIndex * rowCardWidth); + + // When tCollapsed > 0 the titles spread apart + final double columnTitleX = size.width / 10.0; + final double rowTitleWidth = size.width * ((1 + tCollapsed) / 2.25); + double rowTitleX = + (size.width - rowTitleWidth) / 2.0 - selectedIndex * rowTitleWidth; + + // When tCollapsed > 0, the indicators move closer together + //final double rowIndicatorWidth = 48.0 + (1.0 - tCollapsed) * (rowTitleWidth - 48.0); + const double paddedSectionIndicatorWidth = kSectionIndicatorWidth + 8.0; + final double rowIndicatorWidth = paddedSectionIndicatorWidth + + (1.0 - tCollapsed) * (rowTitleWidth - paddedSectionIndicatorWidth); + double rowIndicatorX = (size.width - rowIndicatorWidth) / 2.0 - + selectedIndex * rowIndicatorWidth; + + // Compute the size and origin of each card, title, and indicator for the maxHeight + // "column" layout, and the midHeight "row" layout. The actual layout is just the + // interpolated value between the column and row layouts for t. + for (int index = 0; index < cardCount; index++) { + // Layout the card for index. + final Rect columnCardRect = Rect.fromLTWH( + columnCardX, columnCardY, columnCardWidth, columnCardHeight); + final Rect rowCardRect = + Rect.fromLTWH(rowCardX, 0.0, rowCardWidth, size.height); + final Rect cardRect = + _interpolateRect(columnCardRect, rowCardRect).shift(offset); + final String cardId = 'card$index'; + if (hasChild(cardId)) { + layoutChild(cardId, BoxConstraints.tight(cardRect.size)); + positionChild(cardId, cardRect.topLeft); + } + + // Layout the title for index. + final Size titleSize = + layoutChild('title$index', BoxConstraints.loose(cardRect.size)); + final double columnTitleY = + columnCardRect.centerLeft.dy - titleSize.height / 2.0; + final double rowTitleY = + rowCardRect.centerLeft.dy - titleSize.height / 2.0; + final double centeredRowTitleX = + rowTitleX + (rowTitleWidth - titleSize.width) / 2.0; + final Offset columnTitleOrigin = Offset(columnTitleX, columnTitleY); + final Offset rowTitleOrigin = Offset(centeredRowTitleX, rowTitleY); + final Offset titleOrigin = + _interpolatePoint(columnTitleOrigin, rowTitleOrigin); + positionChild('title$index', titleOrigin + offset); + + // Layout the selection indicator for index. + final Size indicatorSize = + layoutChild('indicator$index', BoxConstraints.loose(cardRect.size)); + final double columnIndicatorX = + cardRect.centerRight.dx - indicatorSize.width - 16.0; + final double columnIndicatorY = + cardRect.bottomRight.dy - indicatorSize.height - 16.0; + final Offset columnIndicatorOrigin = + Offset(columnIndicatorX, columnIndicatorY); + final Rect titleRect = + Rect.fromPoints(titleOrigin, titleSize.bottomRight(titleOrigin)); + final double centeredRowIndicatorX = + rowIndicatorX + (rowIndicatorWidth - indicatorSize.width) / 2.0; + final double rowIndicatorY = titleRect.bottomCenter.dy + 16.0; + final Offset rowIndicatorOrigin = + Offset(centeredRowIndicatorX, rowIndicatorY); + final Offset indicatorOrigin = + _interpolatePoint(columnIndicatorOrigin, rowIndicatorOrigin); + positionChild('indicator$index', indicatorOrigin + offset); + + columnCardY += columnCardHeight; + rowCardX += rowCardWidth; + rowTitleX += rowTitleWidth; + rowIndicatorX += rowIndicatorWidth; + } + } + + @override + bool shouldRelayout(_AllSectionsLayout oldDelegate) { + return tColumnToRow != oldDelegate.tColumnToRow || + cardCount != oldDelegate.cardCount || + selectedIndex != oldDelegate.selectedIndex; + } +} + +class _AllSectionsView extends AnimatedWidget { + _AllSectionsView({ + Key key, + this.sectionIndex, + @required this.sections, + @required this.selectedIndex, + this.minHeight, + this.midHeight, + this.maxHeight, + this.sectionCards = const [], + }) : assert(sections != null), + assert(sectionCards != null), + assert(sectionCards.length == sections.length), + assert(sectionIndex >= 0 && sectionIndex < sections.length), + assert(selectedIndex != null), + assert(selectedIndex.value >= 0.0 && + selectedIndex.value < sections.length.toDouble()), + super(key: key, listenable: selectedIndex); + + final int sectionIndex; + final List

sections; + final ValueNotifier selectedIndex; + final double minHeight; + final double midHeight; + final double maxHeight; + final List sectionCards; + + double _selectedIndexDelta(int index) { + return (index.toDouble() - selectedIndex.value).abs().clamp(0.0, 1.0); + } + + Widget _build(BuildContext context, BoxConstraints constraints) { + final Size size = constraints.biggest; + + // The layout's progress from from a column to a row. Its value is + // 0.0 when size.height equals the maxHeight, 1.0 when the size.height + // equals the midHeight. + final double tColumnToRow = 1.0 - + ((size.height - midHeight) / (maxHeight - midHeight)).clamp(0.0, 1.0); + + // The layout's progress from from the midHeight row layout to + // a minHeight row layout. Its value is 0.0 when size.height equals + // midHeight and 1.0 when size.height equals minHeight. + final double tCollapsed = 1.0 - + ((size.height - minHeight) / (midHeight - minHeight)).clamp(0.0, 1.0); + + double _indicatorOpacity(int index) { + return 1.0 - _selectedIndexDelta(index) * 0.5; + } + + double _titleOpacity(int index) { + return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.5; + } + + double _titleScale(int index) { + return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.15; + } + + final List children = List.from(sectionCards); + + for (int index = 0; index < sections.length; index++) { + final Section section = sections[index]; + children.add(LayoutId( + id: 'title$index', + child: SectionTitle( + section: section, + scale: _titleScale(index), + opacity: _titleOpacity(index), + ), + )); + } + + for (int index = 0; index < sections.length; index++) { + children.add(LayoutId( + id: 'indicator$index', + child: SectionIndicator( + opacity: _indicatorOpacity(index), + ), + )); + } + + return CustomMultiChildLayout( + delegate: _AllSectionsLayout( + translation: + Alignment((selectedIndex.value - sectionIndex) * 2.0 - 1.0, -1.0), + tColumnToRow: tColumnToRow, + tCollapsed: tCollapsed, + cardCount: sections.length, + selectedIndex: selectedIndex.value, + ), + children: children, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: _build); + } +} + +// Support snapping scrolls to the midScrollOffset: the point at which the +// app bar's height is _kAppBarMidHeight and only one section heading is +// visible. +class _SnappingScrollPhysics extends ClampingScrollPhysics { + const _SnappingScrollPhysics({ + ScrollPhysics parent, + @required this.midScrollOffset, + }) : assert(midScrollOffset != null), + super(parent: parent); + + final double midScrollOffset; + + @override + _SnappingScrollPhysics applyTo(ScrollPhysics ancestor) { + return _SnappingScrollPhysics( + parent: buildParent(ancestor), midScrollOffset: midScrollOffset); + } + + Simulation _toMidScrollOffsetSimulation(double offset, double dragVelocity) { + final double velocity = math.max(dragVelocity, minFlingVelocity); + return ScrollSpringSimulation(spring, offset, midScrollOffset, velocity, + tolerance: tolerance); + } + + Simulation _toZeroScrollOffsetSimulation(double offset, double dragVelocity) { + final double velocity = math.max(dragVelocity, minFlingVelocity); + return ScrollSpringSimulation(spring, offset, 0.0, velocity, + tolerance: tolerance); + } + + @override + Simulation createBallisticSimulation( + ScrollMetrics position, double dragVelocity) { + final Simulation simulation = + super.createBallisticSimulation(position, dragVelocity); + final double offset = position.pixels; + + if (simulation != null) { + // The drag ended with sufficient velocity to trigger creating a simulation. + // If the simulation is headed up towards midScrollOffset but will not reach it, + // then snap it there. Similarly if the simulation is headed down past + // midScrollOffset but will not reach zero, then snap it to zero. + final double simulationEnd = simulation.x(double.infinity); + if (simulationEnd >= midScrollOffset) return simulation; + if (dragVelocity > 0.0) + return _toMidScrollOffsetSimulation(offset, dragVelocity); + if (dragVelocity < 0.0) + return _toZeroScrollOffsetSimulation(offset, dragVelocity); + } else { + // The user ended the drag with little or no velocity. If they + // didn't leave the offset above midScrollOffset, then + // snap to midScrollOffset if they're more than halfway there, + // otherwise snap to zero. + final double snapThreshold = midScrollOffset / 2.0; + if (offset >= snapThreshold && offset < midScrollOffset) + return _toMidScrollOffsetSimulation(offset, dragVelocity); + if (offset > 0.0 && offset < snapThreshold) + return _toZeroScrollOffsetSimulation(offset, dragVelocity); + } + return simulation; + } +} + +class AnimationDemoHome extends StatefulWidget { + const AnimationDemoHome({Key key}) : super(key: key); + + static const String routeName = '/animation'; + + @override + _AnimationDemoHomeState createState() => _AnimationDemoHomeState(); +} + +class _AnimationDemoHomeState extends State { + final ScrollController _scrollController = ScrollController(); + final PageController _headingPageController = PageController(); + final PageController _detailsPageController = PageController(); + ScrollPhysics _headingScrollPhysics = const NeverScrollableScrollPhysics(); + ValueNotifier selectedIndex = ValueNotifier(0.0); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _kAppBackgroundColor, + body: Builder( + // Insert an element so that _buildBody can find the PrimaryScrollController. + builder: _buildBody, + ), + ); + } + + void _handleBackButton(double midScrollOffset) { + if (_scrollController.offset >= midScrollOffset) + _scrollController.animateTo(0.0, + curve: _kScrollCurve, duration: _kScrollDuration); + else + Navigator.maybePop(context); + } + + // Only enable paging for the heading when the user has scrolled to midScrollOffset. + // Paging is enabled/disabled by setting the heading's PageView scroll physics. + bool _handleScrollNotification( + ScrollNotification notification, double midScrollOffset) { + if (notification.depth == 0 && notification is ScrollUpdateNotification) { + final ScrollPhysics physics = + _scrollController.position.pixels >= midScrollOffset + ? const PageScrollPhysics() + : const NeverScrollableScrollPhysics(); + if (physics != _headingScrollPhysics) { + setState(() { + _headingScrollPhysics = physics; + }); + } + } + return false; + } + + void _maybeScroll(double midScrollOffset, int pageIndex, double xOffset) { + if (_scrollController.offset < midScrollOffset) { + // Scroll the overall list to the point where only one section card shows. + // At the same time scroll the PageViews to the page at pageIndex. + _headingPageController.animateToPage(pageIndex, + curve: _kScrollCurve, duration: _kScrollDuration); + _scrollController.animateTo(midScrollOffset, + curve: _kScrollCurve, duration: _kScrollDuration); + } else { + // One one section card is showing: scroll one page forward or back. + final double centerX = + _headingPageController.position.viewportDimension / 2.0; + final int newPageIndex = + xOffset > centerX ? pageIndex + 1 : pageIndex - 1; + _headingPageController.animateToPage(newPageIndex, + curve: _kScrollCurve, duration: _kScrollDuration); + } + } + + bool _handlePageNotification(ScrollNotification notification, + PageController leader, PageController follower) { + if (notification.depth == 0 && notification is ScrollUpdateNotification) { + selectedIndex.value = leader.page; + if (follower.page != leader.page) + // ignore: deprecated_member_use + follower.position.jumpToWithoutSettling(leader.position.pixels); + } + return false; + } + + Iterable _detailItemsFor(Section section) { + final Iterable detailItems = + section.details.map((SectionDetail detail) { + return SectionDetailView(detail: detail); + }); + return ListTile.divideTiles(context: context, tiles: detailItems); + } + + Iterable _allHeadingItems(double maxHeight, double midScrollOffset) { + final List sectionCards = []; + for (int index = 0; index < allSections.length; index++) { + sectionCards.add(LayoutId( + id: 'card$index', + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: SectionCard(section: allSections[index]), + onTapUp: (TapUpDetails details) { + final double xOffset = details.globalPosition.dx; + setState(() { + _maybeScroll(midScrollOffset, index, xOffset); + }); + }), + )); + } + + final List headings = []; + for (int index = 0; index < allSections.length; index++) { + headings.add(Container( + color: _kAppBackgroundColor, + child: ClipRect( + child: _AllSectionsView( + sectionIndex: index, + sections: allSections, + selectedIndex: selectedIndex, + minHeight: _kAppBarMinHeight, + midHeight: _kAppBarMidHeight, + maxHeight: maxHeight, + sectionCards: sectionCards, + ), + ), + )); + } + return headings; + } + + Widget _buildBody(BuildContext context) { + final MediaQueryData mediaQueryData = MediaQuery.of(context); + final double statusBarHeight = mediaQueryData.padding.top; + final double screenHeight = mediaQueryData.size.height; + final double appBarMaxHeight = screenHeight - statusBarHeight; + + // The scroll offset that reveals the appBarMidHeight appbar. + final double appBarMidScrollOffset = + statusBarHeight + appBarMaxHeight - _kAppBarMidHeight; + + return SizedBox.expand( + child: Stack( + children: [ + NotificationListener( + onNotification: (ScrollNotification notification) { + return _handleScrollNotification( + notification, appBarMidScrollOffset); + }, + child: CustomScrollView( + controller: _scrollController, + physics: _SnappingScrollPhysics( + midScrollOffset: appBarMidScrollOffset), + slivers: [ + // Start out below the status bar, gradually move to the top of the screen. + _StatusBarPaddingSliver( + maxHeight: statusBarHeight, + scrollFactor: 7.0, + ), + // Section Headings + SliverPersistentHeader( + pinned: true, + delegate: _SliverAppBarDelegate( + minHeight: _kAppBarMinHeight, + maxHeight: appBarMaxHeight, + child: NotificationListener( + onNotification: (ScrollNotification notification) { + return _handlePageNotification(notification, + _headingPageController, _detailsPageController); + }, + child: PageView( + physics: _headingScrollPhysics, + controller: _headingPageController, + children: _allHeadingItems( + appBarMaxHeight, appBarMidScrollOffset), + ), + ), + ), + ), + // Details + SliverToBoxAdapter( + child: SizedBox( + height: 610.0, + child: NotificationListener( + onNotification: (ScrollNotification notification) { + return _handlePageNotification(notification, + _detailsPageController, _headingPageController); + }, + child: PageView( + controller: _detailsPageController, + children: allSections.map((Section section) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _detailItemsFor(section).toList(), + ); + }).toList(), + ), + ), + ), + ), + ], + ), + ), + Positioned( + top: statusBarHeight, + left: 0.0, + child: IconTheme( + data: const IconThemeData(color: Colors.white), + child: SafeArea( + top: false, + bottom: false, + child: IconButton( + icon: const BackButtonIcon(), + tooltip: 'Back', + onPressed: () { + _handleBackButton(appBarMidScrollOffset); + }), + ), + ), + ), + ], + ), + ); + } +} diff --git a/web/gallery/lib/demo/animation/sections.dart b/web/gallery/lib/demo/animation/sections.dart new file mode 100644 index 000000000..19cf4e40b --- /dev/null +++ b/web/gallery/lib/demo/animation/sections.dart @@ -0,0 +1,167 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +const Color _mariner = Color(0xFF3B5F8F); +const Color _mediumPurple = Color(0xFF8266D4); +const Color _tomato = Color(0xFFF95B57); +const Color _mySin = Color(0xFFF3A646); + +const String _kGalleryAssetsPackage = null; + +class SectionDetail { + const SectionDetail({ + this.title, + this.subtitle, + this.imageAsset, + this.imageAssetPackage, + }); + final String title; + final String subtitle; + final String imageAsset; + final String imageAssetPackage; +} + +class Section { + const Section({ + this.title, + this.backgroundAsset, + this.backgroundAssetPackage, + this.leftColor, + this.rightColor, + this.details, + }); + final String title; + final String backgroundAsset; + final String backgroundAssetPackage; + final Color leftColor; + final Color rightColor; + final List details; + + @override + bool operator ==(Object other) { + if (other is! Section) return false; + final Section otherSection = other; + return title == otherSection.title; + } + + @override + int get hashCode => title.hashCode; +} + +// TODO(hansmuller): replace the SectionDetail images and text. Get rid of +// the const vars like _eyeglassesDetail and insert a variety of titles and +// image SectionDetails in the allSections list. + +const SectionDetail _eyeglassesDetail = SectionDetail( + imageAsset: 'products/sunnies.png', + imageAssetPackage: _kGalleryAssetsPackage, + title: 'Flutter enables interactive animation', + subtitle: '3K views - 5 days', +); + +const SectionDetail _eyeglassesImageDetail = SectionDetail( + imageAsset: 'products/sunnies.png', + imageAssetPackage: _kGalleryAssetsPackage, +); + +const SectionDetail _seatingDetail = SectionDetail( + imageAsset: 'products/table.png', + imageAssetPackage: _kGalleryAssetsPackage, + title: 'Flutter enables interactive animation', + subtitle: '3K views - 5 days', +); + +const SectionDetail _seatingImageDetail = SectionDetail( + imageAsset: 'products/table.png', + imageAssetPackage: _kGalleryAssetsPackage, +); + +const SectionDetail _decorationDetail = SectionDetail( + imageAsset: 'products/earrings.png', + imageAssetPackage: _kGalleryAssetsPackage, + title: 'Flutter enables interactive animation', + subtitle: '3K views - 5 days', +); + +const SectionDetail _decorationImageDetail = SectionDetail( + imageAsset: 'products/earrings.png', + imageAssetPackage: _kGalleryAssetsPackage, +); + +const SectionDetail _protectionDetail = SectionDetail( + imageAsset: 'products/hat.png', + imageAssetPackage: _kGalleryAssetsPackage, + title: 'Flutter enables interactive animation', + subtitle: '3K views - 5 days', +); + +const SectionDetail _protectionImageDetail = SectionDetail( + imageAsset: 'products/hat.png', + imageAssetPackage: _kGalleryAssetsPackage, +); + +final List
allSections =
[ + const Section( + title: 'SUNGLASSES', + leftColor: _mediumPurple, + rightColor: _mariner, + backgroundAsset: 'products/sunnies.png', + backgroundAssetPackage: _kGalleryAssetsPackage, + details: [ + _eyeglassesDetail, + _eyeglassesImageDetail, + _eyeglassesDetail, + _eyeglassesDetail, + _eyeglassesDetail, + _eyeglassesDetail, + ], + ), + const Section( + title: 'FURNITURE', + leftColor: _tomato, + rightColor: _mediumPurple, + backgroundAsset: 'products/table.png', + backgroundAssetPackage: _kGalleryAssetsPackage, + details: [ + _seatingDetail, + _seatingImageDetail, + _seatingDetail, + _seatingDetail, + _seatingDetail, + _seatingDetail, + ], + ), + const Section( + title: 'JEWELRY', + leftColor: _mySin, + rightColor: _tomato, + backgroundAsset: 'products/earrings.png', + backgroundAssetPackage: _kGalleryAssetsPackage, + details: [ + _decorationDetail, + _decorationImageDetail, + _decorationDetail, + _decorationDetail, + _decorationDetail, + _decorationDetail, + ], + ), + const Section( + title: 'HEADWEAR', + leftColor: Colors.white, + rightColor: _tomato, + backgroundAsset: 'products/hat.png', + backgroundAssetPackage: _kGalleryAssetsPackage, + details: [ + _protectionDetail, + _protectionImageDetail, + _protectionDetail, + _protectionDetail, + _protectionDetail, + _protectionDetail, + ], + ), +]; diff --git a/web/gallery/lib/demo/animation/widgets.dart b/web/gallery/lib/demo/animation/widgets.dart new file mode 100644 index 000000000..b7a92d8d1 --- /dev/null +++ b/web/gallery/lib/demo/animation/widgets.dart @@ -0,0 +1,172 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import 'sections.dart'; + +const double kSectionIndicatorWidth = 32.0; + +// The card for a single section. Displays the section's gradient and background image. +class SectionCard extends StatelessWidget { + const SectionCard({Key key, @required this.section}) + : assert(section != null), + super(key: key); + + final Section section; + + @override + Widget build(BuildContext context) { + return Semantics( + label: section.title, + button: true, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + section.leftColor, + section.rightColor, + ], + ), + ), + // TODO(b:119312219): Remove Opacity layer when Image Color Filter + // is implemented in paintImage. + child: Opacity( + opacity: 0.075, + child: Image.asset( + section.backgroundAsset, + package: section.backgroundAssetPackage, + color: const Color.fromRGBO(255, 255, 255, 0.075), + colorBlendMode: BlendMode.modulate, + fit: BoxFit.cover, + ), + ), + ), + ); + } +} + +// The title is rendered with two overlapping text widgets that are vertically +// offset a little. It's supposed to look sort-of 3D. +class SectionTitle extends StatelessWidget { + const SectionTitle({ + Key key, + @required this.section, + @required this.scale, + @required this.opacity, + }) : assert(section != null), + assert(scale != null), + assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), + super(key: key); + + final Section section; + final double scale; + final double opacity; + + static const TextStyle sectionTitleStyle = TextStyle( + fontFamily: 'Raleway', + inherit: false, + fontSize: 24.0, + fontWeight: FontWeight.w500, + color: Colors.white, + textBaseline: TextBaseline.alphabetic, + ); + + static final TextStyle sectionTitleShadowStyle = sectionTitleStyle.copyWith( + color: const Color(0x19000000), + ); + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Opacity( + opacity: opacity, + child: Transform( + transform: Matrix4.identity()..scale(scale), + alignment: Alignment.center, + child: Stack( + children: [ + Positioned( + top: 4.0, + child: Text(section.title, style: sectionTitleShadowStyle), + ), + Text(section.title, style: sectionTitleStyle), + ], + ), + ), + ), + ); + } +} + +// Small horizontal bar that indicates the selected section. +class SectionIndicator extends StatelessWidget { + const SectionIndicator({Key key, this.opacity = 1.0}) : super(key: key); + + final double opacity; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Container( + width: kSectionIndicatorWidth, + height: 3.0, + color: Colors.white.withOpacity(opacity), + ), + ); + } +} + +// Display a single SectionDetail. +class SectionDetailView extends StatelessWidget { + SectionDetailView({Key key, @required this.detail}) + : assert(detail != null && detail.imageAsset != null), + assert((detail.imageAsset ?? detail.title) != null), + super(key: key); + + final SectionDetail detail; + + @override + Widget build(BuildContext context) { + final Widget image = DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + image: DecorationImage( + image: AssetImage( + detail.imageAsset, + package: detail.imageAssetPackage, + ), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + ); + + Widget item; + if (detail.title == null && detail.subtitle == null) { + item = Container( + height: 240.0, + padding: const EdgeInsets.all(16.0), + child: SafeArea( + top: false, + bottom: false, + child: image, + ), + ); + } else { + item = ListTile( + title: Text(detail.title), + subtitle: Text(detail.subtitle), + leading: SizedBox(width: 32.0, height: 32.0, child: image), + ); + } + + return DecoratedBox( + decoration: BoxDecoration(color: Colors.grey.shade200), + child: item, + ); + } +} diff --git a/web/gallery/lib/demo/animation_demo.dart b/web/gallery/lib/demo/animation_demo.dart new file mode 100644 index 000000000..74a705428 --- /dev/null +++ b/web/gallery/lib/demo/animation_demo.dart @@ -0,0 +1,16 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import 'animation/home.dart'; + +class AnimationDemo extends StatelessWidget { + const AnimationDemo({Key key}) : super(key: key); + + static const String routeName = '/animation'; + + @override + Widget build(BuildContext context) => const AnimationDemoHome(); +} diff --git a/web/gallery/lib/demo/colors_demo.dart b/web/gallery/lib/demo/colors_demo.dart new file mode 100644 index 000000000..ddc225f19 --- /dev/null +++ b/web/gallery/lib/demo/colors_demo.dart @@ -0,0 +1,222 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +const double kColorItemHeight = 48.0; + +class Palette { + Palette({this.name, this.primary, this.accent, this.threshold = 900}); + + final String name; + final MaterialColor primary; + final MaterialAccentColor accent; + final int + threshold; // titles for indices > threshold are white, otherwise black + + bool get isValid => name != null && primary != null && threshold != null; +} + +final List allPalettes = [ + Palette( + name: 'RED', + primary: Colors.red, + accent: Colors.redAccent, + threshold: 300), + Palette( + name: 'PINK', + primary: Colors.pink, + accent: Colors.pinkAccent, + threshold: 200), + Palette( + name: 'PURPLE', + primary: Colors.purple, + accent: Colors.purpleAccent, + threshold: 200), + Palette( + name: 'DEEP PURPLE', + primary: Colors.deepPurple, + accent: Colors.deepPurpleAccent, + threshold: 200), + Palette( + name: 'INDIGO', + primary: Colors.indigo, + accent: Colors.indigoAccent, + threshold: 200), + Palette( + name: 'BLUE', + primary: Colors.blue, + accent: Colors.blueAccent, + threshold: 400), + Palette( + name: 'LIGHT BLUE', + primary: Colors.lightBlue, + accent: Colors.lightBlueAccent, + threshold: 500), + Palette( + name: 'CYAN', + primary: Colors.cyan, + accent: Colors.cyanAccent, + threshold: 600), + Palette( + name: 'TEAL', + primary: Colors.teal, + accent: Colors.tealAccent, + threshold: 400), + Palette( + name: 'GREEN', + primary: Colors.green, + accent: Colors.greenAccent, + threshold: 500), + Palette( + name: 'LIGHT GREEN', + primary: Colors.lightGreen, + accent: Colors.lightGreenAccent, + threshold: 600), + Palette( + name: 'LIME', + primary: Colors.lime, + accent: Colors.limeAccent, + threshold: 800), + Palette(name: 'YELLOW', primary: Colors.yellow, accent: Colors.yellowAccent), + Palette(name: 'AMBER', primary: Colors.amber, accent: Colors.amberAccent), + Palette( + name: 'ORANGE', + primary: Colors.orange, + accent: Colors.orangeAccent, + threshold: 700), + Palette( + name: 'DEEP ORANGE', + primary: Colors.deepOrange, + accent: Colors.deepOrangeAccent, + threshold: 400), + Palette(name: 'BROWN', primary: Colors.brown, threshold: 200), + Palette(name: 'GREY', primary: Colors.grey, threshold: 500), + Palette(name: 'BLUE GREY', primary: Colors.blueGrey, threshold: 500), +]; + +class ColorItem extends StatelessWidget { + const ColorItem({ + Key key, + @required this.index, + @required this.color, + this.prefix = '', + }) : assert(index != null), + assert(color != null), + assert(prefix != null), + super(key: key); + + final int index; + final Color color; + final String prefix; + + String colorString() => + "#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}"; + + @override + Widget build(BuildContext context) { + return Semantics( + container: true, + child: Container( + height: kColorItemHeight, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + color: color, + child: SafeArea( + top: false, + bottom: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('$prefix$index'), + Text(colorString()), + ], + ), + ), + ), + ); + } +} + +class PaletteTabView extends StatelessWidget { + PaletteTabView({ + Key key, + @required this.colors, + }) : assert(colors != null && colors.isValid), + super(key: key); + + final Palette colors; + + static const List primaryKeys = [ + 50, + 100, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 900 + ]; + static const List accentKeys = [100, 200, 400, 700]; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final TextStyle whiteTextStyle = + textTheme.body1.copyWith(color: Colors.white); + final TextStyle blackTextStyle = + textTheme.body1.copyWith(color: Colors.black); + final List colorItems = primaryKeys.map((int index) { + return DefaultTextStyle( + style: index > colors.threshold ? whiteTextStyle : blackTextStyle, + child: ColorItem(index: index, color: colors.primary[index]), + ); + }).toList(); + + if (colors.accent != null) { + colorItems.addAll(accentKeys.map((int index) { + return DefaultTextStyle( + style: index > colors.threshold ? whiteTextStyle : blackTextStyle, + child: + ColorItem(index: index, color: colors.accent[index], prefix: 'A'), + ); + }).toList()); + } + + return ListView( + itemExtent: kColorItemHeight, + children: colorItems, + ); + } +} + +class ColorsDemo extends StatelessWidget { + static const String routeName = '/colors'; + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: allPalettes.length, + child: Scaffold( + appBar: AppBar( + elevation: 0.0, + title: const Text('Colors'), + bottom: TabBar( + isScrollable: true, + tabs: allPalettes + .map((Palette swatch) => Tab(text: swatch.name)) + .toList(), + ), + ), + body: TabBarView( + children: allPalettes.map((Palette colors) { + return PaletteTabView(colors: colors); + }).toList(), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/contacts_demo.dart b/web/gallery/lib/demo/contacts_demo.dart new file mode 100644 index 000000000..bf3ea9964 --- /dev/null +++ b/web/gallery/lib/demo/contacts_demo.dart @@ -0,0 +1,340 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; +import 'package:flutter_web/services.dart'; + +class _ContactCategory extends StatelessWidget { + const _ContactCategory({Key key, this.icon, this.children}) : super(key: key); + + final IconData icon; + final List children; + + @override + Widget build(BuildContext context) { + final ThemeData themeData = Theme.of(context); + return Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: themeData.dividerColor))), + child: DefaultTextStyle( + style: Theme.of(context).textTheme.subhead, + child: SafeArea( + top: false, + bottom: false, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 24.0), + width: 72.0, + child: Icon(icon, color: themeData.primaryColor)), + Expanded(child: Column(children: children)) + ], + ), + ), + ), + ); + } +} + +class _ContactItem extends StatelessWidget { + _ContactItem({Key key, this.icon, this.lines, this.tooltip, this.onPressed}) + : assert(lines.length > 1), + super(key: key); + + final IconData icon; + final List lines; + final String tooltip; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final ThemeData themeData = Theme.of(context); + final List columnChildren = lines + .sublist(0, lines.length - 1) + .map((String line) => Text(line)) + .toList(); + columnChildren.add(Text(lines.last, style: themeData.textTheme.caption)); + + final List rowChildren = [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: columnChildren)) + ]; + if (icon != null) { + rowChildren.add(SizedBox( + width: 72.0, + child: IconButton( + icon: Icon(icon), + color: themeData.primaryColor, + onPressed: onPressed))); + } + return MergeSemantics( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: rowChildren)), + ); + } +} + +class ContactsDemo extends StatefulWidget { + static const String routeName = '/contacts'; + + @override + ContactsDemoState createState() => ContactsDemoState(); +} + +enum AppBarBehavior { normal, pinned, floating, snapping } + +class ContactsDemoState extends State { + static final GlobalKey _scaffoldKey = + GlobalKey(); + final double _appBarHeight = 256.0; + + AppBarBehavior _appBarBehavior = AppBarBehavior.pinned; + + @override + Widget build(BuildContext context) { + return Theme( + data: ThemeData( + brightness: Brightness.light, + primarySwatch: Colors.indigo, + platform: Theme.of(context).platform, + ), + child: Scaffold( + key: _scaffoldKey, + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: _appBarHeight, + pinned: _appBarBehavior == AppBarBehavior.pinned, + floating: _appBarBehavior == AppBarBehavior.floating || + _appBarBehavior == AppBarBehavior.snapping, + snap: _appBarBehavior == AppBarBehavior.snapping, + actions: [ + IconButton( + icon: const Icon(Icons.create), + tooltip: 'Edit', + onPressed: () { + _scaffoldKey.currentState.showSnackBar(const SnackBar( + content: + Text("Editing isn't supported in this screen."))); + }, + ), + PopupMenuButton( + onSelected: (AppBarBehavior value) { + setState(() { + _appBarBehavior = value; + }); + }, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: AppBarBehavior.normal, + child: Text('App bar scrolls away')), + const PopupMenuItem( + value: AppBarBehavior.pinned, + child: Text('App bar stays put')), + const PopupMenuItem( + value: AppBarBehavior.floating, + child: Text('App bar floats')), + const PopupMenuItem( + value: AppBarBehavior.snapping, + child: Text('App bar snaps')), + ], + ), + ], + flexibleSpace: FlexibleSpaceBar( + title: const Text('Ali Connors'), + background: Stack( + fit: StackFit.expand, + children: [ + Image.asset( + 'people/ali_landscape.png', + // package: 'flutter_gallery_assets', + fit: BoxFit.cover, + height: _appBarHeight, + ), + // This gradient ensures that the toolbar icons are distinct + // against the background image. + const DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment(0.0, -1.0), + end: Alignment(0.0, -0.4), + colors: [Color(0x60000000), Color(0x00000000)], + ), + ), + ), + ], + ), + ), + ), + SliverList( + delegate: SliverChildListDelegate([ + AnnotatedRegion( + value: SystemUiOverlayStyle.dark, + child: _ContactCategory( + icon: Icons.call, + children: [ + _ContactItem( + icon: Icons.message, + tooltip: 'Send message', + onPressed: () { + _scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text( + 'Pretend that this opened your SMS application.'))); + }, + lines: const [ + '(650) 555-1234', + 'Mobile', + ], + ), + _ContactItem( + icon: Icons.message, + tooltip: 'Send message', + onPressed: () { + _scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text('A messaging app appears.'))); + }, + lines: const [ + '(323) 555-6789', + 'Work', + ], + ), + _ContactItem( + icon: Icons.message, + tooltip: 'Send message', + onPressed: () { + _scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text( + 'Imagine if you will, a messaging application.'))); + }, + lines: const [ + '(650) 555-6789', + 'Home', + ], + ), + ], + ), + ), + _ContactCategory( + icon: Icons.contact_mail, + children: [ + _ContactItem( + icon: Icons.email, + tooltip: 'Send personal e-mail', + onPressed: () { + _scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text( + 'Here, your e-mail application would open.'))); + }, + lines: const [ + 'ali_connors@example.com', + 'Personal', + ], + ), + _ContactItem( + icon: Icons.email, + tooltip: 'Send work e-mail', + onPressed: () { + _scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text( + 'Summon your favorite e-mail application here.'))); + }, + lines: const [ + 'aliconnors@example.com', + 'Work', + ], + ), + ], + ), + _ContactCategory( + icon: Icons.location_on, + children: [ + _ContactItem( + icon: Icons.map, + tooltip: 'Open map', + onPressed: () { + _scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text( + 'This would show a map of San Francisco.'))); + }, + lines: const [ + '2000 Main Street', + 'San Francisco, CA', + 'Home', + ], + ), + _ContactItem( + icon: Icons.map, + tooltip: 'Open map', + onPressed: () { + _scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text( + 'This would show a map of Mountain View.'))); + }, + lines: const [ + '1600 Amphitheater Parkway', + 'Mountain View, CA', + 'Work', + ], + ), + _ContactItem( + icon: Icons.map, + tooltip: 'Open map', + onPressed: () { + _scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text( + 'This would also show a map, if this was not a demo.'))); + }, + lines: const [ + '126 Severyns Ave', + 'Mountain View, CA', + 'Jet Travel', + ], + ), + ], + ), + _ContactCategory( + icon: Icons.today, + children: [ + _ContactItem( + lines: const [ + 'Birthday', + 'January 9th, 1989', + ], + ), + _ContactItem( + lines: const [ + 'Wedding anniversary', + 'June 21st, 2014', + ], + ), + _ContactItem( + lines: const [ + 'First day in office', + 'January 20th, 2015', + ], + ), + _ContactItem( + lines: const [ + 'Last day in office', + 'August 9th, 2018', + ], + ), + ], + ), + ]), + ), + ], + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/backdrop_demo.dart b/web/gallery/lib/demo/material/backdrop_demo.dart new file mode 100644 index 000000000..d81b21670 --- /dev/null +++ b/web/gallery/lib/demo/material/backdrop_demo.dart @@ -0,0 +1,411 @@ +// Copyright 2018 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:math' as math; + +import 'package:flutter_web/material.dart'; + +// This demo displays one Category at a time. The backdrop show a list +// of all of the categories and the selected category is displayed +// (CategoryView) on top of the backdrop. + +class Category { + const Category({this.title, this.assets}); + final String title; + final List assets; + @override + String toString() => '$runtimeType("$title")'; +} + +const List allCategories = [ + Category( + title: 'Accessories', + assets: [ + 'products/belt.png', + 'products/earrings.png', + 'products/backpack.png', + 'products/hat.png', + 'products/scarf.png', + 'products/sunnies.png', + ], + ), + Category( + title: 'Blue', + assets: [ + 'products/backpack.png', + 'products/cup.png', + 'products/napkins.png', + 'products/top.png', + ], + ), + Category( + title: 'Cold Weather', + assets: [ + 'products/jacket.png', + 'products/jumper.png', + 'products/scarf.png', + 'products/sweater.png', + 'products/sweats.png', + ], + ), + Category( + title: 'Home', + assets: [ + 'products/cup.png', + 'products/napkins.png', + 'products/planters.png', + 'products/table.png', + 'products/teaset.png', + ], + ), + Category( + title: 'Tops', + assets: [ + 'products/jumper.png', + 'products/shirt.png', + 'products/sweater.png', + 'products/top.png', + ], + ), + Category( + title: 'Everything', + assets: [ + 'products/backpack.png', + 'products/belt.png', + 'products/cup.png', + 'products/dress.png', + 'products/earrings.png', + 'products/flatwear.png', + 'products/hat.png', + 'products/jacket.png', + 'products/jumper.png', + 'products/napkins.png', + 'products/planters.png', + 'products/scarf.png', + 'products/shirt.png', + 'products/sunnies.png', + 'products/sweater.png', + 'products/sweats.png', + 'products/table.png', + 'products/teaset.png', + 'products/top.png', + ], + ), +]; + +class CategoryView extends StatelessWidget { + const CategoryView({Key key, this.category}) : super(key: key); + + final Category category; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return ListView( + key: PageStorageKey(category), + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 64.0, + ), + children: category.assets.map((String asset) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: Container( + width: 144.0, + alignment: Alignment.center, + child: Column( + children: [ + Image.asset( + '$asset', + fit: BoxFit.contain, + ), + Container( + padding: const EdgeInsets.only(bottom: 16.0), + alignment: AlignmentDirectional.center, + child: Text( + asset, + style: theme.textTheme.caption, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24.0), + ], + ); + }).toList(), + ); + } +} + +// One BackdropPanel is visible at a time. It's stacked on top of the +// the BackdropDemo. +class BackdropPanel extends StatelessWidget { + const BackdropPanel({ + Key key, + this.onTap, + this.onVerticalDragUpdate, + this.onVerticalDragEnd, + this.title, + this.child, + }) : super(key: key); + + final VoidCallback onTap; + final GestureDragUpdateCallback onVerticalDragUpdate; + final GestureDragEndCallback onVerticalDragEnd; + final Widget title; + final Widget child; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Material( + elevation: 2.0, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onVerticalDragUpdate: onVerticalDragUpdate, + onVerticalDragEnd: onVerticalDragEnd, + onTap: onTap, + child: Container( + height: 48.0, + padding: const EdgeInsetsDirectional.only(start: 16.0), + alignment: AlignmentDirectional.centerStart, + child: DefaultTextStyle( + style: theme.textTheme.subhead, + child: Tooltip( + message: 'Tap to dismiss', + child: title, + ), + ), + ), + ), + const Divider(height: 1.0), + Expanded(child: child), + ], + ), + ); + } +} + +// Cross fades between 'Select a Category' and 'Asset Viewer'. +class BackdropTitle extends AnimatedWidget { + const BackdropTitle({ + Key key, + Listenable listenable, + }) : super(key: key, listenable: listenable); + + @override + Widget build(BuildContext context) { + final Animation animation = listenable; + return DefaultTextStyle( + style: Theme.of(context).primaryTextTheme.title, + softWrap: false, + overflow: TextOverflow.ellipsis, + child: Stack( + children: [ + Opacity( + opacity: CurvedAnimation( + parent: ReverseAnimation(animation), + curve: const Interval(0.5, 1.0), + ).value, + child: const Text('Select a Category'), + ), + Opacity( + opacity: CurvedAnimation( + parent: animation, + curve: const Interval(0.5, 1.0), + ).value, + child: const Text('Asset Viewer'), + ), + ], + ), + ); + } +} + +// This widget is essentially the backdrop itself. +class BackdropDemo extends StatefulWidget { + static const String routeName = '/material/backdrop'; + + @override + _BackdropDemoState createState() => _BackdropDemoState(); +} + +class _BackdropDemoState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop'); + AnimationController _controller; + Category _category = allCategories[0]; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + value: 1.0, + vsync: this, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _changeCategory(Category category) { + setState(() { + _category = category; + _controller.fling(velocity: 2.0); + }); + } + + bool get _backdropPanelVisible { + final AnimationStatus status = _controller.status; + return status == AnimationStatus.completed || + status == AnimationStatus.forward; + } + + void _toggleBackdropPanelVisibility() { + _controller.fling(velocity: _backdropPanelVisible ? -2.0 : 2.0); + } + + double get _backdropHeight { + final RenderBox renderBox = _backdropKey.currentContext.findRenderObject(); + return renderBox.size.height; + } + + // By design: the panel can only be opened with a swipe. To close the panel + // the user must either tap its heading or the backdrop's menu icon. + + void _handleDragUpdate(DragUpdateDetails details) { + if (_controller.isAnimating || + _controller.status == AnimationStatus.completed) return; + + _controller.value -= + details.primaryDelta / (_backdropHeight ?? details.primaryDelta); + } + + void _handleDragEnd(DragEndDetails details) { + if (_controller.isAnimating || + _controller.status == AnimationStatus.completed) return; + + final double flingVelocity = + details.velocity.pixelsPerSecond.dy / _backdropHeight; + if (flingVelocity < 0.0) + _controller.fling(velocity: math.max(2.0, -flingVelocity)); + else if (flingVelocity > 0.0) + _controller.fling(velocity: math.min(-2.0, -flingVelocity)); + else + _controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0); + } + + // Stacks a BackdropPanel, which displays the selected category, on top + // of the backdrop. The categories are displayed with ListTiles. Just one + // can be selected at a time. This is a LayoutWidgetBuild function because + // we need to know how big the BackdropPanel will be to set up its + // animation. + Widget _buildStack(BuildContext context, BoxConstraints constraints) { + const double panelTitleHeight = 48.0; + final Size panelSize = constraints.biggest; + final double panelTop = panelSize.height - panelTitleHeight; + + final Animation panelAnimation = _controller.drive( + RelativeRectTween( + begin: RelativeRect.fromLTRB( + 0.0, + panelTop - MediaQuery.of(context).padding.bottom, + 0.0, + panelTop - panelSize.height, + ), + end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0), + ), + ); + + final ThemeData theme = Theme.of(context); + final List backdropItems = + allCategories.map((Category category) { + final bool selected = category == _category; + return Material( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + color: selected ? Colors.white.withOpacity(0.25) : Colors.transparent, + child: ListTile( + title: Text(category.title), + selected: selected, + onTap: () { + _changeCategory(category); + }, + ), + ); + }).toList(); + + return Container( + key: _backdropKey, + color: theme.primaryColor, + child: Stack( + children: [ + ListTileTheme( + iconColor: theme.primaryIconTheme.color, + textColor: theme.primaryTextTheme.title.color.withOpacity(0.6), + selectedColor: theme.primaryTextTheme.title.color, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: backdropItems, + ), + ), + ), + PositionedTransition( + rect: panelAnimation, + child: BackdropPanel( + onTap: _toggleBackdropPanelVisibility, + onVerticalDragUpdate: _handleDragUpdate, + onVerticalDragEnd: _handleDragEnd, + title: Text(_category.title), + child: CategoryView(category: _category), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0.0, + title: BackdropTitle( + listenable: _controller.view, + ), + actions: [ + IconButton( + onPressed: _toggleBackdropPanelVisibility, + icon: AnimatedIcon( + icon: AnimatedIcons.close_menu, + semanticLabel: 'close', + progress: _controller.view, + ), + ), + ], + ), + body: LayoutBuilder( + builder: _buildStack, + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/bottom_app_bar_demo.dart b/web/gallery/lib/demo/material/bottom_app_bar_demo.dart new file mode 100644 index 000000000..130436ab7 --- /dev/null +++ b/web/gallery/lib/demo/material/bottom_app_bar_demo.dart @@ -0,0 +1,524 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class BottomAppBarDemo extends StatefulWidget { + static const String routeName = '/material/bottom_app_bar'; + + @override + State createState() => _BottomAppBarDemoState(); +} + +// Flutter generally frowns upon abbrevation however this class uses two +// abbrevations extensively: "fab" for floating action button, and "bab" +// for bottom application bar. + +class _BottomAppBarDemoState extends State { + static final GlobalKey _scaffoldKey = + GlobalKey(); + + // FAB shape + + static const _ChoiceValue kNoFab = _ChoiceValue( + title: 'None', + label: 'do not show a floating action button', + value: null, + ); + + static const _ChoiceValue kCircularFab = _ChoiceValue( + title: 'Circular', + label: 'circular floating action button', + value: FloatingActionButton( + onPressed: _showSnackbar, + child: Icon(Icons.add, semanticLabel: 'Action'), + backgroundColor: Colors.orange, + ), + ); + + static const _ChoiceValue kDiamondFab = _ChoiceValue( + title: 'Diamond', + label: 'diamond shape floating action button', + value: _DiamondFab( + onPressed: _showSnackbar, + child: Icon(Icons.add, semanticLabel: 'Action'), + ), + ); + + // Notch + + static const _ChoiceValue kShowNotchTrue = _ChoiceValue( + title: 'On', + label: 'show bottom appbar notch', + value: true, + ); + + static const _ChoiceValue kShowNotchFalse = _ChoiceValue( + title: 'Off', + label: 'do not show bottom appbar notch', + value: false, + ); + + // FAB Position + + static const _ChoiceValue kFabEndDocked = + _ChoiceValue( + title: 'Attached - End', + label: 'floating action button is docked at the end of the bottom app bar', + value: FloatingActionButtonLocation.endDocked, + ); + + static const _ChoiceValue kFabCenterDocked = + _ChoiceValue( + title: 'Attached - Center', + label: + 'floating action button is docked at the center of the bottom app bar', + value: FloatingActionButtonLocation.centerDocked, + ); + + static const _ChoiceValue kFabEndFloat = + _ChoiceValue( + title: 'Free - End', + label: 'floating action button floats above the end of the bottom app bar', + value: FloatingActionButtonLocation.endFloat, + ); + + static const _ChoiceValue kFabCenterFloat = + _ChoiceValue( + title: 'Free - Center', + label: + 'floating action button is floats above the center of the bottom app bar', + value: FloatingActionButtonLocation.centerFloat, + ); + + static void _showSnackbar() { + const String text = + "When the Scaffold's floating action button location changes, " + 'the floating action button animates to its new position.' + 'The BottomAppBar adapts its shape appropriately.'; + _scaffoldKey.currentState.showSnackBar( + const SnackBar(content: Text(text)), + ); + } + + // App bar color + + static const List<_NamedColor> kBabColors = <_NamedColor>[ + _NamedColor(null, 'Clear'), + _NamedColor(Color(0xFFFFC100), 'Orange'), + _NamedColor(Color(0xFF91FAFF), 'Light Blue'), + _NamedColor(Color(0xFF00D1FF), 'Cyan'), + _NamedColor(Color(0xFF00BCFF), 'Cerulean'), + _NamedColor(Color(0xFF009BEE), 'Blue'), + ]; + + _ChoiceValue _fabShape = kCircularFab; + _ChoiceValue _showNotch = kShowNotchTrue; + _ChoiceValue _fabLocation = kFabEndDocked; + Color _babColor = kBabColors.first.color; + + void _onShowNotchChanged(_ChoiceValue value) { + setState(() { + _showNotch = value; + }); + } + + void _onFabShapeChanged(_ChoiceValue value) { + setState(() { + _fabShape = value; + }); + } + + void _onFabLocationChanged(_ChoiceValue value) { + setState(() { + _fabLocation = value; + }); + } + + void _onBabColorChanged(Color value) { + setState(() { + _babColor = value; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Bottom app bar'), + elevation: 0.0, + actions: [ + MaterialDemoDocumentationButton(BottomAppBarDemo.routeName), + IconButton( + icon: const Icon(Icons.sentiment_very_satisfied, + semanticLabel: 'Update shape'), + onPressed: () { + setState(() { + _fabShape = + _fabShape == kCircularFab ? kDiamondFab : kCircularFab; + }); + }, + ), + ], + ), + body: ListView( + padding: const EdgeInsets.only(bottom: 88.0), + children: [ + const _Heading('FAB Shape'), + _RadioItem(kCircularFab, _fabShape, _onFabShapeChanged), + _RadioItem(kDiamondFab, _fabShape, _onFabShapeChanged), + _RadioItem(kNoFab, _fabShape, _onFabShapeChanged), + const Divider(), + const _Heading('Notch'), + _RadioItem(kShowNotchTrue, _showNotch, _onShowNotchChanged), + _RadioItem(kShowNotchFalse, _showNotch, _onShowNotchChanged), + const Divider(), + const _Heading('FAB Position'), + _RadioItem( + kFabEndDocked, _fabLocation, _onFabLocationChanged), + _RadioItem( + kFabCenterDocked, _fabLocation, _onFabLocationChanged), + _RadioItem( + kFabEndFloat, _fabLocation, _onFabLocationChanged), + _RadioItem( + kFabCenterFloat, _fabLocation, _onFabLocationChanged), + const Divider(), + const _Heading('App bar color'), + _ColorsItem(kBabColors, _babColor, _onBabColorChanged), + ], + ), + floatingActionButton: _fabShape.value, + floatingActionButtonLocation: _fabLocation.value, + bottomNavigationBar: _DemoBottomAppBar( + color: _babColor, + fabLocation: _fabLocation.value, + shape: _selectNotch(), + ), + ); + } + + NotchedShape _selectNotch() { + if (!_showNotch.value) return null; + if (_fabShape == kCircularFab) return const CircularNotchedRectangle(); + if (_fabShape == kDiamondFab) return const _DiamondNotchedRectangle(); + return null; + } +} + +class _ChoiceValue { + const _ChoiceValue({this.value, this.title, this.label}); + + final T value; + final String title; + final String label; // For the Semantics widget that contains title + + @override + String toString() => '$runtimeType("$title")'; +} + +class _RadioItem extends StatelessWidget { + const _RadioItem(this.value, this.groupValue, this.onChanged); + + final _ChoiceValue value; + final _ChoiceValue groupValue; + final ValueChanged<_ChoiceValue> onChanged; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Container( + height: 56.0, + padding: const EdgeInsetsDirectional.only(start: 16.0), + alignment: AlignmentDirectional.centerStart, + child: MergeSemantics( + child: Row(children: [ + Radio<_ChoiceValue>( + value: value, + groupValue: groupValue, + onChanged: onChanged, + ), + Expanded( + child: Semantics( + container: true, + button: true, + label: value.label, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + onChanged(value); + }, + child: Text( + value.title, + style: theme.textTheme.subhead, + ), + ), + ), + ), + ]), + ), + ); + } +} + +class _NamedColor { + const _NamedColor(this.color, this.name); + + final Color color; + final String name; +} + +class _ColorsItem extends StatelessWidget { + const _ColorsItem(this.colors, this.selectedColor, this.onChanged); + + final List<_NamedColor> colors; + final Color selectedColor; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: colors.map((_NamedColor namedColor) { + return RawMaterialButton( + onPressed: () { + onChanged(namedColor.color); + }, + constraints: const BoxConstraints.tightFor( + width: 32.0, + height: 32.0, + ), + fillColor: namedColor.color, + shape: CircleBorder( + side: BorderSide( + color: namedColor.color == selectedColor + ? Colors.black + : const Color(0xFFD5D7DA), + width: 2.0, + ), + ), + child: Semantics( + value: namedColor.name, + selected: namedColor.color == selectedColor, + ), + ); + }).toList(), + ); + } +} + +class _Heading extends StatelessWidget { + const _Heading(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Container( + height: 48.0, + padding: const EdgeInsetsDirectional.only(start: 56.0), + alignment: AlignmentDirectional.centerStart, + child: Text( + text, + style: theme.textTheme.body1.copyWith( + color: theme.primaryColor, + ), + ), + ); + } +} + +class _DemoBottomAppBar extends StatelessWidget { + const _DemoBottomAppBar({this.color, this.fabLocation, this.shape}); + + final Color color; + final FloatingActionButtonLocation fabLocation; + final NotchedShape shape; + + static final List kCenterLocations = + [ + FloatingActionButtonLocation.centerDocked, + FloatingActionButtonLocation.centerFloat, + ]; + + @override + Widget build(BuildContext context) { + final List rowContents = [ + IconButton( + icon: const Icon(Icons.menu, semanticLabel: 'Show bottom sheet'), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) => const _DemoDrawer(), + ); + }, + ), + ]; + + if (kCenterLocations.contains(fabLocation)) { + rowContents.add( + const Expanded(child: SizedBox()), + ); + } + + rowContents.addAll([ + IconButton( + icon: const Icon( + Icons.search, + semanticLabel: 'show search action', + ), + onPressed: () { + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text('This is a dummy search action.')), + ); + }, + ), + IconButton( + icon: Icon( + Theme.of(context).platform == TargetPlatform.iOS + ? Icons.more_horiz + : Icons.more_vert, + semanticLabel: 'Show menu actions', + ), + onPressed: () { + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text('This is a dummy menu action.')), + ); + }, + ), + ]); + + return BottomAppBar( + color: color, + child: Row(children: rowContents), + shape: shape, + ); + } +} + +// A drawer that pops up from the bottom of the screen. +class _DemoDrawer extends StatelessWidget { + const _DemoDrawer(); + + @override + Widget build(BuildContext context) { + return Drawer( + child: Column( + children: const [ + ListTile( + leading: Icon(Icons.search), + title: Text('Search'), + ), + ListTile( + leading: Icon(Icons.threed_rotation), + title: Text('3D'), + ), + ], + ), + ); + } +} + +// A diamond-shaped floating action button. +class _DiamondFab extends StatelessWidget { + const _DiamondFab({ + this.child, + this.onPressed, + }); + + final Widget child; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Material( + shape: const _DiamondBorder(), + color: Colors.orange, + child: InkWell( + onTap: onPressed, + child: Container( + width: 56.0, + height: 56.0, + child: IconTheme.merge( + data: IconThemeData(color: Theme.of(context).accentIconTheme.color), + child: child, + ), + ), + ), + elevation: 6.0, + ); + } +} + +class _DiamondNotchedRectangle implements NotchedShape { + const _DiamondNotchedRectangle(); + + @override + Path getOuterPath(Rect host, Rect guest) { + if (!host.overlaps(guest)) return Path()..addRect(host); + assert(guest.width > 0.0); + + final Rect intersection = guest.intersect(host); + // We are computing a "V" shaped notch, as in this diagram: + // -----\**** /----- + // \ / + // \ / + // \ / + // + // "-" marks the top edge of the bottom app bar. + // "\" and "/" marks the notch outline + // + // notchToCenter is the horizontal distance between the guest's center and + // the host's top edge where the notch starts (marked with "*"). + // We compute notchToCenter by similar triangles: + final double notchToCenter = + intersection.height * (guest.height / 2.0) / (guest.width / 2.0); + + return Path() + ..moveTo(host.left, host.top) + ..lineTo(guest.center.dx - notchToCenter, host.top) + ..lineTo(guest.left + guest.width / 2.0, guest.bottom) + ..lineTo(guest.center.dx + notchToCenter, host.top) + ..lineTo(host.right, host.top) + ..lineTo(host.right, host.bottom) + ..lineTo(host.left, host.bottom) + ..close(); + } +} + +class _DiamondBorder extends ShapeBorder { + const _DiamondBorder(); + + @override + EdgeInsetsGeometry get dimensions { + return const EdgeInsets.only(); + } + + @override + Path getInnerPath(Rect rect, {TextDirection textDirection}) { + return getOuterPath(rect, textDirection: textDirection); + } + + @override + Path getOuterPath(Rect rect, {TextDirection textDirection}) { + return Path() + ..moveTo(rect.left + rect.width / 2.0, rect.top) + ..lineTo(rect.right, rect.top + rect.height / 2.0) + ..lineTo(rect.left + rect.width / 2.0, rect.bottom) + ..lineTo(rect.left, rect.top + rect.height / 2.0) + ..close(); + } + + @override + void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {} + + // This border doesn't support scaling. + @override + ShapeBorder scale(double t) { + return null; + } +} diff --git a/web/gallery/lib/demo/material/bottom_navigation_demo.dart b/web/gallery/lib/demo/material/bottom_navigation_demo.dart new file mode 100644 index 000000000..b3a6456eb --- /dev/null +++ b/web/gallery/lib/demo/material/bottom_navigation_demo.dart @@ -0,0 +1,239 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class NavigationIconView { + NavigationIconView({ + Widget icon, + Widget activeIcon, + String title, + Color color, + TickerProvider vsync, + }) : _icon = icon, + _color = color, + _title = title, + item = BottomNavigationBarItem( + icon: icon, + activeIcon: activeIcon, + title: Text(title), + backgroundColor: color, + ), + controller = AnimationController( + duration: kThemeAnimationDuration, + vsync: vsync, + ) { + _animation = controller.drive(CurveTween( + curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), + )); + } + + final Widget _icon; + final Color _color; + final String _title; + final BottomNavigationBarItem item; + final AnimationController controller; + Animation _animation; + + FadeTransition transition( + BottomNavigationBarType type, BuildContext context) { + Color iconColor; + if (type == BottomNavigationBarType.shifting) { + iconColor = _color; + } else { + final ThemeData themeData = Theme.of(context); + iconColor = themeData.brightness == Brightness.light + ? themeData.primaryColor + : themeData.accentColor; + } + + return FadeTransition( + opacity: _animation, + child: SlideTransition( + position: _animation.drive( + Tween( + begin: const Offset(0.0, 0.02), // Slightly down. + end: Offset.zero, + ), + ), + child: IconTheme( + data: IconThemeData( + color: iconColor, + size: 120.0, + ), + child: Semantics( + label: 'Placeholder for $_title tab', + child: _icon, + ), + ), + ), + ); + } +} + +class CustomIcon extends StatelessWidget { + @override + Widget build(BuildContext context) { + final IconThemeData iconTheme = IconTheme.of(context); + return Container( + margin: const EdgeInsets.all(4.0), + width: iconTheme.size - 8.0, + height: iconTheme.size - 8.0, + color: iconTheme.color, + ); + } +} + +class CustomInactiveIcon extends StatelessWidget { + @override + Widget build(BuildContext context) { + final IconThemeData iconTheme = IconTheme.of(context); + return Container( + margin: const EdgeInsets.all(4.0), + width: iconTheme.size - 8.0, + height: iconTheme.size - 8.0, + decoration: BoxDecoration( + border: Border.all(color: iconTheme.color, width: 2.0), + )); + } +} + +class BottomNavigationDemo extends StatefulWidget { + static const String routeName = '/material/bottom_navigation'; + + @override + _BottomNavigationDemoState createState() => _BottomNavigationDemoState(); +} + +class _BottomNavigationDemoState extends State + with TickerProviderStateMixin { + int _currentIndex = 0; + BottomNavigationBarType _type = BottomNavigationBarType.shifting; + List _navigationViews; + + @override + void initState() { + super.initState(); + _navigationViews = [ + NavigationIconView( + icon: const Icon(Icons.access_alarm), + title: 'Alarm', + color: Colors.deepPurple, + vsync: this, + ), + NavigationIconView( + activeIcon: CustomIcon(), + icon: CustomInactiveIcon(), + title: 'Box', + color: Colors.deepOrange, + vsync: this, + ), + NavigationIconView( + activeIcon: const Icon(Icons.cloud), + icon: const Icon(Icons.cloud_queue), + title: 'Cloud', + color: Colors.teal, + vsync: this, + ), + NavigationIconView( + activeIcon: const Icon(Icons.favorite), + icon: const Icon(Icons.favorite_border), + title: 'Favorites', + color: Colors.indigo, + vsync: this, + ), + NavigationIconView( + icon: const Icon(Icons.event_available), + title: 'Event', + color: Colors.pink, + vsync: this, + ) + ]; + + for (NavigationIconView view in _navigationViews) + view.controller.addListener(_rebuild); + + _navigationViews[_currentIndex].controller.value = 1.0; + } + + @override + void dispose() { + for (NavigationIconView view in _navigationViews) view.controller.dispose(); + super.dispose(); + } + + void _rebuild() { + setState(() { + // Rebuild in order to animate views. + }); + } + + Widget _buildTransitionsStack() { + final List transitions = []; + + for (NavigationIconView view in _navigationViews) + transitions.add(view.transition(_type, context)); + + // We want to have the newly animating (fading in) views on top. + transitions.sort((FadeTransition a, FadeTransition b) { + final Animation aAnimation = a.opacity; + final Animation bAnimation = b.opacity; + final double aValue = aAnimation.value; + final double bValue = bAnimation.value; + return aValue.compareTo(bValue); + }); + + return Stack(children: transitions); + } + + @override + Widget build(BuildContext context) { + final BottomNavigationBar botNavBar = BottomNavigationBar( + items: _navigationViews + .map( + (NavigationIconView navigationView) => navigationView.item) + .toList(), + currentIndex: _currentIndex, + type: _type, + onTap: (int index) { + setState(() { + _navigationViews[_currentIndex].controller.reverse(); + _currentIndex = index; + _navigationViews[_currentIndex].controller.forward(); + }); + }, + ); + + return Scaffold( + appBar: AppBar( + title: const Text('Bottom navigation'), + actions: [ + MaterialDemoDocumentationButton(BottomNavigationDemo.routeName), + PopupMenuButton( + onSelected: (BottomNavigationBarType value) { + setState(() { + _type = value; + }); + }, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: BottomNavigationBarType.fixed, + child: Text('Fixed'), + ), + const PopupMenuItem( + value: BottomNavigationBarType.shifting, + child: Text('Shifting'), + ) + ], + ) + ], + ), + body: Center(child: _buildTransitionsStack()), + bottomNavigationBar: botNavBar, + ); + } +} diff --git a/web/gallery/lib/demo/material/cards_demo.dart b/web/gallery/lib/demo/material/cards_demo.dart new file mode 100644 index 000000000..4323c88ab --- /dev/null +++ b/web/gallery/lib/demo/material/cards_demo.dart @@ -0,0 +1,181 @@ +// Copyright 2018 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 'package:flutter_web/foundation.dart'; +import 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +const String _kGalleryAssetsPackage = 'flutter_gallery_assets'; + +class TravelDestination { + const TravelDestination({ + this.assetName, + this.assetPackage, + this.title, + this.description, + }); + + final String assetName; + final String assetPackage; + final String title; + final List description; + + bool get isValid => + assetName != null && title != null && description?.length == 3; +} + +final List destinations = [ + const TravelDestination( + assetName: 'places/india_thanjavur_market.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Top 10 Cities to Visit in Tamil Nadu', + description: [ + 'Number 10', + 'Thanjavur', + 'Thanjavur, Tamil Nadu', + ], + ), + const TravelDestination( + assetName: 'places/india_chettinad_silk_maker.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Artisans of Southern India', + description: [ + 'Silk Spinners', + 'Chettinad', + 'Sivaganga, Tamil Nadu', + ], + ) +]; + +class TravelDestinationItem extends StatelessWidget { + TravelDestinationItem({Key key, @required this.destination, this.shape}) + : assert(destination != null && destination.isValid), + super(key: key); + + static const double height = 366.0; + final TravelDestination destination; + final ShapeBorder shape; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextStyle titleStyle = + theme.textTheme.headline.copyWith(color: Colors.white); + final TextStyle descriptionStyle = theme.textTheme.subhead; + + return Container( + padding: const EdgeInsets.all(8.0), + height: height, + child: Card( + shape: shape, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // photo and title + SizedBox( + height: 184.0, + child: Stack( + children: [ + Positioned( + bottom: 16.0, + left: 16.0, + right: 16.0, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + destination.title, + style: titleStyle, + ), + ), + ), + ], + ), + ), + // description and share/explore buttons + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0), + child: DefaultTextStyle( + softWrap: false, + overflow: TextOverflow.ellipsis, + style: descriptionStyle, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // three line description + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + destination.description[0], + style: + descriptionStyle.copyWith(color: Colors.black54), + ), + ), + Text(destination.description[1]), + Text(destination.description[2]), + ], + ), + ), + ), + ), + // share, explore buttons + ButtonTheme.bar( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + FlatButton( + child: const Text('SHARE'), + textColor: Colors.amber.shade500, + onPressed: () {/* do nothing */}, + ), + FlatButton( + child: const Text('EXPLORE'), + textColor: Colors.amber.shade500, + onPressed: () {/* do nothing */}, + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class CardsDemo extends StatefulWidget { + static const String routeName = '/material/cards'; + + @override + _CardsDemoState createState() => _CardsDemoState(); +} + +class _CardsDemoState extends State { + ShapeBorder _shape; + + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return wrapScaffold('Cards Demo', context, _scaffoldKey, + _buildContents(context), CardsDemo.routeName); + } + + Widget _buildContents(BuildContext context) { + return ListView( + itemExtent: TravelDestinationItem.height, + padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0), + children: destinations.map((TravelDestination destination) { + return Container( + margin: const EdgeInsets.only(bottom: 8.0), + child: TravelDestinationItem( + destination: destination, + shape: _shape, + ), + ); + }).toList()); + } +} diff --git a/web/gallery/lib/demo/material/chip_demo.dart b/web/gallery/lib/demo/material/chip_demo.dart new file mode 100644 index 000000000..a71cbfb89 --- /dev/null +++ b/web/gallery/lib/demo/material/chip_demo.dart @@ -0,0 +1,79 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; +import '../../gallery/demo.dart'; + +class ChipDemo extends StatefulWidget { + static const routeName = '/material/chip'; + @override + State createState() => _ChipDemoState(); +} + +class _ChipDemoState extends State { + bool _filterChipSelected = false; + bool _hasAvatar = true; + + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return wrapScaffold('Chip Demo', context, _scaffoldKey, _buildContents(), + ChipDemo.routeName); + } + + Widget _buildContents() { + return Material( + child: Column( + children: [ + addPadding(Chip( + label: Text('Chip'), + )), + addPadding(InputChip( + label: Text('InputChip'), + )), + addPadding(ChoiceChip( + label: Text('Selected ChoiceChip'), + selected: true, + )), + addPadding(ChoiceChip( + label: Text('Deselected ChoiceChip'), + selected: false, + )), + addPadding(FilterChip( + label: Text('FilterChip'), + selected: _filterChipSelected, + onSelected: (bool newValue) { + setState(() { + _filterChipSelected = newValue; + }); + }, + )), + addPadding(ActionChip( + label: Text('ActionChip'), + onPressed: () {}, + )), + addPadding(ActionChip( + label: Text('Chip with avatar'), + avatar: _hasAvatar + ? CircleAvatar( + backgroundColor: Colors.amber, + child: Text('Z'), + ) + : null, + onPressed: () { + setState(() { + _hasAvatar = !_hasAvatar; + }); + }, + )), + ], + )); + } +} + +Padding addPadding(Widget widget) => Padding( + padding: EdgeInsets.all(10.0), + child: widget, + ); diff --git a/web/gallery/lib/demo/material/data_table_demo.dart b/web/gallery/lib/demo/material/data_table_demo.dart new file mode 100644 index 000000000..e6c5a2f0e --- /dev/null +++ b/web/gallery/lib/demo/material/data_table_demo.dart @@ -0,0 +1,231 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; + +import '../../gallery/demo.dart'; + +class Dessert { + Dessert(this.name, this.calories, this.fat, this.carbs, this.protein, + this.sodium, this.calcium, this.iron); + final String name; + final int calories; + final double fat; + final int carbs; + final double protein; + final int sodium; + final int calcium; + final int iron; + + bool selected = false; +} + +class DessertDataSource extends DataTableSource { + final List _desserts = [ + Dessert('Frozen yogurt', 159, 6.0, 24, 4.0, 87, 14, 1), + Dessert('Ice cream sandwich', 237, 9.0, 37, 4.3, 129, 8, 1), + Dessert('Eclair', 262, 16.0, 24, 6.0, 337, 6, 7), + Dessert('Cupcake', 305, 3.7, 67, 4.3, 413, 3, 8), + Dessert('Gingerbread', 356, 16.0, 49, 3.9, 327, 7, 16), + Dessert('Jelly bean', 375, 0.0, 94, 0.0, 50, 0, 0), + Dessert('Lollipop', 392, 0.2, 98, 0.0, 38, 0, 2), + Dessert('Honeycomb', 408, 3.2, 87, 6.5, 562, 0, 45), + Dessert('Donut', 452, 25.0, 51, 4.9, 326, 2, 22), + Dessert('KitKat', 518, 26.0, 65, 7.0, 54, 12, 6), + Dessert('Frozen yogurt with sugar', 168, 6.0, 26, 4.0, 87, 14, 1), + Dessert('Ice cream sandwich with sugar', 246, 9.0, 39, 4.3, 129, 8, 1), + Dessert('Eclair with sugar', 271, 16.0, 26, 6.0, 337, 6, 7), + Dessert('Cupcake with sugar', 314, 3.7, 69, 4.3, 413, 3, 8), + Dessert('Gingerbread with sugar', 345, 16.0, 51, 3.9, 327, 7, 16), + Dessert('Jelly bean with sugar', 364, 0.0, 96, 0.0, 50, 0, 0), + Dessert('Lollipop with sugar', 401, 0.2, 100, 0.0, 38, 0, 2), + Dessert('Honeycomb with sugar', 417, 3.2, 89, 6.5, 562, 0, 45), + Dessert('Donut with sugar', 461, 25.0, 53, 4.9, 326, 2, 22), + Dessert('KitKat with sugar', 527, 26.0, 67, 7.0, 54, 12, 6), + Dessert('Frozen yogurt with honey', 223, 6.0, 36, 4.0, 87, 14, 1), + Dessert('Ice cream sandwich with honey', 301, 9.0, 49, 4.3, 129, 8, 1), + Dessert('Eclair with honey', 326, 16.0, 36, 6.0, 337, 6, 7), + Dessert('Cupcake with honey', 369, 3.7, 79, 4.3, 413, 3, 8), + Dessert('Gingerbread with honey', 420, 16.0, 61, 3.9, 327, 7, 16), + Dessert('Jelly bean with honey', 439, 0.0, 106, 0.0, 50, 0, 0), + Dessert('Lollipop with honey', 456, 0.2, 110, 0.0, 38, 0, 2), + Dessert('Honeycomb with honey', 472, 3.2, 99, 6.5, 562, 0, 45), + Dessert('Donut with honey', 516, 25.0, 63, 4.9, 326, 2, 22), + Dessert('KitKat with honey', 582, 26.0, 77, 7.0, 54, 12, 6), + Dessert('Frozen yogurt with milk', 262, 8.4, 36, 12.0, 194, 44, 1), + Dessert('Ice cream sandwich with milk', 339, 11.4, 49, 12.3, 236, 38, 1), + Dessert('Eclair with milk', 365, 18.4, 36, 14.0, 444, 36, 7), + Dessert('Cupcake with milk', 408, 6.1, 79, 12.3, 520, 33, 8), + Dessert('Gingerbread with milk', 459, 18.4, 61, 11.9, 434, 37, 16), + Dessert('Jelly bean with milk', 478, 2.4, 106, 8.0, 157, 30, 0), + Dessert('Lollipop with milk', 495, 2.6, 110, 8.0, 145, 30, 2), + Dessert('Honeycomb with milk', 511, 5.6, 99, 14.5, 669, 30, 45), + Dessert('Donut with milk', 555, 27.4, 63, 12.9, 433, 32, 22), + Dessert('KitKat with milk', 621, 28.4, 77, 15.0, 161, 42, 6), + Dessert('Coconut slice and frozen yogurt', 318, 21.0, 31, 5.5, 96, 14, 7), + Dessert( + 'Coconut slice and ice cream sandwich', 396, 24.0, 44, 5.8, 138, 8, 7), + Dessert('Coconut slice and eclair', 421, 31.0, 31, 7.5, 346, 6, 13), + Dessert('Coconut slice and cupcake', 464, 18.7, 74, 5.8, 422, 3, 14), + Dessert('Coconut slice and gingerbread', 515, 31.0, 56, 5.4, 316, 7, 22), + Dessert('Coconut slice and jelly bean', 534, 15.0, 101, 1.5, 59, 0, 6), + Dessert('Coconut slice and lollipop', 551, 15.2, 105, 1.5, 47, 0, 8), + Dessert('Coconut slice and honeycomb', 567, 18.2, 94, 8.0, 571, 0, 51), + Dessert('Coconut slice and donut', 611, 40.0, 58, 6.4, 335, 2, 28), + Dessert('Coconut slice and KitKat', 677, 41.0, 72, 8.5, 63, 12, 12), + ]; + + void _sort(Comparable getField(Dessert d), bool ascending) { + _desserts.sort((Dessert a, Dessert b) { + if (!ascending) { + final Dessert c = a; + a = b; + b = c; + } + final Comparable aValue = getField(a); + final Comparable bValue = getField(b); + return Comparable.compare(aValue, bValue); + }); + notifyListeners(); + } + + int _selectedCount = 0; + + @override + DataRow getRow(int index) { + assert(index >= 0); + if (index >= _desserts.length) return null; + final Dessert dessert = _desserts[index]; + return DataRow.byIndex( + index: index, + selected: dessert.selected, + onSelectChanged: (bool value) { + if (dessert.selected != value) { + _selectedCount += value ? 1 : -1; + assert(_selectedCount >= 0); + dessert.selected = value; + notifyListeners(); + } + }, + cells: [ + DataCell(Text('${dessert.name}')), + DataCell(Text('${dessert.calories}')), + DataCell(Text('${dessert.fat.toStringAsFixed(1)}')), + DataCell(Text('${dessert.carbs}')), + DataCell(Text('${dessert.protein.toStringAsFixed(1)}')), + DataCell(Text('${dessert.sodium}')), + DataCell(Text('${dessert.calcium}%')), + DataCell(Text('${dessert.iron}%')), + ]); + } + + @override + int get rowCount => _desserts.length; + + @override + bool get isRowCountApproximate => false; + + @override + int get selectedRowCount => _selectedCount; + + void _selectAll(bool checked) { + for (Dessert dessert in _desserts) dessert.selected = checked; + _selectedCount = checked ? _desserts.length : 0; + notifyListeners(); + } +} + +class DataTableDemo extends StatefulWidget { + static const String routeName = '/material/data-table'; + + @override + _DataTableDemoState createState() => _DataTableDemoState(); +} + +class _DataTableDemoState extends State { + int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage; + int _sortColumnIndex; + bool _sortAscending = true; + final DessertDataSource _dessertsDataSource = DessertDataSource(); + + void _sort( + Comparable getField(Dessert d), int columnIndex, bool ascending) { + _dessertsDataSource._sort(getField, ascending); + setState(() { + _sortColumnIndex = columnIndex; + _sortAscending = ascending; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Data tables'), + actions: [ + MaterialDemoDocumentationButton(DataTableDemo.routeName), + ], + ), + body: ListView(padding: const EdgeInsets.all(20.0), children: [ + PaginatedDataTable( + header: const Text('Nutrition'), + rowsPerPage: _rowsPerPage, + onRowsPerPageChanged: (int value) { + setState(() { + _rowsPerPage = value; + }); + }, + sortColumnIndex: _sortColumnIndex, + sortAscending: _sortAscending, + onSelectAll: _dessertsDataSource._selectAll, + columns: [ + DataColumn( + label: const Text('Dessert (100g serving)'), + onSort: (int columnIndex, bool ascending) => _sort( + (Dessert d) => d.name, columnIndex, ascending)), + DataColumn( + label: const Text('Calories'), + tooltip: + 'The total amount of food energy in the given serving size.', + numeric: true, + onSort: (int columnIndex, bool ascending) => _sort( + (Dessert d) => d.calories, columnIndex, ascending)), + DataColumn( + label: const Text('Fat (g)'), + numeric: true, + onSort: (int columnIndex, bool ascending) => _sort( + (Dessert d) => d.fat, columnIndex, ascending)), + DataColumn( + label: const Text('Carbs (g)'), + numeric: true, + onSort: (int columnIndex, bool ascending) => _sort( + (Dessert d) => d.carbs, columnIndex, ascending)), + DataColumn( + label: const Text('Protein (g)'), + numeric: true, + onSort: (int columnIndex, bool ascending) => _sort( + (Dessert d) => d.protein, columnIndex, ascending)), + DataColumn( + label: const Text('Sodium (mg)'), + numeric: true, + onSort: (int columnIndex, bool ascending) => _sort( + (Dessert d) => d.sodium, columnIndex, ascending)), + DataColumn( + label: const Text('Calcium (%)'), + tooltip: + 'The amount of calcium as a percentage of the recommended daily amount.', + numeric: true, + onSort: (int columnIndex, bool ascending) => _sort( + (Dessert d) => d.calcium, columnIndex, ascending)), + DataColumn( + label: const Text('Iron (%)'), + numeric: true, + onSort: (int columnIndex, bool ascending) => _sort( + (Dessert d) => d.iron, columnIndex, ascending)), + ], + source: _dessertsDataSource) + ])); + } +} diff --git a/web/gallery/lib/demo/material/date_and_time_picker_demo.dart b/web/gallery/lib/demo/material/date_and_time_picker_demo.dart new file mode 100644 index 000000000..d4d091f4c --- /dev/null +++ b/web/gallery/lib/demo/material/date_and_time_picker_demo.dart @@ -0,0 +1,230 @@ +// Copyright 2018 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:async'; + +import 'package:flutter_web/material.dart'; +import 'package:intl/intl.dart'; + +import '../../gallery/demo.dart'; + +class _InputDropdown extends StatelessWidget { + const _InputDropdown( + {Key key, + this.child, + this.labelText, + this.valueText, + this.valueStyle, + this.onPressed}) + : super(key: key); + + final String labelText; + final String valueText; + final TextStyle valueStyle; + final VoidCallback onPressed; + final Widget child; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + child: InputDecorator( + decoration: InputDecoration( + labelText: labelText, + ), + baseStyle: valueStyle, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Text(valueText, style: valueStyle), + Icon(Icons.arrow_drop_down, + color: Theme.of(context).brightness == Brightness.light + ? Colors.grey.shade700 + : Colors.white70), + ], + ), + ), + ); + } +} + +class _DateTimePicker extends StatelessWidget { + const _DateTimePicker( + {Key key, + this.labelText, + this.selectedDate, + this.selectedTime, + this.selectDate, + this.selectTime}) + : super(key: key); + + final String labelText; + final DateTime selectedDate; + final TimeOfDay selectedTime; + final ValueChanged selectDate; + final ValueChanged selectTime; + + Future _selectDate(BuildContext context) async { + final DateTime picked = await showDatePicker( + context: context, + initialDate: selectedDate, + firstDate: DateTime(2015, 8), + lastDate: DateTime(2101)); + if (picked != null && picked != selectedDate) selectDate(picked); + } + + Future _selectTime(BuildContext context) async { + final TimeOfDay picked = + await showTimePicker(context: context, initialTime: selectedTime); + if (picked != null && picked != selectedTime) selectTime(picked); + } + + @override + Widget build(BuildContext context) { + final TextStyle valueStyle = Theme.of(context).textTheme.title; + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 4, + child: _InputDropdown( + labelText: labelText, + valueText: DateFormat.yMMMd().format(selectedDate), + valueStyle: valueStyle, + onPressed: () { + _selectDate(context); + }, + ), + ), + const SizedBox(width: 12.0), + Expanded( + flex: 3, + child: _InputDropdown( + valueText: selectedTime.format(context), + valueStyle: valueStyle, + onPressed: () { + _selectTime(context); + }, + ), + ), + ], + ); + } +} + +class DateAndTimePickerDemo extends StatefulWidget { + static const String routeName = '/material/date-and-time-pickers'; + + @override + _DateAndTimePickerDemoState createState() => _DateAndTimePickerDemoState(); +} + +class _DateAndTimePickerDemoState extends State { + DateTime _fromDate = DateTime.now(); + TimeOfDay _fromTime = const TimeOfDay(hour: 7, minute: 28); + DateTime _toDate = DateTime.now(); + TimeOfDay _toTime = const TimeOfDay(hour: 7, minute: 28); + final List _allActivities = [ + 'hiking', + 'swimming', + 'boating', + 'fishing' + ]; + String _activity = 'fishing'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Date and time pickers'), + actions: [ + MaterialDemoDocumentationButton(DateAndTimePickerDemo.routeName) + ], + ), + body: DropdownButtonHideUnderline( + child: SafeArea( + top: false, + bottom: false, + child: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + TextField( + enabled: true, + decoration: const InputDecoration( + labelText: 'Event name', + border: OutlineInputBorder(), + ), + style: Theme.of(context).textTheme.display1, + ), + TextField( + decoration: const InputDecoration( + labelText: 'Location', + ), + style: Theme.of(context) + .textTheme + .display1 + .copyWith(fontSize: 20.0), + ), + _DateTimePicker( + labelText: 'From', + selectedDate: _fromDate, + selectedTime: _fromTime, + selectDate: (DateTime date) { + setState(() { + _fromDate = date; + }); + }, + selectTime: (TimeOfDay time) { + setState(() { + _fromTime = time; + }); + }, + ), + _DateTimePicker( + labelText: 'To', + selectedDate: _toDate, + selectedTime: _toTime, + selectDate: (DateTime date) { + setState(() { + _toDate = date; + }); + }, + selectTime: (TimeOfDay time) { + setState(() { + _toTime = time; + }); + }, + ), + const SizedBox(height: 8.0), + InputDecorator( + decoration: const InputDecoration( + labelText: 'Activity', + hintText: 'Choose an activity', + contentPadding: EdgeInsets.zero, + ), + isEmpty: _activity == null, + child: DropdownButton( + value: _activity, + onChanged: (String newValue) { + setState(() { + _activity = newValue; + }); + }, + items: _allActivities + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/dialog_demo.dart b/web/gallery/lib/demo/material/dialog_demo.dart new file mode 100644 index 000000000..adf86fe74 --- /dev/null +++ b/web/gallery/lib/demo/material/dialog_demo.dart @@ -0,0 +1,211 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; +import 'full_screen_dialog_demo.dart'; + +enum DialogDemoAction { + cancel, + discard, + disagree, + agree, +} + +const String _alertWithoutTitleText = 'Discard draft?'; + +const String _alertWithTitleText = + 'Let Google help apps determine location. This means sending anonymous location ' + 'data to Google, even when no apps are running.'; + +class DialogDemoItem extends StatelessWidget { + const DialogDemoItem( + {Key key, this.icon, this.color, this.text, this.onPressed}) + : super(key: key); + + final IconData icon; + final Color color; + final String text; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return SimpleDialogOption( + onPressed: onPressed, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, size: 36.0, color: color), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text(text), + ), + ], + ), + ); + } +} + +class DialogDemo extends StatefulWidget { + static const String routeName = '/material/dialog'; + + @override + DialogDemoState createState() => DialogDemoState(); +} + +class DialogDemoState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + TimeOfDay _selectedTime; + + @override + void initState() { + super.initState(); + final DateTime now = DateTime.now(); + _selectedTime = TimeOfDay(hour: now.hour, minute: now.minute); + } + + void showDemoDialog({BuildContext context, Widget child}) { + showDialog( + context: context, + builder: (BuildContext context) => child, + ).then((T value) { + // The value passed to Navigator.pop() or null. + if (value != null) { + _scaffoldKey.currentState + .showSnackBar(SnackBar(content: Text('You selected: $value'))); + } + }); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextStyle dialogTextStyle = + theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color); + + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Dialogs'), + actions: [ + MaterialDemoDocumentationButton(DialogDemo.routeName) + ], + ), + body: ListView( + padding: + const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0), + children: [ + RaisedButton( + child: const Text('ALERT'), + onPressed: () { + showDemoDialog( + context: context, + child: AlertDialog( + content: Text(_alertWithoutTitleText, + style: dialogTextStyle), + actions: [ + FlatButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.pop( + context, DialogDemoAction.cancel); + }), + FlatButton( + child: const Text('DISCARD'), + onPressed: () { + Navigator.pop( + context, DialogDemoAction.discard); + }) + ])); + }), + RaisedButton( + child: const Text('ALERT WITH TITLE'), + onPressed: () { + showDemoDialog( + context: context, + child: AlertDialog( + title: + const Text('Use Google\'s location service?'), + content: Text(_alertWithTitleText, + style: dialogTextStyle), + actions: [ + FlatButton( + child: const Text('DISAGREE'), + onPressed: () { + Navigator.pop( + context, DialogDemoAction.disagree); + }), + FlatButton( + child: const Text('AGREE'), + onPressed: () { + Navigator.pop( + context, DialogDemoAction.agree); + }) + ])); + }), + RaisedButton( + child: const Text('SIMPLE'), + onPressed: () { + showDemoDialog( + context: context, + child: SimpleDialog( + title: const Text('Set backup account'), + children: [ + DialogDemoItem( + icon: Icons.account_circle, + color: theme.primaryColor, + text: 'username@gmail.com', + onPressed: () { + Navigator.pop( + context, 'username@gmail.com'); + }), + DialogDemoItem( + icon: Icons.account_circle, + color: theme.primaryColor, + text: 'user02@gmail.com', + onPressed: () { + Navigator.pop(context, 'user02@gmail.com'); + }), + DialogDemoItem( + icon: Icons.add_circle, + text: 'add account', + color: theme.disabledColor) + ])); + }), + RaisedButton( + child: const Text('CONFIRMATION'), + onPressed: () { + showTimePicker(context: context, initialTime: _selectedTime) + .then((TimeOfDay value) { + if (value != null && value != _selectedTime) { + _selectedTime = value; + _scaffoldKey.currentState.showSnackBar(SnackBar( + content: Text( + 'You selected: ${value.format(context)}'))); + } + }); + }), + RaisedButton( + child: const Text('FULLSCREEN'), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + FullScreenDialogDemo(), + fullscreenDialog: true, + )); + }), + ] + // Add a little space between the buttons + .map((Widget button) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: button); + }).toList())); + } +} diff --git a/web/gallery/lib/demo/material/drawer_demo.dart b/web/gallery/lib/demo/material/drawer_demo.dart new file mode 100644 index 000000000..8ef8567db --- /dev/null +++ b/web/gallery/lib/demo/material/drawer_demo.dart @@ -0,0 +1,197 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class DrawerDemo extends StatefulWidget { + static const String routeName = '/material/drawer'; + + @override + _DrawerDemoState createState() => _DrawerDemoState(); +} + +class _DrawerDemoState extends State with TickerProviderStateMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + static const List _drawerContents = [ + 'A', + 'B', + 'C', + 'D', + 'E', + ]; + + static final Animatable _drawerDetailsTween = Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).chain(CurveTween( + curve: Curves.fastOutSlowIn, + )); + + AnimationController _controller; + Animation _drawerContentsOpacity; + Animation _drawerDetailsPosition; + bool _showDrawerContents = true; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + _drawerContentsOpacity = CurvedAnimation( + parent: ReverseAnimation(_controller), + curve: Curves.fastOutSlowIn, + ); + _drawerDetailsPosition = _controller.drive(_drawerDetailsTween); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + IconData _backIcon() { + switch (Theme.of(context).platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + return Icons.arrow_back; + case TargetPlatform.iOS: + return Icons.arrow_back_ios; + } + assert(false); + return null; + } + + void _showNotImplementedMessage() { + Navigator.pop(context); // Dismiss the drawer. + _scaffoldKey.currentState.showSnackBar( + const SnackBar(content: Text("The drawer's items don't do anything"))); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + leading: IconButton( + icon: Icon(_backIcon()), + alignment: Alignment.centerLeft, + tooltip: 'Back', + onPressed: () { + Navigator.pop(context); + }, + ), + title: const Text('Navigation drawer'), + actions: [ + MaterialDemoDocumentationButton(DrawerDemo.routeName) + ], + ), + drawer: Drawer( + child: Column( + children: [ + UserAccountsDrawerHeader( + accountName: const Text('Trevor Widget'), + accountEmail: const Text('trevor.widget@example.com'), + margin: EdgeInsets.zero, + onDetailsPressed: () { + _showDrawerContents = !_showDrawerContents; + if (_showDrawerContents) + _controller.reverse(); + else + _controller.forward(); + }, + ), + MediaQuery.removePadding( + context: context, + // DrawerHeader consumes top MediaQuery padding. + removeTop: true, + child: Expanded( + child: ListView( + padding: const EdgeInsets.only(top: 8.0), + children: [ + Stack( + children: [ + // The initial contents of the drawer. + FadeTransition( + opacity: _drawerContentsOpacity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _drawerContents.map((String id) { + return ListTile( + leading: CircleAvatar(child: Text(id)), + title: Text('Drawer item $id'), + onTap: _showNotImplementedMessage, + ); + }).toList(), + ), + ), + // The drawer's "details" view. + SlideTransition( + position: _drawerDetailsPosition, + child: FadeTransition( + opacity: ReverseAnimation(_drawerContentsOpacity), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListTile( + leading: const Icon(Icons.add), + title: const Text('Add account'), + onTap: _showNotImplementedMessage, + ), + ListTile( + leading: const Icon(Icons.settings), + title: const Text('Manage accounts'), + onTap: _showNotImplementedMessage, + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + body: Center( + child: InkWell( + onTap: () { + _scaffoldKey.currentState.openDrawer(); + }, + child: Semantics( + button: true, + label: 'Open drawer', + excludeSemantics: true, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 100.0, + height: 100.0, + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Tap here to open the drawer', + style: Theme.of(context).textTheme.subhead, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/editable_text_demo.dart b/web/gallery/lib/demo/material/editable_text_demo.dart new file mode 100644 index 000000000..034db5bd7 --- /dev/null +++ b/web/gallery/lib/demo/material/editable_text_demo.dart @@ -0,0 +1,92 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +class EditableTextDemo extends StatefulWidget { + static String routeName = '/material/editable_text'; + + @override + State createState() => EditableTextDemoState(); +} + +class EditableTextDemoState extends State { + final cyanController = TextEditingController(text: 'Cyan'); + final orangeController = TextEditingController(text: 'Orange'); + final thickController = TextEditingController(text: 'Thick Rounded Cursor'); + final multiController = + TextEditingController(text: 'First line\nSecond line'); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Text Editing'), + centerTitle: true, + ), + body: ListView( + children: [ + field( + cyanController, + color: Colors.cyan.shade50, + selection: Colors.cyan.shade200, + cursor: Colors.cyan.shade900, + ), + field( + orangeController, + color: Colors.orange.shade50, + selection: Colors.orange.shade200, + cursor: Colors.orange.shade900, + center: true, + ), + field( + thickController, + color: Colors.white, + selection: Colors.grey.shade200, + cursor: Colors.red.shade900, + radius: const Radius.circular(2), + cursorWidth: 8, + ), + Banner( + child: TextField( + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + controller: multiController, + maxLines: 3, + ), + message: 'W.I.P', + textDirection: TextDirection.ltr, + location: BannerLocation.bottomEnd, + ), + ], + ), + ); + } +} + +Widget field( + TextEditingController controller, { + Color color, + Color selection, + Color cursor, + Radius radius = null, + double cursorWidth = 2, + bool center = false, +}) { + return Theme( + data: ThemeData(textSelectionColor: selection), + child: Container( + color: color, + child: TextField( + textAlign: center ? TextAlign.center : TextAlign.start, + decoration: InputDecoration( + contentPadding: EdgeInsets.fromLTRB(8, 16, 8, 16), + ), + controller: controller, + cursorColor: cursor, + cursorRadius: radius, + cursorWidth: cursorWidth, + ), + ), + ); +} diff --git a/web/gallery/lib/demo/material/elevation_demo.dart b/web/gallery/lib/demo/material/elevation_demo.dart new file mode 100644 index 000000000..7f3f83ba4 --- /dev/null +++ b/web/gallery/lib/demo/material/elevation_demo.dart @@ -0,0 +1,69 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class ElevationDemo extends StatefulWidget { + static const String routeName = '/material/elevation'; + + @override + State createState() => _ElevationDemoState(); +} + +class _ElevationDemoState extends State { + bool _showElevation = true; + + List buildCards() { + const List elevations = [ + 0.0, + 1.0, + 2.0, + 3.0, + 4.0, + 5.0, + 8.0, + 16.0, + 24.0, + ]; + + return elevations.map((double elevation) { + return Center( + child: Card( + margin: const EdgeInsets.all(20.0), + elevation: _showElevation ? elevation : 0.0, + child: SizedBox( + height: 100.0, + width: 100.0, + child: Center( + child: Text('${elevation.toStringAsFixed(0)} pt'), + ), + ), + ), + ); + }).toList(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Elevation'), + actions: [ + MaterialDemoDocumentationButton(ElevationDemo.routeName), + IconButton( + icon: const Icon(Icons.sentiment_very_satisfied), + onPressed: () { + setState(() => _showElevation = !_showElevation); + }, + ) + ], + ), + body: ListView( + children: buildCards(), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/expansion_panels_demo.dart b/web/gallery/lib/demo/material/expansion_panels_demo.dart new file mode 100644 index 000000000..a70fb70fe --- /dev/null +++ b/web/gallery/lib/demo/material/expansion_panels_demo.dart @@ -0,0 +1,335 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +@visibleForTesting +enum Location { Barbados, Bahamas, Bermuda } + +typedef DemoItemBodyBuilder = Widget Function(DemoItem item); +typedef ValueToString = String Function(T value); + +class DualHeaderWithHint extends StatelessWidget { + const DualHeaderWithHint({this.name, this.value, this.hint, this.showHint}); + + final String name; + final String value; + final String hint; + final bool showHint; + + Widget _crossFade(Widget first, Widget second, bool isExpanded) { + return AnimatedCrossFade( + firstChild: first, + secondChild: second, + firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn), + secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn), + sizeCurve: Curves.fastOutSlowIn, + crossFadeState: + isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 200), + ); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextTheme textTheme = theme.textTheme; + + return Row(children: [ + Expanded( + flex: 2, + child: Container( + margin: const EdgeInsets.only(left: 24.0), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + name, + style: textTheme.body1.copyWith(fontSize: 15.0), + ), + ), + ), + ), + Expanded( + flex: 3, + child: Container( + margin: const EdgeInsets.only(left: 24.0), + child: _crossFade( + Text(value, + style: textTheme.caption.copyWith(fontSize: 15.0)), + Text(hint, style: textTheme.caption.copyWith(fontSize: 15.0)), + showHint))) + ]); + } +} + +class CollapsibleBody extends StatelessWidget { + const CollapsibleBody( + {this.margin = EdgeInsets.zero, this.child, this.onSave, this.onCancel}); + + final EdgeInsets margin; + final Widget child; + final VoidCallback onSave; + final VoidCallback onCancel; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextTheme textTheme = theme.textTheme; + + return Column(children: [ + Container( + margin: const EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0) - + margin, + child: Center( + child: DefaultTextStyle( + style: textTheme.caption.copyWith(fontSize: 15.0), + child: child))), + const Divider(height: 1.0), + Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: + Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + Container( + margin: const EdgeInsets.only(right: 8.0), + child: FlatButton( + onPressed: onCancel, + child: const Text('CANCEL', + style: TextStyle( + color: Colors.black54, + fontSize: 15.0, + fontWeight: FontWeight.w500)))), + Container( + margin: const EdgeInsets.only(right: 8.0), + child: FlatButton( + onPressed: onSave, + textTheme: ButtonTextTheme.accent, + child: const Text('SAVE'))) + ])) + ]); + } +} + +class DemoItem { + DemoItem({this.name, this.value, this.hint, this.builder, this.valueToString}) + : textController = TextEditingController(text: valueToString(value)); + + final String name; + final String hint; + final TextEditingController textController; + final DemoItemBodyBuilder builder; + final ValueToString valueToString; + T value; + bool isExpanded = false; + + ExpansionPanelHeaderBuilder get headerBuilder { + return (BuildContext context, bool isExpanded) { + return DualHeaderWithHint( + name: name, + value: valueToString(value), + hint: hint, + showHint: isExpanded); + }; + } + + Widget build() => builder(this); +} + +class ExpansionPanelsDemo extends StatefulWidget { + static const String routeName = '/material/expansion_panels'; + + @override + _ExpansionPanelsDemoState createState() => _ExpansionPanelsDemoState(); +} + +class _ExpansionPanelsDemoState extends State { + List> _demoItems; + + @override + void initState() { + super.initState(); + + _demoItems = >[ + DemoItem( + name: 'Trip', + value: 'Caribbean cruise', + hint: 'Change trip name', + valueToString: (String value) => value, + builder: (DemoItem item) { + void close() { + setState(() { + item.isExpanded = false; + }); + } + + return Form( + child: Builder( + builder: (BuildContext context) { + return CollapsibleBody( + margin: const EdgeInsets.symmetric(horizontal: 16.0), + onSave: () { + Form.of(context).save(); + close(); + }, + onCancel: () { + Form.of(context).reset(); + close(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextFormField( + controller: item.textController, + decoration: InputDecoration( + hintText: item.hint, + labelText: item.name, + ), + onSaved: (String value) { + item.value = value; + }, + ), + ), + ); + }, + ), + ); + }, + ), + DemoItem( + name: 'Location', + value: Location.Bahamas, + hint: 'Select location', + valueToString: (Location location) => + location.toString().split('.')[1], + builder: (DemoItem item) { + void close() { + setState(() { + item.isExpanded = false; + }); + } + + return Form(child: Builder(builder: (BuildContext context) { + return CollapsibleBody( + onSave: () { + Form.of(context).save(); + close(); + }, + onCancel: () { + Form.of(context).reset(); + close(); + }, + child: FormField( + initialValue: item.value, + onSaved: (Location result) { + item.value = result; + }, + builder: (FormFieldState field) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RadioListTile( + value: Location.Bahamas, + title: const Text('Bahamas'), + groupValue: field.value, + onChanged: field.didChange, + ), + RadioListTile( + value: Location.Barbados, + title: const Text('Barbados'), + groupValue: field.value, + onChanged: field.didChange, + ), + RadioListTile( + value: Location.Bermuda, + title: const Text('Bermuda'), + groupValue: field.value, + onChanged: field.didChange, + ), + ]); + }), + ); + })); + }), + DemoItem( + name: 'Sun', + value: 80.0, + hint: 'Select sun level', + valueToString: (double amount) => '${amount.round()}', + builder: (DemoItem item) { + void close() { + setState(() { + item.isExpanded = false; + }); + } + + return Form(child: Builder(builder: (BuildContext context) { + return CollapsibleBody( + onSave: () { + Form.of(context).save(); + close(); + }, + onCancel: () { + Form.of(context).reset(); + close(); + }, + child: FormField( + initialValue: item.value, + onSaved: (double value) { + item.value = value; + }, + builder: (FormFieldState field) { + return Slider( + min: 0.0, + max: 100.0, + divisions: 5, + activeColor: + Colors.orange[100 + (field.value * 5.0).round()], + label: '${field.value.round()}', + value: field.value, + onChanged: field.didChange, + ); + }, + ), + ); + })); + }) + ]; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Expansion panels'), + actions: [ + MaterialDemoDocumentationButton(ExpansionPanelsDemo.routeName), + ], + ), + body: SingleChildScrollView( + child: SafeArea( + top: false, + bottom: false, + child: Container( + margin: const EdgeInsets.all(24.0), + child: ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + setState(() { + _demoItems[index].isExpanded = !isExpanded; + }); + }, + children: + _demoItems.map((DemoItem item) { + return ExpansionPanel( + isExpanded: item.isExpanded, + headerBuilder: item.headerBuilder, + body: item.build()); + }).toList()), + ), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/full_screen_dialog_demo.dart b/web/gallery/lib/demo/material/full_screen_dialog_demo.dart new file mode 100644 index 000000000..65831d9ca --- /dev/null +++ b/web/gallery/lib/demo/material/full_screen_dialog_demo.dart @@ -0,0 +1,232 @@ +// Copyright 2018 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:async'; + +import 'package:flutter_web/material.dart'; +import 'package:intl/intl.dart'; + +// This demo is based on +// https://material.google.com/components/dialogs.html#dialogs-full-screen-dialogs + +enum DismissDialogAction { + cancel, + discard, + save, +} + +class DateTimeItem extends StatelessWidget { + DateTimeItem({Key key, DateTime dateTime, @required this.onChanged}) + : assert(onChanged != null), + date = DateTime(dateTime.year, dateTime.month, dateTime.day), + time = TimeOfDay(hour: dateTime.hour, minute: dateTime.minute), + super(key: key); + + final DateTime date; + final TimeOfDay time; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + + return DefaultTextStyle( + style: theme.textTheme.subhead, + child: Row(children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: theme.dividerColor))), + child: InkWell( + onTap: () { + showDatePicker( + context: context, + initialDate: date, + firstDate: + date.subtract(const Duration(days: 30)), + lastDate: date.add(const Duration(days: 30))) + .then((DateTime value) { + if (value != null) + onChanged(DateTime(value.year, value.month, + value.day, time.hour, time.minute)); + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(DateFormat('EEE, MMM d yyyy').format(date)), + const Icon(Icons.arrow_drop_down, + color: Colors.black54), + ])))), + Container( + margin: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsets.symmetric(vertical: 8.0), + decoration: BoxDecoration( + border: + Border(bottom: BorderSide(color: theme.dividerColor))), + child: InkWell( + onTap: () { + showTimePicker(context: context, initialTime: time) + .then((TimeOfDay value) { + if (value != null) + onChanged(DateTime(date.year, date.month, date.day, + value.hour, value.minute)); + }); + }, + child: Row(children: [ + Text('${time.format(context)}'), + const Icon(Icons.arrow_drop_down, color: Colors.black54), + ]))) + ])); + } +} + +class FullScreenDialogDemo extends StatefulWidget { + @override + FullScreenDialogDemoState createState() => FullScreenDialogDemoState(); +} + +class FullScreenDialogDemoState extends State { + DateTime _fromDateTime = DateTime.now(); + DateTime _toDateTime = DateTime.now(); + bool _allDayValue = false; + bool _saveNeeded = false; + bool _hasLocation = false; + bool _hasName = false; + String _eventName; + + Future _onWillPop() async { + _saveNeeded = _hasLocation || _hasName || _saveNeeded; + if (!_saveNeeded) return true; + + final ThemeData theme = Theme.of(context); + final TextStyle dialogTextStyle = + theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color); + + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: Text('Discard new event?', style: dialogTextStyle), + actions: [ + FlatButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop( + false); // Pops the confirmation dialog but not the page. + }), + FlatButton( + child: const Text('DISCARD'), + onPressed: () { + Navigator.of(context).pop( + true); // Returning true to _onWillPop will pop again. + }) + ], + ); + }, + ) ?? + false; + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: Text(_hasName ? _eventName : 'Event Name TBD'), + actions: [ + FlatButton( + child: Text('SAVE', + style: theme.textTheme.body1.copyWith(color: Colors.white)), + onPressed: () { + Navigator.pop(context, DismissDialogAction.save); + }) + ]), + body: Form( + onWillPop: _onWillPop, + child: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + alignment: Alignment.bottomLeft, + child: TextField( + decoration: const InputDecoration( + labelText: 'Event name', filled: true), + style: theme.textTheme.headline, + onChanged: (String value) { + setState(() { + _hasName = value.isNotEmpty; + if (_hasName) { + _eventName = value; + } + }); + })), + Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + alignment: Alignment.bottomLeft, + child: TextField( + decoration: const InputDecoration( + labelText: 'Location', + hintText: 'Where is the event?', + filled: true), + onChanged: (String value) { + setState(() { + _hasLocation = value.isNotEmpty; + }); + })), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('From', style: theme.textTheme.caption), + DateTimeItem( + dateTime: _fromDateTime, + onChanged: (DateTime value) { + setState(() { + _fromDateTime = value; + _saveNeeded = true; + }); + }) + ]), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('To', style: theme.textTheme.caption), + DateTimeItem( + dateTime: _toDateTime, + onChanged: (DateTime value) { + setState(() { + _toDateTime = value; + _saveNeeded = true; + }); + }), + const Text('All-day'), + ]), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: theme.dividerColor))), + child: Row(children: [ + Checkbox( + value: _allDayValue, + onChanged: (bool value) { + setState(() { + _allDayValue = value; + _saveNeeded = true; + }); + }), + const Text('All-day'), + ])) + ].map((Widget child) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + height: 96.0, + child: child); + }).toList())), + ); + } +} diff --git a/web/gallery/lib/demo/material/grid_list_demo.dart b/web/gallery/lib/demo/material/grid_list_demo.dart new file mode 100644 index 000000000..530c8e3cc --- /dev/null +++ b/web/gallery/lib/demo/material/grid_list_demo.dart @@ -0,0 +1,397 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +enum GridDemoTileStyle { imageOnly, oneLine, twoLine } + +typedef BannerTapCallback = void Function(Photo photo); + +const double _kMinFlingVelocity = 800.0; +const String _kGalleryAssetsPackage = 'flutter_gallery_assets'; + +class Photo { + Photo({ + this.assetName, + this.assetPackage, + this.title, + this.caption, + this.isFavorite = false, + }); + + final String assetName; + final String assetPackage; + final String title; + final String caption; + + bool isFavorite; + String get tag => assetName; // Assuming that all asset names are unique. + + bool get isValid => + assetName != null && + title != null && + caption != null && + isFavorite != null; +} + +class GridPhotoViewer extends StatefulWidget { + const GridPhotoViewer({Key key, this.photo}) : super(key: key); + + final Photo photo; + + @override + _GridPhotoViewerState createState() => _GridPhotoViewerState(); +} + +class _GridTitleText extends StatelessWidget { + const _GridTitleText(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + return FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text(text), + ); + } +} + +class _GridPhotoViewerState extends State + with SingleTickerProviderStateMixin { + AnimationController _controller; + Animation _flingAnimation; + Offset _offset = Offset.zero; + double _scale = 1.0; + Offset _normalizedOffset; + double _previousScale; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this) + ..addListener(_handleFlingAnimation); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + // The maximum offset value is 0,0. If the size of this renderer's box is w,h + // then the minimum offset value is w - _scale * w, h - _scale * h. + Offset _clampOffset(Offset offset) { + final Size size = context.size; + final Offset minOffset = Offset(size.width, size.height) * (1.0 - _scale); + return Offset( + offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0)); + } + + void _handleFlingAnimation() { + setState(() { + _offset = _flingAnimation.value; + }); + } + + void _handleOnScaleStart(ScaleStartDetails details) { + setState(() { + _previousScale = _scale; + _normalizedOffset = (details.focalPoint - _offset) / _scale; + // The fling animation stops if an input gesture starts. + _controller.stop(); + }); + } + + void _handleOnScaleUpdate(ScaleUpdateDetails details) { + setState(() { + _scale = (_previousScale * details.scale).clamp(1.0, 4.0); + // Ensure that image location under the focal point stays in the same place despite scaling. + _offset = _clampOffset(details.focalPoint - _normalizedOffset * _scale); + }); + } + + void _handleOnScaleEnd(ScaleEndDetails details) { + final double magnitude = details.velocity.pixelsPerSecond.distance; + if (magnitude < _kMinFlingVelocity) return; + final Offset direction = details.velocity.pixelsPerSecond / magnitude; + final double distance = (Offset.zero & context.size).shortestSide; + _flingAnimation = _controller.drive(Tween( + begin: _offset, end: _clampOffset(_offset + direction * distance))); + _controller + ..value = 0.0 + ..fling(velocity: magnitude / 1000.0); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onScaleStart: _handleOnScaleStart, + onScaleUpdate: _handleOnScaleUpdate, + onScaleEnd: _handleOnScaleEnd, + child: ClipRect( + child: Transform( + transform: Matrix4.identity() + ..translate(_offset.dx, _offset.dy) + ..scale(_scale), + child: Image.asset( + '${widget.photo.assetName}', + // TODO(flutter_web): package: widget.photo.assetPackage, + fit: BoxFit.cover, + ), + ), + ), + ); + } +} + +class GridDemoPhotoItem extends StatelessWidget { + GridDemoPhotoItem( + {Key key, + @required this.photo, + @required this.tileStyle, + @required this.onBannerTap}) + : assert(photo != null && photo.isValid), + assert(tileStyle != null), + assert(onBannerTap != null), + super(key: key); + + final Photo photo; + final GridDemoTileStyle tileStyle; + final BannerTapCallback + onBannerTap; // User taps on the photo's header or footer. + + void showPhoto(BuildContext context) { + Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(photo.title)), + body: SizedBox.expand( + child: Hero( + tag: photo.tag, + child: GridPhotoViewer(photo: photo), + ), + ), + ); + })); + } + + @override + Widget build(BuildContext context) { + final Widget image = GestureDetector( + onTap: () { + showPhoto(context); + }, + child: Hero( + key: Key(photo.assetName), + tag: photo.tag, + child: Image.asset( + '${photo.assetName}', + // TODO(flutter_web): package: photo.assetPackage, + fit: BoxFit.cover, + ))); + + final IconData icon = photo.isFavorite ? Icons.star : Icons.star_border; + + switch (tileStyle) { + case GridDemoTileStyle.imageOnly: + return image; + + case GridDemoTileStyle.oneLine: + return GridTile( + header: GestureDetector( + onTap: () { + onBannerTap(photo); + }, + child: GridTileBar( + title: _GridTitleText(photo.title), + backgroundColor: Colors.black45, + leading: Icon( + icon, + color: Colors.white, + ), + ), + ), + child: image, + ); + + case GridDemoTileStyle.twoLine: + return GridTile( + footer: GestureDetector( + onTap: () { + onBannerTap(photo); + }, + child: GridTileBar( + backgroundColor: Colors.black45, + title: _GridTitleText(photo.title), + subtitle: _GridTitleText(photo.caption), + trailing: Icon( + icon, + color: Colors.white, + ), + ), + ), + child: image, + ); + } + assert(tileStyle != null); + return null; + } +} + +class GridListDemo extends StatefulWidget { + const GridListDemo({Key key}) : super(key: key); + + static const String routeName = '/material/grid-list'; + + @override + GridListDemoState createState() => GridListDemoState(); +} + +class GridListDemoState extends State { + GridDemoTileStyle _tileStyle = GridDemoTileStyle.twoLine; + + List photos = [ + Photo( + assetName: 'places/india_chennai_flower_market.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Chennai', + caption: 'Flower Market', + ), + Photo( + assetName: 'places/india_tanjore_bronze_works.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Tanjore', + caption: 'Bronze Works', + ), + Photo( + assetName: 'places/india_tanjore_market_merchant.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Tanjore', + caption: 'Market', + ), + Photo( + assetName: 'places/india_tanjore_thanjavur_temple.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Tanjore', + caption: 'Thanjavur Temple', + ), + Photo( + assetName: 'places/india_tanjore_thanjavur_temple_carvings.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Tanjore', + caption: 'Thanjavur Temple', + ), + Photo( + assetName: 'places/india_pondicherry_salt_farm.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Pondicherry', + caption: 'Salt Farm', + ), + Photo( + assetName: 'places/india_chennai_highway.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Chennai', + caption: 'Scooters', + ), + Photo( + assetName: 'places/india_chettinad_silk_maker.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Chettinad', + caption: 'Silk Maker', + ), + Photo( + assetName: 'places/india_chettinad_produce.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Chettinad', + caption: 'Lunch Prep', + ), + Photo( + assetName: 'places/india_tanjore_market_technology.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Tanjore', + caption: 'Market', + ), + Photo( + assetName: 'places/india_pondicherry_beach.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Pondicherry', + caption: 'Beach', + ), + Photo( + assetName: 'places/india_pondicherry_fisherman.png', + assetPackage: _kGalleryAssetsPackage, + title: 'Pondicherry', + caption: 'Fisherman', + ), + ]; + + void changeTileStyle(GridDemoTileStyle value) { + setState(() { + _tileStyle = value; + }); + } + + @override + Widget build(BuildContext context) { + final Orientation orientation = MediaQuery.of(context).orientation; + return Scaffold( + appBar: AppBar( + title: const Text('Grid list'), + actions: [ + MaterialDemoDocumentationButton(GridListDemo.routeName), + PopupMenuButton( + onSelected: changeTileStyle, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: GridDemoTileStyle.imageOnly, + child: Text('Image only'), + ), + const PopupMenuItem( + value: GridDemoTileStyle.oneLine, + child: Text('One line'), + ), + const PopupMenuItem( + value: GridDemoTileStyle.twoLine, + child: Text('Two line'), + ), + ], + ), + ], + ), + body: Column( + children: [ + Expanded( + child: SafeArea( + top: false, + bottom: false, + child: GridView.count( + crossAxisCount: (orientation == Orientation.portrait) ? 2 : 3, + mainAxisSpacing: 4.0, + crossAxisSpacing: 4.0, + padding: const EdgeInsets.all(4.0), + childAspectRatio: + (orientation == Orientation.portrait) ? 1.0 : 1.3, + children: photos.map((Photo photo) { + return GridDemoPhotoItem( + photo: photo, + tileStyle: _tileStyle, + onBannerTap: (Photo photo) { + setState(() { + photo.isFavorite = !photo.isFavorite; + }); + }); + }).toList(), + ), + ), + ), + ], + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/icons_demo.dart b/web/gallery/lib/demo/material/icons_demo.dart new file mode 100644 index 000000000..96ef1b1ee --- /dev/null +++ b/web/gallery/lib/demo/material/icons_demo.dart @@ -0,0 +1,135 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class IconsDemo extends StatefulWidget { + static const String routeName = '/material/icons'; + + @override + IconsDemoState createState() => IconsDemoState(); +} + +class IconsDemoState extends State { + static final List iconColors = [ + Colors.red, + Colors.pink, + Colors.purple, + Colors.deepPurple, + Colors.indigo, + Colors.blue, + Colors.lightBlue, + Colors.cyan, + Colors.teal, + Colors.green, + Colors.lightGreen, + Colors.lime, + Colors.yellow, + Colors.amber, + Colors.orange, + Colors.deepOrange, + Colors.brown, + Colors.grey, + Colors.blueGrey, + ]; + + int iconColorIndex = 8; // teal + + Color get iconColor => iconColors[iconColorIndex]; + + void handleIconButtonPress() { + setState(() { + iconColorIndex = (iconColorIndex + 1) % iconColors.length; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Icons'), + actions: [MaterialDemoDocumentationButton(IconsDemo.routeName)], + ), + body: IconTheme( + data: IconThemeData(color: iconColor), + child: SafeArea( + top: false, + bottom: false, + child: ListView( + padding: const EdgeInsets.all(24.0), + children: [ + _IconsDemoCard( + handleIconButtonPress, Icons.face), // direction-agnostic icon + const SizedBox(height: 24.0), + _IconsDemoCard(handleIconButtonPress, + Icons.battery_unknown), // direction-aware icon + ], + ), + ), + ), + ); + } +} + +class _IconsDemoCard extends StatelessWidget { + const _IconsDemoCard(this.handleIconButtonPress, this.icon); + + final VoidCallback handleIconButtonPress; + final IconData icon; + + Widget _buildIconButton(double iconSize, IconData icon, bool enabled) { + return IconButton( + icon: Icon(icon), + iconSize: iconSize, + tooltip: "${enabled ? 'Enabled' : 'Disabled'} icon button", + onPressed: enabled ? handleIconButtonPress : null); + } + + Widget _centeredText(String label) => Padding( + // Match the default padding of IconButton. + padding: const EdgeInsets.all(8.0), + child: Text(label, textAlign: TextAlign.center), + ); + + TableRow _buildIconRow(double size) { + return TableRow( + children: [ + _centeredText(size.floor().toString()), + _buildIconButton(size, icon, true), + _buildIconButton(size, icon, false), + ], + ); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextStyle textStyle = + theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color); + return Card( + child: DefaultTextStyle( + style: textStyle, + child: Semantics( + explicitChildNodes: true, + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow(children: [ + _centeredText('Size'), + _centeredText('Enabled'), + _centeredText('Disabled'), + ]), + _buildIconRow(18.0), + _buildIconRow(24.0), + _buildIconRow(36.0), + _buildIconRow(48.0), + ], + ), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/leave_behind_demo.dart b/web/gallery/lib/demo/material/leave_behind_demo.dart new file mode 100644 index 000000000..abe42ca24 --- /dev/null +++ b/web/gallery/lib/demo/material/leave_behind_demo.dart @@ -0,0 +1,228 @@ +// Copyright 2018 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 'package:collection/collection.dart' show lowerBound; + +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/semantics.dart'; + +import '../../gallery/demo.dart'; + +enum LeaveBehindDemoAction { reset, horizontalSwipe, leftSwipe, rightSwipe } + +class LeaveBehindItem implements Comparable { + LeaveBehindItem({this.index, this.name, this.subject, this.body}); + + LeaveBehindItem.from(LeaveBehindItem item) + : index = item.index, + name = item.name, + subject = item.subject, + body = item.body; + + final int index; + final String name; + final String subject; + final String body; + + @override + int compareTo(LeaveBehindItem other) => index.compareTo(other.index); +} + +class LeaveBehindDemo extends StatefulWidget { + const LeaveBehindDemo({Key key}) : super(key: key); + + static const String routeName = '/material/leave-behind'; + + @override + LeaveBehindDemoState createState() => LeaveBehindDemoState(); +} + +class LeaveBehindDemoState extends State { + static final GlobalKey _scaffoldKey = + GlobalKey(); + DismissDirection _dismissDirection = DismissDirection.horizontal; + List leaveBehindItems; + + void initListItems() { + leaveBehindItems = List.generate(16, (int index) { + return LeaveBehindItem( + index: index, + name: 'Item $index Sender', + subject: 'Subject: $index', + body: "[$index] first line of the message's body..."); + }); + } + + @override + void initState() { + super.initState(); + initListItems(); + } + + void handleDemoAction(LeaveBehindDemoAction action) { + setState(() { + switch (action) { + case LeaveBehindDemoAction.reset: + initListItems(); + break; + case LeaveBehindDemoAction.horizontalSwipe: + _dismissDirection = DismissDirection.horizontal; + break; + case LeaveBehindDemoAction.leftSwipe: + _dismissDirection = DismissDirection.endToStart; + break; + case LeaveBehindDemoAction.rightSwipe: + _dismissDirection = DismissDirection.startToEnd; + break; + } + }); + } + + void handleUndo(LeaveBehindItem item) { + final int insertionIndex = lowerBound(leaveBehindItems, item); + setState(() { + leaveBehindItems.insert(insertionIndex, item); + }); + } + + void _handleArchive(LeaveBehindItem item) { + setState(() { + leaveBehindItems.remove(item); + }); + _scaffoldKey.currentState.showSnackBar(SnackBar( + content: Text('You archived item ${item.index}'), + action: SnackBarAction( + label: 'UNDO', + onPressed: () { + handleUndo(item); + }))); + } + + void _handleDelete(LeaveBehindItem item) { + setState(() { + leaveBehindItems.remove(item); + }); + _scaffoldKey.currentState.showSnackBar(SnackBar( + content: Text('You deleted item ${item.index}'), + action: SnackBarAction( + label: 'UNDO', + onPressed: () { + handleUndo(item); + }))); + } + + @override + Widget build(BuildContext context) { + Widget body; + if (leaveBehindItems.isEmpty) { + body = Center( + child: RaisedButton( + onPressed: () => handleDemoAction(LeaveBehindDemoAction.reset), + child: const Text('Reset the list'), + ), + ); + } else { + body = ListView( + children: leaveBehindItems.map((LeaveBehindItem item) { + return _LeaveBehindListItem( + item: item, + onArchive: _handleArchive, + onDelete: _handleDelete, + dismissDirection: _dismissDirection, + ); + }).toList()); + } + + return Scaffold( + key: _scaffoldKey, + appBar: AppBar(title: const Text('Swipe to dismiss'), actions: [ + MaterialDemoDocumentationButton(LeaveBehindDemo.routeName), + PopupMenuButton( + onSelected: handleDemoAction, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: LeaveBehindDemoAction.reset, + child: Text('Reset the list')), + const PopupMenuDivider(), + CheckedPopupMenuItem( + value: LeaveBehindDemoAction.horizontalSwipe, + checked: _dismissDirection == DismissDirection.horizontal, + child: const Text('Horizontal swipe')), + CheckedPopupMenuItem( + value: LeaveBehindDemoAction.leftSwipe, + checked: _dismissDirection == DismissDirection.endToStart, + child: const Text('Only swipe left')), + CheckedPopupMenuItem( + value: LeaveBehindDemoAction.rightSwipe, + checked: _dismissDirection == DismissDirection.startToEnd, + child: const Text('Only swipe right')) + ]) + ]), + body: body, + ); + } +} + +class _LeaveBehindListItem extends StatelessWidget { + const _LeaveBehindListItem({ + Key key, + @required this.item, + @required this.onArchive, + @required this.onDelete, + @required this.dismissDirection, + }) : super(key: key); + + final LeaveBehindItem item; + final DismissDirection dismissDirection; + final void Function(LeaveBehindItem) onArchive; + final void Function(LeaveBehindItem) onDelete; + + void _handleArchive() { + onArchive(item); + } + + void _handleDelete() { + onDelete(item); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Semantics( + customSemanticsActions: { + const CustomSemanticsAction(label: 'Archive'): _handleArchive, + const CustomSemanticsAction(label: 'Delete'): _handleDelete, + }, + child: Dismissible( + key: ObjectKey(item), + direction: dismissDirection, + onDismissed: (DismissDirection direction) { + if (direction == DismissDirection.endToStart) + _handleArchive(); + else + _handleDelete(); + }, + background: Container( + color: theme.primaryColor, + child: const ListTile( + leading: Icon(Icons.delete, color: Colors.white, size: 36.0))), + secondaryBackground: Container( + color: theme.primaryColor, + child: const ListTile( + trailing: + Icon(Icons.archive, color: Colors.white, size: 36.0))), + child: Container( + decoration: BoxDecoration( + color: theme.canvasColor, + border: Border(bottom: BorderSide(color: theme.dividerColor))), + child: ListTile( + title: Text(item.name), + subtitle: Text('${item.subject}\n${item.body}'), + isThreeLine: true), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/list_demo.dart b/web/gallery/lib/demo/material/list_demo.dart new file mode 100644 index 000000000..489bdcb16 --- /dev/null +++ b/web/gallery/lib/demo/material/list_demo.dart @@ -0,0 +1,273 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +enum _MaterialListType { + /// A list tile that contains a single line of text. + oneLine, + + /// A list tile that contains a [CircleAvatar] followed by a single line of text. + oneLineWithAvatar, + + /// A list tile that contains two lines of text. + twoLine, + + /// A list tile that contains three lines of text. + threeLine, +} + +class ListDemo extends StatefulWidget { + const ListDemo({Key key}) : super(key: key); + + static const String routeName = '/material/list'; + + @override + _ListDemoState createState() => _ListDemoState(); +} + +class _ListDemoState extends State { + static final GlobalKey scaffoldKey = + GlobalKey(); + + PersistentBottomSheetController _bottomSheet; + _MaterialListType _itemType = _MaterialListType.threeLine; + bool _dense = false; + bool _showAvatars = true; + bool _showIcons = false; + bool _showDividers = false; + bool _reverseSort = false; + List items = [ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + ]; + + void changeItemType(_MaterialListType type) { + setState(() { + _itemType = type; + }); + _bottomSheet?.setState(() {}); + } + + void _showConfigurationSheet() { + final PersistentBottomSheetController bottomSheet = scaffoldKey + .currentState + .showBottomSheet((BuildContext bottomSheetContext) { + return Container( + decoration: const BoxDecoration( + border: Border(top: BorderSide(color: Colors.black26)), + ), + child: ListView( + shrinkWrap: true, + primary: false, + children: [ + MergeSemantics( + child: ListTile( + dense: true, + title: const Text('One-line'), + trailing: Radio<_MaterialListType>( + value: _showAvatars + ? _MaterialListType.oneLineWithAvatar + : _MaterialListType.oneLine, + groupValue: _itemType, + onChanged: changeItemType, + )), + ), + MergeSemantics( + child: ListTile( + dense: true, + title: const Text('Two-line'), + trailing: Radio<_MaterialListType>( + value: _MaterialListType.twoLine, + groupValue: _itemType, + onChanged: changeItemType, + )), + ), + MergeSemantics( + child: ListTile( + dense: true, + title: const Text('Three-line'), + trailing: Radio<_MaterialListType>( + value: _MaterialListType.threeLine, + groupValue: _itemType, + onChanged: changeItemType, + ), + ), + ), + MergeSemantics( + child: ListTile( + dense: true, + title: const Text('Show avatar'), + trailing: Checkbox( + value: _showAvatars, + onChanged: (bool value) { + setState(() { + _showAvatars = value; + }); + _bottomSheet?.setState(() {}); + }, + ), + ), + ), + MergeSemantics( + child: ListTile( + dense: true, + title: const Text('Show icon'), + trailing: Checkbox( + value: _showIcons, + onChanged: (bool value) { + setState(() { + _showIcons = value; + }); + _bottomSheet?.setState(() {}); + }, + ), + ), + ), + MergeSemantics( + child: ListTile( + dense: true, + title: const Text('Show dividers'), + trailing: Checkbox( + value: _showDividers, + onChanged: (bool value) { + setState(() { + _showDividers = value; + }); + _bottomSheet?.setState(() {}); + }, + ), + ), + ), + MergeSemantics( + child: ListTile( + dense: true, + title: const Text('Dense layout'), + trailing: Checkbox( + value: _dense, + onChanged: (bool value) { + setState(() { + _dense = value; + }); + _bottomSheet?.setState(() {}); + }, + ), + ), + ), + ], + ), + ); + }); + + setState(() { + _bottomSheet = bottomSheet; + }); + + _bottomSheet.closed.whenComplete(() { + if (mounted) { + setState(() { + _bottomSheet = null; + }); + } + }); + } + + Widget buildListTile(BuildContext context, String item) { + Widget secondary; + if (_itemType == _MaterialListType.twoLine) { + secondary = const Text('Additional item information.'); + } else if (_itemType == _MaterialListType.threeLine) { + secondary = const Text( + 'Even more additional list item information appears on line three.', + ); + } + return MergeSemantics( + child: ListTile( + isThreeLine: _itemType == _MaterialListType.threeLine, + dense: _dense, + leading: _showAvatars + ? ExcludeSemantics(child: CircleAvatar(child: Text(item))) + : null, + title: Text('This item represents $item.'), + subtitle: secondary, + trailing: _showIcons + ? Icon(Icons.info, color: Theme.of(context).disabledColor) + : null, + ), + ); + } + + @override + Widget build(BuildContext context) { + final String layoutText = _dense ? ' \u2013 Dense' : ''; + String itemTypeText; + switch (_itemType) { + case _MaterialListType.oneLine: + case _MaterialListType.oneLineWithAvatar: + itemTypeText = 'Single-line'; + break; + case _MaterialListType.twoLine: + itemTypeText = 'Two-line'; + break; + case _MaterialListType.threeLine: + itemTypeText = 'Three-line'; + break; + } + + Iterable listTiles = + items.map((String item) => buildListTile(context, item)); + if (_showDividers) + listTiles = ListTile.divideTiles(context: context, tiles: listTiles); + + return Scaffold( + key: scaffoldKey, + appBar: AppBar( + title: Text('Scrolling list\n$itemTypeText$layoutText'), + actions: [ + MaterialDemoDocumentationButton(ListDemo.routeName), + IconButton( + icon: const Icon(Icons.sort_by_alpha), + tooltip: 'Sort', + onPressed: () { + setState(() { + _reverseSort = !_reverseSort; + items.sort((String a, String b) => + _reverseSort ? b.compareTo(a) : a.compareTo(b)); + }); + }, + ), + IconButton( + icon: Icon( + Theme.of(context).platform == TargetPlatform.iOS + ? Icons.more_horiz + : Icons.more_vert, + ), + tooltip: 'Show menu', + onPressed: _bottomSheet == null ? _showConfigurationSheet : null, + ), + ], + ), + body: Scrollbar( + child: ListView( + padding: EdgeInsets.symmetric(vertical: _dense ? 4.0 : 8.0), + children: listTiles.toList(), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/material.dart b/web/gallery/lib/demo/material/material.dart new file mode 100644 index 000000000..7c69b81f9 --- /dev/null +++ b/web/gallery/lib/demo/material/material.dart @@ -0,0 +1,39 @@ +// Copyright 2018 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. + +export 'backdrop_demo.dart'; +export 'bottom_app_bar_demo.dart'; +export 'bottom_navigation_demo.dart'; +export 'material_button_demo.dart'; +export 'cards_demo.dart'; +export 'chip_demo.dart'; +export 'data_table_demo.dart'; +export 'date_and_time_picker_demo.dart'; +export 'dialog_demo.dart'; +export 'drawer_demo.dart'; +export 'editable_text_demo.dart'; +export 'elevation_demo.dart'; +export 'expansion_panels_demo.dart'; +export 'grid_list_demo.dart'; +export 'icons_demo.dart'; +export 'leave_behind_demo.dart'; +export 'list_demo.dart'; +export 'menu_demo.dart'; +export 'modal_bottom_sheet_demo.dart'; +export 'overscroll_demo.dart'; +export 'page_selector_demo.dart'; +export 'persistent_bottom_sheet_demo.dart'; +export 'progress_indicator_demo.dart'; +export 'reorderable_list_demo.dart'; +export 'scrollable_tabs_demo.dart'; +export 'search_demo.dart'; +export 'selection_controls_demo.dart'; +export 'slider_demo.dart'; +export 'snack_bar_demo.dart'; +export 'tabs_demo.dart'; +export 'tabs_fab_demo.dart'; +export 'text_demo.dart'; +export 'text_form_field_demo.dart'; +export 'tooltip_demo.dart'; +export 'two_level_list_demo.dart'; diff --git a/web/gallery/lib/demo/material/material_button_demo.dart b/web/gallery/lib/demo/material/material_button_demo.dart new file mode 100644 index 000000000..d1f3d49fc --- /dev/null +++ b/web/gallery/lib/demo/material/material_button_demo.dart @@ -0,0 +1,103 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class ButtonsDemo extends StatelessWidget { + static const String routeName = '/material/buttons'; + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + IconData _backIcon() { + switch (Theme.of(context).platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + return Icons.arrow_back; + case TargetPlatform.iOS: + return Icons.arrow_back_ios; + } + assert(false); + return null; + } + + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + leading: IconButton( + icon: Icon(_backIcon()), + alignment: Alignment.centerLeft, + tooltip: 'Back', + onPressed: () { + Navigator.pop(context); + }, + ), + title: const Text('Material buttons'), + actions: [ + MaterialDemoDocumentationButton(ButtonsDemo.routeName) + ], + ), + body: Center( + child: _buildButtons(), + ), + ); + } + + Widget _buildButtons() { + return Column( + children: [ + pad(MaterialButton( + onPressed: () { + print('MaterialButton pressed'); + }, + elevation: 3.0, + child: Text('MaterialButton'), + )), + pad(FlatButton( + onPressed: () { + print('FlatButton pressed'); + }, + child: Text('FlatButton'), + )), + pad(RaisedButton( + onPressed: () {}, + elevation: 0.0, + child: Text('RaisedButton 0.0'), + )), + pad(RaisedButton( + onPressed: () {}, + elevation: 1.0, + child: Text('RaisedButton 1.0'), + )), + pad(RaisedButton( + onPressed: () {}, + elevation: 2.0, + child: Text('RaisedButton 2.0'), + )), + pad(RaisedButton( + onPressed: () {}, + elevation: 3.0, + child: Text('RaisedButton 3.0'), + )), + pad(RaisedButton( + onPressed: () {}, + elevation: 4.0, + child: Text('RaisedButton 4.0'), + )), + pad(RaisedButton( + onPressed: () {}, + elevation: 8.0, + child: Text('RaisedButton 8.0'), + )), + ], + ); + } +} + +Padding pad(Widget widget) => Padding( + padding: EdgeInsets.all(10.0), + child: widget, + ); diff --git a/web/gallery/lib/demo/material/menu_demo.dart b/web/gallery/lib/demo/material/menu_demo.dart new file mode 100644 index 000000000..0d66b1983 --- /dev/null +++ b/web/gallery/lib/demo/material/menu_demo.dart @@ -0,0 +1,181 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class MenuDemo extends StatefulWidget { + const MenuDemo({Key key}) : super(key: key); + + static const String routeName = '/material/menu'; + + @override + MenuDemoState createState() => MenuDemoState(); +} + +class MenuDemoState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + final String _simpleValue1 = 'Menu item value one'; + final String _simpleValue2 = 'Menu item value two'; + final String _simpleValue3 = 'Menu item value three'; + String _simpleValue; + + final String _checkedValue1 = 'One'; + final String _checkedValue2 = 'Two'; + final String _checkedValue3 = 'Free'; + final String _checkedValue4 = 'Four'; + List _checkedValues; + + @override + void initState() { + super.initState(); + _simpleValue = _simpleValue2; + _checkedValues = [_checkedValue3]; + } + + void showInSnackBar(String value) { + _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(value))); + } + + void showMenuSelection(String value) { + if ([_simpleValue1, _simpleValue2, _simpleValue3].contains(value)) + _simpleValue = value; + showInSnackBar('You selected: $value'); + } + + void showCheckedMenuSelections(String value) { + if (_checkedValues.contains(value)) + _checkedValues.remove(value); + else + _checkedValues.add(value); + + showInSnackBar('Checked $_checkedValues'); + } + + bool isChecked(String value) => _checkedValues.contains(value); + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Menus'), + actions: [ + MaterialDemoDocumentationButton(MenuDemo.routeName), + PopupMenuButton( + onSelected: showMenuSelection, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 'Toolbar menu', child: Text('Toolbar menu')), + const PopupMenuItem( + value: 'Right here', child: Text('Right here')), + const PopupMenuItem( + value: 'Hooray!', child: Text('Hooray!')), + ], + ), + ], + ), + body: ListView(padding: kMaterialListPadding, children: [ + // Pressing the PopupMenuButton on the right of this item shows + // a simple menu with one disabled item. Typically the contents + // of this "contextual menu" would reflect the app's state. + ListTile( + title: const Text('An item with a context menu button'), + trailing: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: showMenuSelection, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: _simpleValue1, + child: const Text('Context menu item one')), + const PopupMenuItem( + enabled: false, + child: Text('A disabled menu item')), + PopupMenuItem( + value: _simpleValue3, + child: const Text('Context menu item three')), + ])), + // Pressing the PopupMenuButton on the right of this item shows + // a menu whose items have text labels and icons and a divider + // That separates the first three items from the last one. + ListTile( + title: const Text('An item with a sectioned menu'), + trailing: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: showMenuSelection, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: 'Preview', + child: ListTile( + leading: Icon(Icons.visibility), + title: Text('Preview'))), + const PopupMenuItem( + value: 'Share', + child: ListTile( + leading: Icon(Icons.person_add), + title: Text('Share'))), + const PopupMenuItem( + value: 'Get Link', + child: ListTile( + leading: Icon(Icons.link), + title: Text('Get link'))), + const PopupMenuDivider(), + const PopupMenuItem( + value: 'Remove', + child: ListTile( + leading: Icon(Icons.delete), + title: Text('Remove'))) + ])), + // This entire list item is a PopupMenuButton. Tapping anywhere shows + // a menu whose current value is highlighted and aligned over the + // list item's center line. + PopupMenuButton( + padding: EdgeInsets.zero, + initialValue: _simpleValue, + onSelected: showMenuSelection, + child: ListTile( + title: const Text('An item with a simple menu'), + subtitle: Text(_simpleValue)), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: _simpleValue1, child: Text(_simpleValue1)), + PopupMenuItem( + value: _simpleValue2, child: Text(_simpleValue2)), + PopupMenuItem( + value: _simpleValue3, child: Text(_simpleValue3)) + ]), + // Pressing the PopupMenuButton on the right of this item shows a menu + // whose items have checked icons that reflect this app's state. + ListTile( + title: const Text('An item with a checklist menu'), + trailing: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: showCheckedMenuSelections, + itemBuilder: (BuildContext context) => + >[ + CheckedPopupMenuItem( + value: _checkedValue1, + checked: isChecked(_checkedValue1), + child: Text(_checkedValue1)), + CheckedPopupMenuItem( + value: _checkedValue2, + enabled: false, + checked: isChecked(_checkedValue2), + child: Text(_checkedValue2)), + CheckedPopupMenuItem( + value: _checkedValue3, + checked: isChecked(_checkedValue3), + child: Text(_checkedValue3)), + CheckedPopupMenuItem( + value: _checkedValue4, + checked: isChecked(_checkedValue4), + child: Text(_checkedValue4)) + ])) + ])); + } +} diff --git a/web/gallery/lib/demo/material/modal_bottom_sheet_demo.dart b/web/gallery/lib/demo/material/modal_bottom_sheet_demo.dart new file mode 100644 index 000000000..31d9c3d39 --- /dev/null +++ b/web/gallery/lib/demo/material/modal_bottom_sheet_demo.dart @@ -0,0 +1,38 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class ModalBottomSheetDemo extends StatelessWidget { + static const String routeName = '/material/modal-bottom-sheet'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Modal bottom sheet'), + actions: [MaterialDemoDocumentationButton(routeName)], + ), + body: Center( + child: RaisedButton( + child: const Text('SHOW BOTTOM SHEET'), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Container( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Text( + 'This is the modal bottom sheet. Tap anywhere to dismiss.', + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 24.0)))); + }); + }))); + } +} diff --git a/web/gallery/lib/demo/material/overscroll_demo.dart b/web/gallery/lib/demo/material/overscroll_demo.dart new file mode 100644 index 000000000..4195489fb --- /dev/null +++ b/web/gallery/lib/demo/material/overscroll_demo.dart @@ -0,0 +1,92 @@ +// Copyright 2018 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:async'; + +import 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +enum IndicatorType { overscroll, refresh } + +class OverscrollDemo extends StatefulWidget { + const OverscrollDemo({Key key}) : super(key: key); + + static const String routeName = '/material/overscroll'; + + @override + OverscrollDemoState createState() => OverscrollDemoState(); +} + +class OverscrollDemoState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshIndicatorKey = + GlobalKey(); + static final List _items = [ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N' + ]; + + Future _handleRefresh() { + final Completer completer = Completer(); + Timer(const Duration(seconds: 3), () { + completer.complete(); + }); + return completer.future.then((_) { + _scaffoldKey.currentState?.showSnackBar(SnackBar( + content: const Text('Refresh complete'), + action: SnackBarAction( + label: 'RETRY', + onPressed: () { + _refreshIndicatorKey.currentState.show(); + }))); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar(title: const Text('Pull to refresh'), actions: [ + MaterialDemoDocumentationButton(OverscrollDemo.routeName), + IconButton( + icon: const Icon(Icons.refresh), + tooltip: 'Refresh', + onPressed: () { + _refreshIndicatorKey.currentState.show(); + }), + ]), + body: RefreshIndicator( + key: _refreshIndicatorKey, + onRefresh: _handleRefresh, + child: ListView.builder( + padding: kMaterialListPadding, + itemCount: _items.length, + itemBuilder: (BuildContext context, int index) { + final String item = _items[index]; + return ListTile( + isThreeLine: true, + leading: CircleAvatar(child: Text(item)), + title: Text('This item represents $item.'), + subtitle: const Text( + 'Even more additional list item information appears on line three.'), + ); + }, + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/page_selector_demo.dart b/web/gallery/lib/demo/material/page_selector_demo.dart new file mode 100644 index 000000000..a4452749a --- /dev/null +++ b/web/gallery/lib/demo/material/page_selector_demo.dart @@ -0,0 +1,98 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class _PageSelector extends StatelessWidget { + const _PageSelector({this.icons}); + + final List icons; + + void _handleArrowButtonPress(BuildContext context, int delta) { + final TabController controller = DefaultTabController.of(context); + if (!controller.indexIsChanging) + controller + .animateTo((controller.index + delta).clamp(0, icons.length - 1)); + } + + @override + Widget build(BuildContext context) { + final TabController controller = DefaultTabController.of(context); + final Color color = Theme.of(context).accentColor; + return SafeArea( + top: false, + bottom: false, + child: Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 16.0), + child: Row(children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + color: color, + onPressed: () { + _handleArrowButtonPress(context, -1); + }, + tooltip: 'Page back'), + TabPageSelector(controller: controller), + IconButton( + icon: const Icon(Icons.chevron_right), + color: color, + onPressed: () { + _handleArrowButtonPress(context, 1); + }, + tooltip: 'Page forward') + ], mainAxisAlignment: MainAxisAlignment.spaceBetween)), + Expanded( + child: IconTheme( + data: IconThemeData( + size: 128.0, + color: color, + ), + child: TabBarView( + children: icons.map((Icon icon) { + return Container( + padding: const EdgeInsets.all(12.0), + child: Card( + child: Center( + child: icon, + ), + ), + ); + }).toList()), + ), + ), + ], + ), + ); + } +} + +class PageSelectorDemo extends StatelessWidget { + static const String routeName = '/material/page-selector'; + static final List icons = [ + const Icon(Icons.event, semanticLabel: 'Event'), + const Icon(Icons.home, semanticLabel: 'Home'), + const Icon(Icons.android, semanticLabel: 'Android'), + const Icon(Icons.alarm, semanticLabel: 'Alarm'), + const Icon(Icons.face, semanticLabel: 'Face'), + const Icon(Icons.language, semanticLabel: 'Language'), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Page selector'), + actions: [MaterialDemoDocumentationButton(routeName)], + ), + body: DefaultTabController( + length: icons.length, + child: _PageSelector(icons: icons), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/persistent_bottom_sheet_demo.dart b/web/gallery/lib/demo/material/persistent_bottom_sheet_demo.dart new file mode 100644 index 000000000..27dfb2380 --- /dev/null +++ b/web/gallery/lib/demo/material/persistent_bottom_sheet_demo.dart @@ -0,0 +1,103 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class PersistentBottomSheetDemo extends StatefulWidget { + static const String routeName = '/material/persistent-bottom-sheet'; + + @override + _PersistentBottomSheetDemoState createState() => + _PersistentBottomSheetDemoState(); +} + +class _PersistentBottomSheetDemoState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + VoidCallback _showBottomSheetCallback; + + @override + void initState() { + super.initState(); + _showBottomSheetCallback = _showBottomSheet; + } + + void _showBottomSheet() { + setState(() { + // disable the button + _showBottomSheetCallback = null; + }); + _scaffoldKey.currentState + .showBottomSheet((BuildContext context) { + final ThemeData themeData = Theme.of(context); + return Container( + decoration: BoxDecoration( + border: + Border(top: BorderSide(color: themeData.disabledColor))), + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Text( + 'This is a Material persistent bottom sheet. Drag downwards to dismiss it.', + textAlign: TextAlign.center, + style: TextStyle(color: themeData.accentColor, fontSize: 24.0), + ), + ), + ); + }) + .closed + .whenComplete(() { + if (mounted) { + setState(() { + // re-enable the button + _showBottomSheetCallback = _showBottomSheet; + }); + } + }); + } + + void _showMessage() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: const Text('You tapped the floating action button.'), + actions: [ + FlatButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('OK')) + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Persistent bottom sheet'), + actions: [ + MaterialDemoDocumentationButton( + PersistentBottomSheetDemo.routeName), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _showMessage, + backgroundColor: Colors.redAccent, + child: const Icon( + Icons.add, + semanticLabel: 'Add', + ), + ), + body: Center( + child: RaisedButton( + onPressed: _showBottomSheetCallback, + child: const Text('SHOW BOTTOM SHEET')))); + } +} diff --git a/web/gallery/lib/demo/material/progress_indicator_demo.dart b/web/gallery/lib/demo/material/progress_indicator_demo.dart new file mode 100644 index 000000000..e28f34b23 --- /dev/null +++ b/web/gallery/lib/demo/material/progress_indicator_demo.dart @@ -0,0 +1,132 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class ProgressIndicatorDemo extends StatefulWidget { + static const String routeName = '/material/progress-indicator'; + + @override + _ProgressIndicatorDemoState createState() => _ProgressIndicatorDemoState(); +} + +class _ProgressIndicatorDemoState extends State + with SingleTickerProviderStateMixin { + AnimationController _controller; + Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + animationBehavior: AnimationBehavior.preserve, + )..forward(); + + _animation = CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.9, curve: Curves.fastOutSlowIn), + reverseCurve: Curves.fastOutSlowIn) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.dismissed) + _controller.forward(); + else if (status == AnimationStatus.completed) _controller.reverse(); + }); + } + + @override + void dispose() { + _controller.stop(); + super.dispose(); + } + + void _handleTap() { + setState(() { + // valueAnimation.isAnimating is part of our build state + if (_controller.isAnimating) { + _controller.stop(); + } else { + switch (_controller.status) { + case AnimationStatus.dismissed: + case AnimationStatus.forward: + _controller.forward(); + break; + case AnimationStatus.reverse: + case AnimationStatus.completed: + _controller.reverse(); + break; + } + } + }); + } + + Widget _buildIndicators(BuildContext context, Widget child) { + final List indicators = [ + const SizedBox(width: 200.0, child: LinearProgressIndicator()), + const LinearProgressIndicator(), + const LinearProgressIndicator(), + LinearProgressIndicator(value: _animation.value), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const CircularProgressIndicator(), + SizedBox( + width: 20.0, + height: 20.0, + child: CircularProgressIndicator(value: _animation.value)), + SizedBox( + width: 100.0, + height: 20.0, + child: Text('${(_animation.value * 100.0).toStringAsFixed(1)}%', + textAlign: TextAlign.right), + ), + ], + ), + ]; + return Column( + children: indicators + .map((Widget c) => Container( + child: c, + margin: + const EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0))) + .toList(), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Progress indicators'), + actions: [ + MaterialDemoDocumentationButton(ProgressIndicatorDemo.routeName) + ], + ), + body: Center( + child: SingleChildScrollView( + child: DefaultTextStyle( + style: Theme.of(context).textTheme.title, + child: GestureDetector( + onTap: _handleTap, + behavior: HitTestBehavior.opaque, + child: SafeArea( + top: false, + bottom: false, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 8.0), + child: AnimatedBuilder( + animation: _animation, builder: _buildIndicators), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/reorderable_list_demo.dart b/web/gallery/lib/demo/material/reorderable_list_demo.dart new file mode 100644 index 000000000..40477955b --- /dev/null +++ b/web/gallery/lib/demo/material/reorderable_list_demo.dart @@ -0,0 +1,219 @@ +// Copyright 2018 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 'package:flutter_web/foundation.dart'; +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; + +import '../../gallery/demo.dart'; + +enum _ReorderableListType { + /// A list tile that contains a [CircleAvatar]. + horizontalAvatar, + + /// A list tile that contains a [CircleAvatar]. + verticalAvatar, + + /// A list tile that contains three lines of text and a checkbox. + threeLine, +} + +class ReorderableListDemo extends StatefulWidget { + const ReorderableListDemo({Key key}) : super(key: key); + + static const String routeName = '/material/reorderable-list'; + + @override + _ListDemoState createState() => _ListDemoState(); +} + +class _ListItem { + _ListItem(this.value, this.checkState); + + final String value; + + bool checkState; +} + +class _ListDemoState extends State { + static final GlobalKey scaffoldKey = + GlobalKey(); + + PersistentBottomSheetController _bottomSheet; + _ReorderableListType _itemType = _ReorderableListType.threeLine; + bool _reverseSort = false; + final List<_ListItem> _items = [ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + ].map<_ListItem>((String item) => _ListItem(item, false)).toList(); + + void changeItemType(_ReorderableListType type) { + setState(() { + _itemType = type; + }); + // Rebuild the bottom sheet to reflect the selected list view. + _bottomSheet?.setState(() {}); + // Close the bottom sheet to give the user a clear view of the list. + _bottomSheet?.close(); + } + + void _showConfigurationSheet() { + setState(() { + _bottomSheet = scaffoldKey.currentState + .showBottomSheet((BuildContext bottomSheetContext) { + return DecoratedBox( + decoration: const BoxDecoration( + border: Border(top: BorderSide(color: Colors.black26)), + ), + child: ListView( + shrinkWrap: true, + primary: false, + children: [ + RadioListTile<_ReorderableListType>( + dense: true, + title: const Text('Horizontal Avatars'), + value: _ReorderableListType.horizontalAvatar, + groupValue: _itemType, + onChanged: changeItemType, + ), + RadioListTile<_ReorderableListType>( + dense: true, + title: const Text('Vertical Avatars'), + value: _ReorderableListType.verticalAvatar, + groupValue: _itemType, + onChanged: changeItemType, + ), + RadioListTile<_ReorderableListType>( + dense: true, + title: const Text('Three-line'), + value: _ReorderableListType.threeLine, + groupValue: _itemType, + onChanged: changeItemType, + ), + ], + ), + ); + }); + + // Garbage collect the bottom sheet when it closes. + _bottomSheet.closed.whenComplete(() { + if (mounted) { + setState(() { + _bottomSheet = null; + }); + } + }); + }); + } + + Widget buildListTile(_ListItem item) { + const Widget secondary = Text( + 'Even more additional list item information appears on line three.', + ); + Widget listTile; + switch (_itemType) { + case _ReorderableListType.threeLine: + listTile = CheckboxListTile( + key: Key(item.value), + isThreeLine: true, + value: item.checkState ?? false, + onChanged: (bool newValue) { + setState(() { + item.checkState = newValue; + }); + }, + title: Text('This item represents ${item.value}.'), + subtitle: secondary, + secondary: const Icon(Icons.drag_handle), + ); + break; + case _ReorderableListType.horizontalAvatar: + case _ReorderableListType.verticalAvatar: + listTile = Container( + key: Key(item.value), + height: 100.0, + width: 100.0, + child: CircleAvatar( + child: Text(item.value), + backgroundColor: Colors.green, + ), + ); + break; + } + + return listTile; + } + + void _onReorder(int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final _ListItem item = _items.removeAt(oldIndex); + _items.insert(newIndex, item); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: scaffoldKey, + appBar: AppBar( + title: const Text('Reorderable list'), + actions: [ + MaterialDemoDocumentationButton(ReorderableListDemo.routeName), + IconButton( + icon: const Icon(Icons.sort_by_alpha), + tooltip: 'Sort', + onPressed: () { + setState(() { + _reverseSort = !_reverseSort; + _items.sort((_ListItem a, _ListItem b) => _reverseSort + ? b.value.compareTo(a.value) + : a.value.compareTo(b.value)); + }); + }, + ), + IconButton( + icon: Icon( + Theme.of(context).platform == TargetPlatform.iOS + ? Icons.more_horiz + : Icons.more_vert, + ), + tooltip: 'Show menu', + onPressed: _bottomSheet == null ? _showConfigurationSheet : null, + ), + ], + ), + body: Scrollbar( + child: ReorderableListView( + header: _itemType != _ReorderableListType.threeLine + ? Padding( + padding: const EdgeInsets.all(8.0), + child: Text('Header of the list', + style: Theme.of(context).textTheme.headline)) + : null, + onReorder: _onReorder, + scrollDirection: _itemType == _ReorderableListType.horizontalAvatar + ? Axis.horizontal + : Axis.vertical, + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: _items.map(buildListTile).toList(), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/scrollable_tabs_demo.dart b/web/gallery/lib/demo/material/scrollable_tabs_demo.dart new file mode 100644 index 000000000..bf37f62c0 --- /dev/null +++ b/web/gallery/lib/demo/material/scrollable_tabs_demo.dart @@ -0,0 +1,195 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +enum TabsDemoStyle { iconsAndText, iconsOnly, textOnly } + +class _Page { + const _Page({this.icon, this.text}); + final IconData icon; + final String text; +} + +const List<_Page> _allPages = <_Page>[ + _Page(icon: Icons.grade, text: 'TRIUMPH'), + _Page(icon: Icons.playlist_add, text: 'NOTE'), + _Page(icon: Icons.check_circle, text: 'SUCCESS'), + _Page(icon: Icons.question_answer, text: 'OVERSTATE'), + _Page(icon: Icons.sentiment_very_satisfied, text: 'SATISFACTION'), + _Page(icon: Icons.camera, text: 'APERTURE'), + _Page(icon: Icons.assignment_late, text: 'WE MUST'), + _Page(icon: Icons.assignment_turned_in, text: 'WE CAN'), + _Page(icon: Icons.group, text: 'ALL'), + _Page(icon: Icons.block, text: 'EXCEPT'), + _Page(icon: Icons.sentiment_very_dissatisfied, text: 'CRYING'), + _Page(icon: Icons.error, text: 'MISTAKE'), + _Page(icon: Icons.loop, text: 'TRYING'), + _Page(icon: Icons.cake, text: 'CAKE'), +]; + +class ScrollableTabsDemo extends StatefulWidget { + static const String routeName = '/material/scrollable-tabs'; + + @override + ScrollableTabsDemoState createState() => ScrollableTabsDemoState(); +} + +class ScrollableTabsDemoState extends State + with SingleTickerProviderStateMixin { + TabController _controller; + TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText; + bool _customIndicator = false; + + @override + void initState() { + super.initState(); + _controller = TabController(vsync: this, length: _allPages.length); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void changeDemoStyle(TabsDemoStyle style) { + setState(() { + _demoStyle = style; + }); + } + + Decoration getIndicator() { + if (!_customIndicator) return const UnderlineTabIndicator(); + + switch (_demoStyle) { + case TabsDemoStyle.iconsAndText: + return ShapeDecoration( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + side: BorderSide( + color: Colors.white24, + width: 2.0, + ), + ) + + const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + side: BorderSide( + color: Colors.transparent, + width: 4.0, + ), + ), + ); + + case TabsDemoStyle.iconsOnly: + return ShapeDecoration( + shape: const CircleBorder( + side: BorderSide( + color: Colors.white24, + width: 4.0, + ), + ) + + const CircleBorder( + side: BorderSide( + color: Colors.transparent, + width: 4.0, + ), + ), + ); + + case TabsDemoStyle.textOnly: + return ShapeDecoration( + shape: const StadiumBorder( + side: BorderSide( + color: Colors.white24, + width: 2.0, + ), + ) + + const StadiumBorder( + side: BorderSide( + color: Colors.transparent, + width: 4.0, + ), + ), + ); + } + return null; + } + + @override + Widget build(BuildContext context) { + final Color iconColor = Theme.of(context).accentColor; + return Scaffold( + appBar: AppBar( + title: const Text('Scrollable tabs'), + actions: [ + MaterialDemoDocumentationButton(ScrollableTabsDemo.routeName), + IconButton( + icon: const Icon(Icons.sentiment_very_satisfied), + onPressed: () { + setState(() { + _customIndicator = !_customIndicator; + }); + }, + ), + PopupMenuButton( + onSelected: changeDemoStyle, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: TabsDemoStyle.iconsAndText, + child: Text('Icons and text')), + const PopupMenuItem( + value: TabsDemoStyle.iconsOnly, + child: Text('Icons only')), + const PopupMenuItem( + value: TabsDemoStyle.textOnly, child: Text('Text only')), + ], + ), + ], + bottom: TabBar( + controller: _controller, + isScrollable: true, + indicator: getIndicator(), + tabs: _allPages.map((_Page page) { + assert(_demoStyle != null); + switch (_demoStyle) { + case TabsDemoStyle.iconsAndText: + return Tab(text: page.text, icon: Icon(page.icon)); + case TabsDemoStyle.iconsOnly: + return Tab(icon: Icon(page.icon)); + case TabsDemoStyle.textOnly: + return Tab(text: page.text); + } + return null; + }).toList(), + ), + ), + body: TabBarView( + controller: _controller, + children: _allPages.map((_Page page) { + return SafeArea( + top: false, + bottom: false, + child: Container( + key: ObjectKey(page.icon), + padding: const EdgeInsets.all(12.0), + child: Card( + child: Center( + child: Icon( + page.icon, + color: iconColor, + size: 128.0, + semanticLabel: 'Placeholder for ${page.text} tab', + ), + ), + ), + ), + ); + }).toList()), + ); + } +} diff --git a/web/gallery/lib/demo/material/search_demo.dart b/web/gallery/lib/demo/material/search_demo.dart new file mode 100644 index 000000000..d691fe56a --- /dev/null +++ b/web/gallery/lib/demo/material/search_demo.dart @@ -0,0 +1,295 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class SearchDemo extends StatefulWidget { + static const String routeName = '/material/search'; + + @override + _SearchDemoState createState() => _SearchDemoState(); +} + +class _SearchDemoState extends State { + final _SearchDemoSearchDelegate _delegate = _SearchDemoSearchDelegate(); + final GlobalKey _scaffoldKey = GlobalKey(); + + int _lastIntegerSelected; + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + leading: IconButton( + tooltip: 'Navigation menu', + icon: AnimatedIcon( + icon: AnimatedIcons.menu_arrow, + color: Colors.white, + progress: _delegate.transitionAnimation, + ), + onPressed: () { + _scaffoldKey.currentState.openDrawer(); + }, + ), + title: const Text('Numbers'), + actions: [ + IconButton( + tooltip: 'Search', + icon: const Icon(Icons.search), + onPressed: () async { + final int selected = await showSearch( + context: context, + delegate: _delegate, + ); + if (selected != null && selected != _lastIntegerSelected) { + setState(() { + _lastIntegerSelected = selected; + }); + } + }, + ), + MaterialDemoDocumentationButton(SearchDemo.routeName), + IconButton( + tooltip: 'More (not implemented)', + icon: Icon( + Theme.of(context).platform == TargetPlatform.iOS + ? Icons.more_horiz + : Icons.more_vert, + ), + onPressed: () {}, + ), + ], + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MergeSemantics( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text('Press the '), + Tooltip( + message: 'search', + child: Icon( + Icons.search, + size: 18.0, + ), + ), + Text(' icon in the AppBar'), + ], + ), + const Text( + 'and search for an integer between 0 and 100,000.'), + ], + ), + ), + const SizedBox(height: 64.0), + Text('Last selected integer: ${_lastIntegerSelected ?? 'NONE'}.') + ], + ), + ), + floatingActionButton: FloatingActionButton.extended( + tooltip: 'Back', // Tests depend on this label to exit the demo. + onPressed: () { + Navigator.of(context).pop(); + }, + label: const Text('Close demo'), + icon: const Icon(Icons.close), + ), + drawer: Drawer( + child: Column( + children: [ + const UserAccountsDrawerHeader( + accountName: Text('Peter Widget'), + accountEmail: Text('peter.widget@example.com'), + currentAccountPicture: CircleAvatar( + backgroundImage: AssetImage( + 'people/square/peter.png', + ), + ), + margin: EdgeInsets.zero, + ), + MediaQuery.removePadding( + context: context, + // DrawerHeader consumes top MediaQuery padding. + removeTop: true, + child: const ListTile( + leading: Icon(Icons.payment), + title: Text('Placeholder'), + ), + ), + ], + ), + ), + ); + } +} + +class _SearchDemoSearchDelegate extends SearchDelegate { + final List _data = + List.generate(100001, (int i) => i).reversed.toList(); + final List _history = [42607, 85604, 66374, 44, 174]; + + @override + Widget buildLeading(BuildContext context) { + return IconButton( + tooltip: 'Back', + icon: AnimatedIcon( + icon: AnimatedIcons.menu_arrow, + progress: transitionAnimation, + ), + onPressed: () { + close(context, null); + }, + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + final Iterable suggestions = query.isEmpty + ? _history + : _data.where((int i) => '$i'.startsWith(query)); + + return _SuggestionList( + query: query, + suggestions: suggestions.map((int i) => '$i').toList(), + onSelected: (String suggestion) { + query = suggestion; + showResults(context); + }, + ); + } + + @override + Widget buildResults(BuildContext context) { + final int searched = int.tryParse(query); + if (searched == null || !_data.contains(searched)) { + return Center( + child: Text( + '"$query"\n is not a valid integer between 0 and 100,000.\nTry again.', + textAlign: TextAlign.center, + ), + ); + } + + return ListView( + children: [ + _ResultCard( + title: 'This integer', + integer: searched, + searchDelegate: this, + ), + _ResultCard( + title: 'Next integer', + integer: searched + 1, + searchDelegate: this, + ), + _ResultCard( + title: 'Previous integer', + integer: searched - 1, + searchDelegate: this, + ), + ], + ); + } + + @override + List buildActions(BuildContext context) { + return [ + query.isEmpty + ? IconButton( + tooltip: 'Voice Search', + icon: const Icon(Icons.mic), + onPressed: () { + query = 'TODO: implement voice input'; + }, + ) + : IconButton( + tooltip: 'Clear', + icon: const Icon(Icons.clear), + onPressed: () { + query = ''; + showSuggestions(context); + }, + ) + ]; + } +} + +class _ResultCard extends StatelessWidget { + const _ResultCard({this.integer, this.title, this.searchDelegate}); + + final int integer; + final String title; + final SearchDelegate searchDelegate; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return GestureDetector( + onTap: () { + searchDelegate.close(context, integer); + }, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text(title), + Text( + '$integer', + style: theme.textTheme.headline.copyWith(fontSize: 72.0), + ), + ], + ), + ), + ), + ); + } +} + +class _SuggestionList extends StatelessWidget { + const _SuggestionList({this.suggestions, this.query, this.onSelected}); + + final List suggestions; + final String query; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return ListView.builder( + itemCount: suggestions.length, + itemBuilder: (BuildContext context, int i) { + final String suggestion = suggestions[i]; + return ListTile( + leading: query.isEmpty ? const Icon(Icons.history) : const Icon(null), + title: RichText( + text: TextSpan( + text: suggestion.substring(0, query.length), + style: + theme.textTheme.subhead.copyWith(fontWeight: FontWeight.bold), + children: [ + TextSpan( + text: suggestion.substring(query.length), + style: theme.textTheme.subhead, + ), + ], + ), + ), + onTap: () { + onSelected(suggestion); + }, + ); + }, + ); + } +} diff --git a/web/gallery/lib/demo/material/selection_controls_demo.dart b/web/gallery/lib/demo/material/selection_controls_demo.dart new file mode 100644 index 000000000..c95e0f855 --- /dev/null +++ b/web/gallery/lib/demo/material/selection_controls_demo.dart @@ -0,0 +1,111 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class SelectionControlsDemo extends StatefulWidget { + static const String routeName = '/material/selection'; + + _SelectionControlsDemoState createState() => _SelectionControlsDemoState(); +} + +class _SelectionControlsDemoState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + bool checkboxValueA = true; + bool checkboxValueB = false; + bool checkboxValueC; + int radioValue = 0; + + void handleRadioValueChanged(int value) { + setState(() { + radioValue = value; + }); + } + + @override + Widget build(BuildContext context) { + return wrapScaffold('Selection Controls', context, _scaffoldKey, + _buildContents(), SelectionControlsDemo.routeName); + } + + Widget _buildContents() { + return Material( + color: Colors.white, + child: new Column( + children: [buildCheckbox(), Divider(), buildRadio()])); + } + + Widget buildCheckbox() { + return Align( + alignment: const Alignment(0.0, -0.2), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: checkboxValueA, + onChanged: (bool value) { + setState(() { + checkboxValueA = value; + }); + }, + ), + Checkbox( + value: checkboxValueB, + onChanged: (bool value) { + setState(() { + checkboxValueB = value; + }); + }, + ), + Checkbox( + value: checkboxValueC, + tristate: true, + onChanged: (bool value) { + setState(() { + checkboxValueC = value; + }); + }, + ), + ], + ), + Row(mainAxisSize: MainAxisSize.min, children: const [ + // Disabled checkboxes + Checkbox(value: true, onChanged: null), + Checkbox(value: false, onChanged: null), + Checkbox(value: null, tristate: true, onChanged: null), + ]) + ])); + } + + Widget buildRadio() { + return Align( + alignment: const Alignment(0.0, -0.2), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Row(mainAxisSize: MainAxisSize.min, children: [ + Radio( + value: 0, + groupValue: radioValue, + onChanged: handleRadioValueChanged), + Radio( + value: 1, + groupValue: radioValue, + onChanged: handleRadioValueChanged), + Radio( + value: 2, + groupValue: radioValue, + onChanged: handleRadioValueChanged) + ]), + // Disabled radio buttons + Row(mainAxisSize: MainAxisSize.min, children: const [ + Radio(value: 0, groupValue: 0, onChanged: null), + Radio(value: 1, groupValue: 0, onChanged: null), + Radio(value: 2, groupValue: 0, onChanged: null) + ]) + ])); + } +} diff --git a/web/gallery/lib/demo/material/slider_demo.dart b/web/gallery/lib/demo/material/slider_demo.dart new file mode 100644 index 000000000..a2c2ca06c --- /dev/null +++ b/web/gallery/lib/demo/material/slider_demo.dart @@ -0,0 +1,240 @@ +// Copyright 2018 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:math' as math; + +import 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class SliderDemo extends StatefulWidget { + static const String routeName = '/material/slider'; + + @override + _SliderDemoState createState() => _SliderDemoState(); +} + +Path _triangle(double size, Offset thumbCenter, {bool invert = false}) { + final Path thumbPath = Path(); + final double height = math.sqrt(3.0) / 2.0; + final double halfSide = size / 2.0; + final double centerHeight = size * height / 3.0; + final double sign = invert ? -1.0 : 1.0; + thumbPath.moveTo( + thumbCenter.dx - halfSide, thumbCenter.dy + sign * centerHeight); + thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight); + thumbPath.lineTo( + thumbCenter.dx + halfSide, thumbCenter.dy + sign * centerHeight); + thumbPath.close(); + return thumbPath; +} + +class _CustomThumbShape extends SliderComponentShape { + static const double _thumbSize = 4.0; + static const double _disabledThumbSize = 3.0; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return isEnabled + ? const Size.fromRadius(_thumbSize) + : const Size.fromRadius(_disabledThumbSize); + } + + static final Animatable sizeTween = Tween( + begin: _disabledThumbSize, + end: _thumbSize, + ); + + @override + void paint( + PaintingContext context, + Offset thumbCenter, { + Animation activationAnimation, + Animation enableAnimation, + bool isDiscrete, + TextPainter labelPainter, + RenderBox parentBox, + SliderThemeData sliderTheme, + TextDirection textDirection, + double value, + }) { + final Canvas canvas = context.canvas; + final ColorTween colorTween = ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.thumbColor, + ); + final double size = _thumbSize * sizeTween.evaluate(enableAnimation); + final Path thumbPath = _triangle(size, thumbCenter); + canvas.drawPath( + thumbPath, Paint()..color = colorTween.evaluate(enableAnimation)); + } +} + +class _CustomValueIndicatorShape extends SliderComponentShape { + static const double _indicatorSize = 4.0; + static const double _disabledIndicatorSize = 3.0; + static const double _slideUpHeight = 40.0; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return Size.fromRadius(isEnabled ? _indicatorSize : _disabledIndicatorSize); + } + + static final Animatable sizeTween = Tween( + begin: _disabledIndicatorSize, + end: _indicatorSize, + ); + + @override + void paint( + PaintingContext context, + Offset thumbCenter, { + Animation activationAnimation, + Animation enableAnimation, + bool isDiscrete, + TextPainter labelPainter, + RenderBox parentBox, + SliderThemeData sliderTheme, + TextDirection textDirection, + double value, + }) { + final Canvas canvas = context.canvas; + final ColorTween enableColor = ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.valueIndicatorColor, + ); + final Tween slideUpTween = Tween( + begin: 0.0, + end: _slideUpHeight, + ); + final double size = _indicatorSize * sizeTween.evaluate(enableAnimation); + final Offset slideUpOffset = + Offset(0.0, -slideUpTween.evaluate(activationAnimation)); + final Path thumbPath = _triangle( + size, + thumbCenter + slideUpOffset, + invert: true, + ); + final Color paintColor = enableColor + .evaluate(enableAnimation) + .withAlpha((255.0 * activationAnimation.value).round()); + canvas.drawPath( + thumbPath, + Paint()..color = paintColor, + ); + canvas.drawLine( + thumbCenter, + thumbCenter + slideUpOffset, + Paint() + ..color = paintColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0); + labelPainter.paint( + canvas, + thumbCenter + + slideUpOffset + + Offset(-labelPainter.width / 2.0, -labelPainter.height - 4.0)); + } +} + +class _SliderDemoState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + double _value = 25.0; + double _discreteValue = 40.0; + + @override + Widget build(BuildContext context) { + return wrapScaffold('Slider Demo', context, _scaffoldKey, + _buildContents(context), SliderDemo.routeName); + } + + Widget _buildContents(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 40.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Slider( + value: _value, + min: 0.0, + max: 100.0, + onChanged: (double value) { + setState(() { + _value = value; + }); + }, + ), + const Text('Continuous'), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Slider(value: 0.25, onChanged: (double val) {}), + Text('Disabled'), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Slider( + value: _discreteValue, + min: 0.0, + max: 200.0, + divisions: 5, + label: '${_discreteValue.round()}', + onChanged: (double value) { + setState(() { + _discreteValue = value; + }); + }, + ), + const Text('Discrete'), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SliderTheme( + data: theme.sliderTheme.copyWith( + activeTrackColor: Colors.deepPurple, + inactiveTrackColor: Colors.black26, + activeTickMarkColor: Colors.white70, + inactiveTickMarkColor: Colors.black, + overlayColor: Colors.black12, + thumbColor: Colors.deepPurple, + valueIndicatorColor: Colors.deepPurpleAccent, + thumbShape: _CustomThumbShape(), + valueIndicatorShape: _CustomValueIndicatorShape(), + valueIndicatorTextStyle: theme.accentTextTheme.body2 + .copyWith(color: Colors.black87), + ), + child: Slider( + value: _discreteValue, + min: 0.0, + max: 200.0, + divisions: 5, + semanticFormatterCallback: (double value) => + value.round().toString(), + label: '${_discreteValue.round()}', + onChanged: (double value) { + setState(() { + _discreteValue = value; + }); + }, + ), + ), + const Text('Discrete with Custom Theme'), + ], + ), + ], + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/snack_bar_demo.dart b/web/gallery/lib/demo/material/snack_bar_demo.dart new file mode 100644 index 000000000..33ee7cc5e --- /dev/null +++ b/web/gallery/lib/demo/material/snack_bar_demo.dart @@ -0,0 +1,83 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; +import '../../gallery/demo.dart'; + +const String _text1 = + 'Snackbars provide lightweight feedback about an operation by ' + 'showing a brief message at the bottom of the screen. Snackbars ' + 'can contain an action.'; + +const String _text2 = + 'Snackbars should contain a single line of text directly related ' + 'to the operation performed. They cannot contain icons.'; + +const String _text3 = + 'By default snackbars automatically disappear after a few seconds '; + +class SnackBarDemo extends StatefulWidget { + const SnackBarDemo({Key key}) : super(key: key); + + static const String routeName = '/material/snack-bar'; + + @override + _SnackBarDemoState createState() => _SnackBarDemoState(); +} + +class _SnackBarDemoState extends State { + int _snackBarIndex = 1; + + Widget buildBody(BuildContext context) { + return SafeArea( + top: false, + bottom: false, + child: ListView( + padding: const EdgeInsets.all(24.0), + children: [ + const Text(_text1), + const Text(_text2), + Center( + child: Row(children: [ + RaisedButton( + child: const Text('SHOW A SNACKBAR'), + onPressed: () { + final int thisSnackBarIndex = _snackBarIndex++; + Scaffold.of(context).showSnackBar(SnackBar( + content: Text('This is snackbar #$thisSnackBarIndex.'), + action: SnackBarAction( + label: 'ACTION', + onPressed: () { + Scaffold.of(context).showSnackBar(SnackBar( + content: Text( + 'You pressed snackbar $thisSnackBarIndex\'s action.'))); + }), + )); + }), + ]), + ), + const Text(_text3), + ].map((Widget child) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 12.0), + child: child); + }).toList()), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Snackbar'), + actions: [ + MaterialDemoDocumentationButton(SnackBarDemo.routeName) + ], + ), + body: Builder( + // Create an inner BuildContext so that the snackBar onPressed methods + // can refer to the Scaffold with Scaffold.of(). + builder: buildBody)); + } +} diff --git a/web/gallery/lib/demo/material/stack_demo.dart b/web/gallery/lib/demo/material/stack_demo.dart new file mode 100644 index 000000000..61ac4a59d --- /dev/null +++ b/web/gallery/lib/demo/material/stack_demo.dart @@ -0,0 +1,23 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +class StackDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: Colors.greenAccent, + width: 1.0, + ), + ), + child: Stack(children: [ + Text('A'), + Text('B'), + ]), + ); + } +} diff --git a/web/gallery/lib/demo/material/switch_demo.dart b/web/gallery/lib/demo/material/switch_demo.dart new file mode 100644 index 000000000..9380c3475 --- /dev/null +++ b/web/gallery/lib/demo/material/switch_demo.dart @@ -0,0 +1,42 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class SwitchDemo extends StatefulWidget { + static const routeName = '/material/switch'; + + @override + SwitchDemoState createState() => SwitchDemoState(); +} + +class SwitchDemoState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return wrapScaffold('Switch Demo', context, _scaffoldKey, _buildContents(), + SwitchDemo.routeName); + } + + bool _value = true; + + Widget _buildContents() { + return Material( + child: Column( + children: [ + Switch( + value: _value, + onChanged: (bool newValue) { + setState(() { + _value = newValue; + }); + }), + ], + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/tabs_demo.dart b/web/gallery/lib/demo/material/tabs_demo.dart new file mode 100644 index 000000000..d3a747fd0 --- /dev/null +++ b/web/gallery/lib/demo/material/tabs_demo.dart @@ -0,0 +1,209 @@ +// Copyright 2018 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. + +// Each TabBarView contains a _Page and for each _Page there is a list +// of _CardData objects. Each _CardData object is displayed by a _CardItem. + +import 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +const String _kGalleryAssetsPackage = 'flutter_gallery_assets'; + +class _Page { + _Page({this.label}); + final String label; + String get id => label[0]; + @override + String toString() => '$runtimeType("$label")'; +} + +class _CardData { + const _CardData({this.title, this.imageAsset, this.imageAssetPackage}); + final String title; + final String imageAsset; + final String imageAssetPackage; +} + +final Map<_Page, List<_CardData>> _allPages = <_Page, List<_CardData>>{ + _Page(label: 'HOME'): <_CardData>[ + const _CardData( + title: 'Flatwear', + imageAsset: 'products/flatwear.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Pine Table', + imageAsset: 'products/table.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Blue Cup', + imageAsset: 'products/cup.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Tea Set', + imageAsset: 'products/teaset.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Desk Set', + imageAsset: 'products/deskset.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Blue Linen Napkins', + imageAsset: 'products/napkins.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Planters', + imageAsset: 'products/planters.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Kitchen Quattro', + imageAsset: 'products/kitchen_quattro.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Platter', + imageAsset: 'products/platter.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + ], + _Page(label: 'APPAREL'): <_CardData>[ + const _CardData( + title: 'Cloud-White Dress', + imageAsset: 'products/dress.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Ginger Scarf', + imageAsset: 'products/scarf.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + const _CardData( + title: 'Blush Sweats', + imageAsset: 'products/sweats.png', + imageAssetPackage: _kGalleryAssetsPackage, + ), + ], +}; + +class _CardDataItem extends StatelessWidget { + const _CardDataItem({this.page, this.data}); + + static const double height = 272.0; + final _Page page; + final _CardData data; + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Align( + alignment: + page.id == 'H' ? Alignment.centerLeft : Alignment.centerRight, + child: CircleAvatar(child: Text('${page.id}')), + ), + SizedBox(width: 144.0, height: 144.0, child: new Text('image') +// Image.asset( +// data.imageAsset, +// package: data.imageAssetPackage, +// fit: BoxFit.contain, +// ), + ), + Center( + child: Text( + data.title, + style: Theme.of(context).textTheme.title, + ), + ), + ], + ), + ), + ); + } +} + +class TabsDemo extends StatelessWidget { + static const String routeName = '/material/tabs'; + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: _allPages.length, + child: Scaffold( + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + title: const Text('Tabs and scrolling'), + actions: [MaterialDemoDocumentationButton(routeName)], + pinned: true, + expandedHeight: 150.0, + forceElevated: innerBoxIsScrolled, + bottom: TabBar( + tabs: _allPages.keys + .map( + (_Page page) => Tab(text: page.label), + ) + .toList(), + ), + ), + ]; + }, + body: TabBarView( + children: _allPages.keys.map((_Page page) { + return SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (BuildContext context) { + return CustomScrollView( + key: PageStorageKey<_Page>(page), + slivers: [ + SliverPadding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + sliver: SliverFixedExtentList( + itemExtent: _CardDataItem.height, + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final _CardData data = _allPages[page][index]; + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: _CardDataItem( + page: page, + data: data, + ), + ); + }, + childCount: _allPages[page].length, + ), + ), + ), + ], + ); + }, + ), + ); + }).toList(), + ), + ), + ), + ); + } +} diff --git a/web/gallery/lib/demo/material/tabs_fab_demo.dart b/web/gallery/lib/demo/material/tabs_fab_demo.dart new file mode 100644 index 000000000..6120c4540 --- /dev/null +++ b/web/gallery/lib/demo/material/tabs_fab_demo.dart @@ -0,0 +1,150 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +const String _explanatoryText = + "When the Scaffold's floating action button changes, the new button fades and " + 'turns into view. In this demo, changing tabs can cause the app to be rebuilt ' + 'with a FloatingActionButton that the Scaffold distinguishes from the others ' + 'by its key.'; + +class _Page { + _Page({this.label, this.colors, this.icon}); + + final String label; + final MaterialColor colors; + final IconData icon; + + Color get labelColor => + colors != null ? colors.shade300 : Colors.grey.shade300; + bool get fabDefined => colors != null && icon != null; + Color get fabColor => colors.shade400; + Icon get fabIcon => Icon(icon); + Key get fabKey => ValueKey(fabColor); +} + +final List<_Page> _allPages = <_Page>[ + _Page(label: 'Blue', colors: Colors.indigo, icon: Icons.add), + _Page(label: 'Eco', colors: Colors.green, icon: Icons.create), + _Page(label: 'No'), + _Page(label: 'Teal', colors: Colors.teal, icon: Icons.add), + _Page(label: 'Red', colors: Colors.red, icon: Icons.create), +]; + +class TabsFabDemo extends StatefulWidget { + static const String routeName = '/material/tabs-fab'; + + @override + _TabsFabDemoState createState() => _TabsFabDemoState(); +} + +class _TabsFabDemoState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + TabController _controller; + _Page _selectedPage; + bool _extendedButtons = false; + + @override + void initState() { + super.initState(); + _controller = TabController(vsync: this, length: _allPages.length); + _controller.addListener(_handleTabSelection); + _selectedPage = _allPages[0]; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleTabSelection() { + setState(() { + _selectedPage = _allPages[_controller.index]; + }); + } + + void _showExplanatoryText() { + _scaffoldKey.currentState.showBottomSheet((BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: Theme.of(context).dividerColor))), + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Text(_explanatoryText, + style: Theme.of(context).textTheme.subhead))); + }); + } + + Widget buildTabView(_Page page) { + return Builder(builder: (BuildContext context) { + return Container( + key: ValueKey(page.label), + padding: const EdgeInsets.fromLTRB(48.0, 48.0, 48.0, 96.0), + child: Card( + child: Center( + child: Text(page.label, + style: TextStyle(color: page.labelColor, fontSize: 32.0), + textAlign: TextAlign.center)))); + }); + } + + Widget buildFloatingActionButton(_Page page) { + if (!page.fabDefined) return null; + + if (_extendedButtons) { + return FloatingActionButton.extended( + key: ValueKey(page.fabKey), + tooltip: 'Show explanation', + backgroundColor: page.fabColor, + icon: page.fabIcon, + label: Text(page.label.toUpperCase()), + onPressed: _showExplanatoryText); + } + + return FloatingActionButton( + key: page.fabKey, + tooltip: 'Show explanation', + backgroundColor: page.fabColor, + child: page.fabIcon, + onPressed: _showExplanatoryText); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('FAB per tab'), + bottom: TabBar( + controller: _controller, + tabs: _allPages + .map((_Page page) => Tab(text: page.label.toUpperCase())) + .toList(), + ), + actions: [ + MaterialDemoDocumentationButton(TabsFabDemo.routeName), + IconButton( + icon: const Icon(Icons.sentiment_very_satisfied), + onPressed: () { + setState(() { + _extendedButtons = !_extendedButtons; + }); + }, + ), + ], + ), + floatingActionButton: buildFloatingActionButton(_selectedPage), + body: TabBarView( + controller: _controller, + children: _allPages.map(buildTabView).toList()), + ); + } +} diff --git a/web/gallery/lib/demo/material/text_demo.dart b/web/gallery/lib/demo/material/text_demo.dart new file mode 100644 index 000000000..0254752a1 --- /dev/null +++ b/web/gallery/lib/demo/material/text_demo.dart @@ -0,0 +1,52 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +class TextDemo extends StatelessWidget { + static const routeName = '/material/text'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Text'), + centerTitle: true, + ), + body: ListView( + children: [ + pad(Text('Single line of text')), + Divider(), + // Single line with many whitespaces in between. + pad(Text(' Text with a lot of whitespace ')), + Divider(), + // Forced multi-line because of the \n. + pad(Text('Text with a newline\ncharacter should render in 2 lines')), + Divider(), + // Multi-line with regular whitespace. + pad(Text( + '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor +vel ligula eget fermentum. Integer mattis nulla vitae ullamcorper +dignissim. Donec vel velit vel eros lobortis laoreet at sit amet turpis. +Ut in orci blandit, rhoncus metus quis, finibus augue. Nullam a elit +venenatis metus accumsan dapibus. Vestibulum imperdiet tristique viverra.''', + )), + Divider(), + // Multi-line with a lot of whitespace in between. + pad(Text( + ''' + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Maecenas auctor vel ligula eget fermentum. + Integer mattis nulla vitae ullamcorper dignissim. + Donec vel velit vel eros lobortis laoreet at sit amet turpis.''', + )), + Divider(), + ], + ), + ); + } + + Padding pad(Widget child) => + Padding(padding: EdgeInsets.all(12), child: child); +} diff --git a/web/gallery/lib/demo/material/text_form_field_demo.dart b/web/gallery/lib/demo/material/text_form_field_demo.dart new file mode 100644 index 000000000..62dbd8f13 --- /dev/null +++ b/web/gallery/lib/demo/material/text_form_field_demo.dart @@ -0,0 +1,341 @@ +// Copyright 2018 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:async'; + +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/services.dart'; + +import '../../gallery/demo.dart'; + +class TextFormFieldDemo extends StatefulWidget { + const TextFormFieldDemo({Key key}) : super(key: key); + + static const String routeName = '/material/text-form-field'; + + @override + TextFormFieldDemoState createState() => TextFormFieldDemoState(); +} + +class PersonData { + String name = ''; + String phoneNumber = ''; + String email = ''; + String password = ''; +} + +class PasswordField extends StatefulWidget { + const PasswordField({ + this.fieldKey, + this.hintText, + this.labelText, + this.helperText, + this.onSaved, + this.validator, + this.onFieldSubmitted, + }); + + final Key fieldKey; + final String hintText; + final String labelText; + final String helperText; + final FormFieldSetter onSaved; + final FormFieldValidator validator; + final ValueChanged onFieldSubmitted; + + @override + _PasswordFieldState createState() => _PasswordFieldState(); +} + +class _PasswordFieldState extends State { + bool _obscureText = true; + + @override + Widget build(BuildContext context) { + return TextFormField( + key: widget.fieldKey, + obscureText: _obscureText, + maxLength: 8, + onSaved: widget.onSaved, + validator: widget.validator, + onFieldSubmitted: widget.onFieldSubmitted, + decoration: InputDecoration( + border: const UnderlineInputBorder(), + filled: true, + hintText: widget.hintText, + labelText: widget.labelText, + helperText: widget.helperText, + suffixIcon: GestureDetector( + onTap: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + child: Icon( + _obscureText ? Icons.visibility : Icons.visibility_off, + semanticLabel: _obscureText ? 'show password' : 'hide password', + ), + ), + ), + ); + } +} + +class TextFormFieldDemoState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + PersonData person = PersonData(); + + void showInSnackBar(String value) { + _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(value))); + } + + bool _autovalidate = false; + bool _formWasEdited = false; + + final GlobalKey _formKey = GlobalKey(); + final GlobalKey> _passwordFieldKey = + GlobalKey>(); + final _UsNumberTextInputFormatter _phoneNumberFormatter = + _UsNumberTextInputFormatter(); + void _handleSubmitted() { + final FormState form = _formKey.currentState; + if (!form.validate()) { + _autovalidate = true; // Start validating on every change. + showInSnackBar('Please fix the errors in red before submitting.'); + } else { + form.save(); + showInSnackBar('${person.name}\'s phone number is ${person.phoneNumber}'); + } + } + + String _validateName(String value) { + _formWasEdited = true; + if (value.isEmpty) return 'Name is required.'; + final RegExp nameExp = RegExp(r'^[A-Za-z ]+$'); + if (!nameExp.hasMatch(value)) + return 'Please enter only alphabetical characters.'; + return null; + } + + String _validatePhoneNumber(String value) { + _formWasEdited = true; + final RegExp phoneExp = RegExp(r'^\(\d\d\d\) \d\d\d\-\d\d\d\d$'); + if (!phoneExp.hasMatch(value)) + return '(###) ###-#### - Enter a US phone number.'; + return null; + } + + String _validatePassword(String value) { + _formWasEdited = true; + final FormFieldState passwordField = _passwordFieldKey.currentState; + if (passwordField.value == null || passwordField.value.isEmpty) + return 'Please enter a password.'; + if (passwordField.value != value) return 'The passwords don\'t match'; + return null; + } + + Future _warnUserAboutInvalidData() async { + final FormState form = _formKey.currentState; + if (form == null || !_formWasEdited || form.validate()) return true; + + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('This form has errors'), + content: const Text('Really leave this form?'), + actions: [ + FlatButton( + child: const Text('YES'), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + FlatButton( + child: const Text('NO'), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ], + ); + }, + ) ?? + false; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Text fields'), + actions: [ + MaterialDemoDocumentationButton(TextFormFieldDemo.routeName) + ], + ), + body: SafeArea( + top: false, + bottom: false, + child: Form( + key: _formKey, + autovalidate: _autovalidate, + onWillPop: _warnUserAboutInvalidData, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24.0), + TextFormField( + textCapitalization: TextCapitalization.words, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + filled: true, + icon: Icon(Icons.person), + hintText: 'What do people call you?', + labelText: 'Name * ', + ), + onSaved: (String value) { + person.name = value; + }, + validator: _validateName, + ), + const SizedBox(height: 24.0), + TextFormField( + decoration: const InputDecoration( + border: UnderlineInputBorder(), + filled: true, + icon: Icon(Icons.phone), + hintText: 'Where can we reach you?', + labelText: 'Phone Number * ', + prefixText: '+1', + ), + keyboardType: TextInputType.phone, + onSaved: (String value) { + person.phoneNumber = value; + }, + validator: _validatePhoneNumber, + // TextInputFormatters are applied in sequence. + inputFormatters: [ + WhitelistingTextInputFormatter.digitsOnly, + // Fit the validating format. + _phoneNumberFormatter, + ], + ), + const SizedBox(height: 24.0), + TextFormField( + decoration: const InputDecoration( + border: UnderlineInputBorder(), + filled: true, + icon: Icon(Icons.email), + hintText: 'Your email address', + labelText: 'E-mail', + ), + keyboardType: TextInputType.emailAddress, + onSaved: (String value) { + person.email = value; + }, + ), + const SizedBox(height: 24.0), + TextFormField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: + 'Tell us about yourself (e.g., write down what you do or what hobbies you have)', + helperText: 'Keep it short, this is just a demo.', + labelText: 'Life story', + ), + maxLines: 3, + ), + const SizedBox(height: 24.0), + TextFormField( + keyboardType: TextInputType.number, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Salary', + prefixText: '\$', + suffixText: 'USD', + suffixStyle: TextStyle(color: Colors.green)), + maxLines: 1, + ), + const SizedBox(height: 24.0), + PasswordField( + fieldKey: _passwordFieldKey, + helperText: 'No more than 8 characters.', + labelText: 'Password *', + onFieldSubmitted: (String value) { + setState(() { + person.password = value; + }); + }, + ), + const SizedBox(height: 24.0), + TextFormField( + enabled: + person.password != null && person.password.isNotEmpty, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + filled: true, + labelText: 'Re-type password', + ), + maxLength: 8, + obscureText: true, + validator: _validatePassword, + ), + const SizedBox(height: 24.0), + Center( + child: RaisedButton( + child: const Text('SUBMIT'), + onPressed: _handleSubmitted, + ), + ), + const SizedBox(height: 24.0), + Text('* indicates required field', + style: Theme.of(context).textTheme.caption), + const SizedBox(height: 24.0), + ], + ), + ), + ), + ), + ); + } +} + +/// Format incoming numeric text to fit the format of (###) ###-#### ##... +class _UsNumberTextInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + final int newTextLength = newValue.text.length; + int selectionIndex = newValue.selection.end; + int usedSubstringIndex = 0; + final StringBuffer newText = StringBuffer(); + if (newTextLength >= 1) { + newText.write('('); + if (newValue.selection.end >= 1) selectionIndex++; + } + if (newTextLength >= 4) { + newText.write(newValue.text.substring(0, usedSubstringIndex = 3) + ') '); + if (newValue.selection.end >= 3) selectionIndex += 2; + } + if (newTextLength >= 7) { + newText.write(newValue.text.substring(3, usedSubstringIndex = 6) + '-'); + if (newValue.selection.end >= 6) selectionIndex++; + } + if (newTextLength >= 11) { + newText.write(newValue.text.substring(6, usedSubstringIndex = 10) + ' '); + if (newValue.selection.end >= 10) selectionIndex++; + } + // Dump the rest. + if (newTextLength >= usedSubstringIndex) + newText.write(newValue.text.substring(usedSubstringIndex)); + return TextEditingValue( + text: newText.toString(), + selection: TextSelection.collapsed(offset: selectionIndex), + ); + } +} diff --git a/web/gallery/lib/demo/material/tooltip_demo.dart b/web/gallery/lib/demo/material/tooltip_demo.dart new file mode 100644 index 000000000..d83bc3792 --- /dev/null +++ b/web/gallery/lib/demo/material/tooltip_demo.dart @@ -0,0 +1,59 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +const String _introText = + 'Tooltips are short identifying messages that briefly appear in response to ' + 'a long press. Tooltip messages are also used by services that make Flutter ' + 'apps accessible, like screen readers.'; + +class TooltipDemo extends StatelessWidget { + static const String routeName = '/material/tooltips'; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: const Text('Tooltips'), + actions: [MaterialDemoDocumentationButton(routeName)], + ), + body: Builder(builder: (BuildContext context) { + return SafeArea( + top: false, + bottom: false, + child: ListView( + children: [ + Text(_introText, style: theme.textTheme.subhead), + Row(children: [ + Text('Long press the ', style: theme.textTheme.subhead), + Tooltip( + message: 'call icon', + child: Icon(Icons.call, + size: 18.0, color: theme.iconTheme.color)), + Text(' icon.', style: theme.textTheme.subhead) + ]), + Center( + child: IconButton( + iconSize: 48.0, + icon: const Icon(Icons.call), + color: theme.iconTheme.color, + tooltip: 'Place a phone call', + onPressed: () { + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text('That was an ordinary tap.'))); + })) + ].map((Widget widget) { + return Padding( + padding: + const EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0), + child: widget); + }).toList()), + ); + })); + } +} diff --git a/web/gallery/lib/demo/material/two_level_list_demo.dart b/web/gallery/lib/demo/material/two_level_list_demo.dart new file mode 100644 index 000000000..70a2f95e4 --- /dev/null +++ b/web/gallery/lib/demo/material/two_level_list_demo.dart @@ -0,0 +1,34 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../../gallery/demo.dart'; + +class TwoLevelListDemo extends StatelessWidget { + static const String routeName = '/material/two-level-list'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Expand/collapse list control'), + actions: [MaterialDemoDocumentationButton(routeName)], + ), + body: ListView(children: [ + const ListTile(title: Text('Top')), + ExpansionTile( + title: const Text('Sublist'), + backgroundColor: Theme.of(context).accentColor.withOpacity(0.025), + children: const [ + ListTile(title: Text('One')), + ListTile(title: Text('Two')), + // https://en.wikipedia.org/wiki/Free_Four + ListTile(title: Text('Free')), + ListTile(title: Text('Four')) + ]), + const ListTile(title: Text('Bottom')) + ])); + } +} diff --git a/web/gallery/lib/demo/pesto_demo.dart b/web/gallery/lib/demo/pesto_demo.dart new file mode 100644 index 000000000..3f20cd306 --- /dev/null +++ b/web/gallery/lib/demo/pesto_demo.dart @@ -0,0 +1,718 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; + +class PestoDemo extends StatelessWidget { + const PestoDemo({Key key}) : super(key: key); + + static const String routeName = '/pesto'; + + @override + Widget build(BuildContext context) => PestoHome(); +} + +const String _kSmallLogoImage = 'logos/pesto/logo_small.png'; +const double _kAppBarHeight = 128.0; +const double _kFabHalfSize = + 28.0; // TODO(mpcomplete): needs to adapt to screen size +const double _kRecipePageMaxWidth = 500.0; + +final Set _favoriteRecipes = Set(); + +final ThemeData _kTheme = ThemeData( + brightness: Brightness.light, + primarySwatch: Colors.teal, + accentColor: Colors.redAccent, +); + +class PestoHome extends StatelessWidget { + @override + Widget build(BuildContext context) { + return const RecipeGridPage(recipes: kPestoRecipes); + } +} + +class PestoFavorites extends StatelessWidget { + @override + Widget build(BuildContext context) { + return RecipeGridPage(recipes: _favoriteRecipes.toList()); + } +} + +class PestoStyle extends TextStyle { + const PestoStyle({ + double fontSize = 12.0, + FontWeight fontWeight, + Color color = Colors.black87, + double letterSpacing, + double height, + }) : super( + inherit: false, + color: color, + fontFamily: 'Raleway', + fontSize: fontSize, + fontWeight: fontWeight, + textBaseline: TextBaseline.alphabetic, + letterSpacing: letterSpacing, + height: height, + ); +} + +// Displays a grid of recipe cards. +class RecipeGridPage extends StatefulWidget { + const RecipeGridPage({Key key, this.recipes}) : super(key: key); + + final List recipes; + + @override + _RecipeGridPageState createState() => _RecipeGridPageState(); +} + +class _RecipeGridPageState extends State { + final GlobalKey scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + return Theme( + data: _kTheme.copyWith(platform: Theme.of(context).platform), + child: Scaffold( + key: scaffoldKey, + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.edit), + onPressed: () { + scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text('Not supported.'), + )); + }, + ), + body: CustomScrollView( + semanticChildCount: widget.recipes.length, + slivers: [ + _buildAppBar(context, statusBarHeight), + _buildBody(context, statusBarHeight), + ], + ), + ), + ); + } + + Widget _buildAppBar(BuildContext context, double statusBarHeight) { + return SliverAppBar( + pinned: true, + expandedHeight: _kAppBarHeight, + actions: [ + IconButton( + icon: const Icon(Icons.search), + tooltip: 'Search', + onPressed: () { + scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text('Not supported.'), + )); + }, + ), + ], + flexibleSpace: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final Size size = constraints.biggest; + final double appBarHeight = size.height - statusBarHeight; + final double t = (appBarHeight - kToolbarHeight) / + (_kAppBarHeight - kToolbarHeight); + final double extraPadding = + Tween(begin: 10.0, end: 24.0).transform(t); + final double logoHeight = appBarHeight - 1.5 * extraPadding; + return Padding( + padding: EdgeInsets.only( + top: statusBarHeight + 0.5 * extraPadding, + bottom: extraPadding, + ), + child: Center( + child: PestoLogo(height: logoHeight, t: t.clamp(0.0, 1.0))), + ); + }, + ), + ); + } + + Widget _buildBody(BuildContext context, double statusBarHeight) { + final EdgeInsets mediaPadding = MediaQuery.of(context).padding; + final EdgeInsets padding = EdgeInsets.only( + top: 8.0, + left: 8.0 + mediaPadding.left, + right: 8.0 + mediaPadding.right, + bottom: 8.0); + return SliverPadding( + padding: padding, + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: _kRecipePageMaxWidth, + crossAxisSpacing: 8.0, + mainAxisSpacing: 8.0, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final Recipe recipe = widget.recipes[index]; + return RecipeCard( + recipe: recipe, + onTap: () { + showRecipePage(context, recipe); + }, + ); + }, + childCount: widget.recipes.length, + ), + ), + ); + } + + void showFavoritesPage(BuildContext context) { + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: '/pesto/favorites'), + builder: (BuildContext context) => PestoFavorites(), + )); + } + + void showRecipePage(BuildContext context, Recipe recipe) { + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: '/pesto/recipe'), + builder: (BuildContext context) { + return Theme( + data: _kTheme.copyWith(platform: Theme.of(context).platform), + child: RecipePage(recipe: recipe), + ); + }, + )); + } +} + +class PestoLogo extends StatefulWidget { + const PestoLogo({this.height, this.t}); + + final double height; + final double t; + + @override + _PestoLogoState createState() => _PestoLogoState(); +} + +class _PestoLogoState extends State { + // Native sizes for logo and its image/text components. + static const double kLogoHeight = 162.0; + static const double kLogoWidth = 220.0; + static const double kImageHeight = 108.0; + static const double kTextHeight = 48.0; + final TextStyle titleStyle = const PestoStyle( + fontSize: kTextHeight, + fontWeight: FontWeight.w900, + color: Colors.white, + letterSpacing: 3.0); + final RectTween _textRectTween = RectTween( + begin: Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight), + end: Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight)); + final Curve _textOpacity = const Interval(0.4, 1.0, curve: Curves.easeInOut); + final RectTween _imageRectTween = RectTween( + begin: Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight), + end: Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight), + ); + + @override + Widget build(BuildContext context) { + return Semantics( + namesRoute: true, + child: Transform( + transform: Matrix4.identity()..scale(widget.height / kLogoHeight), + alignment: Alignment.topCenter, + child: SizedBox( + width: kLogoWidth, + child: Stack( + overflow: Overflow.visible, + children: [ + Positioned.fromRect( + rect: _imageRectTween.lerp(widget.t), + child: Image.asset( + '$_kSmallLogoImage', + fit: BoxFit.contain, + ), + ), + Positioned.fromRect( + rect: _textRectTween.lerp(widget.t), + child: Opacity( + opacity: _textOpacity.transform(widget.t), + child: Text('PESTO', + style: titleStyle, textAlign: TextAlign.center), + ), + ), + ], + ), + ), + ), + ); + } +} + +// A card with the recipe's image, author, and title. +class RecipeCard extends StatelessWidget { + const RecipeCard({Key key, this.recipe, this.onTap}) : super(key: key); + + final Recipe recipe; + final VoidCallback onTap; + + TextStyle get titleStyle => + const PestoStyle(fontSize: 24.0, fontWeight: FontWeight.w600); + TextStyle get authorStyle => + const PestoStyle(fontWeight: FontWeight.w500, color: Colors.black54); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: '${recipe.imagePath}', + child: AspectRatio( + aspectRatio: 4.0 / 3.0, + child: Image.asset( + '${recipe.imagePath}', + package: recipe.imagePackage, + fit: BoxFit.cover, + semanticLabel: recipe.name, + ), + ), + ), + Expanded( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Image.asset( + '${recipe.ingredientsImagePath}', + package: recipe.ingredientsImagePackage, + width: 48.0, + height: 48.0, + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(recipe.name, + style: titleStyle, + softWrap: false, + overflow: TextOverflow.ellipsis), + Text(recipe.author, style: authorStyle), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +// Displays one recipe. Includes the recipe sheet with a background image. +class RecipePage extends StatefulWidget { + const RecipePage({Key key, this.recipe}) : super(key: key); + + final Recipe recipe; + + @override + _RecipePageState createState() => _RecipePageState(); +} + +class _RecipePageState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + final TextStyle menuItemStyle = const PestoStyle( + fontSize: 15.0, color: Colors.black54, height: 24.0 / 15.0); + + double _getAppBarHeight(BuildContext context) => + MediaQuery.of(context).size.height * 0.3; + + @override + Widget build(BuildContext context) { + // The full page content with the recipe's image behind it. This + // adjusts based on the size of the screen. If the recipe sheet touches + // the edge of the screen, use a slightly different layout. + final double appBarHeight = _getAppBarHeight(context); + final Size screenSize = MediaQuery.of(context).size; + final bool fullWidth = screenSize.width < _kRecipePageMaxWidth; + final bool isFavorite = _favoriteRecipes.contains(widget.recipe); + return Scaffold( + key: _scaffoldKey, + body: Stack( + children: [ + Positioned( + top: 0.0, + left: 0.0, + right: 0.0, + height: appBarHeight + _kFabHalfSize, + child: Hero( + tag: '${widget.recipe.imagePath}', + child: Image.asset( + '${widget.recipe.imagePath}', + package: widget.recipe.imagePackage, + fit: fullWidth ? BoxFit.fitWidth : BoxFit.cover, + ), + ), + ), + CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: appBarHeight - _kFabHalfSize, + backgroundColor: Colors.transparent, + actions: [ + PopupMenuButton( + onSelected: (String item) {}, + itemBuilder: (BuildContext context) => + >[ + _buildMenuItem(Icons.share, 'Tweet recipe'), + _buildMenuItem(Icons.email, 'Email recipe'), + _buildMenuItem(Icons.message, 'Message recipe'), + _buildMenuItem(Icons.people, 'Share on Facebook'), + ], + ), + ], + flexibleSpace: const FlexibleSpaceBar( + background: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment(0.0, -1.0), + end: Alignment(0.0, -0.2), + colors: [Color(0x60000000), Color(0x00000000)], + ), + ), + ), + ), + ), + SliverToBoxAdapter( + child: Stack( + children: [ + Container( + padding: const EdgeInsets.only(top: _kFabHalfSize), + width: fullWidth ? null : _kRecipePageMaxWidth, + child: RecipeSheet(recipe: widget.recipe), + ), + Positioned( + right: 16.0, + child: FloatingActionButton( + child: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border), + onPressed: _toggleFavorite, + ), + ), + ], + )), + ], + ), + ], + ), + ); + } + + PopupMenuItem _buildMenuItem(IconData icon, String label) { + return PopupMenuItem( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 24.0), + child: Icon(icon, color: Colors.black54)), + Text(label, style: menuItemStyle), + ], + ), + ); + } + + void _toggleFavorite() { + setState(() { + if (_favoriteRecipes.contains(widget.recipe)) + _favoriteRecipes.remove(widget.recipe); + else + _favoriteRecipes.add(widget.recipe); + }); + } +} + +/// Displays the recipe's name and instructions. +class RecipeSheet extends StatelessWidget { + RecipeSheet({Key key, this.recipe}) : super(key: key); + + final TextStyle titleStyle = const PestoStyle(fontSize: 34.0); + final TextStyle descriptionStyle = const PestoStyle( + fontSize: 15.0, color: Colors.black54, height: 24.0 / 15.0); + final TextStyle itemStyle = + const PestoStyle(fontSize: 15.0, height: 24.0 / 15.0); + final TextStyle itemAmountStyle = PestoStyle( + fontSize: 15.0, color: _kTheme.primaryColor, height: 24.0 / 15.0); + final TextStyle headingStyle = const PestoStyle( + fontSize: 16.0, fontWeight: FontWeight.bold, height: 24.0 / 15.0); + + final Recipe recipe; + + @override + Widget build(BuildContext context) { + return Material( + child: SafeArea( + top: false, + bottom: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0), + child: Table( + columnWidths: const { + 0: FixedColumnWidth(64.0) + }, + children: [ + TableRow(children: [ + TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Image.asset('${recipe.ingredientsImagePath}', + package: recipe.ingredientsImagePackage, + width: 32.0, + height: 32.0, + alignment: Alignment.centerLeft, + fit: BoxFit.scaleDown)), + TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Text(recipe.name, style: titleStyle)), + ]), + TableRow(children: [ + const SizedBox(), + Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), + child: Text(recipe.description, style: descriptionStyle)), + ]), + TableRow(children: [ + const SizedBox(), + Padding( + padding: const EdgeInsets.only(top: 24.0, bottom: 4.0), + child: Text('Ingredients', style: headingStyle)), + ]), + ] + ..addAll(recipe.ingredients + .map((RecipeIngredient ingredient) { + return _buildItemRow(ingredient.amount, ingredient.description); + })) + ..add(TableRow(children: [ + const SizedBox(), + Padding( + padding: const EdgeInsets.only(top: 24.0, bottom: 4.0), + child: Text('Steps', style: headingStyle)), + ])) + ..addAll(recipe.steps.map((RecipeStep step) { + return _buildItemRow(step.duration ?? '', step.description); + })), + ), + ), + ), + ); + } + + TableRow _buildItemRow(String left, String right) { + return TableRow( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Text(left, style: itemAmountStyle), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Text(right, style: itemStyle), + ), + ], + ); + } +} + +class Recipe { + const Recipe( + {this.name, + this.author, + this.description, + this.imagePath, + this.imagePackage, + this.ingredientsImagePath, + this.ingredientsImagePackage, + this.ingredients, + this.steps}); + + final String name; + final String author; + final String description; + final String imagePath; + final String imagePackage; + final String ingredientsImagePath; + final String ingredientsImagePackage; + final List ingredients; + final List steps; +} + +class RecipeIngredient { + const RecipeIngredient({this.amount, this.description}); + + final String amount; + final String description; +} + +class RecipeStep { + const RecipeStep({this.duration, this.description}); + + final String duration; + final String description; +} + +const List kPestoRecipes = [ + Recipe( + name: 'Roasted Chicken', + author: 'Peter Carlsson', + ingredientsImagePath: 'food/icons/main.png', + description: + 'The perfect dish to welcome your family and friends with on a crisp autumn night. Pair with roasted veggies to truly impress them.', + imagePath: 'food/roasted_chicken.png', + ingredients: [ + RecipeIngredient(amount: '1 whole', description: 'Chicken'), + RecipeIngredient(amount: '1/2 cup', description: 'Butter'), + RecipeIngredient(amount: '1 tbsp', description: 'Onion powder'), + RecipeIngredient(amount: '1 tbsp', description: 'Freshly ground pepper'), + RecipeIngredient(amount: '1 tsp', description: 'Salt'), + ], + steps: [ + RecipeStep(duration: '1 min', description: 'Put in oven'), + RecipeStep(duration: '1hr 45 min', description: 'Cook'), + ], + ), + Recipe( + name: 'Chopped Beet Leaves', + author: 'Trevor Hansen', + ingredientsImagePath: 'food/icons/veggie.png', + description: + 'This vegetable has more to offer than just its root. Beet greens can be tossed into a salad to add some variety or sauteed on its own with some oil and garlic.', + imagePath: 'food/chopped_beet_leaves.png', + ingredients: [ + RecipeIngredient(amount: '3 cups', description: 'Beet greens'), + ], + steps: [ + RecipeStep(duration: '5 min', description: 'Chop'), + ], + ), + Recipe( + name: 'Pesto Pasta', + author: 'Ali Connors', + ingredientsImagePath: 'food/icons/main.png', + description: + 'With this pesto recipe, you can quickly whip up a meal to satisfy your savory needs. And if you\'re feeling festive, you can add bacon to taste.', + imagePath: 'food/pesto_pasta.png', + ingredients: [ + RecipeIngredient(amount: '1/4 cup ', description: 'Pasta'), + RecipeIngredient(amount: '2 cups', description: 'Fresh basil leaves'), + RecipeIngredient(amount: '1/2 cup', description: 'Parmesan cheese'), + RecipeIngredient( + amount: '1/2 cup', description: 'Extra virgin olive oil'), + RecipeIngredient(amount: '1/3 cup', description: 'Pine nuts'), + RecipeIngredient(amount: '1/4 cup', description: 'Lemon juice'), + RecipeIngredient(amount: '3 cloves', description: 'Garlic'), + RecipeIngredient(amount: '1/4 tsp', description: 'Salt'), + RecipeIngredient(amount: '1/8 tsp', description: 'Pepper'), + RecipeIngredient(amount: '3 lbs', description: 'Bacon'), + ], + steps: [ + RecipeStep(duration: '15 min', description: 'Blend'), + ], + ), + Recipe( + name: 'Cherry Pie', + author: 'Sandra Adams', + ingredientsImagePath: 'food/icons/main.png', + description: + 'Sometimes when you\'re craving some cheer in your life you can jumpstart your day with some cherry pie. Dessert for breakfast is perfectly acceptable.', + imagePath: 'food/cherry_pie.png', + ingredients: [ + RecipeIngredient(amount: '1', description: 'Pie crust'), + RecipeIngredient( + amount: '4 cups', description: 'Fresh or frozen cherries'), + RecipeIngredient(amount: '1 cup', description: 'Granulated sugar'), + RecipeIngredient(amount: '4 tbsp', description: 'Cornstarch'), + RecipeIngredient(amount: '1½ tbsp', description: 'Butter'), + ], + steps: [ + RecipeStep(duration: '15 min', description: 'Mix'), + RecipeStep(duration: '1hr 30 min', description: 'Bake'), + ], + ), + Recipe( + name: 'Spinach Salad', + author: 'Peter Carlsson', + ingredientsImagePath: 'food/icons/spicy.png', + description: + 'Everyone\'s favorite leafy green is back. Paired with fresh sliced onion, it\'s ready to tackle any dish, whether it be a salad or an egg scramble.', + imagePath: 'food/spinach_onion_salad.png', + ingredients: [ + RecipeIngredient(amount: '4 cups', description: 'Spinach'), + RecipeIngredient(amount: '1 cup', description: 'Sliced onion'), + ], + steps: [ + RecipeStep(duration: '5 min', description: 'Mix'), + ], + ), + Recipe( + name: 'Butternut Squash Soup', + author: 'Ali Connors', + ingredientsImagePath: 'food/icons/healthy.png', + description: + 'This creamy butternut squash soup will warm you on the chilliest of winter nights and bring a delightful pop of orange to the dinner table.', + imagePath: 'food/butternut_squash_soup.png', + ingredients: [ + RecipeIngredient(amount: '1', description: 'Butternut squash'), + RecipeIngredient(amount: '4 cups', description: 'Chicken stock'), + RecipeIngredient(amount: '2', description: 'Potatoes'), + RecipeIngredient(amount: '1', description: 'Onion'), + RecipeIngredient(amount: '1', description: 'Carrot'), + RecipeIngredient(amount: '1', description: 'Celery'), + RecipeIngredient(amount: '1 tsp', description: 'Salt'), + RecipeIngredient(amount: '1 tsp', description: 'Pepper'), + ], + steps: [ + RecipeStep(duration: '10 min', description: 'Prep vegetables'), + RecipeStep(duration: '5 min', description: 'Stir'), + RecipeStep(duration: '1 hr 10 min', description: 'Cook') + ], + ), + Recipe( + name: 'Spanakopita', + author: 'Trevor Hansen', + ingredientsImagePath: 'food/icons/quick.png', + description: + 'You \'feta\' believe this is a crowd-pleaser! Flaky phyllo pastry surrounds a delicious mixture of spinach and cheeses to create the perfect appetizer.', + imagePath: 'food/spanakopita.png', + ingredients: [ + RecipeIngredient(amount: '1 lb', description: 'Spinach'), + RecipeIngredient(amount: '½ cup', description: 'Feta cheese'), + RecipeIngredient(amount: '½ cup', description: 'Cottage cheese'), + RecipeIngredient(amount: '2', description: 'Eggs'), + RecipeIngredient(amount: '1', description: 'Onion'), + RecipeIngredient(amount: '½ lb', description: 'Phyllo dough'), + ], + steps: [ + RecipeStep(duration: '5 min', description: 'Sauté vegetables'), + RecipeStep( + duration: '3 min', + description: 'Stir vegetables and other filling ingredients'), + RecipeStep( + duration: '10 min', + description: 'Fill phyllo squares half-full with filling and fold.'), + RecipeStep(duration: '40 min', description: 'Bake') + ], + ), +]; diff --git a/web/gallery/lib/demo/shrine/shrine_data.dart b/web/gallery/lib/demo/shrine/shrine_data.dart new file mode 100644 index 000000000..bb2a2c1dc --- /dev/null +++ b/web/gallery/lib/demo/shrine/shrine_data.dart @@ -0,0 +1,254 @@ +// Copyright 2018 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 'shrine_types.dart'; + +const String _kGalleryAssetsPackage = null; + +const Vendor _ali = Vendor( + name: 'Ali’s shop', + avatarAsset: 'people/square/ali.png', + avatarAssetPackage: _kGalleryAssetsPackage, + description: + 'Ali Connor’s makes custom goods for folks of all shapes and sizes ' + 'made by hand and sometimes by machine, but always with love and care. ' + 'Custom orders are available upon request if you need something extra special.'); + +const Vendor _peter = Vendor( + name: 'Peter’s shop', + avatarAsset: 'people/square/peter.png', + avatarAssetPackage: _kGalleryAssetsPackage, + description: + 'Peter makes great stuff for awesome people like you. Super cool and extra ' + 'awesome all of his shop’s goods are handmade with love. Custom orders are ' + 'available upon request if you need something extra special.'); + +const Vendor _sandra = Vendor( + name: 'Sandra’s shop', + avatarAsset: 'people/square/sandra.png', + avatarAssetPackage: _kGalleryAssetsPackage, + description: + 'Sandra specializes in furniture, beauty and travel products with a classic vibe. ' + 'Custom orders are available if you’re looking for a certain color or material.'); + +const Vendor _stella = Vendor( + name: 'Stella’s shop', + avatarAsset: 'people/square/stella.png', + avatarAssetPackage: _kGalleryAssetsPackage, + description: + 'Stella sells awesome stuff at lovely prices. made by hand and sometimes by ' + 'machine, but always with love and care. Custom orders are available upon request ' + 'if you need something extra special.'); + +const Vendor _trevor = Vendor( + name: 'Trevor’s shop', + avatarAsset: 'people/square/trevor.png', + avatarAssetPackage: _kGalleryAssetsPackage, + description: + 'Trevor makes great stuff for awesome people like you. Super cool and extra ' + 'awesome all of his shop’s goods are handmade with love. Custom orders are ' + 'available upon request if you need something extra special.'); + +const List _allProducts = [ + Product( + name: 'Vintage Brown Belt', + imageAsset: 'products/belt.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['fashion', 'latest'], + price: 300.00, + vendor: _sandra, + description: + 'Isn’t it cool when things look old, but they\'re not. Looks Old But Not makes ' + 'awesome vintage goods that are super smart. This ol’ belt just got an upgrade. '), + Product( + name: 'Sunglasses', + imageAsset: 'products/sunnies.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['travel', 'fashion', 'beauty'], + price: 20.00, + vendor: _trevor, + description: + 'Be an optimist. Carry Sunglasses with you at all times. All Tints and ' + 'Shades products come with polarized lenses and super duper UV protection ' + 'so you can look at the sun for however long you want. Sunglasses make you ' + 'look cool, wear them.'), + Product( + name: 'Flatwear', + imageAsset: 'products/flatwear.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['furniture'], + price: 30.00, + vendor: _trevor, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait'), + Product( + name: 'Salmon Sweater', + imageAsset: 'products/sweater.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['fashion'], + price: 300.00, + vendor: _stella, + description: + 'Looks can be deceiving. This sweater comes in a wide variety of ' + 'flavors, including salmon, that pop as soon as they hit your eyes. ' + 'Sweaters heat quickly, so savor the warmth.'), + Product( + name: 'Pine Table', + imageAsset: 'products/table.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['furniture'], + price: 63.00, + vendor: _stella, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait'), + Product( + name: 'Green Comfort Jacket', + imageAsset: 'products/jacket.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['fashion'], + price: 36.00, + vendor: _ali, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait'), + Product( + name: 'Chambray Top', + imageAsset: 'products/top.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['fashion'], + price: 125.00, + vendor: _peter, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait'), + Product( + name: 'Blue Cup', + imageAsset: 'products/cup.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['travel', 'furniture'], + price: 75.00, + vendor: _sandra, + description: + 'Drinksy has been making extraordinary mugs for decades. With each ' + 'cup purchased Drinksy donates a cup to those in need. Buy yourself a mug, ' + 'buy someone else a mug.'), + Product( + name: 'Tea Set', + imageAsset: 'products/teaset.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['furniture', 'fashion'], + price: 70.00, + vendor: _trevor, + featureTitle: 'Beautiful glass teapot', + featureDescription: + 'Teapot holds extremely hot liquids and pours them from the spout.', + description: + 'Impress your guests with Tea Set by Kitchen Stuff. Teapot holds extremely ' + 'hot liquids and pours them from the spout. Use the handle, shown on the right, ' + 'so your fingers don’t get burnt while pouring.'), + Product( + name: 'Blue linen napkins', + imageAsset: 'products/napkins.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['furniture', 'fashion'], + price: 89.00, + vendor: _trevor, + description: + 'Blue linen napkins were meant to go with friends, so you may want to pick ' + 'up a bunch of these. These things are absorbant.'), + Product( + name: 'Dipped Earrings', + imageAsset: 'products/earrings.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['fashion', 'beauty'], + price: 25.00, + vendor: _stella, + description: + 'WeDipIt does it again. These hand-dipped 4 inch earrings are perfect for ' + 'the office or the beach. Just be sure you don’t drop it in a bucket of ' + 'red paint, then they won’t look dipped anymore.'), + Product( + name: 'Perfect Planters', + imageAsset: 'products/planters.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['latest', 'furniture'], + price: 30.00, + vendor: _ali, + description: + 'The Perfect Planter Co makes the best vessels for just about anything you ' + 'can pot. This set of Perfect Planters holds succulents and cuttings perfectly. ' + 'Looks great in any room. Keep out of reach from cats.'), + Product( + name: 'Cloud-White Dress', + imageAsset: 'products/dress.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['fashion'], + price: 54.00, + vendor: _sandra, + description: + 'Trying to find the perfect outift to match your mood? Try no longer. ' + 'This Cloud-White Dress has you covered for those nights when you need ' + 'to get out, or even if you’re just headed to work.'), + Product( + name: 'Backpack', + imageAsset: 'products/backpack.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['travel', 'fashion'], + price: 25.00, + vendor: _peter, + description: + 'This backpack by Bags ‘n’ stuff can hold just about anything: a laptop, ' + 'a pen, a protractor, notebooks, small animals, plugs for your devices, ' + 'sunglasses, gym clothes, shoes, gloves, two kittens, and even lunch!'), + Product( + name: 'Charcoal Straw Hat', + imageAsset: 'products/hat.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['travel', 'fashion', 'latest'], + price: 25.00, + vendor: _ali, + description: + 'This is the helmet for those warm summer days on the road. ' + 'Jetset approved, these hats have been rigorously tested. Keep that face ' + 'protected from the sun.'), + Product( + name: 'Ginger Scarf', + imageAsset: 'products/scarf.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['latest', 'fashion'], + price: 17.00, + vendor: _peter, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait'), + Product( + name: 'Blush Sweats', + imageAsset: 'products/sweats.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['travel', 'fashion', 'latest'], + price: 25.00, + vendor: _stella, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait'), + Product( + name: 'Mint Jumper', + imageAsset: 'products/jumper.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['travel', 'fashion', 'beauty'], + price: 25.00, + vendor: _peter, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait'), + Product( + name: 'Ochre Shirt', + imageAsset: 'products/shirt.png', + imageAssetPackage: _kGalleryAssetsPackage, + categories: ['fashion', 'latest'], + price: 120.00, + vendor: _stella, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait') +]; + +List allProducts() { + assert(_allProducts.every((Product product) => product.isValid())); + return List.unmodifiable(_allProducts); +} diff --git a/web/gallery/lib/demo/shrine/shrine_home.dart b/web/gallery/lib/demo/shrine/shrine_home.dart new file mode 100644 index 000000000..dfcfcc24a --- /dev/null +++ b/web/gallery/lib/demo/shrine/shrine_home.dart @@ -0,0 +1,434 @@ +// Copyright 2018 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:async'; + +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; +import 'package:meta/meta.dart'; + +import 'shrine_data.dart'; +import 'shrine_order.dart'; +import 'shrine_page.dart'; +import 'shrine_theme.dart'; +import 'shrine_types.dart'; + +const double unitSize = kToolbarHeight; + +final List _products = List.from(allProducts()); +final Map _shoppingCart = {}; + +const int _childrenPerBlock = 8; +const int _rowsPerBlock = 5; + +int _minIndexInRow(int rowIndex) { + final int blockIndex = rowIndex ~/ _rowsPerBlock; + return const [0, 2, 4, 6, 7][rowIndex % _rowsPerBlock] + + blockIndex * _childrenPerBlock; +} + +int _maxIndexInRow(int rowIndex) { + final int blockIndex = rowIndex ~/ _rowsPerBlock; + return const [1, 3, 5, 6, 7][rowIndex % _rowsPerBlock] + + blockIndex * _childrenPerBlock; +} + +int _rowAtIndex(int index) { + final int blockCount = index ~/ _childrenPerBlock; + return const [ + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 4 + ][index - blockCount * _childrenPerBlock] + + blockCount * _rowsPerBlock; +} + +int _columnAtIndex(int index) { + return const [0, 1, 0, 1, 0, 1, 0, 0][index % _childrenPerBlock]; +} + +int _columnSpanAtIndex(int index) { + return const [1, 1, 1, 1, 1, 1, 2, 2][index % _childrenPerBlock]; +} + +// The Shrine home page arranges the product cards into two columns. The card +// on every 4th and 5th row spans two columns. +class _ShrineGridLayout extends SliverGridLayout { + const _ShrineGridLayout({ + @required this.rowStride, + @required this.columnStride, + @required this.tileHeight, + @required this.tileWidth, + }); + + final double rowStride; + final double columnStride; + final double tileHeight; + final double tileWidth; + + @override + int getMinChildIndexForScrollOffset(double scrollOffset) { + return _minIndexInRow(scrollOffset ~/ rowStride); + } + + @override + int getMaxChildIndexForScrollOffset(double scrollOffset) { + return _maxIndexInRow(scrollOffset ~/ rowStride); + } + + @override + SliverGridGeometry getGeometryForChildIndex(int index) { + final int row = _rowAtIndex(index); + final int column = _columnAtIndex(index); + final int columnSpan = _columnSpanAtIndex(index); + return SliverGridGeometry( + scrollOffset: row * rowStride, + crossAxisOffset: column * columnStride, + mainAxisExtent: tileHeight, + crossAxisExtent: tileWidth + (columnSpan - 1) * columnStride, + ); + } + + @override + double computeMaxScrollOffset(int childCount) { + if (childCount == 0) return 0.0; + final int rowCount = _rowAtIndex(childCount - 1) + 1; + final double rowSpacing = rowStride - tileHeight; + return rowStride * rowCount - rowSpacing; + } +} + +class _ShrineGridDelegate extends SliverGridDelegate { + static const double _spacing = 8.0; + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + final double tileWidth = (constraints.crossAxisExtent - _spacing) / 2.0; + const double tileHeight = 40.0 + 144.0 + 40.0; + return _ShrineGridLayout( + tileWidth: tileWidth, + tileHeight: tileHeight, + rowStride: tileHeight + _spacing, + columnStride: tileWidth + _spacing, + ); + } + + @override + bool shouldRelayout(covariant SliverGridDelegate oldDelegate) => false; +} + +// Displays the Vendor's name and avatar. +class _VendorItem extends StatelessWidget { + const _VendorItem({Key key, @required this.vendor}) + : assert(vendor != null), + super(key: key); + + final Vendor vendor; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 24.0, + child: Row( + children: [ + SizedBox( + width: 24.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: Image.asset( + vendor.avatarAsset, + package: vendor.avatarAssetPackage, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: Text(vendor.name, + style: ShrineTheme.of(context).vendorItemStyle), + ), + ], + ), + ); + } +} + +// Displays the product's price. If the product is in the shopping cart then the +// background is highlighted. +abstract class _PriceItem extends StatelessWidget { + const _PriceItem({Key key, @required this.product}) + : assert(product != null), + super(key: key); + + final Product product; + + Widget buildItem(BuildContext context, TextStyle style, EdgeInsets padding) { + BoxDecoration decoration; + if (_shoppingCart[product] != null) + decoration = + BoxDecoration(color: ShrineTheme.of(context).priceHighlightColor); + + return Container( + padding: padding, + decoration: decoration, + child: Text(product.priceString, style: style), + ); + } +} + +class _ProductPriceItem extends _PriceItem { + const _ProductPriceItem({Key key, Product product}) + : super(key: key, product: product); + + @override + Widget build(BuildContext context) { + return buildItem( + context, + ShrineTheme.of(context).priceStyle, + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + ); + } +} + +class _FeaturePriceItem extends _PriceItem { + const _FeaturePriceItem({Key key, Product product}) + : super(key: key, product: product); + + @override + Widget build(BuildContext context) { + return buildItem( + context, + ShrineTheme.of(context).featurePriceStyle, + const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + ); + } +} + +class _HeadingLayout extends MultiChildLayoutDelegate { + _HeadingLayout(); + + static const String price = 'price'; + static const String image = 'image'; + static const String title = 'title'; + static const String description = 'description'; + static const String vendor = 'vendor'; + + @override + void performLayout(Size size) { + final Size priceSize = layoutChild(price, BoxConstraints.loose(size)); + positionChild(price, Offset(size.width - priceSize.width, 0.0)); + + final double halfWidth = size.width / 2.0; + final double halfHeight = size.height / 2.0; + const double halfUnit = unitSize / 2.0; + const double margin = 16.0; + + final Size imageSize = layoutChild(image, BoxConstraints.loose(size)); + final double imageX = imageSize.width < halfWidth - halfUnit + ? halfWidth / 2.0 - imageSize.width / 2.0 - halfUnit + : halfWidth - imageSize.width; + positionChild(image, Offset(imageX, halfHeight - imageSize.height / 2.0)); + + final double maxTitleWidth = halfWidth + unitSize - margin; + final BoxConstraints titleBoxConstraints = + BoxConstraints(maxWidth: maxTitleWidth); + final Size titleSize = layoutChild(title, titleBoxConstraints); + final double titleX = halfWidth - unitSize; + final double titleY = halfHeight - titleSize.height; + positionChild(title, Offset(titleX, titleY)); + + final Size descriptionSize = layoutChild(description, titleBoxConstraints); + final double descriptionY = titleY + titleSize.height + margin; + positionChild(description, Offset(titleX, descriptionY)); + + layoutChild(vendor, titleBoxConstraints); + final double vendorY = descriptionY + descriptionSize.height + margin; + positionChild(vendor, Offset(titleX, vendorY)); + } + + @override + bool shouldRelayout(_HeadingLayout oldDelegate) => false; +} + +// A card that highlights the "featured" catalog item. +class _Heading extends StatelessWidget { + _Heading({Key key, @required this.product}) + : assert(product != null), + assert(product.featureTitle != null), + assert(product.featureDescription != null), + super(key: key); + + final Product product; + + @override + Widget build(BuildContext context) { + final Size screenSize = MediaQuery.of(context).size; + final ShrineTheme theme = ShrineTheme.of(context); + return MergeSemantics( + child: SizedBox( + height: screenSize.width > screenSize.height + ? (screenSize.height - kToolbarHeight) * 0.85 + : (screenSize.height - kToolbarHeight) * 0.70, + child: Container( + decoration: BoxDecoration( + color: theme.cardBackgroundColor, + border: Border(bottom: BorderSide(color: theme.dividerColor)), + ), + child: CustomMultiChildLayout( + delegate: _HeadingLayout(), + children: [ + LayoutId( + id: _HeadingLayout.price, + child: _FeaturePriceItem(product: product), + ), + LayoutId( + id: _HeadingLayout.image, + child: Image.asset( + product.imageAsset, + package: product.imageAssetPackage, + fit: BoxFit.cover, + ), + ), + LayoutId( + id: _HeadingLayout.title, + child: + Text(product.featureTitle, style: theme.featureTitleStyle), + ), + LayoutId( + id: _HeadingLayout.description, + child: + Text(product.featureDescription, style: theme.featureStyle), + ), + LayoutId( + id: _HeadingLayout.vendor, + child: _VendorItem(vendor: product.vendor), + ), + ], + ), + ), + ), + ); + } +} + +// A card that displays a product's image, price, and vendor. The _ProductItem +// cards appear in a grid below the heading. +class _ProductItem extends StatelessWidget { + const _ProductItem({Key key, @required this.product, this.onPressed}) + : assert(product != null), + super(key: key); + + final Product product; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return MergeSemantics( + child: Card( + child: Stack( + children: [ + Column( + children: [ + Align( + alignment: Alignment.centerRight, + child: _ProductPriceItem(product: product), + ), + Container( + width: 144.0, + height: 144.0, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Hero( + tag: product.tag, + child: Image.asset( + product.imageAsset, + package: product.imageAssetPackage, + fit: BoxFit.contain, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _VendorItem(vendor: product.vendor), + ), + ], + ), + Material( + type: MaterialType.transparency, + child: InkWell(onTap: onPressed), + ), + ], + ), + ), + ); + } +} + +// The Shrine app's home page. Displays the featured item above a grid +// of the product items. +class ShrineHome extends StatefulWidget { + @override + _ShrineHomeState createState() => _ShrineHomeState(); +} + +class _ShrineHomeState extends State { + static final GlobalKey _scaffoldKey = + GlobalKey(debugLabel: 'Shrine Home'); + static final _ShrineGridDelegate gridDelegate = _ShrineGridDelegate(); + + Future _showOrderPage(Product product) async { + final Order order = _shoppingCart[product] ?? Order(product: product); + final Order completedOrder = await Navigator.push( + context, + ShrineOrderRoute( + order: order, + builder: (BuildContext context) { + return OrderPage( + order: order, + products: _products, + shoppingCart: _shoppingCart, + ); + })); + assert(completedOrder.product != null); + if (completedOrder.quantity == 0) + _shoppingCart.remove(completedOrder.product); + } + + @override + Widget build(BuildContext context) { + final Product featured = _products + .firstWhere((Product product) => product.featureDescription != null); + return ShrinePage( + scaffoldKey: _scaffoldKey, + products: _products, + shoppingCart: _shoppingCart, + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: _Heading(product: featured)), + SliverSafeArea( + top: false, + minimum: const EdgeInsets.all(16.0), + sliver: SliverGrid( + gridDelegate: gridDelegate, + delegate: SliverChildListDelegate( + _products.map((Product product) { + return _ProductItem( + product: product, + onPressed: () { + _showOrderPage(product); + }, + ); + }).toList(), + ), + ), + ), + ], + ), + ); + } +} diff --git a/web/gallery/lib/demo/shrine/shrine_order.dart b/web/gallery/lib/demo/shrine/shrine_order.dart new file mode 100644 index 000000000..788c9b0df --- /dev/null +++ b/web/gallery/lib/demo/shrine/shrine_order.dart @@ -0,0 +1,353 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; + +import '../shrine_demo.dart' show ShrinePageRoute; +import 'shrine_page.dart'; +import 'shrine_theme.dart'; +import 'shrine_types.dart'; + +// Displays the product title's, description, and order quantity dropdown. +class _ProductItem extends StatelessWidget { + const _ProductItem({ + Key key, + @required this.product, + @required this.quantity, + @required this.onChanged, + }) : assert(product != null), + assert(quantity != null), + assert(onChanged != null), + super(key: key); + + final Product product; + final int quantity; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final ShrineTheme theme = ShrineTheme.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(product.name, style: theme.featureTitleStyle), + const SizedBox(height: 24.0), + Text(product.description, style: theme.featureStyle), + const SizedBox(height: 16.0), + Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 88.0), + child: DropdownButtonHideUnderline( + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: const Color(0xFFD9D9D9), + ), + ), + child: DropdownButton( + items: [0, 1, 2, 3, 4, 5] + .map>((int value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text('Quantity $value', + style: theme.quantityMenuStyle), + ), + ); + }).toList(), + value: quantity, + onChanged: onChanged, + ), + ), + ), + ), + ], + ); + } +} + +// Vendor name and description +class _VendorItem extends StatelessWidget { + const _VendorItem({Key key, @required this.vendor}) + : assert(vendor != null), + super(key: key); + + final Vendor vendor; + + @override + Widget build(BuildContext context) { + final ShrineTheme theme = ShrineTheme.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 24.0, + child: Align( + alignment: Alignment.bottomLeft, + child: Text(vendor.name, style: theme.vendorTitleStyle), + ), + ), + const SizedBox(height: 16.0), + Text(vendor.description, style: theme.vendorStyle), + ], + ); + } +} + +// Layout the order page's heading: the product's image, the +// title/description/dropdown product item, and the vendor item. +class _HeadingLayout extends MultiChildLayoutDelegate { + _HeadingLayout(); + + static const String image = 'image'; + static const String icon = 'icon'; + static const String product = 'product'; + static const String vendor = 'vendor'; + + @override + void performLayout(Size size) { + const double margin = 56.0; + final bool landscape = size.width > size.height; + final double imageWidth = + (landscape ? size.width / 2.0 : size.width) - margin * 2.0; + final BoxConstraints imageConstraints = + BoxConstraints(maxHeight: 224.0, maxWidth: imageWidth); + final Size imageSize = layoutChild(image, imageConstraints); + const double imageY = 0.0; + positionChild(image, const Offset(margin, imageY)); + + final double productWidth = + landscape ? size.width / 2.0 : size.width - margin; + final BoxConstraints productConstraints = + BoxConstraints(maxWidth: productWidth); + final Size productSize = layoutChild(product, productConstraints); + final double productX = landscape ? size.width / 2.0 : margin; + final double productY = landscape ? 0.0 : imageY + imageSize.height + 16.0; + positionChild(product, Offset(productX, productY)); + + final Size iconSize = layoutChild(icon, BoxConstraints.loose(size)); + positionChild( + icon, Offset(productX - iconSize.width - 16.0, productY + 8.0)); + + final double vendorWidth = landscape ? size.width - margin : productWidth; + layoutChild(vendor, BoxConstraints(maxWidth: vendorWidth)); + final double vendorX = landscape ? margin : productX; + final double vendorY = productY + productSize.height + 16.0; + positionChild(vendor, Offset(vendorX, vendorY)); + } + + @override + bool shouldRelayout(_HeadingLayout oldDelegate) => true; +} + +// Describes a product and vendor in detail, supports specifying +// a order quantity (0-5). Appears at the top of the OrderPage. +class _Heading extends StatelessWidget { + const _Heading({ + Key key, + @required this.product, + @required this.quantity, + this.quantityChanged, + }) : assert(product != null), + assert(quantity != null && quantity >= 0 && quantity <= 5), + super(key: key); + + final Product product; + final int quantity; + final ValueChanged quantityChanged; + + @override + Widget build(BuildContext context) { + final Size screenSize = MediaQuery.of(context).size; + return SizedBox( + height: (screenSize.height - kToolbarHeight) * 1.35, + child: Material( + type: MaterialType.card, + elevation: 0.0, + child: Padding( + padding: const EdgeInsets.only( + left: 16.0, top: 18.0, right: 16.0, bottom: 24.0), + child: CustomMultiChildLayout( + delegate: _HeadingLayout(), + children: [ + LayoutId( + id: _HeadingLayout.image, + child: Hero( + tag: product.tag, + child: Image.asset( + product.imageAsset, + package: product.imageAssetPackage, + fit: BoxFit.contain, + alignment: Alignment.center, + ), + ), + ), + LayoutId( + id: _HeadingLayout.icon, + child: const Icon( + Icons.info_outline, + size: 24.0, + color: Color(0xFFFFE0E0), + ), + ), + LayoutId( + id: _HeadingLayout.product, + child: _ProductItem( + product: product, + quantity: quantity, + onChanged: quantityChanged, + ), + ), + LayoutId( + id: _HeadingLayout.vendor, + child: _VendorItem(vendor: product.vendor), + ), + ], + ), + ), + ), + ); + } +} + +class OrderPage extends StatefulWidget { + OrderPage({ + Key key, + @required this.order, + @required this.products, + @required this.shoppingCart, + }) : assert(order != null), + assert(products != null && products.isNotEmpty), + assert(shoppingCart != null), + super(key: key); + + final Order order; + final List products; + final Map shoppingCart; + + @override + _OrderPageState createState() => _OrderPageState(); +} + +// Displays a product's heading above photos of all of the other products +// arranged in two columns. Enables the user to specify a quantity and add an +// order to the shopping cart. +class _OrderPageState extends State { + GlobalKey scaffoldKey; + + @override + void initState() { + super.initState(); + scaffoldKey = + GlobalKey(debugLabel: 'Shrine Order ${widget.order}'); + } + + Order get currentOrder => ShrineOrderRoute.of(context).order; + + set currentOrder(Order value) { + ShrineOrderRoute.of(context).order = value; + } + + void updateOrder({int quantity, bool inCart}) { + final Order newOrder = + currentOrder.copyWith(quantity: quantity, inCart: inCart); + if (currentOrder != newOrder) { + setState(() { + widget.shoppingCart[newOrder.product] = newOrder; + currentOrder = newOrder; + }); + } + } + + void showSnackBarMessage(String message) { + scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message))); + } + + @override + Widget build(BuildContext context) { + return ShrinePage( + scaffoldKey: scaffoldKey, + products: widget.products, + shoppingCart: widget.shoppingCart, + floatingActionButton: FloatingActionButton( + onPressed: () { + updateOrder(inCart: true); + final int n = currentOrder.quantity; + final String item = currentOrder.product.name; + showSnackBarMessage( + 'There ${n == 1 ? "is one $item item" : "are $n $item items"} in the shopping cart.'); + }, + backgroundColor: const Color(0xFF16F0F0), + tooltip: 'Add to cart', + child: const Icon( + Icons.add_shopping_cart, + color: Colors.black, + ), + ), + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: _Heading( + product: widget.order.product, + quantity: currentOrder.quantity, + quantityChanged: (int value) { + updateOrder(quantity: value); + }, + ), + ), + SliverSafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(8.0, 32.0, 8.0, 8.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 248.0, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + ), + delegate: SliverChildListDelegate( + widget.products + .where((Product product) => product != widget.order.product) + .map((Product product) { + return Card( + elevation: 1.0, + child: Image.asset( + product.imageAsset, + package: product.imageAssetPackage, + fit: BoxFit.contain, + ), + ); + }).toList(), + ), + ), + ), + ], + ), + ); + } +} + +// Displays a full-screen modal OrderPage. +// +// The order field will be replaced each time the user reconfigures the order. +// When the user backs out of this route the completer's value will be the +// final value of the order field. +class ShrineOrderRoute extends ShrinePageRoute { + ShrineOrderRoute({ + @required this.order, + WidgetBuilder builder, + RouteSettings settings, + }) : assert(order != null), + super(builder: builder, settings: settings); + + Order order; + + @override + Order get currentResult => order; + + static ShrineOrderRoute of(BuildContext context) => + ModalRoute.of(context); +} diff --git a/web/gallery/lib/demo/shrine/shrine_page.dart b/web/gallery/lib/demo/shrine/shrine_page.dart new file mode 100644 index 000000000..506588b1b --- /dev/null +++ b/web/gallery/lib/demo/shrine/shrine_page.dart @@ -0,0 +1,137 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import 'shrine_theme.dart'; +import 'shrine_types.dart'; + +enum ShrineAction { sortByPrice, sortByProduct, emptyCart } + +class ShrinePage extends StatefulWidget { + const ShrinePage( + {Key key, + @required this.scaffoldKey, + @required this.body, + this.floatingActionButton, + this.products, + this.shoppingCart}) + : assert(body != null), + assert(scaffoldKey != null), + super(key: key); + + final GlobalKey scaffoldKey; + final Widget body; + final Widget floatingActionButton; + final List products; + final Map shoppingCart; + + @override + ShrinePageState createState() => ShrinePageState(); +} + +/// Defines the Scaffold, AppBar, etc that the demo pages have in common. +class ShrinePageState extends State { + double _appBarElevation = 0.0; + + bool _handleScrollNotification(ScrollNotification notification) { + final double elevation = + notification.metrics.extentBefore <= 0.0 ? 0.0 : 1.0; + if (elevation != _appBarElevation) { + setState(() { + _appBarElevation = elevation; + }); + } + return false; + } + + void _showShoppingCart() { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + if (widget.shoppingCart.isEmpty) { + return const Padding( + padding: EdgeInsets.all(24.0), + child: Text('The shopping cart is empty')); + } + return ListView( + padding: kMaterialListPadding, + children: widget.shoppingCart.values.map((Order order) { + return ListTile( + title: Text(order.product.name), + leading: Text('${order.quantity}'), + subtitle: Text(order.product.vendor.name)); + }).toList(), + ); + }); + } + + void _sortByPrice() { + widget.products.sort((Product a, Product b) => a.price.compareTo(b.price)); + } + + void _sortByProduct() { + widget.products.sort((Product a, Product b) => a.name.compareTo(b.name)); + } + + void _emptyCart() { + widget.shoppingCart.clear(); + widget.scaffoldKey.currentState + .showSnackBar(const SnackBar(content: Text('Shopping cart is empty'))); + } + + @override + Widget build(BuildContext context) { + final ShrineTheme theme = ShrineTheme.of(context); + return Scaffold( + key: widget.scaffoldKey, + appBar: AppBar( + elevation: _appBarElevation, + backgroundColor: theme.appBarBackgroundColor, + iconTheme: Theme.of(context).iconTheme, + brightness: Brightness.light, + flexibleSpace: Container( + decoration: BoxDecoration( + border: + Border(bottom: BorderSide(color: theme.dividerColor)))), + title: + Text('SHRINE', style: ShrineTheme.of(context).appBarTitleStyle), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.shopping_cart), + tooltip: 'Shopping cart', + onPressed: _showShoppingCart), + PopupMenuButton( + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: ShrineAction.sortByPrice, + child: Text('Sort by price')), + const PopupMenuItem( + value: ShrineAction.sortByProduct, + child: Text('Sort by product')), + const PopupMenuItem( + value: ShrineAction.emptyCart, + child: Text('Empty shopping cart')) + ], + onSelected: (ShrineAction action) { + switch (action) { + case ShrineAction.sortByPrice: + setState(_sortByPrice); + break; + case ShrineAction.sortByProduct: + setState(_sortByProduct); + break; + case ShrineAction.emptyCart: + setState(_emptyCart); + break; + } + }) + ]), + floatingActionButton: widget.floatingActionButton, + body: NotificationListener( + onNotification: _handleScrollNotification, child: widget.body)); + } +} diff --git a/web/gallery/lib/demo/shrine/shrine_theme.dart b/web/gallery/lib/demo/shrine/shrine_theme.dart new file mode 100644 index 000000000..5f04f79ab --- /dev/null +++ b/web/gallery/lib/demo/shrine/shrine_theme.dart @@ -0,0 +1,76 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +class ShrineStyle extends TextStyle { + const ShrineStyle.roboto(double size, FontWeight weight, Color color) + : super( + inherit: false, + color: color, + fontSize: size, + fontWeight: weight, + textBaseline: TextBaseline.alphabetic); + + const ShrineStyle.abrilFatface(double size, FontWeight weight, Color color) + : super( + inherit: false, + color: color, + fontFamily: 'AbrilFatface', + fontSize: size, + fontWeight: weight, + textBaseline: TextBaseline.alphabetic); +} + +TextStyle robotoRegular12(Color color) => + ShrineStyle.roboto(12.0, FontWeight.w500, color); +TextStyle robotoLight12(Color color) => + ShrineStyle.roboto(12.0, FontWeight.w300, color); +TextStyle robotoRegular14(Color color) => + ShrineStyle.roboto(14.0, FontWeight.w500, color); +TextStyle robotoMedium14(Color color) => + ShrineStyle.roboto(14.0, FontWeight.w600, color); +TextStyle robotoLight14(Color color) => + ShrineStyle.roboto(14.0, FontWeight.w300, color); +TextStyle robotoRegular16(Color color) => + ShrineStyle.roboto(16.0, FontWeight.w500, color); +TextStyle robotoRegular20(Color color) => + ShrineStyle.roboto(20.0, FontWeight.w500, color); +TextStyle abrilFatfaceRegular24(Color color) => + ShrineStyle.abrilFatface(24.0, FontWeight.w500, color); +TextStyle abrilFatfaceRegular34(Color color) => + ShrineStyle.abrilFatface(34.0, FontWeight.w500, color); + +/// The TextStyles and Colors used for titles, labels, and descriptions. This +/// InheritedWidget is shared by all of the routes and widgets created for +/// the Shrine app. +class ShrineTheme extends InheritedWidget { + ShrineTheme({Key key, @required Widget child}) + : assert(child != null), + super(key: key, child: child); + + final Color cardBackgroundColor = Colors.white; + final Color appBarBackgroundColor = Colors.white; + final Color dividerColor = const Color(0xFFD9D9D9); + final Color priceHighlightColor = const Color(0xFFFFE0E0); + + final TextStyle appBarTitleStyle = robotoRegular20(Colors.black87); + final TextStyle vendorItemStyle = robotoRegular12(const Color(0xFF81959D)); + final TextStyle priceStyle = robotoRegular14(Colors.black87); + final TextStyle featureTitleStyle = + abrilFatfaceRegular34(const Color(0xFF0A3142)); + final TextStyle featurePriceStyle = robotoRegular16(Colors.black87); + final TextStyle featureStyle = robotoLight14(Colors.black54); + final TextStyle orderTitleStyle = abrilFatfaceRegular24(Colors.black87); + final TextStyle orderStyle = robotoLight14(Colors.black54); + final TextStyle vendorTitleStyle = robotoMedium14(Colors.black87); + final TextStyle vendorStyle = robotoLight14(Colors.black54); + final TextStyle quantityMenuStyle = robotoLight14(Colors.black54); + + static ShrineTheme of(BuildContext context) => + context.inheritFromWidgetOfExactType(ShrineTheme); + + @override + bool updateShouldNotify(ShrineTheme oldWidget) => false; +} diff --git a/web/gallery/lib/demo/shrine/shrine_types.dart b/web/gallery/lib/demo/shrine/shrine_types.dart new file mode 100644 index 000000000..dcf6d8839 --- /dev/null +++ b/web/gallery/lib/demo/shrine/shrine_types.dart @@ -0,0 +1,100 @@ +// Copyright 2018 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 'package:flutter_web/foundation.dart'; +import 'package:flutter_web_ui/ui.dart' show hashValues; + +class Vendor { + const Vendor({ + this.name, + this.description, + this.avatarAsset, + this.avatarAssetPackage, + }); + + final String name; + final String description; + final String avatarAsset; + final String avatarAssetPackage; + + bool isValid() { + return name != null && description != null && avatarAsset != null; + } + + @override + String toString() => 'Vendor($name)'; +} + +class Product { + const Product( + {this.name, + this.description, + this.featureTitle, + this.featureDescription, + this.imageAsset, + this.imageAssetPackage, + this.categories, + this.price, + this.vendor}); + + final String name; + final String description; + final String featureTitle; + final String featureDescription; + final String imageAsset; + final String imageAssetPackage; + final List categories; + final double price; + final Vendor vendor; + + String get tag => name; // Unique value for Heroes + String get priceString => '\$${price.floor()}'; + + bool isValid() { + return name != null && + description != null && + imageAsset != null && + categories != null && + categories.isNotEmpty && + price != null && + vendor.isValid(); + } + + @override + String toString() => 'Product($name)'; +} + +class Order { + Order({@required this.product, this.quantity = 1, this.inCart = false}) + : assert(product != null), + assert(quantity != null && quantity >= 0), + assert(inCart != null); + + final Product product; + final int quantity; + final bool inCart; + + Order copyWith({Product product, int quantity, bool inCart}) { + return Order( + product: product ?? this.product, + quantity: quantity ?? this.quantity, + inCart: inCart ?? this.inCart); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final Order typedOther = other; + return product == typedOther.product && + quantity == typedOther.quantity && + inCart == typedOther.inCart; + } + + @override + int get hashCode => hashValues(product, quantity, inCart); + + @override + String toString() => 'Order($product, quantity=$quantity, inCart=$inCart)'; +} diff --git a/web/gallery/lib/demo/shrine_demo.dart b/web/gallery/lib/demo/shrine_demo.dart new file mode 100644 index 000000000..270ce2bda --- /dev/null +++ b/web/gallery/lib/demo/shrine_demo.dart @@ -0,0 +1,43 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import 'shrine/shrine_home.dart' show ShrineHome; +import 'shrine/shrine_theme.dart' show ShrineTheme; + +// This code would ordinarily be part of the MaterialApp's home. It's being +// used by the ShrineDemo and by each route pushed from there because this +// isn't a standalone app with its own main() and MaterialApp. +Widget buildShrine(BuildContext context, Widget child) { + return Theme( + data: ThemeData( + primarySwatch: Colors.grey, + iconTheme: const IconThemeData(color: Color(0xFF707070)), + platform: Theme.of(context).platform, + ), + child: ShrineTheme(child: child)); +} + +// In a standalone version of this app, MaterialPageRoute could be used directly. +class ShrinePageRoute extends MaterialPageRoute { + ShrinePageRoute({ + WidgetBuilder builder, + RouteSettings settings, + }) : super(builder: builder, settings: settings); + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + return buildShrine( + context, super.buildPage(context, animation, secondaryAnimation)); + } +} + +class ShrineDemo extends StatelessWidget { + static const String routeName = '/shrine'; // Used by the Gallery app. + + @override + Widget build(BuildContext context) => buildShrine(context, ShrineHome()); +} diff --git a/web/gallery/lib/demo/typography_demo.dart b/web/gallery/lib/demo/typography_demo.dart new file mode 100644 index 000000000..4b7e92115 --- /dev/null +++ b/web/gallery/lib/demo/typography_demo.dart @@ -0,0 +1,86 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +class TextStyleItem extends StatelessWidget { + const TextStyleItem({ + Key key, + @required this.name, + @required this.style, + @required this.text, + }) : assert(name != null), + assert(style != null), + assert(text != null), + super(key: key); + + final String name; + final TextStyle style; + final String text; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextStyle nameStyle = + theme.textTheme.caption.copyWith(color: theme.textTheme.caption.color); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 72.0, child: Text(name, style: nameStyle)), + Expanded(child: Text(text, style: style.copyWith(height: 1.0))) + ])); + } +} + +class TypographyDemo extends StatelessWidget { + static const String routeName = '/typography'; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final List styleItems = [ + TextStyleItem( + name: 'Display 3', style: textTheme.display3, text: 'Regular 56sp'), + TextStyleItem( + name: 'Display 2', style: textTheme.display2, text: 'Regular 45sp'), + TextStyleItem( + name: 'Display 1', style: textTheme.display1, text: 'Regular 34sp'), + TextStyleItem( + name: 'Headline', style: textTheme.headline, text: 'Regular 24sp'), + TextStyleItem(name: 'Title', style: textTheme.title, text: 'Medium 20sp'), + TextStyleItem( + name: 'Subheading', style: textTheme.subhead, text: 'Regular 16sp'), + TextStyleItem( + name: 'Body 2', style: textTheme.body2, text: 'Medium 14sp'), + TextStyleItem( + name: 'Body 1', style: textTheme.body1, text: 'Regular 14sp'), + TextStyleItem( + name: 'Caption', style: textTheme.caption, text: 'Regular 12sp'), + TextStyleItem( + name: 'Button', + style: textTheme.button, + text: 'MEDIUM (ALL CAPS) 14sp'), + ]; + + if (MediaQuery.of(context).size.width > 500.0) { + styleItems.insert( + 0, + TextStyleItem( + name: 'Display 4', + style: textTheme.display4, + text: 'Light 112sp')); + } + + return Scaffold( + appBar: AppBar(title: const Text('Typography')), + body: SafeArea( + top: false, + bottom: false, + child: ListView(children: styleItems), + ), + ); + } +} diff --git a/web/gallery/lib/gallery/about.dart b/web/gallery/lib/gallery/about.dart new file mode 100644 index 000000000..3c04d6090 --- /dev/null +++ b/web/gallery/lib/gallery/about.dart @@ -0,0 +1,78 @@ +// Copyright 2018 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 'package:flutter_web/gestures.dart'; +import 'package:flutter_web/material.dart'; + +class _LinkTextSpan extends TextSpan { + // Beware! + // + // This class is only safe because the TapGestureRecognizer is not + // given a deadline and therefore never allocates any resources. + // + // In any other situation -- setting a deadline, using any of the less trivial + // recognizers, etc -- you would have to manage the gesture recognizer's + // lifetime and call dispose() when the TextSpan was no longer being rendered. + // + // Since TextSpan itself is @immutable, this means that you would have to + // manage the recognizer from outside the TextSpan, e.g. in the State of a + // stateful widget that then hands the recognizer to the TextSpan. + + _LinkTextSpan({TextStyle style, String url, String text}) + : super( + style: style, + text: text ?? url, + recognizer: TapGestureRecognizer() + ..onTap = () { + //launch(url, forceSafariVC: false); + }); +} + +void showGalleryAboutDialog(BuildContext context) { + final ThemeData themeData = Theme.of(context); + final TextStyle aboutTextStyle = themeData.textTheme.body2; + final TextStyle linkStyle = + themeData.textTheme.body2.copyWith(color: themeData.accentColor); + + showAboutDialog( + context: context, + applicationVersion: '2018 Preview', + //applicationIcon: const FlutterLogo(), + applicationLegalese: '© 2018 The Chromium Authors', + children: [ + Padding( + padding: const EdgeInsets.only(top: 24.0), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + style: aboutTextStyle, + text: 'Flutter web is an early-stage, web framework. ' + 'This gallery is a preview of ' + "Flutter's many widgets, behaviors, animations, layouts, " + 'and more. Learn more about Flutter at '), + _LinkTextSpan( + style: linkStyle, + url: 'https://flutter.io', + ), + TextSpan( + style: aboutTextStyle, + text: '.\n\nTo see the source code for flutter ', + ), + _LinkTextSpan( + style: linkStyle, + url: 'https://goo.gl/iv1p4G', + text: 'flutter github repo', + ), + TextSpan( + style: aboutTextStyle, + text: '.', + ), + ], + ), + ), + ), + ], + ); +} diff --git a/web/gallery/lib/gallery/app.dart b/web/gallery/lib/gallery/app.dart new file mode 100644 index 000000000..8a0fa5d9e --- /dev/null +++ b/web/gallery/lib/gallery/app.dart @@ -0,0 +1,135 @@ +// Copyright 2018 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:async'; + +import 'package:flutter_web/foundation.dart' show defaultTargetPlatform; +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/scheduler.dart' show timeDilation; + +import 'demos.dart'; +import 'home.dart'; +import 'options.dart'; +import 'scales.dart'; +import 'themes.dart'; + +class GalleryApp extends StatefulWidget { + const GalleryApp({ + Key key, + this.enablePerformanceOverlay = true, + this.enableRasterCacheImagesCheckerboard = true, + this.enableOffscreenLayersCheckerboard = true, + this.onSendFeedback, + this.testMode = false, + }) : super(key: key); + + final bool enablePerformanceOverlay; + final bool enableRasterCacheImagesCheckerboard; + final bool enableOffscreenLayersCheckerboard; + final VoidCallback onSendFeedback; + final bool testMode; + + @override + _GalleryAppState createState() => _GalleryAppState(); +} + +class _GalleryAppState extends State { + GalleryOptions _options; + Timer _timeDilationTimer; + + Map _buildRoutes() { + // For a different example of how to set up an application routing table + // using named routes, consider the example in the Navigator class documentation: + // https://docs.flutter.io/flutter/widgets/Navigator-class.html + return Map.fromIterable( + kAllGalleryDemos, + key: (dynamic demo) => '${demo.routeName}', + value: (dynamic demo) => demo.buildRoute, + ); + } + + @override + void initState() { + super.initState(); + _options = GalleryOptions( + theme: kLightGalleryTheme, + textScaleFactor: kAllGalleryTextScaleValues[0], + timeDilation: timeDilation, + platform: defaultTargetPlatform, + ); + } + + @override + void dispose() { + _timeDilationTimer?.cancel(); + _timeDilationTimer = null; + super.dispose(); + } + + void _handleOptionsChanged(GalleryOptions newOptions) { + setState(() { + if (_options.timeDilation != newOptions.timeDilation) { + _timeDilationTimer?.cancel(); + _timeDilationTimer = null; + if (newOptions.timeDilation > 1.0) { + // We delay the time dilation change long enough that the user can see + // that UI has started reacting and then we slam on the brakes so that + // they see that the time is in fact now dilated. + _timeDilationTimer = Timer(const Duration(milliseconds: 150), () { + timeDilation = newOptions.timeDilation; + }); + } else { + timeDilation = newOptions.timeDilation; + } + } + + _options = newOptions; + }); + } + + Widget _applyTextScaleFactor(Widget child) { + return Builder( + builder: (BuildContext context) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: _options.textScaleFactor.scale, + ), + child: child, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + Widget home = GalleryHome( + testMode: widget.testMode, + optionsPage: GalleryOptionsPage( + options: _options, + onOptionsChanged: _handleOptionsChanged, + onSendFeedback: widget.onSendFeedback ?? + () { + // TODO: launch('https://github.com/flutter/flutter/issues/new', forceSafariVC: false); + }, + ), + ); + + return MaterialApp( + theme: _options.theme.data.copyWith(platform: _options.platform), + title: 'Flutter Web Gallery', + color: Colors.grey, + showPerformanceOverlay: _options.showPerformanceOverlay, + checkerboardOffscreenLayers: _options.showOffscreenLayersCheckerboard, + checkerboardRasterCacheImages: _options.showRasterCacheImagesCheckerboard, + routes: _buildRoutes(), + builder: (BuildContext context, Widget child) { + return Directionality( + textDirection: _options.textDirection, + child: _applyTextScaleFactor(child), + ); + }, + home: home, + ); + } +} diff --git a/web/gallery/lib/gallery/backdrop.dart b/web/gallery/lib/gallery/backdrop.dart new file mode 100644 index 000000000..78c31dddd --- /dev/null +++ b/web/gallery/lib/gallery/backdrop.dart @@ -0,0 +1,366 @@ +// Copyright 2018 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:math' as math; + +import 'package:flutter_web/rendering.dart'; +import 'package:flutter_web/material.dart'; + +const double _kFrontHeadingHeight = 32.0; // front layer beveled rectangle +const double _kFrontClosedHeight = 92.0; // front layer height when closed +const double _kBackAppBarHeight = 56.0; // back layer (options) appbar height + +// The size of the front layer heading's left and right beveled corners. +final Animatable _kFrontHeadingBevelRadius = BorderRadiusTween( + begin: const BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + end: const BorderRadius.only( + topLeft: Radius.circular(_kFrontHeadingHeight), + topRight: Radius.circular(_kFrontHeadingHeight), + ), +); + +class _TappableWhileStatusIs extends StatefulWidget { + const _TappableWhileStatusIs( + this.status, { + Key key, + this.controller, + this.child, + }) : super(key: key); + + final AnimationController controller; + final AnimationStatus status; + final Widget child; + + @override + _TappableWhileStatusIsState createState() => _TappableWhileStatusIsState(); +} + +class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> { + bool _active; + + @override + void initState() { + super.initState(); + widget.controller.addStatusListener(_handleStatusChange); + _active = widget.controller.status == widget.status; + } + + @override + void dispose() { + widget.controller.removeStatusListener(_handleStatusChange); + super.dispose(); + } + + void _handleStatusChange(AnimationStatus status) { + final bool value = widget.controller.status == widget.status; + if (_active != value) { + setState(() { + _active = value; + }); + } + } + + @override + Widget build(BuildContext context) { + return AbsorbPointer( + absorbing: !_active, + child: widget.child, + ); + } +} + +class _CrossFadeTransition extends AnimatedWidget { + const _CrossFadeTransition({ + Key key, + this.alignment = Alignment.center, + Animation progress, + this.child0, + this.child1, + }) : super(key: key, listenable: progress); + + final AlignmentGeometry alignment; + final Widget child0; + final Widget child1; + + @override + Widget build(BuildContext context) { + final Animation progress = listenable; + + final double opacity1 = CurvedAnimation( + parent: ReverseAnimation(progress), + curve: const Interval(0.5, 1.0), + ).value; + + final double opacity2 = CurvedAnimation( + parent: progress, + curve: const Interval(0.5, 1.0), + ).value; + + return Stack( + alignment: alignment, + children: [ + Opacity( + opacity: opacity1, + child: Semantics( + scopesRoute: true, + explicitChildNodes: true, + child: child1, + ), + ), + Opacity( + opacity: opacity2, + child: Semantics( + scopesRoute: true, + explicitChildNodes: true, + child: child0, + ), + ), + ], + ); + } +} + +class _BackAppBar extends StatelessWidget { + const _BackAppBar({ + Key key, + this.leading = const SizedBox(width: 56.0), + @required this.title, + this.trailing, + }) : assert(leading != null), + assert(title != null), + super(key: key); + + final Widget leading; + final Widget title; + final Widget trailing; + + @override + Widget build(BuildContext context) { + final List children = [ + Container( + alignment: Alignment.center, + width: 56.0, + child: leading, + ), + Expanded( + child: title, + ), + ]; + + if (trailing != null) { + children.add( + Container( + alignment: Alignment.center, + width: 56.0, + child: trailing, + ), + ); + } + + final ThemeData theme = Theme.of(context); + + return IconTheme.merge( + data: theme.primaryIconTheme, + child: DefaultTextStyle( + style: theme.primaryTextTheme.title, + child: SizedBox( + height: _kBackAppBarHeight, + child: Row(children: children), + ), + ), + ); + } +} + +class Backdrop extends StatefulWidget { + const Backdrop({ + this.frontAction, + this.frontTitle, + this.frontHeading, + this.frontLayer, + this.backTitle, + this.backLayer, + }); + + final Widget frontAction; + final Widget frontTitle; + final Widget frontLayer; + final Widget frontHeading; + final Widget backTitle; + final Widget backLayer; + + @override + _BackdropState createState() => _BackdropState(); +} + +class _BackdropState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop'); + AnimationController _controller; + Animation _frontOpacity; + + static final Animatable _frontOpacityTween = + Tween(begin: 0.2, end: 1.0).chain( + CurveTween(curve: const Interval(0.0, 0.4, curve: Curves.easeInOut))); + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + value: 1.0, + vsync: this, + ); + _frontOpacity = _controller.drive(_frontOpacityTween); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + double get _backdropHeight { + // Warning: this can be safely called from the event handlers but it may + // not be called at build time. + final RenderBox renderBox = _backdropKey.currentContext.findRenderObject(); + return math.max( + 0.0, renderBox.size.height - _kBackAppBarHeight - _kFrontClosedHeight); + } + + void _handleDragUpdate(DragUpdateDetails details) { + _controller.value -= + details.primaryDelta / (_backdropHeight ?? details.primaryDelta); + } + + void _handleDragEnd(DragEndDetails details) { + if (_controller.isAnimating || + _controller.status == AnimationStatus.completed) return; + + final double flingVelocity = + details.velocity.pixelsPerSecond.dy / _backdropHeight; + if (flingVelocity < 0.0) + _controller.fling(velocity: math.max(2.0, -flingVelocity)); + else if (flingVelocity > 0.0) + _controller.fling(velocity: math.min(-2.0, -flingVelocity)); + else + _controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0); + } + + void _toggleFrontLayer() { + final AnimationStatus status = _controller.status; + final bool isOpen = status == AnimationStatus.completed || + status == AnimationStatus.forward; + _controller.fling(velocity: isOpen ? -2.0 : 2.0); + } + + Widget _buildStack(BuildContext context, BoxConstraints constraints) { + final Animation frontRelativeRect = + _controller.drive(RelativeRectTween( + begin: RelativeRect.fromLTRB( + 0.0, constraints.biggest.height - _kFrontClosedHeight, 0.0, 0.0), + end: const RelativeRect.fromLTRB(0.0, _kBackAppBarHeight, 0.0, 0.0), + )); + + final List layers = [ + // Back layer + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _BackAppBar( + leading: widget.frontAction, + title: _CrossFadeTransition( + progress: _controller, + alignment: AlignmentDirectional.centerStart, + child0: Semantics(namesRoute: true, child: widget.frontTitle), + child1: Semantics(namesRoute: true, child: widget.backTitle), + ), + trailing: IconButton( + onPressed: _toggleFrontLayer, + tooltip: 'Toggle options page', + icon: AnimatedIcon( + icon: AnimatedIcons.close_menu, + progress: _controller, + ), + ), + ), + Expanded( + child: Visibility( + child: widget.backLayer, + visible: _controller.status != AnimationStatus.completed, + maintainState: true, + )), + ], + ), + // Front layer + PositionedTransition( + rect: frontRelativeRect, + child: AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget child) { + return PhysicalShape( + elevation: 12.0, + color: Theme.of(context).canvasColor, + clipper: ShapeBorderClipper( + shape: RoundedRectangleBorder( + borderRadius: + _kFrontHeadingBevelRadius.transform(_controller.value)), +// BeveledRectangleBorder( +// borderRadius: _kFrontHeadingBevelRadius.transform(_controller.value), +// ), + ), + clipBehavior: Clip.antiAlias, + child: child, + ); + }, + child: _TappableWhileStatusIs( + AnimationStatus.completed, + controller: _controller, + child: FadeTransition( + opacity: _frontOpacity, + child: widget.frontLayer, + ), + ), + ), + ), + ]; + + // The front "heading" is a (typically transparent) widget that's stacked on + // top of, and at the top of, the front layer. It adds support for dragging + // the front layer up and down and for opening and closing the front layer + // with a tap. It may obscure part of the front layer's topmost child. + if (widget.frontHeading != null) { + layers.add( + PositionedTransition( + rect: frontRelativeRect, + child: ExcludeSemantics( + child: Container( + alignment: Alignment.topLeft, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _toggleFrontLayer, + onVerticalDragUpdate: _handleDragUpdate, + onVerticalDragEnd: _handleDragEnd, + child: widget.frontHeading, + ), + ), + ), + ), + ); + } + + return Stack( + key: _backdropKey, + children: layers, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: _buildStack); + } +} diff --git a/web/gallery/lib/gallery/demo.dart b/web/gallery/lib/gallery/demo.dart new file mode 100644 index 000000000..f3be9fdee --- /dev/null +++ b/web/gallery/lib/gallery/demo.dart @@ -0,0 +1,201 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; +import 'package:flutter_web/cupertino.dart'; + +class ComponentDemoTabData { + ComponentDemoTabData({ + this.demoWidget, + this.exampleCodeTag, + this.description, + this.tabName, + this.documentationUrl, + }); + + final Widget demoWidget; + final String exampleCodeTag; + final String description; + final String tabName; + final String documentationUrl; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + final ComponentDemoTabData typedOther = other; + return typedOther.tabName == tabName && + typedOther.description == description && + typedOther.documentationUrl == documentationUrl; + } + + @override + int get hashCode => hashValues(tabName, description, documentationUrl); +} + +class TabbedComponentDemoScaffold extends StatelessWidget { + const TabbedComponentDemoScaffold({ + this.title, + this.demos, + this.actions, + }); + + final List demos; + final String title; + final List actions; + + void _showExampleCode(BuildContext context) { + final String tag = + demos[DefaultTabController.of(context).index].exampleCodeTag; + if (tag != null) { + throw new UnimplementedError(); + // TODO: +// Navigator.push(context, MaterialPageRoute( +// builder: (BuildContext context) => FullScreenCodeDialog(exampleCodeTag: tag) +// )); + } + } + + void _showApiDocumentation(BuildContext context) { + final String url = + demos[DefaultTabController.of(context).index].documentationUrl; + if (url != null) { + // launch(url, forceWebView: true); + } + } + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: demos.length, + child: Scaffold( + appBar: AppBar( + title: Text(title), + actions: (actions ?? []) + ..addAll( + [ + Builder( + builder: (BuildContext context) { + return IconButton( + icon: const Icon(Icons.library_books), + onPressed: () => _showApiDocumentation(context), + ); + }, + ), + Builder( + builder: (BuildContext context) { + return IconButton( + icon: const Icon(Icons.code), + tooltip: 'Show example code', + onPressed: () => _showExampleCode(context), + ); + }, + ) + ], + ), + bottom: TabBar( + isScrollable: true, + tabs: demos + .map( + (ComponentDemoTabData data) => Tab(text: data.tabName)) + .toList(), + ), + ), + body: TabBarView( + children: demos.map((ComponentDemoTabData demo) { + return SafeArea( + top: false, + bottom: false, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text(demo.description, + style: Theme.of(context).textTheme.subhead)), + Expanded(child: demo.demoWidget) + ], + ), + ); + }).toList(), + ), + ), + ); + } +} + +class MaterialDemoDocumentationButton extends StatelessWidget { + MaterialDemoDocumentationButton(String routeName, {Key key}) + : documentationUrl = 'todo', + assert( + 'todo' != null, + 'A documentation URL was not specified for demo route $routeName in kAllGalleryDemos', + ), + super(key: key); + + final String documentationUrl; + + @override + Widget build(BuildContext context) { + return IconButton( + icon: const Icon(Icons.library_books), + tooltip: 'API documentation', + // TODO(flutter_web): launch(documentationUrl, forceWebView: true) + onPressed: () => {}); + } +} + +Widget wrapScaffold(String title, BuildContext context, Key key, Widget child, + String routeName) { + IconData _backIcon() { + switch (Theme.of(context).platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + return Icons.arrow_back; + case TargetPlatform.iOS: + return Icons.arrow_back_ios; + } + assert(false); + return null; + } + + return Scaffold( + key: key, + appBar: AppBar( + leading: IconButton( + icon: Icon(_backIcon()), + alignment: Alignment.centerLeft, + tooltip: 'Back', + onPressed: () { + Navigator.pop(context); + }, + ), + title: Text(title), + actions: [MaterialDemoDocumentationButton(routeName)], + ), + body: Material(child: Center(child: child)), + ); +} + +class CupertinoDemoDocumentationButton extends StatelessWidget { + CupertinoDemoDocumentationButton(String routeName, {Key key}) + : documentationUrl = 'todo', + assert( + 'todo' != null, + 'A documentation URL was not specified for demo route $routeName in kAllGalleryDemos', + ), + super(key: key); + + final String documentationUrl; + + @override + Widget build(BuildContext context) { + return CupertinoButton( + padding: EdgeInsets.zero, + child: Semantics( + label: 'API documentation', + child: const Icon(CupertinoIcons.book), + ), + // TODO(flutter_web): launch(documentationUrl, forceWebView: true) + onPressed: () => {}); + } +} diff --git a/web/gallery/lib/gallery/demos.dart b/web/gallery/lib/gallery/demos.dart new file mode 100644 index 000000000..e94af4aac --- /dev/null +++ b/web/gallery/lib/gallery/demos.dart @@ -0,0 +1,640 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import '../demo/all.dart'; +import 'icons.dart'; + +// TODO: As Demos are added and complete, uncomment _buildGalleryDemos sections. + +class GalleryDemoCategory { + const GalleryDemoCategory._({this.name, this.icon}); + @required + final String name; + @required + final IconData icon; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (runtimeType != other.runtimeType) return false; + final GalleryDemoCategory typedOther = other; + return typedOther.name == name && typedOther.icon == icon; + } + + @override + int get hashCode => hashValues(name, icon); + + @override + String toString() { + return '$runtimeType($name)'; + } +} + +const GalleryDemoCategory _kDemos = GalleryDemoCategory._( + name: 'Studies', + icon: GalleryIcons.animation, +); + +const GalleryDemoCategory _kStyle = GalleryDemoCategory._( + name: 'Style', + icon: GalleryIcons.custom_typography, +); + +//const GalleryDemoCategory _kCupertinoComponents = GalleryDemoCategory._( +// name: 'Cupertino', +// icon: GalleryIcons.phone_iphone, +//); + +const GalleryDemoCategory _kMaterialComponents = GalleryDemoCategory._( + name: 'Material', + icon: GalleryIcons.category_mdc, +); + +//const GalleryDemoCategory _kMedia = GalleryDemoCategory._( +// name: 'Media', +// icon: GalleryIcons.drive_video, +//); + +class GalleryDemo { + const GalleryDemo({ + @required this.title, + @required this.icon, + this.subtitle, + @required this.category, + @required this.routeName, + this.documentationUrl, + @required this.buildRoute, + }) : assert(title != null), + assert(category != null), + assert(routeName != null), + assert(buildRoute != null); + + final String title; + final IconData icon; + final String subtitle; + final GalleryDemoCategory category; + final String routeName; + final WidgetBuilder buildRoute; + final String documentationUrl; + + @override + String toString() { + return '$runtimeType($title $routeName)'; + } +} + +List _buildGalleryDemos() { + final List galleryDemos = [ + // Demos + GalleryDemo( + title: 'Contact profile', + subtitle: 'Address book entry with a flexible appbar', + icon: GalleryIcons.account_box, + category: _kDemos, + routeName: ContactsDemo.routeName, + buildRoute: (BuildContext context) => ContactsDemo(), + ), + GalleryDemo( + title: 'Shrine', + subtitle: 'Basic shopping app', + icon: GalleryIcons.shrine, + category: _kDemos, + routeName: ShrineDemo.routeName, + buildRoute: (BuildContext context) => ShrineDemo(), + ), + GalleryDemo( + title: 'Animation', + subtitle: 'Section organizer', + icon: GalleryIcons.animation, + category: _kDemos, + routeName: AnimationDemo.routeName, + buildRoute: (BuildContext context) => const AnimationDemo(), + ), + + // Style + GalleryDemo( + title: 'Colors', + subtitle: 'All of the predefined colors', + icon: GalleryIcons.colors, + category: _kStyle, + routeName: ColorsDemo.routeName, + buildRoute: (BuildContext context) => ColorsDemo(), + ), + GalleryDemo( + title: 'Typography', + subtitle: 'All of the predefined text styles', + icon: GalleryIcons.custom_typography, + category: _kStyle, + routeName: TypographyDemo.routeName, + buildRoute: (BuildContext context) => TypographyDemo(), + ), + // Material Components + GalleryDemo( + title: 'Backdrop', + subtitle: 'Select a front layer from back layer', + icon: GalleryIcons.backdrop, + category: _kMaterialComponents, + routeName: BackdropDemo.routeName, + buildRoute: (BuildContext context) => BackdropDemo(), + ), + GalleryDemo( + title: 'Bottom app bar', + subtitle: 'Optional floating action button notch', + icon: GalleryIcons.bottom_app_bar, + category: _kMaterialComponents, + routeName: BottomAppBarDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/BottomAppBar-class.html', + buildRoute: (BuildContext context) => BottomAppBarDemo(), + ), + GalleryDemo( + title: 'Bottom navigation', + subtitle: 'Bottom navigation with cross-fading views', + icon: GalleryIcons.bottom_navigation, + category: _kMaterialComponents, + routeName: BottomNavigationDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/BottomNavigationBar-class.html', + buildRoute: (BuildContext context) => BottomNavigationDemo(), + ), + GalleryDemo( + title: 'Bottom sheet: Modal', + subtitle: 'A dismissable bottom sheet', + icon: GalleryIcons.bottom_sheets, + category: _kMaterialComponents, + routeName: ModalBottomSheetDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/showModalBottomSheet.html', + buildRoute: (BuildContext context) => ModalBottomSheetDemo(), + ), + GalleryDemo( + title: 'Bottom sheet: Persistent', + subtitle: 'A bottom sheet that sticks around', + icon: GalleryIcons.bottom_sheet_persistent, + category: _kMaterialComponents, + routeName: PersistentBottomSheetDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/ScaffoldState/showBottomSheet.html', + buildRoute: (BuildContext context) => PersistentBottomSheetDemo(), + ), + GalleryDemo( + title: 'Buttons', + subtitle: 'Flat, raised, dropdown, and more', + icon: GalleryIcons.generic_buttons, + category: _kMaterialComponents, + routeName: ButtonsDemo.routeName, + buildRoute: (BuildContext context) => ButtonsDemo(), + ), + GalleryDemo( + title: 'Buttons: Floating Action Button', + subtitle: 'FAB with transitions', + icon: GalleryIcons.buttons, + category: _kMaterialComponents, + routeName: TabsFabDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/FloatingActionButton-class.html', + buildRoute: (BuildContext context) => TabsFabDemo(), + ), + GalleryDemo( + title: 'Cards', + subtitle: 'Baseline cards with rounded corners', + icon: GalleryIcons.cards, + category: _kMaterialComponents, + routeName: CardsDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/Card-class.html', + buildRoute: (BuildContext context) => CardsDemo(), + ), + GalleryDemo( + title: 'Chips', + subtitle: 'Labeled with delete buttons and avatars', + icon: GalleryIcons.chips, + category: _kMaterialComponents, + routeName: ChipDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/Chip-class.html', + buildRoute: (BuildContext context) => ChipDemo(), + ), + GalleryDemo( + title: 'Data tables', + subtitle: 'Rows and columns', + icon: GalleryIcons.data_table, + category: _kMaterialComponents, + routeName: DataTableDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/PaginatedDataTable-class.html', + buildRoute: (BuildContext context) => DataTableDemo(), + ), + GalleryDemo( + title: 'Dialogs', + subtitle: 'Simple, alert, and fullscreen', + icon: GalleryIcons.dialogs, + category: _kMaterialComponents, + routeName: DialogDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/showDialog.html', + buildRoute: (BuildContext context) => DialogDemo(), + ), + GalleryDemo( + title: 'Elevations', + subtitle: 'Shadow values on cards', + // TODO(larche): Change to custom icon for elevations when one exists. + icon: GalleryIcons.cupertino_progress, + category: _kMaterialComponents, + routeName: ElevationDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/Material/elevation.html', + buildRoute: (BuildContext context) => ElevationDemo(), + ), + GalleryDemo( + title: 'Expand/collapse list control', + subtitle: 'A list with one sub-list level', + icon: GalleryIcons.expand_all, + category: _kMaterialComponents, + routeName: TwoLevelListDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/ExpansionTile-class.html', + buildRoute: (BuildContext context) => TwoLevelListDemo(), + ), + GalleryDemo( + title: 'Expansion panels', + subtitle: 'List of expanding panels', + icon: GalleryIcons.expand_all, + category: _kMaterialComponents, + routeName: ExpansionPanelsDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/ExpansionPanel-class.html', + buildRoute: (BuildContext context) => ExpansionPanelsDemo(), + ), + GalleryDemo( + title: 'Grid', + subtitle: 'Row and column layout', + icon: GalleryIcons.grid_on, + category: _kMaterialComponents, + routeName: GridListDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/widgets/GridView-class.html', + buildRoute: (BuildContext context) => const GridListDemo(), + ), + GalleryDemo( + title: 'Icons', + subtitle: 'Enabled and disabled icons with opacity', + icon: GalleryIcons.sentiment_very_satisfied, + category: _kMaterialComponents, + routeName: IconsDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/IconButton-class.html', + buildRoute: (BuildContext context) => IconsDemo(), + ), + GalleryDemo( + title: 'Lists', + subtitle: 'Scrolling list layouts', + icon: GalleryIcons.list_alt, + category: _kMaterialComponents, + routeName: ListDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/ListTile-class.html', + buildRoute: (BuildContext context) => const ListDemo(), + ), + GalleryDemo( + title: 'Lists: leave-behind list items', + subtitle: 'List items with hidden actions', + icon: GalleryIcons.lists_leave_behind, + category: _kMaterialComponents, + routeName: LeaveBehindDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/widgets/Dismissible-class.html', + buildRoute: (BuildContext context) => const LeaveBehindDemo(), + ), + GalleryDemo( + title: 'Lists: reorderable', + subtitle: 'Reorderable lists', + icon: GalleryIcons.list_alt, + category: _kMaterialComponents, + routeName: ReorderableListDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/ReorderableListView-class.html', + buildRoute: (BuildContext context) => const ReorderableListDemo(), + ), + GalleryDemo( + title: 'Menus', + subtitle: 'Menu buttons and simple menus', + icon: GalleryIcons.more_vert, + category: _kMaterialComponents, + routeName: MenuDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/PopupMenuButton-class.html', + buildRoute: (BuildContext context) => const MenuDemo(), + ), + GalleryDemo( + title: 'Navigation drawer', + subtitle: 'Navigation drawer with standard header', + icon: GalleryIcons.menu, + category: _kMaterialComponents, + routeName: DrawerDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/Drawer-class.html', + buildRoute: (BuildContext context) => DrawerDemo(), + ), + GalleryDemo( + title: 'Pagination', + subtitle: 'PageView with indicator', + icon: GalleryIcons.page_control, + category: _kMaterialComponents, + routeName: PageSelectorDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/TabBarView-class.html', + buildRoute: (BuildContext context) => PageSelectorDemo(), + ), + GalleryDemo( + title: 'Pickers', + subtitle: 'Date and time selection widgets', + icon: GalleryIcons.event, + category: _kMaterialComponents, + routeName: DateAndTimePickerDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/showDatePicker.html', + buildRoute: (BuildContext context) => DateAndTimePickerDemo(), + ), + GalleryDemo( + title: 'Progress indicators', + subtitle: 'Linear, circular, indeterminate', + icon: GalleryIcons.progress_activity, + category: _kMaterialComponents, + routeName: ProgressIndicatorDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/LinearProgressIndicator-class.html', + buildRoute: (BuildContext context) => ProgressIndicatorDemo(), + ), + GalleryDemo( + title: 'Pull to refresh', + subtitle: 'Refresh indicators', + icon: GalleryIcons.refresh, + category: _kMaterialComponents, + routeName: OverscrollDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/RefreshIndicator-class.html', + buildRoute: (BuildContext context) => const OverscrollDemo(), + ), + GalleryDemo( + title: 'Search', + subtitle: 'Expandable search', + icon: Icons.search, + category: _kMaterialComponents, + routeName: SearchDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/showSearch.html', + buildRoute: (BuildContext context) => SearchDemo(), + ), + GalleryDemo( + title: 'Selection controls', + subtitle: 'Checkboxes, radio buttons, and switches', + icon: GalleryIcons.check_box, + category: _kMaterialComponents, + routeName: SelectionControlsDemo.routeName, + buildRoute: (BuildContext context) => SelectionControlsDemo(), + ), + GalleryDemo( + title: 'Sliders', + subtitle: 'Widgets for selecting a value by swiping', + icon: GalleryIcons.sliders, + category: _kMaterialComponents, + routeName: SliderDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/Slider-class.html', + buildRoute: (BuildContext context) => SliderDemo(), + ), + GalleryDemo( + title: 'Snackbar', + subtitle: 'Temporary messaging', + icon: GalleryIcons.snackbar, + category: _kMaterialComponents, + routeName: SnackBarDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/ScaffoldState/showSnackBar.html', + buildRoute: (BuildContext context) => const SnackBarDemo(), + ), + GalleryDemo( + title: 'Tabs', + subtitle: 'Tabs with independently scrollable views', + icon: GalleryIcons.tabs, + category: _kMaterialComponents, + routeName: TabsDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/TabBarView-class.html', + buildRoute: (BuildContext context) => TabsDemo(), + ), + GalleryDemo( + title: 'Text', + subtitle: 'Single-line text and multiline paragraphs', + icon: Icons.text_fields, + category: _kMaterialComponents, + routeName: TextDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/widgets/Text-class.html', + buildRoute: (BuildContext context) => TextDemo(), + ), + GalleryDemo( + title: 'Text Editing', + subtitle: 'EditableText with a TextEditingController', + icon: Icons.text_fields, + category: _kMaterialComponents, + routeName: EditableTextDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/widgets/EditableText-class.html', + buildRoute: (BuildContext context) => EditableTextDemo(), + ), + GalleryDemo( + title: 'Tabs: Scrolling', + subtitle: 'Tab bar that scrolls', + category: _kMaterialComponents, + icon: GalleryIcons.tabs, + routeName: ScrollableTabsDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/TabBar-class.html', + buildRoute: (BuildContext context) => ScrollableTabsDemo(), + ), + GalleryDemo( + title: 'Text fields', + subtitle: 'Single line of editable text and numbers', + icon: GalleryIcons.text_fields_alt, + category: _kMaterialComponents, + routeName: TextFormFieldDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/TextFormField-class.html', + buildRoute: (BuildContext context) => const TextFormFieldDemo(), + ), + GalleryDemo( + title: 'Tooltips', + subtitle: 'Short message displayed on long-press', + icon: GalleryIcons.tooltip, + category: _kMaterialComponents, + routeName: TooltipDemo.routeName, + documentationUrl: + 'https://docs.flutter.io/flutter/material/Tooltip-class.html', + buildRoute: (BuildContext context) => TooltipDemo(), + ), + + // Media +// GalleryDemo( +// title: 'Animated images', +// subtitle: 'GIF and WebP animations', +// icon: GalleryIcons.animation, +// category: _kMedia, +// routeName: ImagesDemo.routeName, +// buildRoute: (BuildContext context) => ImagesDemo(), +// ), +// GalleryDemo( +// title: 'Video', +// subtitle: 'Video playback', +// icon: GalleryIcons.drive_video, +// category: _kMedia, +// routeName: VideoDemo.routeName, +// buildRoute: (BuildContext context) => const VideoDemo(), +// ), + // Cupertino Components +// GalleryDemo( +// title: 'Activity Indicator', +// icon: GalleryIcons.cupertino_progress, +// category: _kCupertinoComponents, +// routeName: CupertinoProgressIndicatorDemo.routeName, +// documentationUrl: +// 'https://docs.flutter.io/flutter/cupertino/CupertinoActivityIndicator-class.html', +// buildRoute: (BuildContext context) => CupertinoProgressIndicatorDemo(), +// ), +// GalleryDemo( +// title: 'Alerts', +// icon: GalleryIcons.dialogs, +// category: _kCupertinoComponents, +// routeName: CupertinoAlertDemo.routeName, +// documentationUrl: 'https://docs.flutter.io/flutter/cupertino/showCupertinoDialog.html', +// buildRoute: (BuildContext context) => CupertinoAlertDemo(), +// ), +// GalleryDemo( +// title: 'Buttons', +// icon: GalleryIcons.generic_buttons, +// category: _kCupertinoComponents, +// routeName: CupertinoButtonsDemo.routeName, +// documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoButton-class.html', +// buildRoute: (BuildContext context) => CupertinoButtonsDemo(), +// ), +// GalleryDemo( +// title: 'Navigation', +// icon: GalleryIcons.bottom_navigation, +// category: _kCupertinoComponents, +// routeName: CupertinoNavigationDemo.routeName, +// documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoTabScaffold-class.html', +// buildRoute: (BuildContext context) => CupertinoNavigationDemo(), +// ), +// GalleryDemo( +// title: 'Pickers', +// icon: GalleryIcons.event, +// category: _kCupertinoComponents, +// routeName: CupertinoPickerDemo.routeName, +// documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoPicker-class.html', +// buildRoute: (BuildContext context) => CupertinoPickerDemo(), +// ), +// GalleryDemo( +// title: 'Pull to refresh', +// icon: GalleryIcons.cupertino_pull_to_refresh, +// category: _kCupertinoComponents, +// routeName: CupertinoRefreshControlDemo.routeName, +// documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSliverRefreshControl-class.html', +// buildRoute: (BuildContext context) => CupertinoRefreshControlDemo(), +// ), +// GalleryDemo( +// title: 'Segmented Control', +// icon: GalleryIcons.tabs, +// category: _kCupertinoComponents, +// routeName: CupertinoSegmentedControlDemo.routeName, +// documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSegmentedControl-class.html', +// buildRoute: (BuildContext context) => CupertinoSegmentedControlDemo(), +// ), +// GalleryDemo( +// title: 'Sliders', +// icon: GalleryIcons.sliders, +// category: _kCupertinoComponents, +// routeName: CupertinoSliderDemo.routeName, +// documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSlider-class.html', +// buildRoute: (BuildContext context) => CupertinoSliderDemo(), +// ), +// GalleryDemo( +// title: 'Switches', +// icon: GalleryIcons.cupertino_switch, +// category: _kCupertinoComponents, +// routeName: CupertinoSwitchDemo.routeName, +// documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSwitch-class.html', +// buildRoute: (BuildContext context) => CupertinoSwitchDemo(), +// ), +// GalleryDemo( +// title: 'Text Fields', +// icon: GalleryIcons.text_fields_alt, +// category: _kCupertinoComponents, +// routeName: CupertinoTextFieldDemo.routeName, +// buildRoute: (BuildContext context) => CupertinoTextFieldDemo(), +// ), +// +// // Media +// GalleryDemo( +// title: 'Animated images', +// subtitle: 'GIF and WebP animations', +// icon: GalleryIcons.animation, +// category: _kMedia, +// routeName: ImagesDemo.routeName, +// buildRoute: (BuildContext context) => ImagesDemo(), +// ), +// GalleryDemo( +// title: 'Video', +// subtitle: 'Video playback', +// icon: GalleryIcons.drive_video, +// category: _kMedia, +// routeName: VideoDemo.routeName, +// buildRoute: (BuildContext context) => const VideoDemo(), +// ), + ]; + + // Keep Pesto around for its regression test value. It is not included + // in (release builds) the performance tests. + assert(() { + galleryDemos.insert( + 0, + GalleryDemo( + title: 'Pesto', + subtitle: 'Simple recipe browser', + icon: Icons.adjust, + category: _kDemos, + routeName: PestoDemo.routeName, + buildRoute: (BuildContext context) => const PestoDemo(), + ), + ); + return true; + }()); + + return galleryDemos; +} + +final List kAllGalleryDemos = _buildGalleryDemos(); + +final Set kAllGalleryDemoCategories = kAllGalleryDemos + .map((GalleryDemo demo) => demo.category) + .toSet(); + +final Map> kGalleryCategoryToDemos = + Map>.fromIterable( + kAllGalleryDemoCategories, + value: (dynamic category) { + return kAllGalleryDemos + .where((GalleryDemo demo) => demo.category == category) + .toList(); + }, +); + +final Map kDemoDocumentationUrl = + Map.fromIterable( + kAllGalleryDemos.where((GalleryDemo demo) => demo.documentationUrl != null), + key: (dynamic demo) => demo.routeName, + value: (dynamic demo) => demo.documentationUrl, +); diff --git a/web/gallery/lib/gallery/home.dart b/web/gallery/lib/gallery/home.dart new file mode 100644 index 000000000..9598d8ef1 --- /dev/null +++ b/web/gallery/lib/gallery/home.dart @@ -0,0 +1,418 @@ +// Copyright 2018 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:math' as math; +import 'dart:developer'; + +import 'package:flutter_web/material.dart'; + +import 'backdrop.dart'; +import 'demos.dart'; + +const Color _kFlutterBlue = Color(0xFF003D75); +const double _kDemoItemHeight = 64.0; +const Duration _kFrontLayerSwitchDuration = Duration(milliseconds: 300); + +class _FlutterLogo extends StatelessWidget { + const _FlutterLogo({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 34.0, + height: 34.0, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage( + 'logos/flutter_white/logo.png', + //package: _kGalleryAssetsPackage, + ), + ), + ), + ), + ); + } +} + +class _CategoryItem extends StatelessWidget { + const _CategoryItem({ + Key key, + this.category, + this.onTap, + }) : super(key: key); + + final GalleryDemoCategory category; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final bool isDark = theme.brightness == Brightness.dark; + + // This repaint boundary prevents the entire _CategoriesPage from being + // repainted when the button's ink splash animates. + return RepaintBoundary( + child: RawMaterialButton( + padding: EdgeInsets.zero, + splashColor: theme.primaryColor.withOpacity(0.12), + highlightColor: Colors.transparent, + onPressed: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: Icon( + category.icon, + size: 60.0, + color: isDark ? Colors.white : _kFlutterBlue, + ), + ), + const SizedBox(height: 10.0), + Container( + height: 48.0, + alignment: Alignment.center, + child: Text( + category.name, + textAlign: TextAlign.center, + style: theme.textTheme.subhead.copyWith( + fontFamily: 'GoogleSans', + color: isDark ? Colors.white : _kFlutterBlue, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _CategoriesPage extends StatelessWidget { + const _CategoriesPage({ + Key key, + this.categories, + this.onCategoryTap, + }) : super(key: key); + + final Iterable categories; + final ValueChanged onCategoryTap; + + @override + Widget build(BuildContext context) { + const double aspectRatio = 160.0 / 180.0; + final List categoriesList = categories.toList(); + final int columnCount = + (MediaQuery.of(context).orientation == Orientation.portrait) ? 2 : 3; + + return Semantics( + scopesRoute: true, + namesRoute: true, + label: 'categories', + explicitChildNodes: true, + child: SingleChildScrollView( + key: const PageStorageKey('categories'), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final double columnWidth = + constraints.biggest.width / columnCount.toDouble(); + final double rowHeight = math.min(225.0, columnWidth * aspectRatio); + final int rowCount = + (categories.length + columnCount - 1) ~/ columnCount; + + // This repaint boundary prevents the inner contents of the front layer + // from repainting when the backdrop toggle triggers a repaint on the + // LayoutBuilder. + return RepaintBoundary( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: List.generate(rowCount, (int rowIndex) { + final int columnCountForRow = rowIndex == rowCount - 1 + ? categories.length - + columnCount * math.max(0, rowCount - 1) + : columnCount; + + return Row( + children: List.generate(columnCountForRow, + (int columnIndex) { + final int index = rowIndex * columnCount + columnIndex; + final GalleryDemoCategory category = + categoriesList[index]; + + return SizedBox( + width: columnWidth, + height: rowHeight, + child: _CategoryItem( + category: category, + onTap: () { + onCategoryTap(category); + }, + ), + ); + }), + ); + }), + ), + ); + }, + ), + ), + ); + } +} + +class _DemoItem extends StatelessWidget { + const _DemoItem({Key key, this.demo}) : super(key: key); + + final GalleryDemo demo; + + void _launchDemo(BuildContext context) { + if (demo.routeName != null) { + Timeline.instantSync('Start Transition', arguments: { + 'from': '/', + 'to': demo.routeName, + }); + Navigator.pushNamed(context, demo.routeName); + } + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final bool isDark = theme.brightness == Brightness.dark; + final double textScaleFactor = MediaQuery.textScaleFactorOf(context); + + final List titleChildren = [ + Text( + demo.title, + style: theme.textTheme.subhead.copyWith( + color: isDark ? Colors.white : const Color(0xFF202124), + ), + ), + ]; + if (demo.subtitle != null) { + titleChildren.add( + Text( + demo.subtitle, + style: theme.textTheme.body1 + .copyWith(color: isDark ? Colors.white : const Color(0xFF60646B)), + ), + ); + } + + return RawMaterialButton( + padding: EdgeInsets.zero, + splashColor: theme.primaryColor.withOpacity(0.12), + highlightColor: Colors.transparent, + onPressed: () { + _launchDemo(context); + }, + child: Container( + constraints: + BoxConstraints(minHeight: _kDemoItemHeight * textScaleFactor), + child: Row( + children: [ + Container( + width: 56.0, + height: 56.0, + alignment: Alignment.center, + child: Icon( + demo.icon, + size: 24.0, + color: isDark ? Colors.white : _kFlutterBlue, + ), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: titleChildren, + ), + ), + const SizedBox(width: 44.0), + ], + ), + ), + ); + } +} + +class _DemosPage extends StatelessWidget { + const _DemosPage(this.category); + + final GalleryDemoCategory category; + + @override + Widget build(BuildContext context) { + // When overriding ListView.padding, it is necessary to manually handle + // safe areas. + final double windowBottomPadding = MediaQuery.of(context).padding.bottom; + return KeyedSubtree( + key: const ValueKey( + 'GalleryDemoList'), // So the tests can find this ListView + child: Semantics( + scopesRoute: true, + namesRoute: true, + label: category.name, + explicitChildNodes: true, + child: ListView( + key: PageStorageKey(category.name), + padding: EdgeInsets.only(top: 8.0, bottom: windowBottomPadding), + children: + kGalleryCategoryToDemos[category].map((GalleryDemo demo) { + return _DemoItem(demo: demo); + }).toList(), + ), + ), + ); + } +} + +class GalleryHome extends StatefulWidget { + const GalleryHome({ + Key key, + this.testMode = false, + this.optionsPage, + }) : super(key: key); + + final Widget optionsPage; + final bool testMode; + + // In checked mode our MaterialApp will show the default "debug" banner. + // Otherwise show the "preview" banner. + static bool showPreviewBanner = true; + + @override + _GalleryHomeState createState() => _GalleryHomeState(); +} + +class _GalleryHomeState extends State + with SingleTickerProviderStateMixin { + static final GlobalKey _scaffoldKey = + GlobalKey(); + AnimationController _controller; + GalleryDemoCategory _category; + + static Widget _topHomeLayout( + Widget currentChild, List previousChildren) { + List children = previousChildren; + if (currentChild != null) children = children.toList()..add(currentChild); + return Stack( + children: children, + alignment: Alignment.topCenter, + ); + } + + static const AnimatedSwitcherLayoutBuilder _centerHomeLayout = + AnimatedSwitcher.defaultLayoutBuilder; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 600), + debugLabel: 'preview banner', + vsync: this, + )..forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final bool isDark = theme.brightness == Brightness.dark; + final MediaQueryData media = MediaQuery.of(context); + final bool centerHome = + media.orientation == Orientation.portrait && media.size.height < 800.0; + + const Curve switchOutCurve = + Interval(0.4, 1.0, curve: Curves.fastOutSlowIn); + const Curve switchInCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn); + + Widget home = Scaffold( + key: _scaffoldKey, + backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor, + body: SafeArea( + bottom: false, + child: WillPopScope( + onWillPop: () { + // Pop the category page if Android back button is pressed. + if (_category != null) { + setState(() => _category = null); + return Future.value(false); + } + return Future.value(true); + }, + child: Backdrop( + backTitle: const Text('Options'), + backLayer: widget.optionsPage, + frontAction: AnimatedSwitcher( + duration: _kFrontLayerSwitchDuration, + switchOutCurve: switchOutCurve, + switchInCurve: switchInCurve, + child: _category == null + ? const _FlutterLogo() + : IconButton( + icon: const BackButtonIcon(), + tooltip: 'Back', + onPressed: () => setState(() => _category = null), + ), + ), + frontTitle: AnimatedSwitcher( + duration: _kFrontLayerSwitchDuration, + child: _category == null + ? const Text('Flutter web gallery') + : Text(_category.name), + ), + frontHeading: widget.testMode ? null : Container(height: 24.0), + frontLayer: AnimatedSwitcher( + duration: _kFrontLayerSwitchDuration, + switchOutCurve: switchOutCurve, + switchInCurve: switchInCurve, + layoutBuilder: centerHome ? _centerHomeLayout : _topHomeLayout, + child: _category != null + ? _DemosPage(_category) + : _CategoriesPage( + categories: kAllGalleryDemoCategories, + onCategoryTap: (GalleryDemoCategory category) { + setState(() => _category = category); + }, + ), + ), + ), + ), + ), + ); + + assert(() { + GalleryHome.showPreviewBanner = false; + return true; + }()); + + if (GalleryHome.showPreviewBanner) { + home = Stack(fit: StackFit.expand, children: [ + home, + FadeTransition( + opacity: + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + child: const Banner( + message: 'PREVIEW', + location: BannerLocation.topEnd, + )), + ]); + } + home = AnnotatedRegion( + child: home, value: SystemUiOverlayStyle.light); + + return home; + } +} diff --git a/web/gallery/lib/gallery/icons.dart b/web/gallery/lib/gallery/icons.dart new file mode 100644 index 000000000..d571a5449 --- /dev/null +++ b/web/gallery/lib/gallery/icons.dart @@ -0,0 +1,73 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +class GalleryIcons { + GalleryIcons._(); + + static const IconData tooltip = IconData(0xe900, fontFamily: 'GalleryIcons'); + static const IconData text_fields_alt = + IconData(0xe901, fontFamily: 'GalleryIcons'); + static const IconData tabs = IconData(0xe902, fontFamily: 'GalleryIcons'); + static const IconData switches = IconData(0xe903, fontFamily: 'GalleryIcons'); + static const IconData sliders = IconData(0xe904, fontFamily: 'GalleryIcons'); + static const IconData shrine = IconData(0xe905, fontFamily: 'GalleryIcons'); + static const IconData sentiment_very_satisfied = + IconData(0xe906, fontFamily: 'GalleryIcons'); + static const IconData refresh = IconData(0xe907, fontFamily: 'GalleryIcons'); + static const IconData progress_activity = + IconData(0xe908, fontFamily: 'GalleryIcons'); + static const IconData phone_iphone = + IconData(0xe909, fontFamily: 'GalleryIcons'); + static const IconData page_control = + IconData(0xe90a, fontFamily: 'GalleryIcons'); + static const IconData more_vert = + IconData(0xe90b, fontFamily: 'GalleryIcons'); + static const IconData menu = IconData(0xe90c, fontFamily: 'GalleryIcons'); + static const IconData list_alt = IconData(0xe90d, fontFamily: 'GalleryIcons'); + static const IconData grid_on = IconData(0xe90e, fontFamily: 'GalleryIcons'); + static const IconData expand_all = + IconData(0xe90f, fontFamily: 'GalleryIcons'); + static const IconData event = IconData(0xe910, fontFamily: 'GalleryIcons'); + static const IconData drive_video = + IconData(0xe911, fontFamily: 'GalleryIcons'); + static const IconData dialogs = IconData(0xe912, fontFamily: 'GalleryIcons'); + static const IconData data_table = + IconData(0xe913, fontFamily: 'GalleryIcons'); + static const IconData custom_typography = + IconData(0xe914, fontFamily: 'GalleryIcons'); + static const IconData colors = IconData(0xe915, fontFamily: 'GalleryIcons'); + static const IconData chips = IconData(0xe916, fontFamily: 'GalleryIcons'); + static const IconData check_box = + IconData(0xe917, fontFamily: 'GalleryIcons'); + static const IconData cards = IconData(0xe918, fontFamily: 'GalleryIcons'); + static const IconData buttons = IconData(0xe919, fontFamily: 'GalleryIcons'); + static const IconData bottom_sheets = + IconData(0xe91a, fontFamily: 'GalleryIcons'); + static const IconData bottom_navigation = + IconData(0xe91b, fontFamily: 'GalleryIcons'); + static const IconData animation = + IconData(0xe91c, fontFamily: 'GalleryIcons'); + static const IconData account_box = + IconData(0xe91d, fontFamily: 'GalleryIcons'); + static const IconData snackbar = IconData(0xe91e, fontFamily: 'GalleryIcons'); + static const IconData category_mdc = + IconData(0xe91f, fontFamily: 'GalleryIcons'); + static const IconData cupertino_progress = + IconData(0xe920, fontFamily: 'GalleryIcons'); + static const IconData cupertino_pull_to_refresh = + IconData(0xe921, fontFamily: 'GalleryIcons'); + static const IconData cupertino_switch = + IconData(0xe922, fontFamily: 'GalleryIcons'); + static const IconData generic_buttons = + IconData(0xe923, fontFamily: 'GalleryIcons'); + static const IconData backdrop = IconData(0xe924, fontFamily: 'GalleryIcons'); + static const IconData bottom_app_bar = + IconData(0xe925, fontFamily: 'GalleryIcons'); + static const IconData bottom_sheet_persistent = + IconData(0xe926, fontFamily: 'GalleryIcons'); + static const IconData lists_leave_behind = + IconData(0xe927, fontFamily: 'GalleryIcons'); +} diff --git a/web/gallery/lib/gallery/options.dart b/web/gallery/lib/gallery/options.dart new file mode 100644 index 000000000..f49569b34 --- /dev/null +++ b/web/gallery/lib/gallery/options.dart @@ -0,0 +1,480 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import 'about.dart'; +import 'scales.dart'; +import 'themes.dart'; + +class GalleryOptions { + GalleryOptions({ + this.theme, + this.textScaleFactor, + this.textDirection = TextDirection.ltr, + this.timeDilation = 1.0, + this.platform, + this.showOffscreenLayersCheckerboard = false, + this.showRasterCacheImagesCheckerboard = false, + this.showPerformanceOverlay = false, + }); + + final GalleryTheme theme; + final GalleryTextScaleValue textScaleFactor; + final TextDirection textDirection; + final double timeDilation; + final TargetPlatform platform; + final bool showPerformanceOverlay; + final bool showRasterCacheImagesCheckerboard; + final bool showOffscreenLayersCheckerboard; + + GalleryOptions copyWith({ + GalleryTheme theme, + GalleryTextScaleValue textScaleFactor, + TextDirection textDirection, + double timeDilation, + TargetPlatform platform, + bool showPerformanceOverlay, + bool showRasterCacheImagesCheckerboard, + bool showOffscreenLayersCheckerboard, + }) { + return GalleryOptions( + theme: theme ?? this.theme, + textScaleFactor: textScaleFactor ?? this.textScaleFactor, + textDirection: textDirection ?? this.textDirection, + timeDilation: timeDilation ?? this.timeDilation, + platform: platform ?? this.platform, + showPerformanceOverlay: + showPerformanceOverlay ?? this.showPerformanceOverlay, + showOffscreenLayersCheckerboard: showOffscreenLayersCheckerboard ?? + this.showOffscreenLayersCheckerboard, + showRasterCacheImagesCheckerboard: showRasterCacheImagesCheckerboard ?? + this.showRasterCacheImagesCheckerboard, + ); + } + + @override + bool operator ==(dynamic other) { + if (runtimeType != other.runtimeType) return false; + final GalleryOptions typedOther = other; + return theme == typedOther.theme && + textScaleFactor == typedOther.textScaleFactor && + textDirection == typedOther.textDirection && + platform == typedOther.platform && + showPerformanceOverlay == typedOther.showPerformanceOverlay && + showRasterCacheImagesCheckerboard == + typedOther.showRasterCacheImagesCheckerboard && + showOffscreenLayersCheckerboard == + typedOther.showRasterCacheImagesCheckerboard; + } + + @override + int get hashCode => hashValues( + theme, + textScaleFactor, + textDirection, + timeDilation, + platform, + showPerformanceOverlay, + showRasterCacheImagesCheckerboard, + showOffscreenLayersCheckerboard, + ); + + @override + String toString() { + return '$runtimeType($theme)'; + } +} + +const double _kItemHeight = 48.0; +const EdgeInsetsDirectional _kItemPadding = + EdgeInsetsDirectional.only(start: 56.0); + +class _OptionsItem extends StatelessWidget { + const _OptionsItem({Key key, this.child}) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + final double textScaleFactor = MediaQuery.textScaleFactorOf(context); + + return MergeSemantics( + child: Container( + constraints: BoxConstraints(minHeight: _kItemHeight * textScaleFactor), + padding: _kItemPadding, + alignment: AlignmentDirectional.centerStart, + child: DefaultTextStyle( + style: DefaultTextStyle.of(context).style, + maxLines: 2, + overflow: TextOverflow.fade, + child: IconTheme( + data: Theme.of(context).primaryIconTheme, + child: child, + ), + ), + ), + ); + } +} + +class _BooleanItem extends StatelessWidget { + const _BooleanItem(this.title, this.value, this.onChanged, {this.switchKey}); + + final String title; + final bool value; + final ValueChanged onChanged; + // [switchKey] is used for accessing the switch from driver tests. + final Key switchKey; + + @override + Widget build(BuildContext context) { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + return _OptionsItem( + child: Row( + children: [ + Expanded(child: Text(title)), + Switch( + key: switchKey, + value: value, + onChanged: onChanged, + activeColor: const Color(0xFF39CEFD), + activeTrackColor: isDark ? Colors.white30 : Colors.black26, + ), + ], + ), + ); + } +} + +class _ActionItem extends StatelessWidget { + const _ActionItem(this.text, this.onTap); + + final String text; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return _OptionsItem( + child: _FlatButton( + onPressed: onTap, + child: Text(text), + ), + ); + } +} + +class _FlatButton extends StatelessWidget { + const _FlatButton({Key key, this.onPressed, this.child}) : super(key: key); + + final VoidCallback onPressed; + final Widget child; + + @override + Widget build(BuildContext context) { + return FlatButton( + padding: EdgeInsets.zero, + onPressed: onPressed, + child: DefaultTextStyle( + style: Theme.of(context).primaryTextTheme.subhead, + child: child, + ), + ); + } +} + +class _Heading extends StatelessWidget { + const _Heading(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return _OptionsItem( + child: DefaultTextStyle( + style: theme.textTheme.body1.copyWith( + fontFamily: 'GoogleSans', + color: theme.accentColor, + ), + child: Semantics( + child: Text(text), + header: true, + ), + ), + ); + } +} + +class _ThemeItem extends StatelessWidget { + const _ThemeItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + @override + Widget build(BuildContext context) { + return _BooleanItem( + 'Dark Theme', + options.theme == kDarkGalleryTheme, + (bool value) { + onOptionsChanged( + options.copyWith( + theme: value ? kDarkGalleryTheme : kLightGalleryTheme, + ), + ); + }, + switchKey: const Key('dark_theme'), + ); + } +} + +class _TextScaleFactorItem extends StatelessWidget { + const _TextScaleFactorItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + @override + Widget build(BuildContext context) { + return _OptionsItem( + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Text size'), + Text( + '${options.textScaleFactor.label}', + style: Theme.of(context).primaryTextTheme.body1, + ), + ], + ), + ), + PopupMenuButton( + padding: const EdgeInsetsDirectional.only(end: 16.0), + icon: const Icon(Icons.arrow_drop_down), + itemBuilder: (BuildContext context) { + return kAllGalleryTextScaleValues + .map>( + (GalleryTextScaleValue scaleValue) { + return PopupMenuItem( + value: scaleValue, + child: Text(scaleValue.label), + ); + }).toList(); + }, + onSelected: (GalleryTextScaleValue scaleValue) { + onOptionsChanged( + options.copyWith(textScaleFactor: scaleValue), + ); + }, + ), + ], + ), + ); + } +} + +class _TextDirectionItem extends StatelessWidget { + const _TextDirectionItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + @override + Widget build(BuildContext context) { + return _BooleanItem( + 'Force RTL', + options.textDirection == TextDirection.rtl, + (bool value) { + onOptionsChanged( + options.copyWith( + textDirection: value ? TextDirection.rtl : TextDirection.ltr, + ), + ); + }, + switchKey: const Key('text_direction'), + ); + } +} + +class _TimeDilationItem extends StatelessWidget { + const _TimeDilationItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + @override + Widget build(BuildContext context) { + return _BooleanItem( + 'Slow motion', + options.timeDilation != 1.0, + (bool value) { + onOptionsChanged( + options.copyWith( + timeDilation: value ? 20.0 : 1.0, + ), + ); + }, + switchKey: const Key('slow_motion'), + ); + } +} + +class _PlatformItem extends StatelessWidget { + const _PlatformItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + String _platformLabel(TargetPlatform platform) { + switch (platform) { + case TargetPlatform.android: + return 'Mountain View'; + case TargetPlatform.fuchsia: + return 'Fuchsia'; + case TargetPlatform.iOS: + return 'Cupertino'; + } + assert(false); + return null; + } + + @override + Widget build(BuildContext context) { + return _OptionsItem( + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Platform mechanics'), + Text( + '${_platformLabel(options.platform)}', + style: Theme.of(context).primaryTextTheme.body1, + ), + ], + ), + ), + PopupMenuButton( + padding: const EdgeInsetsDirectional.only(end: 16.0), + icon: const Icon(Icons.arrow_drop_down), + itemBuilder: (BuildContext context) { + return TargetPlatform.values.map((TargetPlatform platform) { + return PopupMenuItem( + value: platform, + child: Text(_platformLabel(platform)), + ); + }).toList(); + }, + onSelected: (TargetPlatform platform) { + onOptionsChanged( + options.copyWith(platform: platform), + ); + }, + ), + ], + ), + ); + } +} + +class GalleryOptionsPage extends StatelessWidget { + const GalleryOptionsPage({ + Key key, + this.options, + this.onOptionsChanged, + this.onSendFeedback, + }) : super(key: key); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + final VoidCallback onSendFeedback; + + List _enabledDiagnosticItems() { + // Boolean showFoo options with a value of null: don't display + // the showFoo option at all. + if (null == options.showOffscreenLayersCheckerboard ?? + options.showRasterCacheImagesCheckerboard ?? + options.showPerformanceOverlay) return const []; + + final List items = [ + const Divider(), + const _Heading('Diagnostics'), + ]; + + if (options.showOffscreenLayersCheckerboard != null) { + items.add( + _BooleanItem('Highlight offscreen layers', + options.showOffscreenLayersCheckerboard, (bool value) { + onOptionsChanged( + options.copyWith(showOffscreenLayersCheckerboard: value)); + }), + ); + } + if (options.showRasterCacheImagesCheckerboard != null) { + items.add( + _BooleanItem( + 'Highlight raster cache images', + options.showRasterCacheImagesCheckerboard, + (bool value) { + onOptionsChanged( + options.copyWith(showRasterCacheImagesCheckerboard: value)); + }, + ), + ); + } + if (options.showPerformanceOverlay != null) { + items.add( + _BooleanItem( + 'Show performance overlay', + options.showPerformanceOverlay, + (bool value) { + onOptionsChanged(options.copyWith(showPerformanceOverlay: value)); + }, + ), + ); + } + + return items; + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + + return DefaultTextStyle( + style: theme.primaryTextTheme.subhead, + child: ListView( + padding: const EdgeInsets.only(bottom: 124.0), + children: [ + const _Heading('Display'), + _ThemeItem(options, onOptionsChanged), + _TextScaleFactorItem(options, onOptionsChanged), + _TextDirectionItem(options, onOptionsChanged), + _TimeDilationItem(options, onOptionsChanged), + const Divider(), + const _Heading('Platform mechanics'), + _PlatformItem(options, onOptionsChanged), + ] + ..addAll( + _enabledDiagnosticItems(), + ) + ..addAll( + [ + const Divider(), + const _Heading('Flutter Web gallery'), + _ActionItem('About Flutter Web Gallery', () { + showGalleryAboutDialog(context); + }), + _ActionItem('Send feedback', onSendFeedback), + ], + ), + ), + ); + } +} diff --git a/web/gallery/lib/gallery/scales.dart b/web/gallery/lib/gallery/scales.dart new file mode 100644 index 000000000..34236909c --- /dev/null +++ b/web/gallery/lib/gallery/scales.dart @@ -0,0 +1,36 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +class GalleryTextScaleValue { + const GalleryTextScaleValue(this.scale, this.label); + + final double scale; + final String label; + + @override + bool operator ==(dynamic other) { + if (runtimeType != other.runtimeType) return false; + final GalleryTextScaleValue typedOther = other; + return scale == typedOther.scale && label == typedOther.label; + } + + @override + int get hashCode => hashValues(scale, label); + + @override + String toString() { + return '$runtimeType($label)'; + } +} + +const List kAllGalleryTextScaleValues = + [ + GalleryTextScaleValue(null, 'System Default'), + GalleryTextScaleValue(0.8, 'Small'), + GalleryTextScaleValue(1.0, 'Normal'), + GalleryTextScaleValue(1.3, 'Large'), + GalleryTextScaleValue(2.0, 'Huge'), +]; diff --git a/web/gallery/lib/gallery/themes.dart b/web/gallery/lib/gallery/themes.dart new file mode 100644 index 000000000..ba04c47ab --- /dev/null +++ b/web/gallery/lib/gallery/themes.dart @@ -0,0 +1,82 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +class GalleryTheme { + const GalleryTheme._(this.name, this.data); + + final String name; + final ThemeData data; +} + +final GalleryTheme kDarkGalleryTheme = + GalleryTheme._('Dark', _buildDarkTheme()); +final GalleryTheme kLightGalleryTheme = + GalleryTheme._('Light', _buildLightTheme()); + +TextTheme _buildTextTheme(TextTheme base) { + return base.copyWith( + title: base.title.copyWith( + fontFamily: 'GoogleSans', + ), + ); +} + +ThemeData _buildDarkTheme() { + const Color primaryColor = Color(0xFF0175c2); + const Color secondaryColor = Color(0xFF13B9FD); + final ThemeData base = ThemeData.dark(); + final ColorScheme colorScheme = const ColorScheme.dark().copyWith( + primary: primaryColor, + secondary: secondaryColor, + ); + return base.copyWith( + primaryColor: primaryColor, + buttonColor: primaryColor, + indicatorColor: Colors.white, + accentColor: secondaryColor, + canvasColor: const Color(0xFF202124), + scaffoldBackgroundColor: const Color(0xFF202124), + backgroundColor: const Color(0xFF202124), + errorColor: const Color(0xFFB00020), + buttonTheme: ButtonThemeData( + colorScheme: colorScheme, + textTheme: ButtonTextTheme.primary, + ), + textTheme: _buildTextTheme(base.textTheme), + primaryTextTheme: _buildTextTheme(base.primaryTextTheme), + accentTextTheme: _buildTextTheme(base.accentTextTheme), + ); +} + +ThemeData _buildLightTheme() { + const Color primaryColor = Color(0xFF0175c2); + const Color secondaryColor = Color(0xFF13B9FD); + final ColorScheme colorScheme = const ColorScheme.light().copyWith( + primary: primaryColor, + secondary: secondaryColor, + ); + final ThemeData base = ThemeData.light(); + return base.copyWith( + colorScheme: colorScheme, + primaryColor: primaryColor, + buttonColor: primaryColor, + indicatorColor: Colors.white, + splashColor: Colors.white24, + splashFactory: InkRipple.splashFactory, + accentColor: secondaryColor, + canvasColor: Colors.white, + scaffoldBackgroundColor: Colors.white, + backgroundColor: Colors.white, + errorColor: const Color(0xFFB00020), + buttonTheme: ButtonThemeData( + colorScheme: colorScheme, + textTheme: ButtonTextTheme.primary, + ), + textTheme: _buildTextTheme(base.textTheme), + primaryTextTheme: _buildTextTheme(base.primaryTextTheme), + accentTextTheme: _buildTextTheme(base.accentTextTheme), + ); +} diff --git a/web/gallery/lib/main.dart b/web/gallery/lib/main.dart new file mode 100644 index 000000000..6877ddabe --- /dev/null +++ b/web/gallery/lib/main.dart @@ -0,0 +1,11 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +import 'gallery/app.dart'; + +void main() { + runApp(GalleryApp()); +} diff --git a/web/gallery/lib/main_houdini.dart b/web/gallery/lib/main_houdini.dart new file mode 100644 index 000000000..a199749a3 --- /dev/null +++ b/web/gallery/lib/main_houdini.dart @@ -0,0 +1,12 @@ +// Copyright 2018 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 'package:flutter_web_ui/ui.dart' as ui; + +import 'main.dart' as app; + +void main() { + ui.persistedPictureFactory = ui.houdiniPictureFactory; + app.main(); +} diff --git a/web/gallery/pubspec.lock b/web/gallery/pubspec.lock new file mode 100644 index 000000000..bcc48af0f --- /dev/null +++ b/web/gallery/pubspec.lock @@ -0,0 +1,557 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_test: + dependency: "direct dev" + description: + path: "packages/flutter_web_test" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct main" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + multi_server_socket: + dependency: transitive + description: + name: multi_server_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.5" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.3" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + vm_service_client: + dependency: transitive + description: + name: vm_service_client + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6+2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/gallery/pubspec.yaml b/web/gallery/pubspec.yaml new file mode 100644 index 000000000..077f1c1b3 --- /dev/null +++ b/web/gallery/pubspec.yaml @@ -0,0 +1,31 @@ +name: flutter_web.examples.gallery + +environment: + sdk: ">=2.2.0 <3.0.0" + +dependencies: + flutter_web: any + flutter_web_ui: any + intl: ^0.15.7 + +dev_dependencies: + build_runner: any + build_web_compilers: any + flutter_web_test: any + test: ^1.0.0 + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_test: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_test + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/gallery/test/demo/material/text_form_field_demo_test.dart b/web/gallery/test/demo/material/text_form_field_demo_test.dart new file mode 100644 index 000000000..1083a2def --- /dev/null +++ b/web/gallery/test/demo/material/text_form_field_demo_test.dart @@ -0,0 +1,60 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; +import 'package:flutter_web.examples.gallery/demo/material/text_form_field_demo.dart'; +import 'package:flutter_web_test/flutter_web_test.dart'; + +void main() { + testWidgets('validates name field correctly', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: TextFormFieldDemo())); + + final Finder submitButton = find.widgetWithText(RaisedButton, 'SUBMIT'); + expect(submitButton, findsOneWidget); + + final Finder nameField = find.widgetWithText(TextFormField, 'Name * '); + expect(nameField, findsOneWidget); + + final Finder passwordField = + find.widgetWithText(TextFormField, 'Password *'); + expect(passwordField, findsOneWidget); + + await tester.enterText(nameField, ''); + await tester.pumpAndSettle(); + // The submit button isn't initially visible. Drag it into view so that + // it will see the tap. + await tester.drag(nameField, const Offset(0.0, -1200.0)); + await tester.pumpAndSettle(); + await tester.tap(submitButton); + await tester.pumpAndSettle(); + + // Now drag the password field (the submit button will be obscured by + // the snackbar) and expose the name field again. + await tester.drag(passwordField, const Offset(0.0, 1200.0)); + await tester.pumpAndSettle(); + expect(find.text('Name is required.'), findsOneWidget); + expect( + find.text('Please enter only alphabetical characters.'), findsNothing); + await tester.enterText(nameField, '#'); + await tester.pumpAndSettle(); + + // Make the submit button visible again (by dragging the name field), so + // it will see the tap. + await tester.drag(nameField, const Offset(0.0, -1200.0)); + await tester.tap(submitButton); + await tester.pumpAndSettle(); + expect(find.text('Name is required.'), findsNothing); + expect(find.text('Please enter only alphabetical characters.'), + findsOneWidget); + + await tester.enterText(nameField, 'Jane Doe'); + // TODO(b/123539399): Why does it pass in Flutter without this `drag`? + await tester.drag(nameField, const Offset(0.0, -1200.0)); + await tester.tap(submitButton); + await tester.pumpAndSettle(); + expect(find.text('Name is required.'), findsNothing); + expect( + find.text('Please enter only alphabetical characters.'), findsNothing); + }); +} diff --git a/web/gallery/test/gallery_test.dart b/web/gallery/test/gallery_test.dart new file mode 100644 index 000000000..b3f9d7fcd --- /dev/null +++ b/web/gallery/test/gallery_test.dart @@ -0,0 +1,13 @@ +// Copyright 2018 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 'package:flutter_web_test/flutter_web_test.dart'; + +import 'package:flutter_web.examples.gallery/gallery/app.dart'; + +void main() { + testWidgets('Gallery starts', (WidgetTester tester) async { + await tester.pumpWidget(GalleryApp()); + }); +} diff --git a/web/gallery/web/assets/AbrilFatface-Regular.ttf b/web/gallery/web/assets/AbrilFatface-Regular.ttf new file mode 100644 index 000000000..e761f7b9c Binary files /dev/null and b/web/gallery/web/assets/AbrilFatface-Regular.ttf differ diff --git a/web/gallery/web/assets/FontManifest.json b/web/gallery/web/assets/FontManifest.json new file mode 100644 index 000000000..da07ed4f7 --- /dev/null +++ b/web/gallery/web/assets/FontManifest.json @@ -0,0 +1,58 @@ +[ + { + "family": "MaterialIcons", + "fonts": [ + { + "asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" + } + ] + }, + { + "family": "GoogleSans", + "fonts": [ + { + "asset": "GoogleSans-Regular.ttf" + } + ] + }, + { + "family": "GalleryIcons", + "fonts": [ + { + "asset": "GalleryIcons.ttf" + } + ] + }, + { + "family": "AbrilFatface", + "fonts": [ + { + "asset": "AbrilFatface-Regular.ttf" + } + ] + }, + { + "family": "LibreFranklin", + "fonts": [ + { + "asset": "LibreFranklin-Regular.ttf" + } + ] + }, + { + "family": "Merriweather", + "fonts": [ + { + "asset": "Merriweather-Regular.ttf" + } + ] + }, + { + "family": "Raleway", + "fonts": [ + { + "asset": "Raleway-Regular.ttf" + } + ] + } +] diff --git a/web/gallery/web/assets/GalleryIcons.ttf b/web/gallery/web/assets/GalleryIcons.ttf new file mode 100644 index 000000000..53c5d2d0c Binary files /dev/null and b/web/gallery/web/assets/GalleryIcons.ttf differ diff --git a/web/gallery/web/assets/GoogleSans-Regular.ttf b/web/gallery/web/assets/GoogleSans-Regular.ttf new file mode 100644 index 000000000..5bb6e03c7 Binary files /dev/null and b/web/gallery/web/assets/GoogleSans-Regular.ttf differ diff --git a/web/gallery/web/assets/LibreFranklin-Regular.ttf b/web/gallery/web/assets/LibreFranklin-Regular.ttf new file mode 100644 index 000000000..487c31e2a Binary files /dev/null and b/web/gallery/web/assets/LibreFranklin-Regular.ttf differ diff --git a/web/gallery/web/assets/Merriweather-Regular.ttf b/web/gallery/web/assets/Merriweather-Regular.ttf new file mode 100644 index 000000000..2ee9a6978 Binary files /dev/null and b/web/gallery/web/assets/Merriweather-Regular.ttf differ diff --git a/web/gallery/web/assets/README.md b/web/gallery/web/assets/README.md new file mode 100644 index 000000000..29de56321 --- /dev/null +++ b/web/gallery/web/assets/README.md @@ -0,0 +1,34 @@ +Note: a reference to `MaterialIcons` is intentionally omitted because the +corresponding font is not included in this source. + +If you add `MaterialIcons-Extended.ttf` to this directory, you can update +`FontManifest.json` as follows: + +```json +[ + { + "family": "MaterialIcons", + "fonts": [ + { + "asset": "MaterialIcons-Extended.ttf" + } + ] + }, + { + "family": "GoogleSans", + "fonts": [ + { + "asset": "GoogleSans-Regular.ttf" + } + ] + }, + { + "family": "GalleryIcons", + "fonts": [ + { + "asset": "GalleryIcons.ttf" + } + ] + } +] +``` diff --git a/web/gallery/web/assets/Raleway-Regular.ttf b/web/gallery/web/assets/Raleway-Regular.ttf new file mode 100644 index 000000000..e570a2d5c Binary files /dev/null and b/web/gallery/web/assets/Raleway-Regular.ttf differ diff --git a/web/gallery/web/assets/food/butternut_squash_soup.png b/web/gallery/web/assets/food/butternut_squash_soup.png new file mode 100644 index 000000000..e980d2fe0 Binary files /dev/null and b/web/gallery/web/assets/food/butternut_squash_soup.png differ diff --git a/web/gallery/web/assets/food/cherry_pie.png b/web/gallery/web/assets/food/cherry_pie.png new file mode 100644 index 000000000..c8fff3882 Binary files /dev/null and b/web/gallery/web/assets/food/cherry_pie.png differ diff --git a/web/gallery/web/assets/food/chopped_beet_leaves.png b/web/gallery/web/assets/food/chopped_beet_leaves.png new file mode 100644 index 000000000..59585c97a Binary files /dev/null and b/web/gallery/web/assets/food/chopped_beet_leaves.png differ diff --git a/web/gallery/web/assets/food/icons/fish.png b/web/gallery/web/assets/food/icons/fish.png new file mode 100644 index 000000000..c57ffc8d6 Binary files /dev/null and b/web/gallery/web/assets/food/icons/fish.png differ diff --git a/web/gallery/web/assets/food/icons/healthy.png b/web/gallery/web/assets/food/icons/healthy.png new file mode 100644 index 000000000..8fbbf49a1 Binary files /dev/null and b/web/gallery/web/assets/food/icons/healthy.png differ diff --git a/web/gallery/web/assets/food/icons/main.png b/web/gallery/web/assets/food/icons/main.png new file mode 100644 index 000000000..1f9ada263 Binary files /dev/null and b/web/gallery/web/assets/food/icons/main.png differ diff --git a/web/gallery/web/assets/food/icons/meat.png b/web/gallery/web/assets/food/icons/meat.png new file mode 100644 index 000000000..a84a4f436 Binary files /dev/null and b/web/gallery/web/assets/food/icons/meat.png differ diff --git a/web/gallery/web/assets/food/icons/quick.png b/web/gallery/web/assets/food/icons/quick.png new file mode 100644 index 000000000..c5728dcf5 Binary files /dev/null and b/web/gallery/web/assets/food/icons/quick.png differ diff --git a/web/gallery/web/assets/food/icons/spicy.png b/web/gallery/web/assets/food/icons/spicy.png new file mode 100644 index 000000000..0c59eea8e Binary files /dev/null and b/web/gallery/web/assets/food/icons/spicy.png differ diff --git a/web/gallery/web/assets/food/icons/veggie.png b/web/gallery/web/assets/food/icons/veggie.png new file mode 100644 index 000000000..b826733f0 Binary files /dev/null and b/web/gallery/web/assets/food/icons/veggie.png differ diff --git a/web/gallery/web/assets/food/pesto_pasta.png b/web/gallery/web/assets/food/pesto_pasta.png new file mode 100644 index 000000000..dace4d3a8 Binary files /dev/null and b/web/gallery/web/assets/food/pesto_pasta.png differ diff --git a/web/gallery/web/assets/food/roasted_chicken.png b/web/gallery/web/assets/food/roasted_chicken.png new file mode 100644 index 000000000..800a4a5f1 Binary files /dev/null and b/web/gallery/web/assets/food/roasted_chicken.png differ diff --git a/web/gallery/web/assets/food/spanakopita.png b/web/gallery/web/assets/food/spanakopita.png new file mode 100644 index 000000000..58a22b653 Binary files /dev/null and b/web/gallery/web/assets/food/spanakopita.png differ diff --git a/web/gallery/web/assets/food/spinach_onion_salad.png b/web/gallery/web/assets/food/spinach_onion_salad.png new file mode 100644 index 000000000..e05b05c08 Binary files /dev/null and b/web/gallery/web/assets/food/spinach_onion_salad.png differ diff --git a/web/gallery/web/assets/logos/flutter_white/1.5x/logo.png b/web/gallery/web/assets/logos/flutter_white/1.5x/logo.png new file mode 100644 index 000000000..1449289d0 Binary files /dev/null and b/web/gallery/web/assets/logos/flutter_white/1.5x/logo.png differ diff --git a/web/gallery/web/assets/logos/flutter_white/2.5x/logo.png b/web/gallery/web/assets/logos/flutter_white/2.5x/logo.png new file mode 100644 index 000000000..2020e760d Binary files /dev/null and b/web/gallery/web/assets/logos/flutter_white/2.5x/logo.png differ diff --git a/web/gallery/web/assets/logos/flutter_white/3.0x/logo.png b/web/gallery/web/assets/logos/flutter_white/3.0x/logo.png new file mode 100644 index 000000000..b42acc11c Binary files /dev/null and b/web/gallery/web/assets/logos/flutter_white/3.0x/logo.png differ diff --git a/web/gallery/web/assets/logos/flutter_white/4.0x/logo.png b/web/gallery/web/assets/logos/flutter_white/4.0x/logo.png new file mode 100644 index 000000000..58bc2d906 Binary files /dev/null and b/web/gallery/web/assets/logos/flutter_white/4.0x/logo.png differ diff --git a/web/gallery/web/assets/logos/flutter_white/logo.png b/web/gallery/web/assets/logos/flutter_white/logo.png new file mode 100644 index 000000000..3277025d9 Binary files /dev/null and b/web/gallery/web/assets/logos/flutter_white/logo.png differ diff --git a/web/gallery/web/assets/logos/pesto/logo_small.png b/web/gallery/web/assets/logos/pesto/logo_small.png new file mode 100644 index 000000000..f41fbb869 Binary files /dev/null and b/web/gallery/web/assets/logos/pesto/logo_small.png differ diff --git a/web/gallery/web/assets/people/ali_landscape.png b/web/gallery/web/assets/people/ali_landscape.png new file mode 100644 index 000000000..d886764e4 Binary files /dev/null and b/web/gallery/web/assets/people/ali_landscape.png differ diff --git a/web/gallery/web/assets/people/square/ali.png b/web/gallery/web/assets/people/square/ali.png new file mode 100644 index 000000000..c4f2b8c8f Binary files /dev/null and b/web/gallery/web/assets/people/square/ali.png differ diff --git a/web/gallery/web/assets/people/square/peter.png b/web/gallery/web/assets/people/square/peter.png new file mode 100644 index 000000000..fdb5fa0a3 Binary files /dev/null and b/web/gallery/web/assets/people/square/peter.png differ diff --git a/web/gallery/web/assets/people/square/sandra.png b/web/gallery/web/assets/people/square/sandra.png new file mode 100644 index 000000000..098891ddc Binary files /dev/null and b/web/gallery/web/assets/people/square/sandra.png differ diff --git a/web/gallery/web/assets/people/square/stella.png b/web/gallery/web/assets/people/square/stella.png new file mode 100644 index 000000000..aaf0d260e Binary files /dev/null and b/web/gallery/web/assets/people/square/stella.png differ diff --git a/web/gallery/web/assets/people/square/trevor.png b/web/gallery/web/assets/people/square/trevor.png new file mode 100644 index 000000000..c290d3ac6 Binary files /dev/null and b/web/gallery/web/assets/people/square/trevor.png differ diff --git a/web/gallery/web/assets/places/india_chennai_flower_market.png b/web/gallery/web/assets/places/india_chennai_flower_market.png new file mode 100644 index 000000000..52d495022 Binary files /dev/null and b/web/gallery/web/assets/places/india_chennai_flower_market.png differ diff --git a/web/gallery/web/assets/places/india_chennai_highway.png b/web/gallery/web/assets/places/india_chennai_highway.png new file mode 100644 index 000000000..3d202ca44 Binary files /dev/null and b/web/gallery/web/assets/places/india_chennai_highway.png differ diff --git a/web/gallery/web/assets/places/india_chettinad_produce.png b/web/gallery/web/assets/places/india_chettinad_produce.png new file mode 100644 index 000000000..a915d37b7 Binary files /dev/null and b/web/gallery/web/assets/places/india_chettinad_produce.png differ diff --git a/web/gallery/web/assets/places/india_chettinad_silk_maker.png b/web/gallery/web/assets/places/india_chettinad_silk_maker.png new file mode 100644 index 000000000..32d1f3aeb Binary files /dev/null and b/web/gallery/web/assets/places/india_chettinad_silk_maker.png differ diff --git a/web/gallery/web/assets/places/india_pondicherry_beach.png b/web/gallery/web/assets/places/india_pondicherry_beach.png new file mode 100644 index 000000000..207defa27 Binary files /dev/null and b/web/gallery/web/assets/places/india_pondicherry_beach.png differ diff --git a/web/gallery/web/assets/places/india_pondicherry_fisherman.png b/web/gallery/web/assets/places/india_pondicherry_fisherman.png new file mode 100644 index 000000000..72b38c403 Binary files /dev/null and b/web/gallery/web/assets/places/india_pondicherry_fisherman.png differ diff --git a/web/gallery/web/assets/places/india_pondicherry_salt_farm.png b/web/gallery/web/assets/places/india_pondicherry_salt_farm.png new file mode 100644 index 000000000..b363ef61a Binary files /dev/null and b/web/gallery/web/assets/places/india_pondicherry_salt_farm.png differ diff --git a/web/gallery/web/assets/places/india_tanjore_bronze_works.png b/web/gallery/web/assets/places/india_tanjore_bronze_works.png new file mode 100644 index 000000000..d4e0b121c Binary files /dev/null and b/web/gallery/web/assets/places/india_tanjore_bronze_works.png differ diff --git a/web/gallery/web/assets/places/india_tanjore_market_merchant.png b/web/gallery/web/assets/places/india_tanjore_market_merchant.png new file mode 100644 index 000000000..013971754 Binary files /dev/null and b/web/gallery/web/assets/places/india_tanjore_market_merchant.png differ diff --git a/web/gallery/web/assets/places/india_tanjore_market_technology.png b/web/gallery/web/assets/places/india_tanjore_market_technology.png new file mode 100644 index 000000000..1bc629159 Binary files /dev/null and b/web/gallery/web/assets/places/india_tanjore_market_technology.png differ diff --git a/web/gallery/web/assets/places/india_tanjore_thanjavur_temple.png b/web/gallery/web/assets/places/india_tanjore_thanjavur_temple.png new file mode 100644 index 000000000..9cd0562a9 Binary files /dev/null and b/web/gallery/web/assets/places/india_tanjore_thanjavur_temple.png differ diff --git a/web/gallery/web/assets/places/india_tanjore_thanjavur_temple_carvings.png b/web/gallery/web/assets/places/india_tanjore_thanjavur_temple_carvings.png new file mode 100644 index 000000000..4ba27dec6 Binary files /dev/null and b/web/gallery/web/assets/places/india_tanjore_thanjavur_temple_carvings.png differ diff --git a/web/gallery/web/assets/places/india_thanjavur_market.png b/web/gallery/web/assets/places/india_thanjavur_market.png new file mode 100644 index 000000000..62349af18 Binary files /dev/null and b/web/gallery/web/assets/places/india_thanjavur_market.png differ diff --git a/web/gallery/web/assets/products/backpack.png b/web/gallery/web/assets/products/backpack.png new file mode 100644 index 000000000..cb93dcc6b Binary files /dev/null and b/web/gallery/web/assets/products/backpack.png differ diff --git a/web/gallery/web/assets/products/belt.png b/web/gallery/web/assets/products/belt.png new file mode 100644 index 000000000..eda9b484d Binary files /dev/null and b/web/gallery/web/assets/products/belt.png differ diff --git a/web/gallery/web/assets/products/cup.png b/web/gallery/web/assets/products/cup.png new file mode 100644 index 000000000..9ad1a3a9c Binary files /dev/null and b/web/gallery/web/assets/products/cup.png differ diff --git a/web/gallery/web/assets/products/deskset.png b/web/gallery/web/assets/products/deskset.png new file mode 100644 index 000000000..76e6ec171 Binary files /dev/null and b/web/gallery/web/assets/products/deskset.png differ diff --git a/web/gallery/web/assets/products/dress.png b/web/gallery/web/assets/products/dress.png new file mode 100644 index 000000000..6b52bad60 Binary files /dev/null and b/web/gallery/web/assets/products/dress.png differ diff --git a/web/gallery/web/assets/products/earrings.png b/web/gallery/web/assets/products/earrings.png new file mode 100644 index 000000000..80c3bf863 Binary files /dev/null and b/web/gallery/web/assets/products/earrings.png differ diff --git a/web/gallery/web/assets/products/flatwear.png b/web/gallery/web/assets/products/flatwear.png new file mode 100644 index 000000000..98b6be32b Binary files /dev/null and b/web/gallery/web/assets/products/flatwear.png differ diff --git a/web/gallery/web/assets/products/hat.png b/web/gallery/web/assets/products/hat.png new file mode 100644 index 000000000..7bf2a658a Binary files /dev/null and b/web/gallery/web/assets/products/hat.png differ diff --git a/web/gallery/web/assets/products/jacket.png b/web/gallery/web/assets/products/jacket.png new file mode 100644 index 000000000..a2c4bbe21 Binary files /dev/null and b/web/gallery/web/assets/products/jacket.png differ diff --git a/web/gallery/web/assets/products/jumper.png b/web/gallery/web/assets/products/jumper.png new file mode 100644 index 000000000..741d158ff Binary files /dev/null and b/web/gallery/web/assets/products/jumper.png differ diff --git a/web/gallery/web/assets/products/kitchen_quattro.png b/web/gallery/web/assets/products/kitchen_quattro.png new file mode 100644 index 000000000..76dbf5377 Binary files /dev/null and b/web/gallery/web/assets/products/kitchen_quattro.png differ diff --git a/web/gallery/web/assets/products/napkins.png b/web/gallery/web/assets/products/napkins.png new file mode 100644 index 000000000..e949fb32e Binary files /dev/null and b/web/gallery/web/assets/products/napkins.png differ diff --git a/web/gallery/web/assets/products/planters.png b/web/gallery/web/assets/products/planters.png new file mode 100644 index 000000000..de2c08044 Binary files /dev/null and b/web/gallery/web/assets/products/planters.png differ diff --git a/web/gallery/web/assets/products/platter.png b/web/gallery/web/assets/products/platter.png new file mode 100644 index 000000000..52fb19b01 Binary files /dev/null and b/web/gallery/web/assets/products/platter.png differ diff --git a/web/gallery/web/assets/products/scarf.png b/web/gallery/web/assets/products/scarf.png new file mode 100644 index 000000000..68ba83ec5 Binary files /dev/null and b/web/gallery/web/assets/products/scarf.png differ diff --git a/web/gallery/web/assets/products/shirt.png b/web/gallery/web/assets/products/shirt.png new file mode 100644 index 000000000..7b5320b0b Binary files /dev/null and b/web/gallery/web/assets/products/shirt.png differ diff --git a/web/gallery/web/assets/products/sunnies.png b/web/gallery/web/assets/products/sunnies.png new file mode 100644 index 000000000..10c6355e6 Binary files /dev/null and b/web/gallery/web/assets/products/sunnies.png differ diff --git a/web/gallery/web/assets/products/sweater.png b/web/gallery/web/assets/products/sweater.png new file mode 100644 index 000000000..d65629e1d Binary files /dev/null and b/web/gallery/web/assets/products/sweater.png differ diff --git a/web/gallery/web/assets/products/sweats.png b/web/gallery/web/assets/products/sweats.png new file mode 100644 index 000000000..6c206a5ef Binary files /dev/null and b/web/gallery/web/assets/products/sweats.png differ diff --git a/web/gallery/web/assets/products/table.png b/web/gallery/web/assets/products/table.png new file mode 100644 index 000000000..354c73e1d Binary files /dev/null and b/web/gallery/web/assets/products/table.png differ diff --git a/web/gallery/web/assets/products/teaset.png b/web/gallery/web/assets/products/teaset.png new file mode 100644 index 000000000..7ff5c423f Binary files /dev/null and b/web/gallery/web/assets/products/teaset.png differ diff --git a/web/gallery/web/assets/products/top.png b/web/gallery/web/assets/products/top.png new file mode 100644 index 000000000..923fc1bd2 Binary files /dev/null and b/web/gallery/web/assets/products/top.png differ diff --git a/web/gallery/web/frame.html b/web/gallery/web/frame.html new file mode 100644 index 000000000..29b0cd889 --- /dev/null +++ b/web/gallery/web/frame.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/web/gallery/web/index.html b/web/gallery/web/index.html new file mode 100644 index 000000000..3cda94763 --- /dev/null +++ b/web/gallery/web/index.html @@ -0,0 +1,36 @@ + + + + + Gallery sample - Flutter for web + + + +
+ +
+ + diff --git a/web/gallery/web/main.dart b/web/gallery/web/main.dart new file mode 100644 index 000000000..fcabacbde --- /dev/null +++ b/web/gallery/web/main.dart @@ -0,0 +1,10 @@ +// 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 'package:flutter_web_ui/ui.dart' as ui; +import 'package:flutter_web.examples.gallery/main.dart' as app; + +main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/gallery/web/preview.png b/web/gallery/web/preview.png new file mode 100644 index 000000000..a70b50cf9 Binary files /dev/null and b/web/gallery/web/preview.png differ diff --git a/web/github_dataviz/LICENSE b/web/github_dataviz/LICENSE new file mode 100644 index 000000000..bc67b8f95 --- /dev/null +++ b/web/github_dataviz/LICENSE @@ -0,0 +1,27 @@ +Copyright 2019 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/web/github_dataviz/README.md b/web/github_dataviz/README.md new file mode 100644 index 000000000..77b6877b8 --- /dev/null +++ b/web/github_dataviz/README.md @@ -0,0 +1,11 @@ +A visualization for +[Flutter GitHub repository](https://github.com/flutter/flutter/) metadata. + +Created for Google by [Larva Labs](http://larvalabs.com/), a team of creative +technologists who make things for Android, iPhone, and the Web. + +## Data Notes + +The data starts the week of Oct 19, 2014 which is the first commit. This is week +0 in our data arrays, but it is week 43 in 2014. Year boundaries are then offset +by 9 weeks. diff --git a/web/github_dataviz/lib/catmull.dart b/web/github_dataviz/lib/catmull.dart new file mode 100644 index 000000000..a0a5fa7a9 --- /dev/null +++ b/web/github_dataviz/lib/catmull.dart @@ -0,0 +1,79 @@ +import 'package:github_dataviz/mathutils.dart'; + +class ControlPointAndValue { + int point; + double value; + + ControlPointAndValue() { + value = 0; + point = 2; + } +} + +class CatmullInterpolator implements Interpolator { + List controlPoints; + + CatmullInterpolator(this.controlPoints); + + @override + double get(double v) { + for (int i = 2; i < controlPoints.length - 1; i++) { + if (controlPoints[i].x >= v) { + double t = (v - controlPoints[i - 1].x) / + (controlPoints[i].x - controlPoints[i - 1].x); + double p0 = controlPoints[i - 2].y; + double p1 = controlPoints[i - 1].y; + double p2 = controlPoints[i].y; + double p3 = controlPoints[i + 1].y; + return 0.5 * + ((2 * p1) + + (p2 - p0) * t + + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + + (3 * p1 - p0 - 3 * p2 + p3) * t * t * t); + } + } + // Will be unreachable if the control points were set up right + return 0; + } + + ControlPointAndValue progressiveGet(ControlPointAndValue cpv) { + double v = cpv.value; + for (int i = cpv.point; i < controlPoints.length - 1; i++) { + if (controlPoints[i].x >= v) { + double t = (v - controlPoints[i - 1].x) / + (controlPoints[i].x - controlPoints[i - 1].x); + double p0 = controlPoints[i - 2].y; + double p1 = controlPoints[i - 1].y; + double p2 = controlPoints[i].y; + double p3 = controlPoints[i + 1].y; + cpv.value = 0.5 * + ((2 * p1) + + (p2 - p0) * t + + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + + (3 * p1 - p0 - 3 * p2 + p3) * t * t * t); + cpv.point = i; + return cpv; + } + } + // Will be unreachable if the control points were set up right + return cpv; + } + + static void test() { + List controlPoints = List(); + controlPoints.add(Point2D(-1, 1)); + controlPoints.add(Point2D(0, 1)); + controlPoints.add(Point2D(1, -1)); + controlPoints.add(Point2D(3, 4)); + controlPoints.add(Point2D(10, -2)); + controlPoints.add(Point2D(11, -2)); + CatmullInterpolator catmull = CatmullInterpolator(controlPoints); + print(catmull.get(0)); + print(catmull.get(1)); + print(catmull.get(2)); + print(catmull.get(5)); + print(catmull.get(7)); + print(catmull.get(8)); + print(catmull.get(10)); + } +} diff --git a/web/github_dataviz/lib/constants.dart b/web/github_dataviz/lib/constants.dart new file mode 100644 index 000000000..d27f99271 --- /dev/null +++ b/web/github_dataviz/lib/constants.dart @@ -0,0 +1,9 @@ +import 'package:flutter_web/material.dart'; +import 'package:flutter_web_ui/ui.dart'; + +class Constants { + static final Color backgroundColor = const Color(0xFF000020); + static final Color timelineLineColor = Color(0x60FFFFFF); + static final Color milestoneColor = Color(0x40FFFFFF); + static final Color milestoneTimelineColor = Colors.white; +} diff --git a/web/github_dataviz/lib/data/contribution_data.dart b/web/github_dataviz/lib/data/contribution_data.dart new file mode 100644 index 000000000..3c47afced --- /dev/null +++ b/web/github_dataviz/lib/data/contribution_data.dart @@ -0,0 +1,14 @@ +class ContributionData { + int weekTime; + int add; + int delete; + int change; + + ContributionData(this.weekTime, this.add, this.delete, this.change); + + static ContributionData fromJson(Map jsonMap) { + ContributionData data = ContributionData( + jsonMap["w"], jsonMap["a"], jsonMap["d"], jsonMap["c"]); + return data; + } +} diff --git a/web/github_dataviz/lib/data/data_series.dart b/web/github_dataviz/lib/data/data_series.dart new file mode 100644 index 000000000..effdb6b91 --- /dev/null +++ b/web/github_dataviz/lib/data/data_series.dart @@ -0,0 +1,6 @@ +class DataSeries { + String label; + List series; + + DataSeries(this.label, this.series); +} diff --git a/web/github_dataviz/lib/data/stat_for_week.dart b/web/github_dataviz/lib/data/stat_for_week.dart new file mode 100644 index 000000000..89350d57e --- /dev/null +++ b/web/github_dataviz/lib/data/stat_for_week.dart @@ -0,0 +1,6 @@ +class StatForWeek { + int weekIndex; + int stat; + + StatForWeek(this.weekIndex, this.stat); +} diff --git a/web/github_dataviz/lib/data/user.dart b/web/github_dataviz/lib/data/user.dart new file mode 100644 index 000000000..0716e6cbf --- /dev/null +++ b/web/github_dataviz/lib/data/user.dart @@ -0,0 +1,12 @@ +class User { + int id; + String username; + String avatarUrl; + + User(this.id, this.username, this.avatarUrl); + + static User fromJson(Map jsonMap) { + User user = User(jsonMap["id"], jsonMap["login"], jsonMap["avatar_url"]); + return user; + } +} diff --git a/web/github_dataviz/lib/data/user_contribution.dart b/web/github_dataviz/lib/data/user_contribution.dart new file mode 100644 index 000000000..cc0be09ce --- /dev/null +++ b/web/github_dataviz/lib/data/user_contribution.dart @@ -0,0 +1,18 @@ +import 'package:github_dataviz/data/contribution_data.dart'; +import 'package:github_dataviz/data/user.dart'; + +class UserContribution { + User user; + List contributions; + + UserContribution(this.user, this.contributions); + + static UserContribution fromJson(Map jsonMap) { + List contributionList = (jsonMap["weeks"] as List) + .map((e) => ContributionData.fromJson(e)) + .toList(); + var userContribution = + UserContribution(User.fromJson(jsonMap["author"]), contributionList); + return userContribution; + } +} diff --git a/web/github_dataviz/lib/data/week_label.dart b/web/github_dataviz/lib/data/week_label.dart new file mode 100644 index 000000000..b4bf6928e --- /dev/null +++ b/web/github_dataviz/lib/data/week_label.dart @@ -0,0 +1,24 @@ +import 'package:intl/intl.dart'; + +class WeekLabel { + int weekNum; + String label; + + WeekLabel(this.weekNum, this.label); + + WeekLabel.forDate(DateTime date, String label) { + this.label = label; + int year = getYear(date); + int weekOfYearNum = getWeekNumber(date); + this.weekNum = 9 + ((year - 2015) * 52) + weekOfYearNum; + } + + int getYear(DateTime date) { + return int.parse(DateFormat("y").format(date)); + } + + int getWeekNumber(DateTime date) { + int dayOfYear = int.parse(DateFormat("D").format(date)); + return ((dayOfYear - date.weekday + 10) / 7).floor(); + } +} diff --git a/web/github_dataviz/lib/layered_chart.dart b/web/github_dataviz/lib/layered_chart.dart new file mode 100644 index 000000000..a66628dbf --- /dev/null +++ b/web/github_dataviz/lib/layered_chart.dart @@ -0,0 +1,336 @@ +import 'dart:math'; + +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/painting.dart'; +import 'package:flutter_web/widgets.dart'; +import 'package:github_dataviz/catmull.dart'; +import 'package:github_dataviz/constants.dart'; +import 'package:github_dataviz/data/data_series.dart'; +import 'package:github_dataviz/data/week_label.dart'; +import 'package:github_dataviz/mathutils.dart'; + +class LayeredChart extends StatefulWidget { + final List dataToPlot; + final List milestones; + final double animationValue; + + LayeredChart(this.dataToPlot, this.milestones, this.animationValue); + + @override + State createState() { + return LayeredChartState(); + } +} + +class LayeredChartState extends State { + List paths; + List capPaths; + List maxValues; + double theta; + double graphHeight; + List labelPainter; + List milestonePainter; + Size lastSize = null; + + void buildPaths( + Size size, + List dataToPlot, + List milestones, + int numPoints, + double graphGap, + double margin, + double capTheta, + double capSize) { + double screenRatio = size.width / size.height; + double degrees = MathUtils.clampedMap(screenRatio, 0.5, 2.5, 50, 5); + theta = pi * degrees / 180; + graphHeight = MathUtils.clampedMap(screenRatio, 0.5, 2.5, 50, 150); + + int m = dataToPlot.length; + paths = List(m); + capPaths = List(m); + maxValues = List(m); + for (int i = 0; i < m; i++) { + int n = dataToPlot[i].series.length; + maxValues[i] = 0; + for (int j = 0; j < n; j++) { + double v = dataToPlot[i].series[j].toDouble(); + if (v > maxValues[i]) { + maxValues[i] = v; + } + } + } + double totalGap = m * graphGap; + double xIndent = totalGap / tan(capTheta); + double startX = margin + xIndent; + double endX = size.width - margin; + double startY = size.height; + double endY = startY - (endX - startX) * tan(theta); + double xWidth = (endX - startX) / numPoints; + double capRangeX = capSize * cos(capTheta); + double tanCapTheta = tan(capTheta); + List curvePoints = List(numPoints); + for (int i = 0; i < m; i++) { + List series = dataToPlot[i].series; + int n = series.length; + List controlPoints = List(); + controlPoints.add(Point2D(-1, 0)); + double last = 0; + for (int j = 0; j < n; j++) { + double v = series[j].toDouble(); + controlPoints.add(Point2D(j.toDouble(), v)); + last = v; + } + controlPoints.add(Point2D(n.toDouble(), last)); + CatmullInterpolator curve = CatmullInterpolator(controlPoints); + ControlPointAndValue cpv = ControlPointAndValue(); + for (int j = 0; j < numPoints; j++) { + cpv.value = MathUtils.map( + j.toDouble(), 0, (numPoints - 1).toDouble(), 0, (n - 1).toDouble()); + curve.progressiveGet(cpv); + curvePoints[j] = MathUtils.map( + max(0, cpv.value), 0, maxValues[i].toDouble(), 0, graphHeight); + } + paths[i] = Path(); + capPaths[i] = Path(); + paths[i].moveTo(startX, startY); + capPaths[i].moveTo(startX, startY); + for (int j = 0; j < numPoints; j++) { + double v = curvePoints[j]; + int k = j + 1; + double xDist = xWidth; + double capV = v; + while (k < numPoints && xDist <= capRangeX) { + double cy = curvePoints[k] + xDist * tanCapTheta; + capV = max(capV, cy); + k++; + xDist += xWidth; + } + double x = MathUtils.map( + j.toDouble(), 0, (numPoints - 1).toDouble(), startX, endX); + double baseY = MathUtils.map( + j.toDouble(), 0, (numPoints - 1).toDouble(), startY, endY); + double y = baseY - v; + double cY = baseY - capV; + paths[i].lineTo(x, y); + if (j == 0) { + int k = capRangeX ~/ xWidth; + double mx = MathUtils.map( + -k.toDouble(), 0, (numPoints - 1).toDouble(), startX, endX); + double my = MathUtils.map( + -k.toDouble(), 0, (numPoints - 1).toDouble(), startY, endY) - + capV; + capPaths[i].lineTo(mx, my); + } + capPaths[i].lineTo(x, cY); + } + paths[i].lineTo(endX, endY); + paths[i].lineTo(endX, endY + 1); + paths[i].lineTo(startX, startY + 1); + paths[i].close(); + capPaths[i].lineTo(endX, endY); + capPaths[i].lineTo(endX, endY + 1); + capPaths[i].lineTo(startX, startY + 1); + capPaths[i].close(); + } + labelPainter = List(); + for (int i = 0; i < dataToPlot.length; i++) { + TextSpan span = TextSpan( + style: TextStyle( + color: Color.fromARGB(255, 255, 255, 255), fontSize: 12), + text: dataToPlot[i].label.toUpperCase()); + TextPainter tp = TextPainter( + text: span, + textAlign: TextAlign.left, + textDirection: TextDirection.ltr); + tp.layout(); + labelPainter.add(tp); + } + milestonePainter = List(); + for (int i = 0; i < milestones.length; i++) { + TextSpan span = TextSpan( + style: TextStyle( + color: Color.fromARGB(255, 255, 255, 255), fontSize: 10), + text: milestones[i].label.toUpperCase()); + TextPainter tp = TextPainter( + text: span, + textAlign: TextAlign.left, + textDirection: TextDirection.ltr); + tp.layout(); + milestonePainter.add(tp); + } + lastSize = Size(size.width, size.height); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Constants.backgroundColor, + child: CustomPaint( + foregroundPainter: ChartPainter(this, widget.dataToPlot, + widget.milestones, 80, 50, 50, 12, 500, widget.animationValue), + child: Container())); + } +} + +class ChartPainter extends CustomPainter { + static List colors = [ + Colors.red[900], + Color(0xffc4721a), + Colors.lime[900], + Colors.green[900], + Colors.blue[900], + Colors.purple[900], + ]; + static List capColors = [ + Colors.red[500], + Colors.amber[500], + Colors.lime[500], + Colors.green[500], + Colors.blue[500], + Colors.purple[500], + ]; + + List dataToPlot; + List milestones; + + double margin; + double graphGap; + double capTheta; + double capSize; + int numPoints; + double amount = 1.0; + + Paint pathPaint; + Paint capPaint; + Paint textPaint; + Paint milestonePaint; + Paint linePaint; + Paint fillPaint; + + LayeredChartState state; + + ChartPainter( + this.state, + this.dataToPlot, + this.milestones, + this.margin, + this.graphGap, + double capDegrees, + this.capSize, + this.numPoints, + this.amount) { + this.capTheta = pi * capDegrees / 180; + pathPaint = Paint(); + pathPaint.style = PaintingStyle.fill; + capPaint = Paint(); + capPaint.style = PaintingStyle.fill; + textPaint = Paint(); + textPaint.color = Color(0xFFFFFFFF); + milestonePaint = Paint(); + milestonePaint.color = Constants.milestoneColor; + milestonePaint.style = PaintingStyle.stroke; + milestonePaint.strokeWidth = 2; + linePaint = Paint(); + linePaint.style = PaintingStyle.stroke; + linePaint.strokeWidth = 0.5; + fillPaint = Paint(); + fillPaint.style = PaintingStyle.fill; + fillPaint.color = Color(0xFF000000); + } + + @override + void paint(Canvas canvas, Size size) { + if (dataToPlot.length == 0) { + return; + } + + if (state.lastSize == null || + size.width != state.lastSize.width || + size.height != state.lastSize.height) { + print("Building paths, lastsize = ${state.lastSize}"); + state.buildPaths(size, dataToPlot, milestones, numPoints, graphGap, + margin, capTheta, capSize); + } + int m = dataToPlot.length; + int numWeeks = dataToPlot[0].series.length; + // How far along to draw + double totalGap = m * graphGap; + double xIndent = totalGap / tan(capTheta); + double dx = xIndent / (m - 1); + double startX = margin + xIndent; + double endX = size.width - margin; + double startY = size.height; + double endY = startY - (endX - startX) * tan(state.theta); + // MILESTONES + { + for (int i = 0; i < milestones.length; i++) { + WeekLabel milestone = milestones[i]; + double p = (milestone.weekNum.toDouble() / numWeeks) + (1 - amount); + if (p < 1) { + double x1 = MathUtils.map(p, 0, 1, startX, endX); + double y1 = MathUtils.map(p, 0, 1, startY, endY); + double x2 = x1 - xIndent; + double y2 = y1 - graphGap * (m - 1); + x1 += dx * 0.5; + y1 += graphGap * 0.5; + double textY = y1 + 5; + double textX = x1 + 5 * tan(capTheta); + canvas.drawLine(Offset(x1, y1), Offset(x2, y2), milestonePaint); + canvas.save(); + TextPainter tp = state.milestonePainter[i]; + canvas.translate(textX, textY); + canvas.skew(tan(capTheta * 1.0), -tan(state.theta)); + canvas.translate(-tp.width / 2, 0); + tp.paint(canvas, Offset(0, 0)); + canvas.restore(); + } + } + } + for (int i = m - 1; i >= 0; i--) { + canvas.save(); + canvas.translate(-dx * i, -graphGap * i); + + { + // TEXT LABELS + canvas.save(); + double textPosition = 0.2; + double textX = MathUtils.map(textPosition, 0, 1, startX, endX); + double textY = MathUtils.map(textPosition, 0, 1, startY, endY) + 5; + canvas.translate(textX, textY); + TextPainter tp = state.labelPainter[i]; + canvas.skew(0, -tan(state.theta)); + canvas.drawRect( + Rect.fromLTWH(-1, -1, tp.width + 2, tp.height + 2), fillPaint); + tp.paint(canvas, Offset(0, 0)); + canvas.restore(); + } + + linePaint.color = capColors[i]; + canvas.drawLine(Offset(startX, startY), Offset(endX, endY), linePaint); + + Path clipPath = Path(); + clipPath.moveTo(startX - capSize, startY + 11); + clipPath.lineTo(endX, endY + 1); + clipPath.lineTo(endX, endY - state.graphHeight - capSize); + clipPath.lineTo(startX - capSize, startY - state.graphHeight - capSize); + clipPath.close(); + canvas.clipPath(clipPath); + + pathPaint.color = colors[i]; + capPaint.color = capColors[i]; + double offsetX = MathUtils.map(1 - amount, 0, 1, startX, endX); + double offsetY = MathUtils.map(1 - amount, 0, 1, startY, endY); + canvas.translate(offsetX - startX, offsetY - startY); + canvas.drawPath(state.capPaths[i], capPaint); + canvas.drawPath(state.paths[i], pathPaint); + + canvas.restore(); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} diff --git a/web/github_dataviz/lib/main.dart b/web/github_dataviz/lib/main.dart new file mode 100644 index 000000000..aaf78fe8a --- /dev/null +++ b/web/github_dataviz/lib/main.dart @@ -0,0 +1,255 @@ +// Copyright 2018 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:collection'; +import 'dart:convert'; +import 'dart:html'; + +import 'package:flutter_web/material.dart'; +import 'package:github_dataviz/constants.dart'; +import 'package:github_dataviz/data/contribution_data.dart'; +import 'package:github_dataviz/data/data_series.dart'; +import 'package:github_dataviz/data/stat_for_week.dart'; +import 'package:github_dataviz/data/user_contribution.dart'; +import 'package:github_dataviz/data/week_label.dart'; +import 'package:github_dataviz/layered_chart.dart'; +import 'package:github_dataviz/mathutils.dart'; +import 'package:github_dataviz/timeline.dart'; + +class MainLayout extends StatefulWidget { + @override + _MainLayoutState createState() => _MainLayoutState(); +} + +class _MainLayoutState extends State with TickerProviderStateMixin { + AnimationController _animation; + List contributions; + List starsByWeek; + List forksByWeek; + List pushesByWeek; + List issueCommentsByWeek; + List pullRequestActivityByWeek; + List weekLabels; + + static final double earlyInterpolatorFraction = 0.8; + static final EarlyInterpolator interpolator = + EarlyInterpolator(earlyInterpolatorFraction); + double animationValue = 1.0; + double interpolatedAnimationValue = 1.0; + bool timelineOverride = false; + + @override + void initState() { + super.initState(); + + createAnimation(0); + + weekLabels = List(); + weekLabels.add(WeekLabel.forDate(DateTime(2019, 2, 26), "v1.2")); + weekLabels.add(WeekLabel.forDate(DateTime(2018, 12, 4), "v1.0")); +// weekLabels.add(WeekLabel.forDate(new DateTime(2018, 9, 19), "Preview 2")); + weekLabels.add(WeekLabel.forDate(DateTime(2018, 6, 21), "Preview 1")); +// weekLabels.add(WeekLabel.forDate(new DateTime(2018, 5, 7), "Beta 3")); + weekLabels.add(WeekLabel.forDate(DateTime(2018, 2, 27), "Beta 1")); + weekLabels.add(WeekLabel.forDate(DateTime(2017, 5, 1), "Alpha")); + weekLabels.add(WeekLabel(48, "Repo Made Public")); + + loadGitHubData(); + } + + void createAnimation(double startValue) { + _animation?.dispose(); + _animation = AnimationController( + value: startValue, + duration: const Duration(milliseconds: 14400), + vsync: this, + )..repeat(); + _animation.addListener(() { + setState(() { + if (!timelineOverride) { + animationValue = _animation.value; + interpolatedAnimationValue = interpolator.get(animationValue); + } + }); + }); + } + + @override + Widget build(BuildContext context) { + // Combined contributions data + List dataToPlot = List(); + if (contributions != null) { + List series = List(); + for (UserContribution userContrib in contributions) { + for (int i = 0; i < userContrib.contributions.length; i++) { + ContributionData data = userContrib.contributions[i]; + if (series.length > i) { + series[i] = series[i] + data.add; + } else { + series.add(data.add); + } + } + } + dataToPlot.add(DataSeries("Added Lines", series)); + } + + if (starsByWeek != null) { + dataToPlot + .add(DataSeries("Stars", starsByWeek.map((e) => e.stat).toList())); + } + + if (forksByWeek != null) { + dataToPlot + .add(DataSeries("Forks", forksByWeek.map((e) => e.stat).toList())); + } + + if (pushesByWeek != null) { + dataToPlot + .add(DataSeries("Pushes", pushesByWeek.map((e) => e.stat).toList())); + } + + if (issueCommentsByWeek != null) { + dataToPlot.add(DataSeries( + "Issue Comments", issueCommentsByWeek.map((e) => e.stat).toList())); + } + + if (pullRequestActivityByWeek != null) { + dataToPlot.add(DataSeries("Pull Request Activity", + pullRequestActivityByWeek.map((e) => e.stat).toList())); + } + + LayeredChart layeredChart = + LayeredChart(dataToPlot, weekLabels, interpolatedAnimationValue); + + const double timelinePadding = 60.0; + + var timeline = Timeline( + numWeeks: dataToPlot != null && dataToPlot.length > 0 + ? dataToPlot.last.series.length + : 0, + animationValue: interpolatedAnimationValue, + weekLabels: weekLabels, + mouseDownCallback: (double xFraction) { + setState(() { + timelineOverride = true; + _animation.stop(); + interpolatedAnimationValue = xFraction; + }); + }, + mouseMoveCallback: (double xFraction) { + setState(() { + interpolatedAnimationValue = xFraction; + }); + }, + mouseUpCallback: () { + setState(() { + timelineOverride = false; + createAnimation( + interpolatedAnimationValue * earlyInterpolatorFraction); + }); + }, + ); + + Column mainColumn = Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded(child: layeredChart), + Padding( + padding: const EdgeInsets.only( + left: timelinePadding, + right: timelinePadding, + bottom: timelinePadding), + child: timeline, + ), + ], + ); + + return Container( + color: Constants.backgroundColor, + child: + Directionality(textDirection: TextDirection.ltr, child: mainColumn), + ); + } + + @override + void dispose() { + _animation.dispose(); + super.dispose(); + } + + Future loadGitHubData() async { + String contributorsJsonStr = + await HttpRequest.getString("github_data/contributors.json"); + List jsonObjs = jsonDecode(contributorsJsonStr) as List; + List contributionList = + jsonObjs.map((e) => UserContribution.fromJson(e)).toList(); + print( + "Loaded ${contributionList.length} code contributions to /flutter/flutter repo."); + + int numWeeksTotal = contributionList[0].contributions.length; + + String starsByWeekStr = + await HttpRequest.getString("github_data/stars.tsv"); + List starsByWeekLoaded = + summarizeWeeksFromTSV(starsByWeekStr, numWeeksTotal); + + String forksByWeekStr = + await HttpRequest.getString("github_data/forks.tsv"); + List forksByWeekLoaded = + summarizeWeeksFromTSV(forksByWeekStr, numWeeksTotal); + + String commitsByWeekStr = + await HttpRequest.getString("github_data/commits.tsv"); + List commitsByWeekLoaded = + summarizeWeeksFromTSV(commitsByWeekStr, numWeeksTotal); + + String commentsByWeekStr = + await HttpRequest.getString("github_data/comments.tsv"); + List commentsByWeekLoaded = + summarizeWeeksFromTSV(commentsByWeekStr, numWeeksTotal); + + String pullRequestActivityByWeekStr = + await HttpRequest.getString("github_data/pull_requests.tsv"); + List pullRequestActivityByWeekLoaded = + summarizeWeeksFromTSV(pullRequestActivityByWeekStr, numWeeksTotal); + + setState(() { + this.contributions = contributionList; + this.starsByWeek = starsByWeekLoaded; + this.forksByWeek = forksByWeekLoaded; + this.pushesByWeek = commitsByWeekLoaded; + this.issueCommentsByWeek = commentsByWeekLoaded; + this.pullRequestActivityByWeek = pullRequestActivityByWeekLoaded; + }); + } + + List summarizeWeeksFromTSV( + String statByWeekStr, int numWeeksTotal) { + List loadedStats = List(); + HashMap statMap = HashMap(); + statByWeekStr.split("\n").forEach((s) { + List split = s.split("\t"); + if (split.length == 2) { + int weekNum = int.parse(split[0]); + statMap[weekNum] = StatForWeek(weekNum, int.parse(split[1])); + } + }); + print("Laoded ${statMap.length} weeks."); + // Convert into a list by week, but fill in empty weeks with 0 + for (int i = 0; i < numWeeksTotal; i++) { + StatForWeek starsForWeek = statMap[i]; + if (starsForWeek == null) { + loadedStats.add(StatForWeek(i, 0)); + } else { + loadedStats.add(starsForWeek); + } + } + return loadedStats; + } +} + +void main() { + runApp(Center(child: MainLayout())); +} diff --git a/web/github_dataviz/lib/mathutils.dart b/web/github_dataviz/lib/mathutils.dart new file mode 100644 index 000000000..854662af1 --- /dev/null +++ b/web/github_dataviz/lib/mathutils.dart @@ -0,0 +1,48 @@ +abstract class Interpolator { + double get(double x); +} + +class EarlyInterpolator implements Interpolator { + double amount; + + EarlyInterpolator(this.amount); + + @override + double get(double x) { + if (x >= amount) { + return 1; + } else { + return MathUtils.map(x, 0, amount, 0, 1); + } + } +} + +class Point2D { + double x, y; + + Point2D(this.x, this.y); +} + +class MathUtils { + static double map(double x, double a, double b, double u, double v) { + double p = (x - a) / (b - a); + return u + p * (v - u); + } + + static double clampedMap(double x, double a, double b, double u, double v) { + if (x <= a) { + return u; + } else if (x >= b) { + return v; + } else { + double p = (x - a) / (b - a); + return u + p * (v - u); + } + } + + static double clamp(double x, double a, double b) { + if (x < a) return a; + if (x > b) return b; + return x; + } +} diff --git a/web/github_dataviz/lib/timeline.dart b/web/github_dataviz/lib/timeline.dart new file mode 100644 index 000000000..85e851d73 --- /dev/null +++ b/web/github_dataviz/lib/timeline.dart @@ -0,0 +1,222 @@ +import 'dart:collection'; + +import 'package:flutter_web/material.dart'; +import 'package:github_dataviz/constants.dart'; +import 'package:github_dataviz/data/week_label.dart'; +import 'package:github_dataviz/mathutils.dart'; + +typedef MouseDownCallback = void Function(double xFraction); +typedef MouseMoveCallback = void Function(double xFraction); +typedef MouseUpCallback = void Function(); + +class Timeline extends StatefulWidget { + final int numWeeks; + final double animationValue; + final List weekLabels; + + final MouseDownCallback mouseDownCallback; + final MouseMoveCallback mouseMoveCallback; + final MouseUpCallback mouseUpCallback; + + Timeline( + {@required this.numWeeks, + @required this.animationValue, + @required this.weekLabels, + this.mouseDownCallback, + this.mouseMoveCallback, + this.mouseUpCallback}); + + @override + State createState() { + return TimelineState(); + } +} + +class TimelineState extends State { + HashMap labelPainters = HashMap(); + + @override + void initState() { + super.initState(); + for (int year = 2015; year < 2020; year++) { + String yearLabel = "${year}"; + labelPainters[yearLabel] = + _makeTextPainter(Constants.timelineLineColor, yearLabel); + } + + widget.weekLabels.forEach((WeekLabel weekLabel) { + labelPainters[weekLabel.label] = + _makeTextPainter(Constants.milestoneTimelineColor, weekLabel.label); + labelPainters[weekLabel.label + "_red"] = + _makeTextPainter(Colors.redAccent, weekLabel.label); + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onHorizontalDragDown: (DragDownDetails details) { + if (widget.mouseDownCallback != null) { + widget.mouseDownCallback( + _getClampedXFractionLocalCoords(context, details.globalPosition)); + } + }, + onHorizontalDragEnd: (DragEndDetails details) { + if (widget.mouseUpCallback != null) { + widget.mouseUpCallback(); + } + }, + onHorizontalDragUpdate: (DragUpdateDetails details) { + if (widget.mouseMoveCallback != null) { + widget.mouseMoveCallback( + _getClampedXFractionLocalCoords(context, details.globalPosition)); + } + }, + child: CustomPaint( + foregroundPainter: TimelinePainter( + this, widget.numWeeks, widget.animationValue, widget.weekLabels), + child: Container( + height: 200, + )), + ); + } + + TextPainter _makeTextPainter(Color color, String label) { + TextSpan span = + TextSpan(style: TextStyle(color: color, fontSize: 12), text: label); + TextPainter tp = TextPainter( + text: span, + textAlign: TextAlign.left, + textDirection: TextDirection.ltr); + tp.layout(); + return tp; + } + + double _getClampedXFractionLocalCoords( + BuildContext context, Offset globalOffset) { + final RenderBox box = context.findRenderObject(); + final Offset localOffset = box.globalToLocal(globalOffset); + return MathUtils.clamp(localOffset.dx / context.size.width, 0, 1); + } +} + +class TimelinePainter extends CustomPainter { + TimelineState state; + + Paint mainLinePaint; + Paint milestoneLinePaint; + + Color lineColor = Colors.white; + + int numWeeks; + double animationValue; + int weekYearOffset = + 9; // Week 0 in our data is 9 weeks before the year boundary (i.e. week 43) + + List weekLabels; + + int yearNumber = 2015; + + TimelinePainter( + this.state, this.numWeeks, this.animationValue, this.weekLabels) { + mainLinePaint = Paint(); + mainLinePaint.style = PaintingStyle.stroke; + mainLinePaint.color = Constants.timelineLineColor; + milestoneLinePaint = Paint(); + milestoneLinePaint.style = PaintingStyle.stroke; + milestoneLinePaint.color = Constants.milestoneTimelineColor; + } + + @override + void paint(Canvas canvas, Size size) { + double labelHeight = 20; + double labelHeightDoubled = labelHeight * 2; + + double mainLineY = size.height / 2; + canvas.drawLine( + Offset(0, mainLineY), Offset(size.width, mainLineY), mainLinePaint); + + double currTimeX = size.width * animationValue; + canvas.drawLine( + Offset(currTimeX, labelHeightDoubled), + Offset(currTimeX, size.height - labelHeightDoubled), + milestoneLinePaint); + + { + for (int week = 0; week < numWeeks; week++) { + double lineHeight = size.height / 32; + bool isYear = false; + if ((week - 9) % 52 == 0) { + // Year + isYear = true; + lineHeight = size.height / 2; + } else if ((week - 1) % 4 == 0) { + // Month + lineHeight = size.height / 8; + } + + double currX = (week / numWeeks.toDouble()) * size.width; + if (lineHeight > 0) { + double margin = (size.height - lineHeight) / 2; + double currTimeXDiff = (currTimeX - currX) / size.width; + if (currTimeXDiff > 0) { + var mappedValue = + MathUtils.clampedMap(currTimeXDiff, 0, 0.025, 0, 1); + var lerpedColor = Color.lerp(Constants.milestoneTimelineColor, + Constants.timelineLineColor, mappedValue); + mainLinePaint.color = lerpedColor; + } else { + mainLinePaint.color = Constants.timelineLineColor; + } + canvas.drawLine(Offset(currX, margin), + Offset(currX, size.height - margin), mainLinePaint); + } + + if (isYear) { + var yearLabel = "${yearNumber}"; + state.labelPainters[yearLabel] + .paint(canvas, Offset(currX, size.height - labelHeight)); + yearNumber++; + } + } + } + + { + for (int i = 0; i < weekLabels.length; i++) { + WeekLabel weekLabel = weekLabels[i]; + double currX = (weekLabel.weekNum / numWeeks.toDouble()) * size.width; + var timelineXDiff = (currTimeX - currX) / size.width; + double maxTimelineDiff = 0.08; + TextPainter textPainter = state.labelPainters[weekLabel.label]; + if (timelineXDiff > 0 && + timelineXDiff < maxTimelineDiff && + animationValue < 1) { + var mappedValue = + MathUtils.clampedMap(timelineXDiff, 0, maxTimelineDiff, 0, 1); + var lerpedColor = Color.lerp( + Colors.redAccent, Constants.milestoneTimelineColor, mappedValue); + milestoneLinePaint.strokeWidth = + MathUtils.clampedMap(timelineXDiff, 0, maxTimelineDiff, 6, 1); + milestoneLinePaint.color = lerpedColor; + } else { + milestoneLinePaint.strokeWidth = 1; + milestoneLinePaint.color = Constants.milestoneTimelineColor; + } + + double lineHeight = size.height / 2; + double margin = (size.height - lineHeight) / 2; + canvas.drawLine(Offset(currX, margin), + Offset(currX, size.height - margin), milestoneLinePaint); + + textPainter.paint( + canvas, Offset(currX, size.height - labelHeightDoubled)); + } + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} diff --git a/web/github_dataviz/pubspec.lock b/web/github_dataviz/pubspec.lock new file mode 100644 index 000000000..cdf5d8c14 --- /dev/null +++ b/web/github_dataviz/pubspec.lock @@ -0,0 +1,471 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct overridden" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/github_dataviz/pubspec.yaml b/web/github_dataviz/pubspec.yaml new file mode 100644 index 000000000..1d89f087b --- /dev/null +++ b/web/github_dataviz/pubspec.yaml @@ -0,0 +1,23 @@ +name: github_dataviz + +environment: + sdk: ">=2.2.0 <3.0.0" + +dependencies: + flutter_web: any + +dev_dependencies: + build_runner: any + build_web_compilers: any + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/github_dataviz/web/github_data/comments.tsv b/web/github_dataviz/web/github_data/comments.tsv new file mode 100644 index 000000000..63893c044 --- /dev/null +++ b/web/github_dataviz/web/github_data/comments.tsv @@ -0,0 +1,178 @@ +54 30 +55 448 +56 286 +57 156 +58 293 +59 289 +60 230 +61 96 +62 83 +63 254 +64 254 +65 227 +66 344 +67 337 +68 428 +69 487 +70 547 +71 446 +72 425 +73 331 +74 324 +75 399 +76 399 +77 301 +78 318 +79 283 +80 351 +81 428 +82 452 +83 447 +84 310 +85 310 +86 270 +87 331 +88 144 +89 150 +90 176 +91 218 +92 253 +93 302 +94 276 +95 452 +96 347 +97 340 +98 246 +99 392 +100 430 +101 298 +102 267 +103 233 +104 316 +105 307 +106 416 +107 268 +108 335 +109 213 +110 342 +111 227 +112 166 +113 56 +114 63 +115 230 +116 362 +117 248 +118 550 +119 809 +120 677 +121 667 +122 425 +123 544 +124 420 +125 457 +126 415 +127 480 +128 569 +129 402 +130 459 +131 466 +132 523 +133 634 +134 479 +135 564 +136 436 +137 519 +138 808 +139 589 +140 367 +141 169 +142 316 +143 370 +144 334 +145 281 +146 256 +147 357 +148 293 +149 349 +150 300 +151 342 +152 291 +153 327 +154 329 +155 346 +156 386 +157 318 +158 310 +159 361 +160 360 +161 352 +162 474 +163 495 +164 569 +165 426 +166 89 +167 17 +171 208 +172 502 +173 565 +174 273 +175 1002 +176 920 +177 808 +178 870 +179 808 +180 537 +181 745 +182 717 +183 778 +184 676 +185 562 +186 708 +187 957 +188 853 +189 703 +190 1013 +191 1040 +192 759 +193 811 +194 1086 +195 1031 +196 1080 +197 1159 +198 622 +199 892 +200 1188 +201 1273 +202 1214 +203 1103 +204 876 +205 1248 +206 956 +207 1119 +208 1218 +209 1126 +210 1058 +211 872 +212 1005 +213 805 +214 1146 +215 1132 +216 1404 +217 1436 +218 819 +219 2152 +220 1651 +221 1418 +222 1387 +223 1496 +224 1262 +225 1784 +226 1681 +227 1401 +228 1150 +229 1269 +230 1162 +231 1136 +232 1045 +233 1370 +234 41 diff --git a/web/github_dataviz/web/github_data/commits.tsv b/web/github_dataviz/web/github_data/commits.tsv new file mode 100644 index 000000000..99f30951e --- /dev/null +++ b/web/github_dataviz/web/github_data/commits.tsv @@ -0,0 +1,177 @@ +48 1 +53 1 +54 16 +55 70 +56 78 +57 46 +58 74 +59 67 +60 50 +61 10 +62 9 +63 47 +64 53 +65 55 +66 49 +67 59 +68 96 +69 67 +70 92 +71 75 +72 88 +73 69 +74 69 +75 84 +76 82 +77 59 +78 63 +79 67 +80 62 +81 60 +82 76 +83 63 +84 58 +85 55 +86 44 +87 64 +88 27 +89 24 +90 32 +91 46 +92 58 +93 53 +94 56 +95 57 +96 37 +97 41 +98 27 +99 52 +100 63 +101 51 +102 49 +103 44 +104 65 +105 55 +106 65 +107 53 +108 47 +109 34 +110 43 +111 34 +112 24 +113 5 +115 32 +116 62 +117 39 +118 88 +119 85 +120 100 +121 97 +122 65 +123 77 +124 71 +125 77 +126 63 +127 57 +128 84 +129 44 +130 68 +131 58 +132 86 +133 96 +134 56 +135 61 +136 42 +137 45 +138 66 +139 59 +140 44 +141 15 +142 47 +143 50 +144 17 +145 36 +146 27 +147 18 +148 57 +149 77 +150 38 +151 50 +152 40 +153 48 +154 45 +155 35 +156 55 +157 33 +158 48 +159 53 +160 32 +161 46 +162 67 +163 70 +164 70 +165 48 +166 3 +171 20 +172 71 +173 79 +174 38 +175 58 +176 64 +177 63 +178 70 +179 43 +180 25 +181 48 +182 60 +183 62 +184 69 +185 65 +186 44 +187 41 +188 56 +189 76 +190 61 +191 46 +192 49 +193 30 +194 55 +195 51 +196 50 +197 68 +198 51 +199 52 +200 52 +201 70 +202 78 +203 51 +204 46 +205 58 +206 75 +207 60 +208 69 +209 62 +210 75 +211 86 +212 75 +213 31 +214 31 +215 2 +216 60 +217 60 +218 16 +219 60 +220 90 +221 77 +222 50 +223 49 +224 72 +225 86 +226 64 +227 101 +228 69 +229 87 +230 80 +231 82 +232 81 +233 74 diff --git a/web/github_dataviz/web/github_data/contributors.json b/web/github_dataviz/web/github_data/contributors.json new file mode 100644 index 000000000..ef0615eea --- /dev/null +++ b/web/github_dataviz/web/github_data/contributors.json @@ -0,0 +1,142902 @@ +[ + { + "total": 5, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 78, + "d": 66, + "c": 3 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 29, + "d": 12, + "c": 2 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "jamesderlin", + "id": 17391434, + "node_id": "MDQ6VXNlcjE3MzkxNDM0", + "avatar_url": "https://avatars3.githubusercontent.com/u/17391434?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jamesderlin", + "html_url": "https://github.com/jamesderlin", + "followers_url": "https://api.github.com/users/jamesderlin/followers", + "following_url": "https://api.github.com/users/jamesderlin/following{/other_user}", + "gists_url": "https://api.github.com/users/jamesderlin/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jamesderlin/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jamesderlin/subscriptions", + "organizations_url": "https://api.github.com/users/jamesderlin/orgs", + "repos_url": "https://api.github.com/users/jamesderlin/repos", + "events_url": "https://api.github.com/users/jamesderlin/events{/privacy}", + "received_events_url": "https://api.github.com/users/jamesderlin/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 5, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 50, + "d": 50, + "c": 2 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 97, + "d": 224, + "c": 1 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 62, + "d": 0, + "c": 1 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 183, + "d": 33, + "c": 1 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "tianlunlee", + "id": 13839358, + "node_id": "MDQ6VXNlcjEzODM5MzU4", + "avatar_url": "https://avatars3.githubusercontent.com/u/13839358?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tianlunlee", + "html_url": "https://github.com/tianlunlee", + "followers_url": "https://api.github.com/users/tianlunlee/followers", + "following_url": "https://api.github.com/users/tianlunlee/following{/other_user}", + "gists_url": "https://api.github.com/users/tianlunlee/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tianlunlee/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tianlunlee/subscriptions", + "organizations_url": "https://api.github.com/users/tianlunlee/orgs", + "repos_url": "https://api.github.com/users/tianlunlee/repos", + "events_url": "https://api.github.com/users/tianlunlee/events{/privacy}", + "received_events_url": "https://api.github.com/users/tianlunlee/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 5, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 282, + "d": 5, + "c": 1 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 41, + "d": 0, + "c": 1 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 118, + "d": 1, + "c": 1 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 52, + "d": 0, + "c": 1 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "creativecreatorormaybenot", + "id": 19204050, + "node_id": "MDQ6VXNlcjE5MjA0MDUw", + "avatar_url": "https://avatars2.githubusercontent.com/u/19204050?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/creativecreatorormaybenot", + "html_url": "https://github.com/creativecreatorormaybenot", + "followers_url": "https://api.github.com/users/creativecreatorormaybenot/followers", + "following_url": "https://api.github.com/users/creativecreatorormaybenot/following{/other_user}", + "gists_url": "https://api.github.com/users/creativecreatorormaybenot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/creativecreatorormaybenot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/creativecreatorormaybenot/subscriptions", + "organizations_url": "https://api.github.com/users/creativecreatorormaybenot/orgs", + "repos_url": "https://api.github.com/users/creativecreatorormaybenot/repos", + "events_url": "https://api.github.com/users/creativecreatorormaybenot/events{/privacy}", + "received_events_url": "https://api.github.com/users/creativecreatorormaybenot/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 5, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 41, + "d": 7, + "c": 2 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 3, + "d": 3, + "c": 3 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "sivachandra", + "id": 635361, + "node_id": "MDQ6VXNlcjYzNTM2MQ==", + "avatar_url": "https://avatars1.githubusercontent.com/u/635361?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/sivachandra", + "html_url": "https://github.com/sivachandra", + "followers_url": "https://api.github.com/users/sivachandra/followers", + "following_url": "https://api.github.com/users/sivachandra/following{/other_user}", + "gists_url": "https://api.github.com/users/sivachandra/gists{/gist_id}", + "starred_url": "https://api.github.com/users/sivachandra/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/sivachandra/subscriptions", + "organizations_url": "https://api.github.com/users/sivachandra/orgs", + "repos_url": "https://api.github.com/users/sivachandra/repos", + "events_url": "https://api.github.com/users/sivachandra/events{/privacy}", + "received_events_url": "https://api.github.com/users/sivachandra/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 5, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 34, + "d": 1, + "c": 1 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 67, + "d": 3, + "c": 1 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 59, + "d": 0, + "c": 1 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "Skylled", + "id": 13603296, + "node_id": "MDQ6VXNlcjEzNjAzMjk2", + "avatar_url": "https://avatars3.githubusercontent.com/u/13603296?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Skylled", + "html_url": "https://github.com/Skylled", + "followers_url": "https://api.github.com/users/Skylled/followers", + "following_url": "https://api.github.com/users/Skylled/following{/other_user}", + "gists_url": "https://api.github.com/users/Skylled/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Skylled/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Skylled/subscriptions", + "organizations_url": "https://api.github.com/users/Skylled/orgs", + "repos_url": "https://api.github.com/users/Skylled/repos", + "events_url": "https://api.github.com/users/Skylled/events{/privacy}", + "received_events_url": "https://api.github.com/users/Skylled/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 5, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 74, + "d": 75, + "c": 3 + }, + { + "w": 1477180800, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1477785600, + "a": 3, + "d": 5, + "c": 1 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "warent", + "id": 13342266, + "node_id": "MDQ6VXNlcjEzMzQyMjY2", + "avatar_url": "https://avatars0.githubusercontent.com/u/13342266?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/warent", + "html_url": "https://github.com/warent", + "followers_url": "https://api.github.com/users/warent/followers", + "following_url": "https://api.github.com/users/warent/following{/other_user}", + "gists_url": "https://api.github.com/users/warent/gists{/gist_id}", + "starred_url": "https://api.github.com/users/warent/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/warent/subscriptions", + "organizations_url": "https://api.github.com/users/warent/orgs", + "repos_url": "https://api.github.com/users/warent/repos", + "events_url": "https://api.github.com/users/warent/events{/privacy}", + "received_events_url": "https://api.github.com/users/warent/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 5, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 8, + "d": 7, + "c": 2 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 57, + "d": 17, + "c": 2 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "eukreign", + "id": 69960, + "node_id": "MDQ6VXNlcjY5OTYw", + "avatar_url": "https://avatars1.githubusercontent.com/u/69960?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/eukreign", + "html_url": "https://github.com/eukreign", + "followers_url": "https://api.github.com/users/eukreign/followers", + "following_url": "https://api.github.com/users/eukreign/following{/other_user}", + "gists_url": "https://api.github.com/users/eukreign/gists{/gist_id}", + "starred_url": "https://api.github.com/users/eukreign/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/eukreign/subscriptions", + "organizations_url": "https://api.github.com/users/eukreign/orgs", + "repos_url": "https://api.github.com/users/eukreign/repos", + "events_url": "https://api.github.com/users/eukreign/events{/privacy}", + "received_events_url": "https://api.github.com/users/eukreign/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 5, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 48, + "d": 61, + "c": 4 + }, + { + "w": 1443916800, + "a": 18, + "d": 10, + "c": 1 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "mdakin", + "id": 1647551, + "node_id": "MDQ6VXNlcjE2NDc1NTE=", + "avatar_url": "https://avatars1.githubusercontent.com/u/1647551?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mdakin", + "html_url": "https://github.com/mdakin", + "followers_url": "https://api.github.com/users/mdakin/followers", + "following_url": "https://api.github.com/users/mdakin/following{/other_user}", + "gists_url": "https://api.github.com/users/mdakin/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mdakin/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mdakin/subscriptions", + "organizations_url": "https://api.github.com/users/mdakin/orgs", + "repos_url": "https://api.github.com/users/mdakin/repos", + "events_url": "https://api.github.com/users/mdakin/events{/privacy}", + "received_events_url": "https://api.github.com/users/mdakin/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 6, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 65, + "d": 58, + "c": 1 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 1, + "c": 1 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 26, + "d": 9, + "c": 1 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 139, + "d": 139, + "c": 1 + }, + { + "w": 1554595200, + "a": 55, + "d": 111, + "c": 1 + } + ], + "author": { + "login": "timsneath", + "id": 2319867, + "node_id": "MDQ6VXNlcjIzMTk4Njc=", + "avatar_url": "https://avatars3.githubusercontent.com/u/2319867?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/timsneath", + "html_url": "https://github.com/timsneath", + "followers_url": "https://api.github.com/users/timsneath/followers", + "following_url": "https://api.github.com/users/timsneath/following{/other_user}", + "gists_url": "https://api.github.com/users/timsneath/gists{/gist_id}", + "starred_url": "https://api.github.com/users/timsneath/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/timsneath/subscriptions", + "organizations_url": "https://api.github.com/users/timsneath/orgs", + "repos_url": "https://api.github.com/users/timsneath/repos", + "events_url": "https://api.github.com/users/timsneath/events{/privacy}", + "received_events_url": "https://api.github.com/users/timsneath/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 6, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 312, + "d": 47, + "c": 1 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 222, + "d": 203, + "c": 1 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 73, + "d": 25, + "c": 1 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 254, + "d": 173, + "c": 1 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 125, + "d": 89, + "c": 1 + }, + { + "w": 1547942400, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "tonyzhao1", + "id": 42278985, + "node_id": "MDQ6VXNlcjQyMjc4OTg1", + "avatar_url": "https://avatars1.githubusercontent.com/u/42278985?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tonyzhao1", + "html_url": "https://github.com/tonyzhao1", + "followers_url": "https://api.github.com/users/tonyzhao1/followers", + "following_url": "https://api.github.com/users/tonyzhao1/following{/other_user}", + "gists_url": "https://api.github.com/users/tonyzhao1/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tonyzhao1/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tonyzhao1/subscriptions", + "organizations_url": "https://api.github.com/users/tonyzhao1/orgs", + "repos_url": "https://api.github.com/users/tonyzhao1/repos", + "events_url": "https://api.github.com/users/tonyzhao1/events{/privacy}", + "received_events_url": "https://api.github.com/users/tonyzhao1/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 6, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 533, + "d": 38, + "c": 1 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 889, + "d": 111, + "c": 1 + }, + { + "w": 1534636800, + "a": 297, + "d": 0, + "c": 1 + }, + { + "w": 1535241600, + "a": 723, + "d": 92, + "c": 1 + }, + { + "w": 1535846400, + "a": 7, + "d": 4, + "c": 1 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 1474, + "d": 99, + "c": 1 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "doxuanviet1996", + "id": 9079172, + "node_id": "MDQ6VXNlcjkwNzkxNzI=", + "avatar_url": "https://avatars0.githubusercontent.com/u/9079172?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/doxuanviet1996", + "html_url": "https://github.com/doxuanviet1996", + "followers_url": "https://api.github.com/users/doxuanviet1996/followers", + "following_url": "https://api.github.com/users/doxuanviet1996/following{/other_user}", + "gists_url": "https://api.github.com/users/doxuanviet1996/gists{/gist_id}", + "starred_url": "https://api.github.com/users/doxuanviet1996/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/doxuanviet1996/subscriptions", + "organizations_url": "https://api.github.com/users/doxuanviet1996/orgs", + "repos_url": "https://api.github.com/users/doxuanviet1996/repos", + "events_url": "https://api.github.com/users/doxuanviet1996/events{/privacy}", + "received_events_url": "https://api.github.com/users/doxuanviet1996/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 6, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 10, + "d": 0, + "c": 1 + }, + { + "w": 1457827200, + "a": 40, + "d": 7, + "c": 1 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 17, + "d": 3, + "c": 4 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "qchong", + "id": 5986149, + "node_id": "MDQ6VXNlcjU5ODYxNDk=", + "avatar_url": "https://avatars3.githubusercontent.com/u/5986149?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/qchong", + "html_url": "https://github.com/qchong", + "followers_url": "https://api.github.com/users/qchong/followers", + "following_url": "https://api.github.com/users/qchong/following{/other_user}", + "gists_url": "https://api.github.com/users/qchong/gists{/gist_id}", + "starred_url": "https://api.github.com/users/qchong/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/qchong/subscriptions", + "organizations_url": "https://api.github.com/users/qchong/orgs", + "repos_url": "https://api.github.com/users/qchong/repos", + "events_url": "https://api.github.com/users/qchong/events{/privacy}", + "received_events_url": "https://api.github.com/users/qchong/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 6, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 16, + "d": 5, + "c": 2 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 3, + "d": 3, + "c": 2 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "afandria", + "id": 2432033, + "node_id": "MDQ6VXNlcjI0MzIwMzM=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2432033?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/afandria", + "html_url": "https://github.com/afandria", + "followers_url": "https://api.github.com/users/afandria/followers", + "following_url": "https://api.github.com/users/afandria/following{/other_user}", + "gists_url": "https://api.github.com/users/afandria/gists{/gist_id}", + "starred_url": "https://api.github.com/users/afandria/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/afandria/subscriptions", + "organizations_url": "https://api.github.com/users/afandria/orgs", + "repos_url": "https://api.github.com/users/afandria/repos", + "events_url": "https://api.github.com/users/afandria/events{/privacy}", + "received_events_url": "https://api.github.com/users/afandria/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 7, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 16, + "d": 6, + "c": 1 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 50, + "d": 1, + "c": 1 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 38, + "d": 1, + "c": 1 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 65, + "d": 3, + "c": 1 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 18, + "d": 2, + "c": 1 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 21, + "d": 1, + "c": 1 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 108, + "d": 53, + "c": 1 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "ds84182", + "id": 2268005, + "node_id": "MDQ6VXNlcjIyNjgwMDU=", + "avatar_url": "https://avatars0.githubusercontent.com/u/2268005?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ds84182", + "html_url": "https://github.com/ds84182", + "followers_url": "https://api.github.com/users/ds84182/followers", + "following_url": "https://api.github.com/users/ds84182/following{/other_user}", + "gists_url": "https://api.github.com/users/ds84182/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ds84182/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ds84182/subscriptions", + "organizations_url": "https://api.github.com/users/ds84182/orgs", + "repos_url": "https://api.github.com/users/ds84182/repos", + "events_url": "https://api.github.com/users/ds84182/events{/privacy}", + "received_events_url": "https://api.github.com/users/ds84182/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 7, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 75, + "d": 16, + "c": 2 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 3, + "d": 14, + "c": 1 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 26, + "d": 33, + "c": 1 + }, + { + "w": 1552780800, + "a": 22, + "d": 36, + "c": 1 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "fkorotkov", + "id": 989066, + "node_id": "MDQ6VXNlcjk4OTA2Ng==", + "avatar_url": "https://avatars3.githubusercontent.com/u/989066?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/fkorotkov", + "html_url": "https://github.com/fkorotkov", + "followers_url": "https://api.github.com/users/fkorotkov/followers", + "following_url": "https://api.github.com/users/fkorotkov/following{/other_user}", + "gists_url": "https://api.github.com/users/fkorotkov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/fkorotkov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/fkorotkov/subscriptions", + "organizations_url": "https://api.github.com/users/fkorotkov/orgs", + "repos_url": "https://api.github.com/users/fkorotkov/repos", + "events_url": "https://api.github.com/users/fkorotkov/events{/privacy}", + "received_events_url": "https://api.github.com/users/fkorotkov/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 7, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 4, + "d": 0, + "c": 1 + }, + { + "w": 1512259200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 35, + "d": 0, + "c": 1 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 111, + "d": 19, + "c": 2 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 52, + "d": 0, + "c": 1 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "mjohnsullivan", + "id": 102488, + "node_id": "MDQ6VXNlcjEwMjQ4OA==", + "avatar_url": "https://avatars3.githubusercontent.com/u/102488?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mjohnsullivan", + "html_url": "https://github.com/mjohnsullivan", + "followers_url": "https://api.github.com/users/mjohnsullivan/followers", + "following_url": "https://api.github.com/users/mjohnsullivan/following{/other_user}", + "gists_url": "https://api.github.com/users/mjohnsullivan/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mjohnsullivan/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mjohnsullivan/subscriptions", + "organizations_url": "https://api.github.com/users/mjohnsullivan/orgs", + "repos_url": "https://api.github.com/users/mjohnsullivan/repos", + "events_url": "https://api.github.com/users/mjohnsullivan/events{/privacy}", + "received_events_url": "https://api.github.com/users/mjohnsullivan/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 7, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 274, + "d": 235, + "c": 3 + }, + { + "w": 1520726400, + "a": 10, + "d": 6, + "c": 1 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 60, + "d": 42, + "c": 1 + }, + { + "w": 1528588800, + "a": 5, + "d": 5, + "c": 1 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 43, + "d": 0, + "c": 1 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "keertip", + "id": 2192312, + "node_id": "MDQ6VXNlcjIxOTIzMTI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2192312?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/keertip", + "html_url": "https://github.com/keertip", + "followers_url": "https://api.github.com/users/keertip/followers", + "following_url": "https://api.github.com/users/keertip/following{/other_user}", + "gists_url": "https://api.github.com/users/keertip/gists{/gist_id}", + "starred_url": "https://api.github.com/users/keertip/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/keertip/subscriptions", + "organizations_url": "https://api.github.com/users/keertip/orgs", + "repos_url": "https://api.github.com/users/keertip/repos", + "events_url": "https://api.github.com/users/keertip/events{/privacy}", + "received_events_url": "https://api.github.com/users/keertip/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 7, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 38, + "d": 39, + "c": 1 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 21, + "d": 7, + "c": 1 + }, + { + "w": 1515283200, + "a": 43, + "d": 39, + "c": 2 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1531612800, + "a": 77, + "d": 80, + "c": 1 + }, + { + "w": 1532217600, + "a": 81, + "d": 80, + "c": 1 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "leafpetersen", + "id": 8484504, + "node_id": "MDQ6VXNlcjg0ODQ1MDQ=", + "avatar_url": "https://avatars1.githubusercontent.com/u/8484504?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/leafpetersen", + "html_url": "https://github.com/leafpetersen", + "followers_url": "https://api.github.com/users/leafpetersen/followers", + "following_url": "https://api.github.com/users/leafpetersen/following{/other_user}", + "gists_url": "https://api.github.com/users/leafpetersen/gists{/gist_id}", + "starred_url": "https://api.github.com/users/leafpetersen/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/leafpetersen/subscriptions", + "organizations_url": "https://api.github.com/users/leafpetersen/orgs", + "repos_url": "https://api.github.com/users/leafpetersen/repos", + "events_url": "https://api.github.com/users/leafpetersen/events{/privacy}", + "received_events_url": "https://api.github.com/users/leafpetersen/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 7, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 6, + "d": 0, + "c": 1 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 33, + "d": 0, + "c": 4 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 3, + "d": 0, + "c": 2 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "ojanvafai", + "id": 1607171, + "node_id": "MDQ6VXNlcjE2MDcxNzE=", + "avatar_url": "https://avatars1.githubusercontent.com/u/1607171?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ojanvafai", + "html_url": "https://github.com/ojanvafai", + "followers_url": "https://api.github.com/users/ojanvafai/followers", + "following_url": "https://api.github.com/users/ojanvafai/following{/other_user}", + "gists_url": "https://api.github.com/users/ojanvafai/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ojanvafai/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ojanvafai/subscriptions", + "organizations_url": "https://api.github.com/users/ojanvafai/orgs", + "repos_url": "https://api.github.com/users/ojanvafai/repos", + "events_url": "https://api.github.com/users/ojanvafai/events{/privacy}", + "received_events_url": "https://api.github.com/users/ojanvafai/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 7, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 335, + "d": 12, + "c": 3 + }, + { + "w": 1426377600, + "a": 36, + "d": 10, + "c": 2 + }, + { + "w": 1426982400, + "a": 60, + "d": 16, + "c": 2 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "viettrungluu-cr", + "id": 7061740, + "node_id": "MDQ6VXNlcjcwNjE3NDA=", + "avatar_url": "https://avatars0.githubusercontent.com/u/7061740?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/viettrungluu-cr", + "html_url": "https://github.com/viettrungluu-cr", + "followers_url": "https://api.github.com/users/viettrungluu-cr/followers", + "following_url": "https://api.github.com/users/viettrungluu-cr/following{/other_user}", + "gists_url": "https://api.github.com/users/viettrungluu-cr/gists{/gist_id}", + "starred_url": "https://api.github.com/users/viettrungluu-cr/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/viettrungluu-cr/subscriptions", + "organizations_url": "https://api.github.com/users/viettrungluu-cr/orgs", + "repos_url": "https://api.github.com/users/viettrungluu-cr/repos", + "events_url": "https://api.github.com/users/viettrungluu-cr/events{/privacy}", + "received_events_url": "https://api.github.com/users/viettrungluu-cr/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 8, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 17, + "d": 9, + "c": 1 + }, + { + "w": 1551571200, + "a": 129, + "d": 11, + "c": 1 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 393, + "d": 243, + "c": 4 + }, + { + "w": 1554595200, + "a": 32, + "d": 32, + "c": 2 + } + ], + "author": { + "login": "dkwingsmt", + "id": 1596656, + "node_id": "MDQ6VXNlcjE1OTY2NTY=", + "avatar_url": "https://avatars2.githubusercontent.com/u/1596656?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/dkwingsmt", + "html_url": "https://github.com/dkwingsmt", + "followers_url": "https://api.github.com/users/dkwingsmt/followers", + "following_url": "https://api.github.com/users/dkwingsmt/following{/other_user}", + "gists_url": "https://api.github.com/users/dkwingsmt/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dkwingsmt/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dkwingsmt/subscriptions", + "organizations_url": "https://api.github.com/users/dkwingsmt/orgs", + "repos_url": "https://api.github.com/users/dkwingsmt/repos", + "events_url": "https://api.github.com/users/dkwingsmt/events{/privacy}", + "received_events_url": "https://api.github.com/users/dkwingsmt/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 8, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 385, + "d": 403, + "c": 1 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 72, + "d": 0, + "c": 1 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 27, + "d": 7, + "c": 1 + }, + { + "w": 1536451200, + "a": 123, + "d": 1, + "c": 1 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 61, + "d": 0, + "c": 1 + }, + { + "w": 1547942400, + "a": 370, + "d": 17, + "c": 1 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 321, + "d": 9, + "c": 2 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "willlarche", + "id": 1271525, + "node_id": "MDQ6VXNlcjEyNzE1MjU=", + "avatar_url": "https://avatars1.githubusercontent.com/u/1271525?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/willlarche", + "html_url": "https://github.com/willlarche", + "followers_url": "https://api.github.com/users/willlarche/followers", + "following_url": "https://api.github.com/users/willlarche/following{/other_user}", + "gists_url": "https://api.github.com/users/willlarche/gists{/gist_id}", + "starred_url": "https://api.github.com/users/willlarche/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/willlarche/subscriptions", + "organizations_url": "https://api.github.com/users/willlarche/orgs", + "repos_url": "https://api.github.com/users/willlarche/repos", + "events_url": "https://api.github.com/users/willlarche/events{/privacy}", + "received_events_url": "https://api.github.com/users/willlarche/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 8, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 40, + "d": 21, + "c": 1 + }, + { + "w": 1457222400, + "a": 9, + "d": 3, + "c": 1 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 54, + "d": 1, + "c": 1 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 75, + "d": 10, + "c": 2 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 118, + "d": 0, + "c": 1 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 130, + "d": 59, + "c": 1 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "stevemessick", + "id": 8518285, + "node_id": "MDQ6VXNlcjg1MTgyODU=", + "avatar_url": "https://avatars0.githubusercontent.com/u/8518285?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/stevemessick", + "html_url": "https://github.com/stevemessick", + "followers_url": "https://api.github.com/users/stevemessick/followers", + "following_url": "https://api.github.com/users/stevemessick/following{/other_user}", + "gists_url": "https://api.github.com/users/stevemessick/gists{/gist_id}", + "starred_url": "https://api.github.com/users/stevemessick/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/stevemessick/subscriptions", + "organizations_url": "https://api.github.com/users/stevemessick/orgs", + "repos_url": "https://api.github.com/users/stevemessick/repos", + "events_url": "https://api.github.com/users/stevemessick/events{/privacy}", + "received_events_url": "https://api.github.com/users/stevemessick/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 8, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 78, + "d": 77, + "c": 1 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 9, + "d": 2, + "c": 1 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 33, + "d": 1, + "c": 1 + }, + { + "w": 1488067200, + "a": 18, + "d": 4, + "c": 1 + }, + { + "w": 1488672000, + "a": 79, + "d": 28, + "c": 1 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 206, + "d": 8, + "c": 1 + }, + { + "w": 1490486400, + "a": 93, + "d": 5, + "c": 1 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "lukef", + "id": 37941, + "node_id": "MDQ6VXNlcjM3OTQx", + "avatar_url": "https://avatars3.githubusercontent.com/u/37941?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/lukef", + "html_url": "https://github.com/lukef", + "followers_url": "https://api.github.com/users/lukef/followers", + "following_url": "https://api.github.com/users/lukef/following{/other_user}", + "gists_url": "https://api.github.com/users/lukef/gists{/gist_id}", + "starred_url": "https://api.github.com/users/lukef/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lukef/subscriptions", + "organizations_url": "https://api.github.com/users/lukef/orgs", + "repos_url": "https://api.github.com/users/lukef/repos", + "events_url": "https://api.github.com/users/lukef/events{/privacy}", + "received_events_url": "https://api.github.com/users/lukef/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 9, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 27, + "d": 1, + "c": 1 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 27, + "d": 3, + "c": 2 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 48, + "d": 17, + "c": 1 + }, + { + "w": 1512259200, + "a": 78, + "d": 45, + "c": 3 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 27, + "d": 27, + "c": 1 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 3, + "c": 1 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "kevmoo", + "id": 17034, + "node_id": "MDQ6VXNlcjE3MDM0", + "avatar_url": "https://avatars2.githubusercontent.com/u/17034?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/kevmoo", + "html_url": "https://github.com/kevmoo", + "followers_url": "https://api.github.com/users/kevmoo/followers", + "following_url": "https://api.github.com/users/kevmoo/following{/other_user}", + "gists_url": "https://api.github.com/users/kevmoo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/kevmoo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/kevmoo/subscriptions", + "organizations_url": "https://api.github.com/users/kevmoo/orgs", + "repos_url": "https://api.github.com/users/kevmoo/repos", + "events_url": "https://api.github.com/users/kevmoo/events{/privacy}", + "received_events_url": "https://api.github.com/users/kevmoo/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 10, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 40, + "d": 21, + "c": 1 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 957, + "d": 311, + "c": 1 + }, + { + "w": 1549152000, + "a": 261, + "d": 157, + "c": 1 + }, + { + "w": 1549756800, + "a": 277, + "d": 17, + "c": 2 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 60, + "d": 11, + "c": 2 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1552780800, + "a": 456, + "d": 42, + "c": 1 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "clocksmith", + "id": 591699, + "node_id": "MDQ6VXNlcjU5MTY5OQ==", + "avatar_url": "https://avatars1.githubusercontent.com/u/591699?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/clocksmith", + "html_url": "https://github.com/clocksmith", + "followers_url": "https://api.github.com/users/clocksmith/followers", + "following_url": "https://api.github.com/users/clocksmith/following{/other_user}", + "gists_url": "https://api.github.com/users/clocksmith/gists{/gist_id}", + "starred_url": "https://api.github.com/users/clocksmith/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/clocksmith/subscriptions", + "organizations_url": "https://api.github.com/users/clocksmith/orgs", + "repos_url": "https://api.github.com/users/clocksmith/repos", + "events_url": "https://api.github.com/users/clocksmith/events{/privacy}", + "received_events_url": "https://api.github.com/users/clocksmith/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 10, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 5, + "d": 3, + "c": 3 + }, + { + "w": 1519516800, + "a": 15, + "d": 9, + "c": 1 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 16, + "d": 1, + "c": 1 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 34, + "d": 19, + "c": 1 + }, + { + "w": 1523145600, + "a": 19, + "d": 13, + "c": 1 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 397, + "d": 24, + "c": 1 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "xqwzts", + "id": 798935, + "node_id": "MDQ6VXNlcjc5ODkzNQ==", + "avatar_url": "https://avatars1.githubusercontent.com/u/798935?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/xqwzts", + "html_url": "https://github.com/xqwzts", + "followers_url": "https://api.github.com/users/xqwzts/followers", + "following_url": "https://api.github.com/users/xqwzts/following{/other_user}", + "gists_url": "https://api.github.com/users/xqwzts/gists{/gist_id}", + "starred_url": "https://api.github.com/users/xqwzts/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/xqwzts/subscriptions", + "organizations_url": "https://api.github.com/users/xqwzts/orgs", + "repos_url": "https://api.github.com/users/xqwzts/repos", + "events_url": "https://api.github.com/users/xqwzts/events{/privacy}", + "received_events_url": "https://api.github.com/users/xqwzts/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 10, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 86, + "d": 17, + "c": 4 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 8, + "d": 8, + "c": 2 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 32, + "d": 13, + "c": 3 + }, + { + "w": 1466294400, + "a": 73, + "d": 1, + "c": 1 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "aghassemi", + "id": 2099009, + "node_id": "MDQ6VXNlcjIwOTkwMDk=", + "avatar_url": "https://avatars3.githubusercontent.com/u/2099009?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/aghassemi", + "html_url": "https://github.com/aghassemi", + "followers_url": "https://api.github.com/users/aghassemi/followers", + "following_url": "https://api.github.com/users/aghassemi/following{/other_user}", + "gists_url": "https://api.github.com/users/aghassemi/gists{/gist_id}", + "starred_url": "https://api.github.com/users/aghassemi/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/aghassemi/subscriptions", + "organizations_url": "https://api.github.com/users/aghassemi/orgs", + "repos_url": "https://api.github.com/users/aghassemi/repos", + "events_url": "https://api.github.com/users/aghassemi/events{/privacy}", + "received_events_url": "https://api.github.com/users/aghassemi/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 10, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1438473600, + "a": 121, + "d": 40, + "c": 2 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 24, + "d": 4, + "c": 1 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 14, + "d": 7, + "c": 1 + }, + { + "w": 1445126400, + "a": 73, + "d": 36, + "c": 2 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 5, + "d": 5, + "c": 1 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 58, + "d": 11, + "c": 1 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "jimbeveridge", + "id": 7953123, + "node_id": "MDQ6VXNlcjc5NTMxMjM=", + "avatar_url": "https://avatars1.githubusercontent.com/u/7953123?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jimbeveridge", + "html_url": "https://github.com/jimbeveridge", + "followers_url": "https://api.github.com/users/jimbeveridge/followers", + "following_url": "https://api.github.com/users/jimbeveridge/following{/other_user}", + "gists_url": "https://api.github.com/users/jimbeveridge/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jimbeveridge/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jimbeveridge/subscriptions", + "organizations_url": "https://api.github.com/users/jimbeveridge/orgs", + "repos_url": "https://api.github.com/users/jimbeveridge/repos", + "events_url": "https://api.github.com/users/jimbeveridge/events{/privacy}", + "received_events_url": "https://api.github.com/users/jimbeveridge/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 11, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 46, + "d": 46, + "c": 2 + }, + { + "w": 1515888000, + "a": 70, + "d": 71, + "c": 1 + }, + { + "w": 1516492800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 69, + "d": 85, + "c": 1 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 34, + "d": 35, + "c": 1 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 56, + "d": 56, + "c": 1 + }, + { + "w": 1526169600, + "a": 15, + "d": 15, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 74, + "d": 46, + "c": 1 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 159, + "d": 163, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 12, + "d": 2, + "c": 1 + } + ], + "author": { + "login": "srawlins", + "id": 103167, + "node_id": "MDQ6VXNlcjEwMzE2Nw==", + "avatar_url": "https://avatars3.githubusercontent.com/u/103167?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/srawlins", + "html_url": "https://github.com/srawlins", + "followers_url": "https://api.github.com/users/srawlins/followers", + "following_url": "https://api.github.com/users/srawlins/following{/other_user}", + "gists_url": "https://api.github.com/users/srawlins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/srawlins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/srawlins/subscriptions", + "organizations_url": "https://api.github.com/users/srawlins/orgs", + "repos_url": "https://api.github.com/users/srawlins/repos", + "events_url": "https://api.github.com/users/srawlins/events{/privacy}", + "received_events_url": "https://api.github.com/users/srawlins/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 11, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 307, + "d": 65, + "c": 2 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 1, + "d": 5, + "c": 1 + }, + { + "w": 1512864000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 17, + "d": 17, + "c": 1 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 789, + "d": 633, + "c": 3 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "mkustermann", + "id": 5757092, + "node_id": "MDQ6VXNlcjU3NTcwOTI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/5757092?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mkustermann", + "html_url": "https://github.com/mkustermann", + "followers_url": "https://api.github.com/users/mkustermann/followers", + "following_url": "https://api.github.com/users/mkustermann/following{/other_user}", + "gists_url": "https://api.github.com/users/mkustermann/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mkustermann/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mkustermann/subscriptions", + "organizations_url": "https://api.github.com/users/mkustermann/orgs", + "repos_url": "https://api.github.com/users/mkustermann/repos", + "events_url": "https://api.github.com/users/mkustermann/events{/privacy}", + "received_events_url": "https://api.github.com/users/mkustermann/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 11, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 16, + "d": 15, + "c": 2 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 65, + "d": 53, + "c": 2 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 17, + "d": 4, + "c": 1 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 35, + "d": 23, + "c": 3 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 11, + "d": 3, + "c": 1 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "alhaad", + "id": 5509332, + "node_id": "MDQ6VXNlcjU1MDkzMzI=", + "avatar_url": "https://avatars1.githubusercontent.com/u/5509332?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/alhaad", + "html_url": "https://github.com/alhaad", + "followers_url": "https://api.github.com/users/alhaad/followers", + "following_url": "https://api.github.com/users/alhaad/following{/other_user}", + "gists_url": "https://api.github.com/users/alhaad/gists{/gist_id}", + "starred_url": "https://api.github.com/users/alhaad/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/alhaad/subscriptions", + "organizations_url": "https://api.github.com/users/alhaad/orgs", + "repos_url": "https://api.github.com/users/alhaad/repos", + "events_url": "https://api.github.com/users/alhaad/events{/privacy}", + "received_events_url": "https://api.github.com/users/alhaad/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 12, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 1442, + "d": 1, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 847, + "d": 327, + "c": 2 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 182, + "d": 39, + "c": 2 + }, + { + "w": 1533427200, + "a": 3181, + "d": 221, + "c": 6 + }, + { + "w": 1534032000, + "a": 263, + "d": 206, + "c": 1 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "nataliesampsell", + "id": 22058239, + "node_id": "MDQ6VXNlcjIyMDU4MjM5", + "avatar_url": "https://avatars2.githubusercontent.com/u/22058239?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/nataliesampsell", + "html_url": "https://github.com/nataliesampsell", + "followers_url": "https://api.github.com/users/nataliesampsell/followers", + "following_url": "https://api.github.com/users/nataliesampsell/following{/other_user}", + "gists_url": "https://api.github.com/users/nataliesampsell/gists{/gist_id}", + "starred_url": "https://api.github.com/users/nataliesampsell/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/nataliesampsell/subscriptions", + "organizations_url": "https://api.github.com/users/nataliesampsell/orgs", + "repos_url": "https://api.github.com/users/nataliesampsell/repos", + "events_url": "https://api.github.com/users/nataliesampsell/events{/privacy}", + "received_events_url": "https://api.github.com/users/nataliesampsell/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 12, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 65, + "d": 14, + "c": 2 + }, + { + "w": 1492300800, + "a": 26, + "d": 10, + "c": 2 + }, + { + "w": 1492905600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1493510400, + "a": 156, + "d": 0, + "c": 1 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 13, + "d": 1, + "c": 1 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 271, + "d": 109, + "c": 3 + }, + { + "w": 1497744000, + "a": 11, + "d": 4, + "c": 1 + }, + { + "w": 1498348800, + "a": 98, + "d": 27, + "c": 1 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "skybrian", + "id": 129084, + "node_id": "MDQ6VXNlcjEyOTA4NA==", + "avatar_url": "https://avatars1.githubusercontent.com/u/129084?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/skybrian", + "html_url": "https://github.com/skybrian", + "followers_url": "https://api.github.com/users/skybrian/followers", + "following_url": "https://api.github.com/users/skybrian/following{/other_user}", + "gists_url": "https://api.github.com/users/skybrian/gists{/gist_id}", + "starred_url": "https://api.github.com/users/skybrian/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/skybrian/subscriptions", + "organizations_url": "https://api.github.com/users/skybrian/orgs", + "repos_url": "https://api.github.com/users/skybrian/repos", + "events_url": "https://api.github.com/users/skybrian/events{/privacy}", + "received_events_url": "https://api.github.com/users/skybrian/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 15, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 159, + "d": 7, + "c": 3 + }, + { + "w": 1552780800, + "a": 212, + "d": 13, + "c": 2 + }, + { + "w": 1553385600, + "a": 405, + "d": 7, + "c": 4 + }, + { + "w": 1553990400, + "a": 167, + "d": 5, + "c": 2 + }, + { + "w": 1554595200, + "a": 393, + "d": 49, + "c": 4 + } + ], + "author": { + "login": "shihaohong", + "id": 27032613, + "node_id": "MDQ6VXNlcjI3MDMyNjEz", + "avatar_url": "https://avatars3.githubusercontent.com/u/27032613?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/shihaohong", + "html_url": "https://github.com/shihaohong", + "followers_url": "https://api.github.com/users/shihaohong/followers", + "following_url": "https://api.github.com/users/shihaohong/following{/other_user}", + "gists_url": "https://api.github.com/users/shihaohong/gists{/gist_id}", + "starred_url": "https://api.github.com/users/shihaohong/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/shihaohong/subscriptions", + "organizations_url": "https://api.github.com/users/shihaohong/orgs", + "repos_url": "https://api.github.com/users/shihaohong/repos", + "events_url": "https://api.github.com/users/shihaohong/events{/privacy}", + "received_events_url": "https://api.github.com/users/shihaohong/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 15, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 98, + "d": 23, + "c": 2 + }, + { + "w": 1547942400, + "a": 372, + "d": 19, + "c": 1 + }, + { + "w": 1548547200, + "a": 323, + "d": 74, + "c": 5 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 321, + "d": 105, + "c": 1 + }, + { + "w": 1550361600, + "a": 138, + "d": 9, + "c": 2 + }, + { + "w": 1550966400, + "a": 169, + "d": 30, + "c": 1 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 37, + "d": 1, + "c": 1 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 42, + "d": 2, + "c": 1 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 6, + "d": 4, + "c": 1 + } + ], + "author": { + "login": "rami-a", + "id": 2364772, + "node_id": "MDQ6VXNlcjIzNjQ3NzI=", + "avatar_url": "https://avatars3.githubusercontent.com/u/2364772?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/rami-a", + "html_url": "https://github.com/rami-a", + "followers_url": "https://api.github.com/users/rami-a/followers", + "following_url": "https://api.github.com/users/rami-a/following{/other_user}", + "gists_url": "https://api.github.com/users/rami-a/gists{/gist_id}", + "starred_url": "https://api.github.com/users/rami-a/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/rami-a/subscriptions", + "organizations_url": "https://api.github.com/users/rami-a/orgs", + "repos_url": "https://api.github.com/users/rami-a/repos", + "events_url": "https://api.github.com/users/rami-a/events{/privacy}", + "received_events_url": "https://api.github.com/users/rami-a/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 15, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 62, + "d": 3, + "c": 2 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 93, + "d": 0, + "c": 2 + }, + { + "w": 1551571200, + "a": 99, + "d": 0, + "c": 2 + }, + { + "w": 1552176000, + "a": 137, + "d": 33, + "c": 3 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 37, + "d": 5, + "c": 2 + }, + { + "w": 1553990400, + "a": 75, + "d": 37, + "c": 4 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "Piinks", + "id": 16964204, + "node_id": "MDQ6VXNlcjE2OTY0MjA0", + "avatar_url": "https://avatars3.githubusercontent.com/u/16964204?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Piinks", + "html_url": "https://github.com/Piinks", + "followers_url": "https://api.github.com/users/Piinks/followers", + "following_url": "https://api.github.com/users/Piinks/following{/other_user}", + "gists_url": "https://api.github.com/users/Piinks/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Piinks/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Piinks/subscriptions", + "organizations_url": "https://api.github.com/users/Piinks/orgs", + "repos_url": "https://api.github.com/users/Piinks/repos", + "events_url": "https://api.github.com/users/Piinks/events{/privacy}", + "received_events_url": "https://api.github.com/users/Piinks/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 15, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 189, + "d": 90, + "c": 3 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 58, + "d": 0, + "c": 1 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 164, + "d": 63, + "c": 1 + }, + { + "w": 1523145600, + "a": 592, + "d": 13, + "c": 4 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 6, + "d": 1, + "c": 1 + }, + { + "w": 1524960000, + "a": 10, + "d": 124, + "c": 1 + }, + { + "w": 1525564800, + "a": 58, + "d": 58, + "c": 1 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 4, + "d": 0, + "c": 1 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 29, + "d": 29, + "c": 2 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "scheglov", + "id": 384794, + "node_id": "MDQ6VXNlcjM4NDc5NA==", + "avatar_url": "https://avatars0.githubusercontent.com/u/384794?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/scheglov", + "html_url": "https://github.com/scheglov", + "followers_url": "https://api.github.com/users/scheglov/followers", + "following_url": "https://api.github.com/users/scheglov/following{/other_user}", + "gists_url": "https://api.github.com/users/scheglov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/scheglov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/scheglov/subscriptions", + "organizations_url": "https://api.github.com/users/scheglov/orgs", + "repos_url": "https://api.github.com/users/scheglov/repos", + "events_url": "https://api.github.com/users/scheglov/events{/privacy}", + "received_events_url": "https://api.github.com/users/scheglov/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 15, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 57, + "d": 12, + "c": 1 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 293, + "d": 350, + "c": 3 + }, + { + "w": 1538265600, + "a": 46, + "d": 44, + "c": 1 + }, + { + "w": 1538870400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 128, + "d": 70, + "c": 1 + }, + { + "w": 1540684800, + "a": 187, + "d": 12, + "c": 2 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 187, + "d": 47, + "c": 1 + }, + { + "w": 1544918400, + "a": 210, + "d": 9, + "c": 1 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 281, + "d": 12, + "c": 1 + }, + { + "w": 1547337600, + "a": 215, + "d": 79, + "c": 3 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "johnsonmh", + "id": 4229329, + "node_id": "MDQ6VXNlcjQyMjkzMjk=", + "avatar_url": "https://avatars2.githubusercontent.com/u/4229329?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/johnsonmh", + "html_url": "https://github.com/johnsonmh", + "followers_url": "https://api.github.com/users/johnsonmh/followers", + "following_url": "https://api.github.com/users/johnsonmh/following{/other_user}", + "gists_url": "https://api.github.com/users/johnsonmh/gists{/gist_id}", + "starred_url": "https://api.github.com/users/johnsonmh/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/johnsonmh/subscriptions", + "organizations_url": "https://api.github.com/users/johnsonmh/orgs", + "repos_url": "https://api.github.com/users/johnsonmh/repos", + "events_url": "https://api.github.com/users/johnsonmh/events{/privacy}", + "received_events_url": "https://api.github.com/users/johnsonmh/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 15, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 1120, + "d": 2, + "c": 3 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 119, + "d": 85, + "c": 3 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 32, + "d": 8, + "c": 3 + }, + { + "w": 1420934400, + "a": 38, + "d": 28, + "c": 3 + }, + { + "w": 1421539200, + "a": 4, + "d": 4, + "c": 2 + }, + { + "w": 1422144000, + "a": 0, + "d": 5, + "c": 1 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "esprehn", + "id": 415779, + "node_id": "MDQ6VXNlcjQxNTc3OQ==", + "avatar_url": "https://avatars1.githubusercontent.com/u/415779?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/esprehn", + "html_url": "https://github.com/esprehn", + "followers_url": "https://api.github.com/users/esprehn/followers", + "following_url": "https://api.github.com/users/esprehn/following{/other_user}", + "gists_url": "https://api.github.com/users/esprehn/gists{/gist_id}", + "starred_url": "https://api.github.com/users/esprehn/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/esprehn/subscriptions", + "organizations_url": "https://api.github.com/users/esprehn/orgs", + "repos_url": "https://api.github.com/users/esprehn/repos", + "events_url": "https://api.github.com/users/esprehn/events{/privacy}", + "received_events_url": "https://api.github.com/users/esprehn/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 16, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 79, + "d": 21, + "c": 1 + }, + { + "w": 1536451200, + "a": 107, + "d": 21, + "c": 1 + }, + { + "w": 1537056000, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1537660800, + "a": 67, + "d": 9, + "c": 2 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 119, + "d": 18, + "c": 3 + }, + { + "w": 1540080000, + "a": 829, + "d": 386, + "c": 4 + }, + { + "w": 1540684800, + "a": 4, + "d": 2, + "c": 1 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 31, + "d": 16, + "c": 1 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 96, + "d": 3, + "c": 1 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "mklim", + "id": 4615911, + "node_id": "MDQ6VXNlcjQ2MTU5MTE=", + "avatar_url": "https://avatars3.githubusercontent.com/u/4615911?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mklim", + "html_url": "https://github.com/mklim", + "followers_url": "https://api.github.com/users/mklim/followers", + "following_url": "https://api.github.com/users/mklim/following{/other_user}", + "gists_url": "https://api.github.com/users/mklim/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mklim/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mklim/subscriptions", + "organizations_url": "https://api.github.com/users/mklim/orgs", + "repos_url": "https://api.github.com/users/mklim/repos", + "events_url": "https://api.github.com/users/mklim/events{/privacy}", + "received_events_url": "https://api.github.com/users/mklim/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 19, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 10, + "d": 15, + "c": 6 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 10, + "d": 3, + "c": 4 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1540684800, + "a": 113, + "d": 2, + "c": 1 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 6, + "d": 6, + "c": 2 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 77, + "d": 31, + "c": 1 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "bkonyi", + "id": 24210656, + "node_id": "MDQ6VXNlcjI0MjEwNjU2", + "avatar_url": "https://avatars2.githubusercontent.com/u/24210656?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/bkonyi", + "html_url": "https://github.com/bkonyi", + "followers_url": "https://api.github.com/users/bkonyi/followers", + "following_url": "https://api.github.com/users/bkonyi/following{/other_user}", + "gists_url": "https://api.github.com/users/bkonyi/gists{/gist_id}", + "starred_url": "https://api.github.com/users/bkonyi/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/bkonyi/subscriptions", + "organizations_url": "https://api.github.com/users/bkonyi/orgs", + "repos_url": "https://api.github.com/users/bkonyi/repos", + "events_url": "https://api.github.com/users/bkonyi/events{/privacy}", + "received_events_url": "https://api.github.com/users/bkonyi/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 20, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 194, + "d": 72, + "c": 1 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 44, + "d": 0, + "c": 1 + }, + { + "w": 1546732800, + "a": 53, + "d": 2, + "c": 2 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 60, + "d": 4, + "c": 2 + }, + { + "w": 1549152000, + "a": 155, + "d": 21, + "c": 4 + }, + { + "w": 1549756800, + "a": 540, + "d": 195, + "c": 9 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 137, + "d": 11, + "c": 1 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "kangwang1988", + "id": 817851, + "node_id": "MDQ6VXNlcjgxNzg1MQ==", + "avatar_url": "https://avatars1.githubusercontent.com/u/817851?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/kangwang1988", + "html_url": "https://github.com/kangwang1988", + "followers_url": "https://api.github.com/users/kangwang1988/followers", + "following_url": "https://api.github.com/users/kangwang1988/following{/other_user}", + "gists_url": "https://api.github.com/users/kangwang1988/gists{/gist_id}", + "starred_url": "https://api.github.com/users/kangwang1988/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/kangwang1988/subscriptions", + "organizations_url": "https://api.github.com/users/kangwang1988/orgs", + "repos_url": "https://api.github.com/users/kangwang1988/repos", + "events_url": "https://api.github.com/users/kangwang1988/events{/privacy}", + "received_events_url": "https://api.github.com/users/kangwang1988/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 20, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 3, + "d": 3, + "c": 3 + }, + { + "w": 1511654400, + "a": 387, + "d": 1, + "c": 1 + }, + { + "w": 1512259200, + "a": 105, + "d": 48, + "c": 2 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 3, + "d": 11, + "c": 1 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 212, + "d": 23, + "c": 1 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 894, + "d": 623, + "c": 3 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 114, + "d": 114, + "c": 2 + }, + { + "w": 1531612800, + "a": 1274, + "d": 1226, + "c": 6 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "sigurdm", + "id": 8613953, + "node_id": "MDQ6VXNlcjg2MTM5NTM=", + "avatar_url": "https://avatars1.githubusercontent.com/u/8613953?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/sigurdm", + "html_url": "https://github.com/sigurdm", + "followers_url": "https://api.github.com/users/sigurdm/followers", + "following_url": "https://api.github.com/users/sigurdm/following{/other_user}", + "gists_url": "https://api.github.com/users/sigurdm/gists{/gist_id}", + "starred_url": "https://api.github.com/users/sigurdm/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/sigurdm/subscriptions", + "organizations_url": "https://api.github.com/users/sigurdm/orgs", + "repos_url": "https://api.github.com/users/sigurdm/repos", + "events_url": "https://api.github.com/users/sigurdm/events{/privacy}", + "received_events_url": "https://api.github.com/users/sigurdm/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 21, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 244, + "d": 4, + "c": 2 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 350, + "d": 339, + "c": 3 + }, + { + "w": 1449360000, + "a": 248, + "d": 242, + "c": 2 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 16, + "d": 2, + "c": 1 + }, + { + "w": 1527379200, + "a": 36, + "d": 2, + "c": 1 + }, + { + "w": 1527984000, + "a": 7, + "d": 3, + "c": 2 + }, + { + "w": 1528588800, + "a": 3, + "d": 3, + "c": 3 + }, + { + "w": 1529193600, + "a": 139, + "d": 76, + "c": 3 + }, + { + "w": 1529798400, + "a": 345, + "d": 3, + "c": 3 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "floitschG", + "id": 8631949, + "node_id": "MDQ6VXNlcjg2MzE5NDk=", + "avatar_url": "https://avatars2.githubusercontent.com/u/8631949?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/floitschG", + "html_url": "https://github.com/floitschG", + "followers_url": "https://api.github.com/users/floitschG/followers", + "following_url": "https://api.github.com/users/floitschG/following{/other_user}", + "gists_url": "https://api.github.com/users/floitschG/gists{/gist_id}", + "starred_url": "https://api.github.com/users/floitschG/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/floitschG/subscriptions", + "organizations_url": "https://api.github.com/users/floitschG/orgs", + "repos_url": "https://api.github.com/users/floitschG/repos", + "events_url": "https://api.github.com/users/floitschG/events{/privacy}", + "received_events_url": "https://api.github.com/users/floitschG/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 21, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 213, + "d": 116, + "c": 6 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 46, + "d": 26, + "c": 2 + }, + { + "w": 1503187200, + "a": 62, + "d": 29, + "c": 6 + }, + { + "w": 1503792000, + "a": 1, + "d": 2, + "c": 2 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 32, + "d": 12, + "c": 4 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "B3rn475", + "id": 2686902, + "node_id": "MDQ6VXNlcjI2ODY5MDI=", + "avatar_url": "https://avatars1.githubusercontent.com/u/2686902?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/B3rn475", + "html_url": "https://github.com/B3rn475", + "followers_url": "https://api.github.com/users/B3rn475/followers", + "following_url": "https://api.github.com/users/B3rn475/following{/other_user}", + "gists_url": "https://api.github.com/users/B3rn475/gists{/gist_id}", + "starred_url": "https://api.github.com/users/B3rn475/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/B3rn475/subscriptions", + "organizations_url": "https://api.github.com/users/B3rn475/orgs", + "repos_url": "https://api.github.com/users/B3rn475/repos", + "events_url": "https://api.github.com/users/B3rn475/events{/privacy}", + "received_events_url": "https://api.github.com/users/B3rn475/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 22, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 338, + "d": 23, + "c": 4 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 245, + "d": 124, + "c": 5 + }, + { + "w": 1544918400, + "a": 562, + "d": 97, + "c": 3 + }, + { + "w": 1545523200, + "a": 78, + "d": 2, + "c": 1 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 289, + "d": 8, + "c": 3 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 774, + "d": 170, + "c": 1 + }, + { + "w": 1552176000, + "a": 897, + "d": 171, + "c": 1 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 7, + "d": 0, + "c": 1 + }, + { + "w": 1553990400, + "a": 1261, + "d": 0, + "c": 2 + }, + { + "w": 1554595200, + "a": 81, + "d": 3, + "c": 1 + } + ], + "author": { + "login": "justinmc", + "id": 389558, + "node_id": "MDQ6VXNlcjM4OTU1OA==", + "avatar_url": "https://avatars2.githubusercontent.com/u/389558?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/justinmc", + "html_url": "https://github.com/justinmc", + "followers_url": "https://api.github.com/users/justinmc/followers", + "following_url": "https://api.github.com/users/justinmc/following{/other_user}", + "gists_url": "https://api.github.com/users/justinmc/gists{/gist_id}", + "starred_url": "https://api.github.com/users/justinmc/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/justinmc/subscriptions", + "organizations_url": "https://api.github.com/users/justinmc/orgs", + "repos_url": "https://api.github.com/users/justinmc/repos", + "events_url": "https://api.github.com/users/justinmc/events{/privacy}", + "received_events_url": "https://api.github.com/users/justinmc/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 22, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 12, + "d": 3, + "c": 1 + }, + { + "w": 1527984000, + "a": 129, + "d": 10, + "c": 2 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 273, + "d": 21, + "c": 3 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 832, + "d": 79, + "c": 2 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 87, + "d": 0, + "c": 1 + }, + { + "w": 1532217600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 2083, + "d": 467, + "c": 2 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 114, + "d": 114, + "c": 1 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 4823, + "d": 4844, + "c": 3 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 186, + "d": 16, + "c": 1 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 1152, + "d": 164, + "c": 5 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "matthew-carroll", + "id": 7259036, + "node_id": "MDQ6VXNlcjcyNTkwMzY=", + "avatar_url": "https://avatars3.githubusercontent.com/u/7259036?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/matthew-carroll", + "html_url": "https://github.com/matthew-carroll", + "followers_url": "https://api.github.com/users/matthew-carroll/followers", + "following_url": "https://api.github.com/users/matthew-carroll/following{/other_user}", + "gists_url": "https://api.github.com/users/matthew-carroll/gists{/gist_id}", + "starred_url": "https://api.github.com/users/matthew-carroll/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/matthew-carroll/subscriptions", + "organizations_url": "https://api.github.com/users/matthew-carroll/orgs", + "repos_url": "https://api.github.com/users/matthew-carroll/repos", + "events_url": "https://api.github.com/users/matthew-carroll/events{/privacy}", + "received_events_url": "https://api.github.com/users/matthew-carroll/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 22, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 83, + "d": 44, + "c": 1 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 65, + "d": 10, + "c": 1 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 59, + "d": 14, + "c": 1 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 19, + "d": 11, + "c": 2 + }, + { + "w": 1504396800, + "a": 5, + "d": 5, + "c": 1 + }, + { + "w": 1505001600, + "a": 98, + "d": 11, + "c": 1 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 22, + "d": 22, + "c": 1 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 44, + "d": 12, + "c": 1 + }, + { + "w": 1512259200, + "a": 39, + "d": 8, + "c": 1 + }, + { + "w": 1512864000, + "a": 121, + "d": 106, + "c": 2 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 148, + "d": 148, + "c": 1 + }, + { + "w": 1528588800, + "a": 10, + "d": 10, + "c": 1 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 4, + "d": 0, + "c": 1 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 7, + "d": 1, + "c": 1 + }, + { + "w": 1534636800, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 17, + "d": 816, + "c": 1 + }, + { + "w": 1536451200, + "a": 5, + "d": 1, + "c": 1 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 1, + "d": 36, + "c": 1 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 36, + "d": 33, + "c": 1 + }, + { + "w": 1547337600, + "a": 50, + "d": 26, + "c": 1 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "mehmetf", + "id": 6687929, + "node_id": "MDQ6VXNlcjY2ODc5Mjk=", + "avatar_url": "https://avatars3.githubusercontent.com/u/6687929?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mehmetf", + "html_url": "https://github.com/mehmetf", + "followers_url": "https://api.github.com/users/mehmetf/followers", + "following_url": "https://api.github.com/users/mehmetf/following{/other_user}", + "gists_url": "https://api.github.com/users/mehmetf/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mehmetf/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mehmetf/subscriptions", + "organizations_url": "https://api.github.com/users/mehmetf/orgs", + "repos_url": "https://api.github.com/users/mehmetf/repos", + "events_url": "https://api.github.com/users/mehmetf/events{/privacy}", + "received_events_url": "https://api.github.com/users/mehmetf/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 22, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1518912000, + "a": 101, + "d": 2, + "c": 1 + }, + { + "w": 1519516800, + "a": 14, + "d": 6, + "c": 1 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 1076, + "d": 147, + "c": 1 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 637, + "d": 256, + "c": 4 + }, + { + "w": 1523145600, + "a": 11, + "d": 22, + "c": 1 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 173, + "d": 3, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1532822400, + "a": 1128, + "d": 0, + "c": 1 + }, + { + "w": 1533427200, + "a": 1766, + "d": 107, + "c": 1 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 66, + "d": 4, + "c": 1 + }, + { + "w": 1535241600, + "a": 248, + "d": 67, + "c": 2 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 339, + "d": 204, + "c": 4 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 172, + "d": 38, + "c": 1 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "DaveShuckerow", + "id": 14947366, + "node_id": "MDQ6VXNlcjE0OTQ3MzY2", + "avatar_url": "https://avatars2.githubusercontent.com/u/14947366?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/DaveShuckerow", + "html_url": "https://github.com/DaveShuckerow", + "followers_url": "https://api.github.com/users/DaveShuckerow/followers", + "following_url": "https://api.github.com/users/DaveShuckerow/following{/other_user}", + "gists_url": "https://api.github.com/users/DaveShuckerow/gists{/gist_id}", + "starred_url": "https://api.github.com/users/DaveShuckerow/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/DaveShuckerow/subscriptions", + "organizations_url": "https://api.github.com/users/DaveShuckerow/orgs", + "repos_url": "https://api.github.com/users/DaveShuckerow/repos", + "events_url": "https://api.github.com/users/DaveShuckerow/events{/privacy}", + "received_events_url": "https://api.github.com/users/DaveShuckerow/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 23, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 1557, + "d": 42, + "c": 1 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 72, + "d": 24, + "c": 2 + }, + { + "w": 1524960000, + "a": 442, + "d": 68, + "c": 3 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 19, + "d": 0, + "c": 1 + }, + { + "w": 1526774400, + "a": 7, + "d": 9, + "c": 1 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 46, + "d": 4, + "c": 1 + }, + { + "w": 1529193600, + "a": 133, + "d": 24, + "c": 3 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 54, + "d": 25, + "c": 1 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 40, + "d": 22, + "c": 2 + }, + { + "w": 1538265600, + "a": 37, + "d": 19, + "c": 1 + }, + { + "w": 1538870400, + "a": 399, + "d": 207, + "c": 2 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 8, + "d": 1, + "c": 1 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 3, + "d": 4, + "c": 1 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 10, + "d": 7, + "c": 1 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 9, + "d": 3, + "c": 1 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "awdavies", + "id": 2238468, + "node_id": "MDQ6VXNlcjIyMzg0Njg=", + "avatar_url": "https://avatars0.githubusercontent.com/u/2238468?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/awdavies", + "html_url": "https://github.com/awdavies", + "followers_url": "https://api.github.com/users/awdavies/followers", + "following_url": "https://api.github.com/users/awdavies/following{/other_user}", + "gists_url": "https://api.github.com/users/awdavies/gists{/gist_id}", + "starred_url": "https://api.github.com/users/awdavies/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/awdavies/subscriptions", + "organizations_url": "https://api.github.com/users/awdavies/orgs", + "repos_url": "https://api.github.com/users/awdavies/repos", + "events_url": "https://api.github.com/users/awdavies/events{/privacy}", + "received_events_url": "https://api.github.com/users/awdavies/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 23, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1439078400, + "a": 18, + "d": 13, + "c": 1 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 50, + "d": 48, + "c": 2 + }, + { + "w": 1440892800, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1441497600, + "a": 18, + "d": 2, + "c": 1 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 10, + "d": 6, + "c": 1 + }, + { + "w": 1443312000, + "a": 8, + "d": 5, + "c": 2 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 23, + "d": 10, + "c": 1 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 269, + "d": 81, + "c": 2 + }, + { + "w": 1458432000, + "a": 400, + "d": 34, + "c": 3 + }, + { + "w": 1459036800, + "a": 522, + "d": 323, + "c": 2 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 54, + "d": 0, + "c": 2 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 85, + "d": 6, + "c": 1 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 62, + "d": 5, + "c": 1 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "apwilson", + "id": 142874, + "node_id": "MDQ6VXNlcjE0Mjg3NA==", + "avatar_url": "https://avatars3.githubusercontent.com/u/142874?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/apwilson", + "html_url": "https://github.com/apwilson", + "followers_url": "https://api.github.com/users/apwilson/followers", + "following_url": "https://api.github.com/users/apwilson/following{/other_user}", + "gists_url": "https://api.github.com/users/apwilson/gists{/gist_id}", + "starred_url": "https://api.github.com/users/apwilson/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/apwilson/subscriptions", + "organizations_url": "https://api.github.com/users/apwilson/orgs", + "repos_url": "https://api.github.com/users/apwilson/repos", + "events_url": "https://api.github.com/users/apwilson/events{/privacy}", + "received_events_url": "https://api.github.com/users/apwilson/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 24, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 154, + "d": 30, + "c": 1 + }, + { + "w": 1438473600, + "a": 355, + "d": 263, + "c": 2 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 164, + "d": 29, + "c": 4 + }, + { + "w": 1442707200, + "a": 291, + "d": 139, + "c": 14 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 62, + "d": 65, + "c": 1 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "jamesr", + "id": 7497301, + "node_id": "MDQ6VXNlcjc0OTczMDE=", + "avatar_url": "https://avatars1.githubusercontent.com/u/7497301?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jamesr", + "html_url": "https://github.com/jamesr", + "followers_url": "https://api.github.com/users/jamesr/followers", + "following_url": "https://api.github.com/users/jamesr/following{/other_user}", + "gists_url": "https://api.github.com/users/jamesr/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jamesr/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jamesr/subscriptions", + "organizations_url": "https://api.github.com/users/jamesr/orgs", + "repos_url": "https://api.github.com/users/jamesr/repos", + "events_url": "https://api.github.com/users/jamesr/events{/privacy}", + "received_events_url": "https://api.github.com/users/jamesr/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 25, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 5740, + "d": 0, + "c": 1 + }, + { + "w": 1414886400, + "a": 22, + "d": 13, + "c": 2 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1416700800, + "a": 27, + "d": 3, + "c": 3 + }, + { + "w": 1417305600, + "a": 18, + "d": 0, + "c": 1 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 19, + "d": 110, + "c": 1 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 5537, + "d": 48, + "c": 6 + }, + { + "w": 1425772800, + "a": 106, + "d": 116, + "c": 7 + }, + { + "w": 1426377600, + "a": 40, + "d": 158, + "c": 3 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "rafaelw", + "id": 646051, + "node_id": "MDQ6VXNlcjY0NjA1MQ==", + "avatar_url": "https://avatars0.githubusercontent.com/u/646051?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/rafaelw", + "html_url": "https://github.com/rafaelw", + "followers_url": "https://api.github.com/users/rafaelw/followers", + "following_url": "https://api.github.com/users/rafaelw/following{/other_user}", + "gists_url": "https://api.github.com/users/rafaelw/gists{/gist_id}", + "starred_url": "https://api.github.com/users/rafaelw/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/rafaelw/subscriptions", + "organizations_url": "https://api.github.com/users/rafaelw/orgs", + "repos_url": "https://api.github.com/users/rafaelw/repos", + "events_url": "https://api.github.com/users/rafaelw/events{/privacy}", + "received_events_url": "https://api.github.com/users/rafaelw/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 31, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 40, + "d": 0, + "c": 1 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 9, + "d": 5, + "c": 1 + }, + { + "w": 1515888000, + "a": 1453, + "d": 84, + "c": 4 + }, + { + "w": 1516492800, + "a": 90, + "d": 68, + "c": 2 + }, + { + "w": 1517097600, + "a": 20, + "d": 6, + "c": 1 + }, + { + "w": 1517702400, + "a": 198, + "d": 170, + "c": 6 + }, + { + "w": 1518307200, + "a": 58, + "d": 37, + "c": 6 + }, + { + "w": 1518912000, + "a": 10, + "d": 10, + "c": 1 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 341, + "d": 274, + "c": 2 + }, + { + "w": 1521331200, + "a": 10, + "d": 3, + "c": 1 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1525564800, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1526169600, + "a": 144, + "d": 71, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 96, + "d": 30, + "c": 2 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 68, + "d": 5, + "c": 1 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "mraleph", + "id": 131846, + "node_id": "MDQ6VXNlcjEzMTg0Ng==", + "avatar_url": "https://avatars3.githubusercontent.com/u/131846?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mraleph", + "html_url": "https://github.com/mraleph", + "followers_url": "https://api.github.com/users/mraleph/followers", + "following_url": "https://api.github.com/users/mraleph/following{/other_user}", + "gists_url": "https://api.github.com/users/mraleph/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mraleph/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mraleph/subscriptions", + "organizations_url": "https://api.github.com/users/mraleph/orgs", + "repos_url": "https://api.github.com/users/mraleph/repos", + "events_url": "https://api.github.com/users/mraleph/events{/privacy}", + "received_events_url": "https://api.github.com/users/mraleph/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 33, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 10, + "d": 0, + "c": 1 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 35, + "d": 0, + "c": 1 + }, + { + "w": 1474761600, + "a": 157, + "d": 84, + "c": 3 + }, + { + "w": 1475366400, + "a": 24, + "d": 4, + "c": 1 + }, + { + "w": 1475971200, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 8, + "d": 4, + "c": 1 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 10, + "d": 4, + "c": 2 + }, + { + "w": 1480204800, + "a": 9, + "d": 2, + "c": 1 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 47, + "d": 7, + "c": 1 + }, + { + "w": 1490486400, + "a": 4, + "d": 2, + "c": 1 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 126, + "d": 3, + "c": 1 + }, + { + "w": 1492905600, + "a": 25, + "d": 26, + "c": 1 + }, + { + "w": 1493510400, + "a": 9, + "d": 3, + "c": 1 + }, + { + "w": 1494115200, + "a": 4, + "d": 4, + "c": 2 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 142, + "d": 4, + "c": 1 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 4, + "d": 0, + "c": 1 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 6, + "d": 6, + "c": 1 + }, + { + "w": 1507420800, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1508025600, + "a": 10, + "d": 7, + "c": 3 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 7, + "d": 7, + "c": 1 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 24, + "d": 0, + "c": 1 + }, + { + "w": 1520726400, + "a": 7, + "d": 22, + "c": 1 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "pylaligand", + "id": 1115379, + "node_id": "MDQ6VXNlcjExMTUzNzk=", + "avatar_url": "https://avatars2.githubusercontent.com/u/1115379?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/pylaligand", + "html_url": "https://github.com/pylaligand", + "followers_url": "https://api.github.com/users/pylaligand/followers", + "following_url": "https://api.github.com/users/pylaligand/following{/other_user}", + "gists_url": "https://api.github.com/users/pylaligand/gists{/gist_id}", + "starred_url": "https://api.github.com/users/pylaligand/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/pylaligand/subscriptions", + "organizations_url": "https://api.github.com/users/pylaligand/orgs", + "repos_url": "https://api.github.com/users/pylaligand/repos", + "events_url": "https://api.github.com/users/pylaligand/events{/privacy}", + "received_events_url": "https://api.github.com/users/pylaligand/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 34, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 93, + "d": 8, + "c": 1 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 39, + "d": 9, + "c": 1 + }, + { + "w": 1512259200, + "a": 7, + "d": 2, + "c": 3 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 11, + "d": 11, + "c": 3 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1517702400, + "a": 17, + "d": 11, + "c": 1 + }, + { + "w": 1518307200, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1520121600, + "a": 79, + "d": 38, + "c": 5 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 10, + "d": 10, + "c": 2 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 13, + "d": 0, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 3, + "d": 3, + "c": 3 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 3, + "d": 3, + "c": 2 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 4, + "d": 86, + "c": 3 + }, + { + "w": 1537660800, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 22, + "d": 6, + "c": 1 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "alexmarkov", + "id": 29557594, + "node_id": "MDQ6VXNlcjI5NTU3NTk0", + "avatar_url": "https://avatars3.githubusercontent.com/u/29557594?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/alexmarkov", + "html_url": "https://github.com/alexmarkov", + "followers_url": "https://api.github.com/users/alexmarkov/followers", + "following_url": "https://api.github.com/users/alexmarkov/following{/other_user}", + "gists_url": "https://api.github.com/users/alexmarkov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/alexmarkov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/alexmarkov/subscriptions", + "organizations_url": "https://api.github.com/users/alexmarkov/orgs", + "repos_url": "https://api.github.com/users/alexmarkov/repos", + "events_url": "https://api.github.com/users/alexmarkov/events{/privacy}", + "received_events_url": "https://api.github.com/users/alexmarkov/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 40, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 16, + "d": 0, + "c": 1 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1536451200, + "a": 42, + "d": 2, + "c": 2 + }, + { + "w": 1537056000, + "a": 3, + "d": 3, + "c": 2 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 37, + "d": 7, + "c": 3 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 83, + "d": 87, + "c": 2 + }, + { + "w": 1540080000, + "a": 26, + "d": 14, + "c": 2 + }, + { + "w": 1540684800, + "a": 57, + "d": 37, + "c": 2 + }, + { + "w": 1541289600, + "a": 1741, + "d": 283, + "c": 4 + }, + { + "w": 1541894400, + "a": 28, + "d": 0, + "c": 1 + }, + { + "w": 1542499200, + "a": 41, + "d": 7, + "c": 1 + }, + { + "w": 1543104000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1543708800, + "a": 2, + "d": 3, + "c": 1 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 287, + "d": 36, + "c": 4 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 51, + "d": 5, + "c": 1 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 51, + "d": 1, + "c": 1 + }, + { + "w": 1547942400, + "a": 139, + "d": 140, + "c": 4 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 787, + "d": 46, + "c": 1 + }, + { + "w": 1549756800, + "a": 103, + "d": 8, + "c": 2 + }, + { + "w": 1550361600, + "a": 682, + "d": 9, + "c": 1 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 56, + "d": 1, + "c": 1 + }, + { + "w": 1552176000, + "a": 209, + "d": 12, + "c": 2 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "GaryQian", + "id": 1887398, + "node_id": "MDQ6VXNlcjE4ODczOTg=", + "avatar_url": "https://avatars2.githubusercontent.com/u/1887398?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/GaryQian", + "html_url": "https://github.com/GaryQian", + "followers_url": "https://api.github.com/users/GaryQian/followers", + "following_url": "https://api.github.com/users/GaryQian/following{/other_user}", + "gists_url": "https://api.github.com/users/GaryQian/gists{/gist_id}", + "starred_url": "https://api.github.com/users/GaryQian/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/GaryQian/subscriptions", + "organizations_url": "https://api.github.com/users/GaryQian/orgs", + "repos_url": "https://api.github.com/users/GaryQian/repos", + "events_url": "https://api.github.com/users/GaryQian/events{/privacy}", + "received_events_url": "https://api.github.com/users/GaryQian/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 44, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 334, + "d": 36, + "c": 2 + }, + { + "w": 1469318400, + "a": 206, + "d": 86, + "c": 1 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 1869, + "d": 7, + "c": 4 + }, + { + "w": 1471132800, + "a": 785, + "d": 32, + "c": 5 + }, + { + "w": 1471737600, + "a": 426, + "d": 9, + "c": 2 + }, + { + "w": 1472342400, + "a": 663, + "d": 168, + "c": 7 + }, + { + "w": 1472947200, + "a": 23, + "d": 139, + "c": 3 + }, + { + "w": 1473552000, + "a": 1593, + "d": 528, + "c": 11 + }, + { + "w": 1474156800, + "a": 993, + "d": 71, + "c": 7 + }, + { + "w": 1474761600, + "a": 65, + "d": 0, + "c": 2 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "dragostis", + "id": 4136413, + "node_id": "MDQ6VXNlcjQxMzY0MTM=", + "avatar_url": "https://avatars3.githubusercontent.com/u/4136413?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/dragostis", + "html_url": "https://github.com/dragostis", + "followers_url": "https://api.github.com/users/dragostis/followers", + "following_url": "https://api.github.com/users/dragostis/following{/other_user}", + "gists_url": "https://api.github.com/users/dragostis/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dragostis/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dragostis/subscriptions", + "organizations_url": "https://api.github.com/users/dragostis/orgs", + "repos_url": "https://api.github.com/users/dragostis/repos", + "events_url": "https://api.github.com/users/dragostis/events{/privacy}", + "received_events_url": "https://api.github.com/users/dragostis/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 45, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 233, + "d": 94, + "c": 1 + }, + { + "w": 1486252800, + "a": 393, + "d": 275, + "c": 1 + }, + { + "w": 1486857600, + "a": 15, + "d": 35, + "c": 5 + }, + { + "w": 1487462400, + "a": 714, + "d": 208, + "c": 11 + }, + { + "w": 1488067200, + "a": 35, + "d": 12, + "c": 3 + }, + { + "w": 1488672000, + "a": 106, + "d": 11, + "c": 2 + }, + { + "w": 1489276800, + "a": 116, + "d": 24, + "c": 4 + }, + { + "w": 1489881600, + "a": 439, + "d": 722, + "c": 5 + }, + { + "w": 1490486400, + "a": 471, + "d": 90, + "c": 4 + }, + { + "w": 1491091200, + "a": 101, + "d": 3, + "c": 1 + }, + { + "w": 1491696000, + "a": 232, + "d": 77, + "c": 1 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 132, + "d": 67, + "c": 2 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 159, + "d": 34, + "c": 1 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 6, + "d": 1, + "c": 1 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 161, + "d": 85, + "c": 2 + }, + { + "w": 1512259200, + "a": 40, + "d": 9, + "c": 1 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "jakobr-google", + "id": 20046617, + "node_id": "MDQ6VXNlcjIwMDQ2NjE3", + "avatar_url": "https://avatars3.githubusercontent.com/u/20046617?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jakobr-google", + "html_url": "https://github.com/jakobr-google", + "followers_url": "https://api.github.com/users/jakobr-google/followers", + "following_url": "https://api.github.com/users/jakobr-google/following{/other_user}", + "gists_url": "https://api.github.com/users/jakobr-google/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jakobr-google/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jakobr-google/subscriptions", + "organizations_url": "https://api.github.com/users/jakobr-google/orgs", + "repos_url": "https://api.github.com/users/jakobr-google/repos", + "events_url": "https://api.github.com/users/jakobr-google/events{/privacy}", + "received_events_url": "https://api.github.com/users/jakobr-google/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 46, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 513, + "d": 88, + "c": 3 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 276, + "d": 98, + "c": 2 + }, + { + "w": 1492300800, + "a": 119, + "d": 33, + "c": 2 + }, + { + "w": 1492905600, + "a": 653, + "d": 347, + "c": 1 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 141, + "d": 41, + "c": 2 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1503187200, + "a": 4, + "d": 1, + "c": 1 + }, + { + "w": 1503792000, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 139, + "d": 42, + "c": 1 + }, + { + "w": 1505606400, + "a": 9, + "d": 3, + "c": 2 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 2, + "c": 1 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1510444800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1512259200, + "a": 12, + "d": 0, + "c": 1 + }, + { + "w": 1512864000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1513468800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1522540800, + "a": 25, + "d": 10, + "c": 1 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 6, + "d": 2, + "c": 1 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 7, + "d": 7, + "c": 2 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 12, + "d": 42, + "c": 1 + }, + { + "w": 1531612800, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 51, + "d": 2, + "c": 1 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 61, + "d": 72, + "c": 1 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 51, + "d": 4, + "c": 1 + }, + { + "w": 1549152000, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 5, + "d": 4, + "c": 2 + }, + { + "w": 1550966400, + "a": 7, + "d": 5, + "c": 1 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1553990400, + "a": 19, + "d": 26, + "c": 2 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "zanderso", + "id": 6343103, + "node_id": "MDQ6VXNlcjYzNDMxMDM=", + "avatar_url": "https://avatars0.githubusercontent.com/u/6343103?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/zanderso", + "html_url": "https://github.com/zanderso", + "followers_url": "https://api.github.com/users/zanderso/followers", + "following_url": "https://api.github.com/users/zanderso/following{/other_user}", + "gists_url": "https://api.github.com/users/zanderso/gists{/gist_id}", + "starred_url": "https://api.github.com/users/zanderso/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/zanderso/subscriptions", + "organizations_url": "https://api.github.com/users/zanderso/orgs", + "repos_url": "https://api.github.com/users/zanderso/repos", + "events_url": "https://api.github.com/users/zanderso/events{/privacy}", + "received_events_url": "https://api.github.com/users/zanderso/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 46, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 148, + "d": 33, + "c": 2 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 24, + "d": 4, + "c": 2 + }, + { + "w": 1528588800, + "a": 551, + "d": 27, + "c": 1 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 53, + "d": 28, + "c": 1 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 265, + "d": 110, + "c": 4 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 20, + "d": 6, + "c": 1 + }, + { + "w": 1537660800, + "a": 177, + "d": 75, + "c": 1 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 16, + "d": 32, + "c": 2 + }, + { + "w": 1539475200, + "a": 82, + "d": 61, + "c": 2 + }, + { + "w": 1540080000, + "a": 31, + "d": 12, + "c": 1 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 105, + "d": 65, + "c": 2 + }, + { + "w": 1541894400, + "a": 0, + "d": 1, + "c": 1 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 83, + "d": 1, + "c": 1 + }, + { + "w": 1544918400, + "a": 914, + "d": 241, + "c": 12 + }, + { + "w": 1545523200, + "a": 19, + "d": 6, + "c": 1 + }, + { + "w": 1546128000, + "a": 15, + "d": 13, + "c": 2 + }, + { + "w": 1546732800, + "a": 36, + "d": 18, + "c": 4 + }, + { + "w": 1547337600, + "a": 30, + "d": 6, + "c": 1 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 32, + "d": 13, + "c": 1 + }, + { + "w": 1549756800, + "a": 478, + "d": 478, + "c": 2 + }, + { + "w": 1550361600, + "a": 86, + "d": 74, + "c": 2 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "sbaranov", + "id": 1067150, + "node_id": "MDQ6VXNlcjEwNjcxNTA=", + "avatar_url": "https://avatars3.githubusercontent.com/u/1067150?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/sbaranov", + "html_url": "https://github.com/sbaranov", + "followers_url": "https://api.github.com/users/sbaranov/followers", + "following_url": "https://api.github.com/users/sbaranov/following{/other_user}", + "gists_url": "https://api.github.com/users/sbaranov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/sbaranov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/sbaranov/subscriptions", + "organizations_url": "https://api.github.com/users/sbaranov/orgs", + "repos_url": "https://api.github.com/users/sbaranov/repos", + "events_url": "https://api.github.com/users/sbaranov/events{/privacy}", + "received_events_url": "https://api.github.com/users/sbaranov/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 49, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 18, + "d": 1, + "c": 1 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 4, + "d": 2, + "c": 1 + }, + { + "w": 1494115200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 35, + "d": 36, + "c": 1 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 6, + "d": 1, + "c": 2 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 5, + "d": 2, + "c": 1 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 43, + "d": 16, + "c": 1 + }, + { + "w": 1515283200, + "a": 138, + "d": 138, + "c": 2 + }, + { + "w": 1515888000, + "a": 154, + "d": 53, + "c": 3 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 269, + "d": 37, + "c": 3 + }, + { + "w": 1517702400, + "a": 76, + "d": 20, + "c": 3 + }, + { + "w": 1518307200, + "a": 306, + "d": 87, + "c": 1 + }, + { + "w": 1518912000, + "a": 66, + "d": 17, + "c": 1 + }, + { + "w": 1519516800, + "a": 594, + "d": 144, + "c": 4 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 31, + "d": 25, + "c": 1 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 69, + "d": 4, + "c": 1 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1526774400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 19, + "d": 11, + "c": 2 + }, + { + "w": 1534636800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1535241600, + "a": 988, + "d": 988, + "c": 2 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 6, + "d": 25, + "c": 2 + }, + { + "w": 1539475200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 7, + "d": 7, + "c": 1 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 9, + "d": 1, + "c": 1 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 13, + "d": 10, + "c": 2 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "jcollins-g", + "id": 14116827, + "node_id": "MDQ6VXNlcjE0MTE2ODI3", + "avatar_url": "https://avatars3.githubusercontent.com/u/14116827?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jcollins-g", + "html_url": "https://github.com/jcollins-g", + "followers_url": "https://api.github.com/users/jcollins-g/followers", + "following_url": "https://api.github.com/users/jcollins-g/following{/other_user}", + "gists_url": "https://api.github.com/users/jcollins-g/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jcollins-g/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jcollins-g/subscriptions", + "organizations_url": "https://api.github.com/users/jcollins-g/orgs", + "repos_url": "https://api.github.com/users/jcollins-g/repos", + "events_url": "https://api.github.com/users/jcollins-g/events{/privacy}", + "received_events_url": "https://api.github.com/users/jcollins-g/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 50, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1468713600, + "a": 12, + "d": 6, + "c": 2 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 3, + "c": 1 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 27, + "c": 1 + }, + { + "w": 1477180800, + "a": 43, + "d": 34, + "c": 1 + }, + { + "w": 1477785600, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1479600000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 28, + "d": 28, + "c": 1 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 25, + "d": 15, + "c": 4 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 10, + "d": 2, + "c": 1 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1500163200, + "a": 7, + "d": 7, + "c": 4 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 157, + "d": 155, + "c": 4 + }, + { + "w": 1507420800, + "a": 145, + "d": 4, + "c": 2 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 55, + "d": 44, + "c": 1 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1526774400, + "a": 11, + "d": 2, + "c": 1 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 29, + "d": 4, + "c": 6 + }, + { + "w": 1532217600, + "a": 0, + "d": 9, + "c": 1 + }, + { + "w": 1532822400, + "a": 170, + "d": 5, + "c": 2 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 14, + "d": 6, + "c": 1 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 17, + "d": 71, + "c": 1 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 9, + "d": 0, + "c": 1 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "rmacnak-google", + "id": 8495071, + "node_id": "MDQ6VXNlcjg0OTUwNzE=", + "avatar_url": "https://avatars3.githubusercontent.com/u/8495071?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/rmacnak-google", + "html_url": "https://github.com/rmacnak-google", + "followers_url": "https://api.github.com/users/rmacnak-google/followers", + "following_url": "https://api.github.com/users/rmacnak-google/following{/other_user}", + "gists_url": "https://api.github.com/users/rmacnak-google/gists{/gist_id}", + "starred_url": "https://api.github.com/users/rmacnak-google/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/rmacnak-google/subscriptions", + "organizations_url": "https://api.github.com/users/rmacnak-google/orgs", + "repos_url": "https://api.github.com/users/rmacnak-google/repos", + "events_url": "https://api.github.com/users/rmacnak-google/events{/privacy}", + "received_events_url": "https://api.github.com/users/rmacnak-google/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 52, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 19, + "d": 18, + "c": 1 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 95, + "d": 8, + "c": 1 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 67, + "d": 29, + "c": 1 + }, + { + "w": 1497744000, + "a": 532, + "d": 165, + "c": 4 + }, + { + "w": 1498348800, + "a": 1248, + "d": 80, + "c": 3 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 4609, + "d": 1490, + "c": 1 + }, + { + "w": 1501372800, + "a": 3972, + "d": 4016, + "c": 3 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 50, + "d": 49, + "c": 2 + }, + { + "w": 1503187200, + "a": 1005, + "d": 37, + "c": 2 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 1171, + "d": 794, + "c": 2 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 999, + "d": 43, + "c": 1 + }, + { + "w": 1512259200, + "a": 27, + "d": 63, + "c": 1 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 43, + "d": 4, + "c": 1 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 98, + "d": 1, + "c": 1 + }, + { + "w": 1518307200, + "a": 83, + "d": 14, + "c": 1 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 24, + "d": 6, + "c": 1 + }, + { + "w": 1520121600, + "a": 248, + "d": 41, + "c": 1 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 33, + "d": 1, + "c": 1 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 110, + "d": 118, + "c": 1 + }, + { + "w": 1523750400, + "a": 1868, + "d": 675, + "c": 3 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 56, + "d": 3, + "c": 1 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1535846400, + "a": 2755, + "d": 85, + "c": 2 + }, + { + "w": 1536451200, + "a": 33, + "d": 61, + "c": 5 + }, + { + "w": 1537056000, + "a": 249, + "d": 11, + "c": 1 + }, + { + "w": 1537660800, + "a": 5, + "d": 5, + "c": 2 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 92, + "d": 36, + "c": 4 + }, + { + "w": 1540080000, + "a": 189, + "d": 54, + "c": 2 + }, + { + "w": 1540684800, + "a": 853, + "d": 15, + "c": 1 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 13, + "d": 2, + "c": 1 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "jacob314", + "id": 1226812, + "node_id": "MDQ6VXNlcjEyMjY4MTI=", + "avatar_url": "https://avatars1.githubusercontent.com/u/1226812?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jacob314", + "html_url": "https://github.com/jacob314", + "followers_url": "https://api.github.com/users/jacob314/followers", + "following_url": "https://api.github.com/users/jacob314/following{/other_user}", + "gists_url": "https://api.github.com/users/jacob314/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jacob314/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jacob314/subscriptions", + "organizations_url": "https://api.github.com/users/jacob314/orgs", + "repos_url": "https://api.github.com/users/jacob314/repos", + "events_url": "https://api.github.com/users/jacob314/events{/privacy}", + "received_events_url": "https://api.github.com/users/jacob314/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 52, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 104, + "d": 9, + "c": 3 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1517097600, + "a": 12, + "d": 8, + "c": 3 + }, + { + "w": 1517702400, + "a": 36, + "d": 140, + "c": 6 + }, + { + "w": 1518307200, + "a": 13, + "d": 41, + "c": 4 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 4, + "d": 4, + "c": 2 + }, + { + "w": 1520121600, + "a": 6, + "d": 6, + "c": 1 + }, + { + "w": 1520726400, + "a": 17, + "d": 15, + "c": 4 + }, + { + "w": 1521331200, + "a": 5, + "d": 1, + "c": 1 + }, + { + "w": 1521936000, + "a": 8, + "d": 6, + "c": 1 + }, + { + "w": 1522540800, + "a": 3, + "d": 3, + "c": 3 + }, + { + "w": 1523145600, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1523750400, + "a": 8, + "d": 8, + "c": 2 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 7, + "d": 2, + "c": 1 + }, + { + "w": 1533427200, + "a": 3, + "d": 3, + "c": 3 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 6, + "d": 6, + "c": 4 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 19, + "d": 21, + "c": 1 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 3, + "d": 1, + "c": 1 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 18, + "d": 2, + "c": 1 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "a-siva", + "id": 8633293, + "node_id": "MDQ6VXNlcjg2MzMyOTM=", + "avatar_url": "https://avatars2.githubusercontent.com/u/8633293?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/a-siva", + "html_url": "https://github.com/a-siva", + "followers_url": "https://api.github.com/users/a-siva/followers", + "following_url": "https://api.github.com/users/a-siva/following{/other_user}", + "gists_url": "https://api.github.com/users/a-siva/gists{/gist_id}", + "starred_url": "https://api.github.com/users/a-siva/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/a-siva/subscriptions", + "organizations_url": "https://api.github.com/users/a-siva/orgs", + "repos_url": "https://api.github.com/users/a-siva/repos", + "events_url": "https://api.github.com/users/a-siva/events{/privacy}", + "received_events_url": "https://api.github.com/users/a-siva/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 56, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 7, + "d": 2, + "c": 2 + }, + { + "w": 1448150400, + "a": 2, + "d": 1, + "c": 2 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 4, + "d": 5, + "c": 1 + }, + { + "w": 1449964800, + "a": 145, + "d": 3, + "c": 3 + }, + { + "w": 1450569600, + "a": 5, + "d": 1, + "c": 2 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 5, + "d": 0, + "c": 1 + }, + { + "w": 1452384000, + "a": 25, + "d": 10, + "c": 3 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 8, + "d": 3, + "c": 3 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 22, + "d": 7, + "c": 2 + }, + { + "w": 1460246400, + "a": 5, + "d": 6, + "c": 2 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 24, + "d": 0, + "c": 1 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 16, + "d": 15, + "c": 2 + }, + { + "w": 1463270400, + "a": 76, + "d": 22, + "c": 2 + }, + { + "w": 1463875200, + "a": 11, + "d": 0, + "c": 1 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 35, + "d": 2, + "c": 1 + }, + { + "w": 1468713600, + "a": 20, + "d": 2, + "c": 2 + }, + { + "w": 1469318400, + "a": 9, + "d": 1, + "c": 1 + }, + { + "w": 1469923200, + "a": 10, + "d": 19, + "c": 1 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 435, + "d": 440, + "c": 4 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1472947200, + "a": 18, + "d": 18, + "c": 1 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 10, + "d": 0, + "c": 3 + }, + { + "w": 1474761600, + "a": 0, + "d": 24, + "c": 1 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 11, + "d": 6, + "c": 1 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 3, + "d": 1, + "c": 1 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 11, + "d": 0, + "c": 1 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 13, + "d": 6, + "c": 1 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 20, + "d": 6, + "c": 2 + }, + { + "w": 1497744000, + "a": 15, + "d": 4, + "c": 2 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 45, + "d": 19, + "c": 1 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 4, + "d": 1, + "c": 1 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 21, + "d": 4, + "c": 1 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "sethladd", + "id": 5479, + "node_id": "MDQ6VXNlcjU0Nzk=", + "avatar_url": "https://avatars3.githubusercontent.com/u/5479?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/sethladd", + "html_url": "https://github.com/sethladd", + "followers_url": "https://api.github.com/users/sethladd/followers", + "following_url": "https://api.github.com/users/sethladd/following{/other_user}", + "gists_url": "https://api.github.com/users/sethladd/gists{/gist_id}", + "starred_url": "https://api.github.com/users/sethladd/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/sethladd/subscriptions", + "organizations_url": "https://api.github.com/users/sethladd/orgs", + "repos_url": "https://api.github.com/users/sethladd/repos", + "events_url": "https://api.github.com/users/sethladd/events{/privacy}", + "received_events_url": "https://api.github.com/users/sethladd/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 57, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 6, + "d": 0, + "c": 1 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 50, + "d": 0, + "c": 2 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 115, + "d": 100, + "c": 2 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 418, + "d": 169, + "c": 2 + }, + { + "w": 1487462400, + "a": 317, + "d": 68, + "c": 1 + }, + { + "w": 1488067200, + "a": 58, + "d": 42, + "c": 5 + }, + { + "w": 1488672000, + "a": 7, + "d": 7, + "c": 1 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 8, + "d": 8, + "c": 2 + }, + { + "w": 1490486400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1491091200, + "a": 39, + "d": 72, + "c": 2 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 63, + "d": 25, + "c": 3 + }, + { + "w": 1492905600, + "a": 74, + "d": 1337, + "c": 5 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 41, + "d": 42, + "c": 5 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 138, + "d": 9, + "c": 4 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 2, + "d": 3, + "c": 1 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 5, + "d": 13, + "c": 2 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 27, + "d": 4, + "c": 1 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 4, + "d": 6, + "c": 2 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 5, + "d": 2, + "c": 1 + }, + { + "w": 1518912000, + "a": 6, + "d": 3, + "c": 2 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 4, + "d": 4, + "c": 2 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 9, + "d": 2, + "c": 1 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 25, + "d": 39, + "c": 1 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 6, + "d": 5, + "c": 1 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 12, + "d": 14, + "c": 1 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 120, + "d": 1, + "c": 1 + } + ], + "author": { + "login": "mit-mit", + "id": 13644170, + "node_id": "MDQ6VXNlcjEzNjQ0MTcw", + "avatar_url": "https://avatars3.githubusercontent.com/u/13644170?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mit-mit", + "html_url": "https://github.com/mit-mit", + "followers_url": "https://api.github.com/users/mit-mit/followers", + "following_url": "https://api.github.com/users/mit-mit/following{/other_user}", + "gists_url": "https://api.github.com/users/mit-mit/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mit-mit/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mit-mit/subscriptions", + "organizations_url": "https://api.github.com/users/mit-mit/orgs", + "repos_url": "https://api.github.com/users/mit-mit/repos", + "events_url": "https://api.github.com/users/mit-mit/events{/privacy}", + "received_events_url": "https://api.github.com/users/mit-mit/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 57, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 23, + "d": 19, + "c": 1 + }, + { + "w": 1531612800, + "a": 53, + "d": 6, + "c": 1 + }, + { + "w": 1532217600, + "a": 274, + "d": 14, + "c": 1 + }, + { + "w": 1532822400, + "a": 178, + "d": 70, + "c": 1 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 504, + "d": 0, + "c": 1 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 462, + "d": 23, + "c": 2 + }, + { + "w": 1536451200, + "a": 387, + "d": 21, + "c": 1 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 194, + "d": 13, + "c": 2 + }, + { + "w": 1538265600, + "a": 191, + "d": 44, + "c": 5 + }, + { + "w": 1538870400, + "a": 269, + "d": 13, + "c": 3 + }, + { + "w": 1539475200, + "a": 142, + "d": 9, + "c": 2 + }, + { + "w": 1540080000, + "a": 196, + "d": 0, + "c": 2 + }, + { + "w": 1540684800, + "a": 444, + "d": 13, + "c": 6 + }, + { + "w": 1541289600, + "a": 478, + "d": 110, + "c": 6 + }, + { + "w": 1541894400, + "a": 344, + "d": 23, + "c": 4 + }, + { + "w": 1542499200, + "a": 44, + "d": 1, + "c": 1 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 1226, + "d": 38, + "c": 3 + }, + { + "w": 1544918400, + "a": 1027, + "d": 2, + "c": 1 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 1800, + "d": 1034, + "c": 3 + }, + { + "w": 1547337600, + "a": 283, + "d": 97, + "c": 3 + }, + { + "w": 1547942400, + "a": 185, + "d": 107, + "c": 1 + }, + { + "w": 1548547200, + "a": 1029, + "d": 363, + "c": 2 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 209, + "d": 37, + "c": 2 + }, + { + "w": 1550361600, + "a": 412, + "d": 34, + "c": 2 + }, + { + "w": 1550966400, + "a": 532, + "d": 111, + "c": 1 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "jslavitz", + "id": 7806031, + "node_id": "MDQ6VXNlcjc4MDYwMzE=", + "avatar_url": "https://avatars1.githubusercontent.com/u/7806031?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jslavitz", + "html_url": "https://github.com/jslavitz", + "followers_url": "https://api.github.com/users/jslavitz/followers", + "following_url": "https://api.github.com/users/jslavitz/following{/other_user}", + "gists_url": "https://api.github.com/users/jslavitz/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jslavitz/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jslavitz/subscriptions", + "organizations_url": "https://api.github.com/users/jslavitz/orgs", + "repos_url": "https://api.github.com/users/jslavitz/repos", + "events_url": "https://api.github.com/users/jslavitz/events{/privacy}", + "received_events_url": "https://api.github.com/users/jslavitz/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 61, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 33, + "d": 0, + "c": 1 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 83, + "d": 3, + "c": 1 + }, + { + "w": 1443916800, + "a": 301, + "d": 45, + "c": 2 + }, + { + "w": 1444521600, + "a": 625, + "d": 355, + "c": 6 + }, + { + "w": 1445126400, + "a": 1020, + "d": 310, + "c": 7 + }, + { + "w": 1445731200, + "a": 5121, + "d": 50, + "c": 4 + }, + { + "w": 1446336000, + "a": 150, + "d": 123, + "c": 3 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 46, + "d": 17, + "c": 7 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1449964800, + "a": 5, + "d": 5, + "c": 3 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 24, + "d": 23, + "c": 3 + }, + { + "w": 1453593600, + "a": 415, + "d": 72, + "c": 1 + }, + { + "w": 1454198400, + "a": 156, + "d": 85, + "c": 6 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 324, + "d": 16, + "c": 3 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 275, + "d": 169, + "c": 2 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 368, + "d": 162, + "c": 3 + }, + { + "w": 1459641600, + "a": 198, + "d": 15, + "c": 3 + }, + { + "w": 1460246400, + "a": 359, + "d": 0, + "c": 1 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 113, + "d": 61, + "c": 2 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 72, + "d": 3, + "c": 1 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "krisgiesing", + "id": 13855787, + "node_id": "MDQ6VXNlcjEzODU1Nzg3", + "avatar_url": "https://avatars2.githubusercontent.com/u/13855787?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/krisgiesing", + "html_url": "https://github.com/krisgiesing", + "followers_url": "https://api.github.com/users/krisgiesing/followers", + "following_url": "https://api.github.com/users/krisgiesing/following{/other_user}", + "gists_url": "https://api.github.com/users/krisgiesing/gists{/gist_id}", + "starred_url": "https://api.github.com/users/krisgiesing/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/krisgiesing/subscriptions", + "organizations_url": "https://api.github.com/users/krisgiesing/orgs", + "repos_url": "https://api.github.com/users/krisgiesing/repos", + "events_url": "https://api.github.com/users/krisgiesing/events{/privacy}", + "received_events_url": "https://api.github.com/users/krisgiesing/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 72, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 216, + "c": 1 + }, + { + "w": 1434844800, + "a": 54, + "d": 65534, + "c": 2 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 41220, + "d": 41202, + "c": 5 + }, + { + "w": 1439078400, + "a": 303, + "d": 231, + "c": 7 + }, + { + "w": 1439683200, + "a": 498, + "d": 39, + "c": 2 + }, + { + "w": 1440288000, + "a": 520, + "d": 332, + "c": 10 + }, + { + "w": 1440892800, + "a": 375, + "d": 141, + "c": 14 + }, + { + "w": 1441497600, + "a": 187, + "d": 23, + "c": 6 + }, + { + "w": 1442102400, + "a": 533, + "d": 71, + "c": 7 + }, + { + "w": 1442707200, + "a": 554, + "d": 367, + "c": 4 + }, + { + "w": 1443312000, + "a": 853, + "d": 128, + "c": 8 + }, + { + "w": 1443916800, + "a": 690, + "d": 95, + "c": 6 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "iansf", + "id": 12631059, + "node_id": "MDQ6VXNlcjEyNjMxMDU5", + "avatar_url": "https://avatars0.githubusercontent.com/u/12631059?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/iansf", + "html_url": "https://github.com/iansf", + "followers_url": "https://api.github.com/users/iansf/followers", + "following_url": "https://api.github.com/users/iansf/following{/other_user}", + "gists_url": "https://api.github.com/users/iansf/gists{/gist_id}", + "starred_url": "https://api.github.com/users/iansf/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/iansf/subscriptions", + "organizations_url": "https://api.github.com/users/iansf/orgs", + "repos_url": "https://api.github.com/users/iansf/repos", + "events_url": "https://api.github.com/users/iansf/events{/privacy}", + "received_events_url": "https://api.github.com/users/iansf/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 80, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 1158, + "d": 0, + "c": 1 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 2409, + "d": 192, + "c": 3 + }, + { + "w": 1489276800, + "a": 45, + "d": 28, + "c": 2 + }, + { + "w": 1489881600, + "a": 306, + "d": 1478, + "c": 9 + }, + { + "w": 1490486400, + "a": 2369, + "d": 1405, + "c": 6 + }, + { + "w": 1491091200, + "a": 157, + "d": 66, + "c": 1 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 131, + "d": 41, + "c": 2 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 56, + "d": 56, + "c": 4 + }, + { + "w": 1494115200, + "a": 230, + "d": 236, + "c": 6 + }, + { + "w": 1494720000, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 1770, + "d": 0, + "c": 1 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 67, + "d": 45, + "c": 2 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 1366, + "d": 1, + "c": 1 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1504396800, + "a": 576, + "d": 38, + "c": 1 + }, + { + "w": 1505001600, + "a": 289, + "d": 89, + "c": 3 + }, + { + "w": 1505606400, + "a": 494, + "d": 31, + "c": 2 + }, + { + "w": 1506211200, + "a": 1048, + "d": 307, + "c": 4 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 51, + "d": 29, + "c": 3 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 8, + "d": 0, + "c": 1 + }, + { + "w": 1512259200, + "a": 55, + "d": 7, + "c": 5 + }, + { + "w": 1512864000, + "a": 179, + "d": 37, + "c": 5 + }, + { + "w": 1513468800, + "a": 76, + "d": 77, + "c": 4 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 5, + "d": 0, + "c": 1 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 9, + "d": 9, + "c": 1 + }, + { + "w": 1523145600, + "a": 354, + "d": 618, + "c": 1 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 59, + "d": 0, + "c": 1 + }, + { + "w": 1526774400, + "a": 15, + "d": 18, + "c": 1 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 258, + "d": 52, + "c": 1 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 22, + "d": 16, + "c": 1 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 287, + "d": 30, + "c": 1 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "szakarias", + "id": 3865266, + "node_id": "MDQ6VXNlcjM4NjUyNjY=", + "avatar_url": "https://avatars0.githubusercontent.com/u/3865266?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/szakarias", + "html_url": "https://github.com/szakarias", + "followers_url": "https://api.github.com/users/szakarias/followers", + "following_url": "https://api.github.com/users/szakarias/following{/other_user}", + "gists_url": "https://api.github.com/users/szakarias/gists{/gist_id}", + "starred_url": "https://api.github.com/users/szakarias/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/szakarias/subscriptions", + "organizations_url": "https://api.github.com/users/szakarias/orgs", + "repos_url": "https://api.github.com/users/szakarias/repos", + "events_url": "https://api.github.com/users/szakarias/events{/privacy}", + "received_events_url": "https://api.github.com/users/szakarias/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 84, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 7, + "d": 7, + "c": 1 + }, + { + "w": 1471737600, + "a": 372, + "d": 31, + "c": 6 + }, + { + "w": 1472342400, + "a": 260, + "d": 75, + "c": 5 + }, + { + "w": 1472947200, + "a": 243, + "d": 66, + "c": 6 + }, + { + "w": 1473552000, + "a": 489, + "d": 492, + "c": 6 + }, + { + "w": 1474156800, + "a": 683, + "d": 514, + "c": 8 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 1098, + "d": 1066, + "c": 9 + }, + { + "w": 1475971200, + "a": 157, + "d": 70, + "c": 2 + }, + { + "w": 1476576000, + "a": 17, + "d": 6, + "c": 1 + }, + { + "w": 1477180800, + "a": 128, + "d": 44, + "c": 8 + }, + { + "w": 1477785600, + "a": 221, + "d": 151, + "c": 5 + }, + { + "w": 1478390400, + "a": 211, + "d": 83, + "c": 6 + }, + { + "w": 1478995200, + "a": 481, + "d": 723, + "c": 3 + }, + { + "w": 1479600000, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1480204800, + "a": 511, + "d": 412, + "c": 4 + }, + { + "w": 1480809600, + "a": 223, + "d": 163, + "c": 4 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 599, + "d": 387, + "c": 1 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 150, + "d": 88, + "c": 2 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 5, + "d": 3, + "c": 2 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 308, + "d": 39, + "c": 1 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 163, + "d": 164, + "c": 1 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 111, + "d": 62, + "c": 1 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 325, + "d": 325, + "c": 1 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "danrubel", + "id": 2891888, + "node_id": "MDQ6VXNlcjI4OTE4ODg=", + "avatar_url": "https://avatars0.githubusercontent.com/u/2891888?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/danrubel", + "html_url": "https://github.com/danrubel", + "followers_url": "https://api.github.com/users/danrubel/followers", + "following_url": "https://api.github.com/users/danrubel/following{/other_user}", + "gists_url": "https://api.github.com/users/danrubel/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danrubel/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danrubel/subscriptions", + "organizations_url": "https://api.github.com/users/danrubel/orgs", + "repos_url": "https://api.github.com/users/danrubel/repos", + "events_url": "https://api.github.com/users/danrubel/events{/privacy}", + "received_events_url": "https://api.github.com/users/danrubel/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 91, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 23, + "d": 0, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 76, + "d": 10, + "c": 1 + }, + { + "w": 1529193600, + "a": 52, + "d": 38, + "c": 3 + }, + { + "w": 1529798400, + "a": 150, + "d": 6, + "c": 5 + }, + { + "w": 1530403200, + "a": 31, + "d": 14, + "c": 2 + }, + { + "w": 1531008000, + "a": 9, + "d": 9, + "c": 1 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 25, + "d": 2, + "c": 3 + }, + { + "w": 1532822400, + "a": 259, + "d": 97, + "c": 2 + }, + { + "w": 1533427200, + "a": 349, + "d": 78, + "c": 6 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 192, + "d": 178, + "c": 5 + }, + { + "w": 1535241600, + "a": 157, + "d": 15, + "c": 3 + }, + { + "w": 1535846400, + "a": 4, + "d": 19, + "c": 1 + }, + { + "w": 1536451200, + "a": 26, + "d": 7, + "c": 3 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 77, + "d": 41, + "c": 7 + }, + { + "w": 1538265600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1538870400, + "a": 10, + "d": 11, + "c": 3 + }, + { + "w": 1539475200, + "a": 6, + "d": 8, + "c": 2 + }, + { + "w": 1540080000, + "a": 441, + "d": 74, + "c": 2 + }, + { + "w": 1540684800, + "a": 130, + "d": 0, + "c": 1 + }, + { + "w": 1541289600, + "a": 10, + "d": 2, + "c": 3 + }, + { + "w": 1541894400, + "a": 38, + "d": 1, + "c": 1 + }, + { + "w": 1542499200, + "a": 15, + "d": 3, + "c": 1 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1544918400, + "a": 1421, + "d": 121, + "c": 3 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 12, + "d": 1, + "c": 2 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 1, + "d": 2, + "c": 1 + }, + { + "w": 1549152000, + "a": 30, + "d": 0, + "c": 1 + }, + { + "w": 1549756800, + "a": 39, + "d": 39, + "c": 1 + }, + { + "w": 1550361600, + "a": 783, + "d": 783, + "c": 2 + }, + { + "w": 1550966400, + "a": 952, + "d": 87, + "c": 8 + }, + { + "w": 1551571200, + "a": 23, + "d": 54, + "c": 2 + }, + { + "w": 1552176000, + "a": 82, + "d": 31, + "c": 5 + }, + { + "w": 1552780800, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1553385600, + "a": 16, + "d": 12, + "c": 3 + }, + { + "w": 1553990400, + "a": 1, + "d": 14, + "c": 1 + }, + { + "w": 1554595200, + "a": 93, + "d": 2, + "c": 2 + } + ], + "author": { + "login": "liyuqian", + "id": 22987568, + "node_id": "MDQ6VXNlcjIyOTg3NTY4", + "avatar_url": "https://avatars2.githubusercontent.com/u/22987568?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/liyuqian", + "html_url": "https://github.com/liyuqian", + "followers_url": "https://api.github.com/users/liyuqian/followers", + "following_url": "https://api.github.com/users/liyuqian/following{/other_user}", + "gists_url": "https://api.github.com/users/liyuqian/gists{/gist_id}", + "starred_url": "https://api.github.com/users/liyuqian/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/liyuqian/subscriptions", + "organizations_url": "https://api.github.com/users/liyuqian/orgs", + "repos_url": "https://api.github.com/users/liyuqian/repos", + "events_url": "https://api.github.com/users/liyuqian/events{/privacy}", + "received_events_url": "https://api.github.com/users/liyuqian/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 107, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 7, + "d": 380, + "c": 4 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 56, + "d": 70, + "c": 14 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 13, + "d": 6, + "c": 2 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 142, + "d": 2, + "c": 2 + }, + { + "w": 1457222400, + "a": 704, + "d": 117, + "c": 3 + }, + { + "w": 1457827200, + "a": 37, + "d": 7, + "c": 1 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 1068, + "d": 671, + "c": 4 + }, + { + "w": 1469318400, + "a": 71, + "d": 10, + "c": 1 + }, + { + "w": 1469923200, + "a": 744, + "d": 358, + "c": 2 + }, + { + "w": 1470528000, + "a": 1046, + "d": 585, + "c": 14 + }, + { + "w": 1471132800, + "a": 851, + "d": 420, + "c": 3 + }, + { + "w": 1471737600, + "a": 39, + "d": 9, + "c": 2 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 129, + "d": 41, + "c": 3 + }, + { + "w": 1474156800, + "a": 28, + "d": 3, + "c": 1 + }, + { + "w": 1474761600, + "a": 115, + "d": 40, + "c": 2 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 4, + "d": 1, + "c": 2 + }, + { + "w": 1476576000, + "a": 36, + "d": 15, + "c": 5 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 134, + "d": 55, + "c": 3 + }, + { + "w": 1478390400, + "a": 192, + "d": 480, + "c": 8 + }, + { + "w": 1478995200, + "a": 105, + "d": 3, + "c": 2 + }, + { + "w": 1479600000, + "a": 121, + "d": 225, + "c": 7 + }, + { + "w": 1480204800, + "a": 211, + "d": 186, + "c": 3 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 355, + "d": 49, + "c": 2 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 118, + "d": 67, + "c": 1 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 30, + "d": 9, + "c": 1 + }, + { + "w": 1489276800, + "a": 187, + "d": 108, + "c": 5 + }, + { + "w": 1489881600, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1490486400, + "a": 24, + "d": 12, + "c": 2 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 174, + "d": 39, + "c": 7 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "johnmccutchan", + "id": 224266, + "node_id": "MDQ6VXNlcjIyNDI2Ng==", + "avatar_url": "https://avatars2.githubusercontent.com/u/224266?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/johnmccutchan", + "html_url": "https://github.com/johnmccutchan", + "followers_url": "https://api.github.com/users/johnmccutchan/followers", + "following_url": "https://api.github.com/users/johnmccutchan/following{/other_user}", + "gists_url": "https://api.github.com/users/johnmccutchan/gists{/gist_id}", + "starred_url": "https://api.github.com/users/johnmccutchan/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/johnmccutchan/subscriptions", + "organizations_url": "https://api.github.com/users/johnmccutchan/orgs", + "repos_url": "https://api.github.com/users/johnmccutchan/repos", + "events_url": "https://api.github.com/users/johnmccutchan/events{/privacy}", + "received_events_url": "https://api.github.com/users/johnmccutchan/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 129, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 136, + "d": 43, + "c": 5 + }, + { + "w": 1508025600, + "a": 132, + "d": 23, + "c": 4 + }, + { + "w": 1508630400, + "a": 234, + "d": 41, + "c": 3 + }, + { + "w": 1509235200, + "a": 2023, + "d": 2009, + "c": 2 + }, + { + "w": 1509840000, + "a": 525, + "d": 4, + "c": 4 + }, + { + "w": 1510444800, + "a": 181, + "d": 83, + "c": 6 + }, + { + "w": 1511049600, + "a": 319, + "d": 15, + "c": 2 + }, + { + "w": 1511654400, + "a": 119, + "d": 67, + "c": 4 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 234, + "d": 146, + "c": 4 + }, + { + "w": 1513468800, + "a": 4576, + "d": 3, + "c": 2 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 3083, + "d": 3083, + "c": 3 + }, + { + "w": 1515283200, + "a": 456, + "d": 87, + "c": 4 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 40, + "d": 7, + "c": 1 + }, + { + "w": 1517097600, + "a": 33442, + "d": 189, + "c": 10 + }, + { + "w": 1517702400, + "a": 322, + "d": 11, + "c": 4 + }, + { + "w": 1518307200, + "a": 460, + "d": 58, + "c": 2 + }, + { + "w": 1518912000, + "a": 940, + "d": 113, + "c": 5 + }, + { + "w": 1519516800, + "a": 117, + "d": 8, + "c": 2 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 2, + "d": 8, + "c": 1 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 408, + "d": 629, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1531612800, + "a": 802, + "d": 3, + "c": 3 + }, + { + "w": 1532217600, + "a": 2187, + "d": 45, + "c": 6 + }, + { + "w": 1532822400, + "a": 468, + "d": 172, + "c": 2 + }, + { + "w": 1533427200, + "a": 561, + "d": 406, + "c": 7 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 469, + "d": 65, + "c": 8 + }, + { + "w": 1535241600, + "a": 679, + "d": 46, + "c": 5 + }, + { + "w": 1535846400, + "a": 145, + "d": 1, + "c": 3 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 181, + "d": 64, + "c": 1 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 3695, + "d": 2912, + "c": 4 + }, + { + "w": 1541289600, + "a": 655, + "d": 58, + "c": 5 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 4, + "d": 11, + "c": 2 + }, + { + "w": 1545523200, + "a": 45, + "d": 8, + "c": 4 + }, + { + "w": 1546128000, + "a": 12, + "d": 0, + "c": 1 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 1, + "d": 11, + "c": 1 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 62, + "d": 2, + "c": 1 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 22, + "d": 283, + "c": 3 + }, + { + "w": 1553385600, + "a": 9, + "d": 1, + "c": 1 + }, + { + "w": 1553990400, + "a": 119, + "d": 9, + "c": 1 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "amirh", + "id": 1024117, + "node_id": "MDQ6VXNlcjEwMjQxMTc=", + "avatar_url": "https://avatars2.githubusercontent.com/u/1024117?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/amirh", + "html_url": "https://github.com/amirh", + "followers_url": "https://api.github.com/users/amirh/followers", + "following_url": "https://api.github.com/users/amirh/following{/other_user}", + "gists_url": "https://api.github.com/users/amirh/gists{/gist_id}", + "starred_url": "https://api.github.com/users/amirh/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/amirh/subscriptions", + "organizations_url": "https://api.github.com/users/amirh/orgs", + "repos_url": "https://api.github.com/users/amirh/repos", + "events_url": "https://api.github.com/users/amirh/events{/privacy}", + "received_events_url": "https://api.github.com/users/amirh/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 131, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 52, + "d": 40, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 117, + "d": 48, + "c": 1 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 6, + "d": 5, + "c": 2 + }, + { + "w": 1529193600, + "a": 194, + "d": 8, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 227, + "d": 19, + "c": 1 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 1484, + "d": 1576, + "c": 6 + }, + { + "w": 1538265600, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 80, + "d": 21, + "c": 3 + }, + { + "w": 1540080000, + "a": 637, + "d": 636, + "c": 6 + }, + { + "w": 1540684800, + "a": 3931, + "d": 2082, + "c": 5 + }, + { + "w": 1541289600, + "a": 326, + "d": 84, + "c": 10 + }, + { + "w": 1541894400, + "a": 656, + "d": 144, + "c": 5 + }, + { + "w": 1542499200, + "a": 12, + "d": 3, + "c": 2 + }, + { + "w": 1543104000, + "a": 85, + "d": 15, + "c": 1 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 9, + "d": 2, + "c": 2 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 570, + "d": 558, + "c": 8 + }, + { + "w": 1547337600, + "a": 252, + "d": 259, + "c": 11 + }, + { + "w": 1547942400, + "a": 707, + "d": 399, + "c": 10 + }, + { + "w": 1548547200, + "a": 36, + "d": 255, + "c": 2 + }, + { + "w": 1549152000, + "a": 62, + "d": 3, + "c": 1 + }, + { + "w": 1549756800, + "a": 417, + "d": 130, + "c": 5 + }, + { + "w": 1550361600, + "a": 19575, + "d": 408, + "c": 11 + }, + { + "w": 1550966400, + "a": 14, + "d": 5, + "c": 3 + }, + { + "w": 1551571200, + "a": 724, + "d": 169, + "c": 9 + }, + { + "w": 1552176000, + "a": 636, + "d": 388, + "c": 11 + }, + { + "w": 1552780800, + "a": 1269, + "d": 414, + "c": 7 + }, + { + "w": 1553385600, + "a": 7, + "d": 9, + "c": 2 + }, + { + "w": 1553990400, + "a": 640, + "d": 0, + "c": 2 + }, + { + "w": 1554595200, + "a": 903, + "d": 25, + "c": 1 + } + ], + "author": { + "login": "dnfield", + "id": 8620741, + "node_id": "MDQ6VXNlcjg2MjA3NDE=", + "avatar_url": "https://avatars3.githubusercontent.com/u/8620741?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/dnfield", + "html_url": "https://github.com/dnfield", + "followers_url": "https://api.github.com/users/dnfield/followers", + "following_url": "https://api.github.com/users/dnfield/following{/other_user}", + "gists_url": "https://api.github.com/users/dnfield/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dnfield/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dnfield/subscriptions", + "organizations_url": "https://api.github.com/users/dnfield/orgs", + "repos_url": "https://api.github.com/users/dnfield/repos", + "events_url": "https://api.github.com/users/dnfield/events{/privacy}", + "received_events_url": "https://api.github.com/users/dnfield/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 139, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 63, + "d": 17, + "c": 4 + }, + { + "w": 1456617600, + "a": 14, + "d": 0, + "c": 1 + }, + { + "w": 1457222400, + "a": 10, + "d": 19, + "c": 3 + }, + { + "w": 1457827200, + "a": 0, + "d": 2, + "c": 1 + }, + { + "w": 1458432000, + "a": 0, + "d": 3, + "c": 1 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1460246400, + "a": 11, + "d": 13, + "c": 4 + }, + { + "w": 1460851200, + "a": 8, + "d": 5, + "c": 2 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 676, + "d": 596, + "c": 12 + }, + { + "w": 1462665600, + "a": 243, + "d": 276, + "c": 10 + }, + { + "w": 1463270400, + "a": 38, + "d": 36, + "c": 9 + }, + { + "w": 1463875200, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1464480000, + "a": 13, + "d": 9, + "c": 5 + }, + { + "w": 1465084800, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1465689600, + "a": 143, + "d": 31, + "c": 6 + }, + { + "w": 1466294400, + "a": 59, + "d": 44, + "c": 5 + }, + { + "w": 1466899200, + "a": 25, + "d": 27, + "c": 4 + }, + { + "w": 1467504000, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1469318400, + "a": 21, + "d": 21, + "c": 3 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 19, + "d": 6, + "c": 2 + }, + { + "w": 1471132800, + "a": 7, + "d": 1, + "c": 1 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 47, + "d": 47, + "c": 1 + }, + { + "w": 1473552000, + "a": 46, + "d": 19, + "c": 2 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 8, + "d": 9, + "c": 2 + }, + { + "w": 1475971200, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1476576000, + "a": 2, + "d": 3, + "c": 2 + }, + { + "w": 1477180800, + "a": 5, + "d": 3, + "c": 1 + }, + { + "w": 1477785600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1478390400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1478995200, + "a": 4, + "d": 9, + "c": 2 + }, + { + "w": 1479600000, + "a": 4, + "d": 1, + "c": 1 + }, + { + "w": 1480204800, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1480809600, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 91, + "d": 29, + "c": 2 + }, + { + "w": 1484438400, + "a": 37, + "d": 94, + "c": 2 + }, + { + "w": 1485043200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1486857600, + "a": 11, + "d": 5, + "c": 2 + }, + { + "w": 1487462400, + "a": 16, + "d": 15, + "c": 1 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 10, + "d": 7, + "c": 4 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 6, + "d": 6, + "c": 1 + }, + { + "w": 1491091200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1491696000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1492300800, + "a": 7, + "d": 16, + "c": 3 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 12, + "d": 9, + "c": 1 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 9, + "d": 4, + "c": 1 + }, + { + "w": 1500163200, + "a": 81, + "d": 10, + "c": 2 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 10, + "d": 9, + "c": 2 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 56, + "d": 19, + "c": 2 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 133, + "d": 138, + "c": 1 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "pq", + "id": 67586, + "node_id": "MDQ6VXNlcjY3NTg2", + "avatar_url": "https://avatars3.githubusercontent.com/u/67586?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/pq", + "html_url": "https://github.com/pq", + "followers_url": "https://api.github.com/users/pq/followers", + "following_url": "https://api.github.com/users/pq/following{/other_user}", + "gists_url": "https://api.github.com/users/pq/gists{/gist_id}", + "starred_url": "https://api.github.com/users/pq/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/pq/subscriptions", + "organizations_url": "https://api.github.com/users/pq/orgs", + "repos_url": "https://api.github.com/users/pq/repos", + "events_url": "https://api.github.com/users/pq/events{/privacy}", + "received_events_url": "https://api.github.com/users/pq/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 144, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 46, + "d": 0, + "c": 1 + }, + { + "w": 1431820800, + "a": 4, + "d": 2, + "c": 1 + }, + { + "w": 1432425600, + "a": 65, + "d": 9, + "c": 3 + }, + { + "w": 1433030400, + "a": 65, + "d": 50, + "c": 7 + }, + { + "w": 1433635200, + "a": 55, + "d": 41, + "c": 10 + }, + { + "w": 1434240000, + "a": 93, + "d": 2, + "c": 2 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 76, + "d": 59, + "c": 3 + }, + { + "w": 1437264000, + "a": 450, + "d": 416, + "c": 4 + }, + { + "w": 1437868800, + "a": 556, + "d": 375, + "c": 4 + }, + { + "w": 1438473600, + "a": 433, + "d": 448, + "c": 7 + }, + { + "w": 1439078400, + "a": 211, + "d": 105, + "c": 5 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 5, + "d": 1, + "c": 1 + }, + { + "w": 1441497600, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 53, + "d": 0, + "c": 1 + }, + { + "w": 1443312000, + "a": 213, + "d": 7, + "c": 1 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 529, + "d": 50, + "c": 5 + }, + { + "w": 1445731200, + "a": 343, + "d": 324, + "c": 6 + }, + { + "w": 1446336000, + "a": 217, + "d": 112, + "c": 10 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 188, + "d": 4, + "c": 1 + }, + { + "w": 1452988800, + "a": 160, + "d": 62, + "c": 1 + }, + { + "w": 1453593600, + "a": 8, + "d": 6, + "c": 1 + }, + { + "w": 1454198400, + "a": 343, + "d": 188, + "c": 1 + }, + { + "w": 1454803200, + "a": 235, + "d": 231, + "c": 3 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 6, + "d": 8, + "c": 1 + }, + { + "w": 1456617600, + "a": 0, + "d": 245, + "c": 1 + }, + { + "w": 1457222400, + "a": 189, + "d": 23, + "c": 2 + }, + { + "w": 1457827200, + "a": 32, + "d": 10, + "c": 2 + }, + { + "w": 1458432000, + "a": 393, + "d": 76, + "c": 1 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 600, + "d": 54, + "c": 3 + }, + { + "w": 1461456000, + "a": 13, + "d": 8, + "c": 1 + }, + { + "w": 1462060800, + "a": 374, + "d": 102, + "c": 1 + }, + { + "w": 1462665600, + "a": 146, + "d": 67, + "c": 3 + }, + { + "w": 1463270400, + "a": 1, + "d": 4, + "c": 2 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 781, + "d": 4, + "c": 1 + }, + { + "w": 1465084800, + "a": 155, + "d": 111, + "c": 6 + }, + { + "w": 1465689600, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 13, + "d": 9, + "c": 1 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 32, + "d": 38, + "c": 1 + }, + { + "w": 1469318400, + "a": 7, + "d": 4, + "c": 2 + }, + { + "w": 1469923200, + "a": 130, + "d": 37, + "c": 3 + }, + { + "w": 1470528000, + "a": 18, + "d": 5, + "c": 1 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 354, + "d": 13, + "c": 1 + }, + { + "w": 1472342400, + "a": 101, + "d": 18, + "c": 3 + }, + { + "w": 1472947200, + "a": 162, + "d": 42, + "c": 2 + }, + { + "w": 1473552000, + "a": 99, + "d": 10, + "c": 2 + }, + { + "w": 1474156800, + "a": 153, + "d": 6, + "c": 4 + }, + { + "w": 1474761600, + "a": 29, + "d": 16, + "c": 2 + }, + { + "w": 1475366400, + "a": 256, + "d": 96, + "c": 1 + }, + { + "w": 1475971200, + "a": 298, + "d": 101, + "c": 1 + }, + { + "w": 1476576000, + "a": 85, + "d": 80, + "c": 3 + }, + { + "w": 1477180800, + "a": 91, + "d": 7, + "c": 2 + }, + { + "w": 1477785600, + "a": 456, + "d": 335, + "c": 2 + }, + { + "w": 1478390400, + "a": 43, + "d": 8, + "c": 1 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 18, + "d": 2, + "c": 1 + }, + { + "w": 1481414400, + "a": 316, + "d": 48, + "c": 4 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 37, + "d": 1, + "c": 1 + }, + { + "w": 1484438400, + "a": 181, + "d": 43, + "c": 1 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "mpcomplete", + "id": 1071986, + "node_id": "MDQ6VXNlcjEwNzE5ODY=", + "avatar_url": "https://avatars2.githubusercontent.com/u/1071986?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mpcomplete", + "html_url": "https://github.com/mpcomplete", + "followers_url": "https://api.github.com/users/mpcomplete/followers", + "following_url": "https://api.github.com/users/mpcomplete/following{/other_user}", + "gists_url": "https://api.github.com/users/mpcomplete/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mpcomplete/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mpcomplete/subscriptions", + "organizations_url": "https://api.github.com/users/mpcomplete/orgs", + "repos_url": "https://api.github.com/users/mpcomplete/repos", + "events_url": "https://api.github.com/users/mpcomplete/events{/privacy}", + "received_events_url": "https://api.github.com/users/mpcomplete/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 146, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 37, + "d": 37, + "c": 1 + }, + { + "w": 1480809600, + "a": 815, + "d": 814, + "c": 5 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 387, + "d": 387, + "c": 6 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 360, + "d": 376, + "c": 4 + }, + { + "w": 1488067200, + "a": 52, + "d": 44, + "c": 2 + }, + { + "w": 1488672000, + "a": 321, + "d": 324, + "c": 5 + }, + { + "w": 1489276800, + "a": 190, + "d": 296, + "c": 4 + }, + { + "w": 1489881600, + "a": 84, + "d": 84, + "c": 3 + }, + { + "w": 1490486400, + "a": 291, + "d": 208, + "c": 2 + }, + { + "w": 1491091200, + "a": 128, + "d": 143, + "c": 3 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 539, + "d": 531, + "c": 7 + }, + { + "w": 1492905600, + "a": 593, + "d": 671, + "c": 2 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 147, + "d": 67, + "c": 6 + }, + { + "w": 1494720000, + "a": 4, + "d": 4, + "c": 2 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 372, + "d": 432, + "c": 2 + }, + { + "w": 1496534400, + "a": 499, + "d": 557, + "c": 6 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 66, + "d": 40, + "c": 1 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 20, + "d": 19, + "c": 1 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 184, + "d": 170, + "c": 2 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 320, + "d": 314, + "c": 2 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 58, + "d": 57, + "c": 2 + }, + { + "w": 1508630400, + "a": 776, + "d": 792, + "c": 4 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 92, + "d": 96, + "c": 3 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 69, + "d": 79, + "c": 2 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 436, + "d": 432, + "c": 1 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 137, + "d": 137, + "c": 1 + }, + { + "w": 1517097600, + "a": 1195, + "d": 1186, + "c": 3 + }, + { + "w": 1517702400, + "a": 78, + "d": 79, + "c": 2 + }, + { + "w": 1518307200, + "a": 46, + "d": 42, + "c": 4 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 35, + "d": 35, + "c": 1 + }, + { + "w": 1520726400, + "a": 84, + "d": 84, + "c": 1 + }, + { + "w": 1521331200, + "a": 1115, + "d": 1122, + "c": 3 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 12, + "d": 0, + "c": 1 + }, + { + "w": 1523145600, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1523750400, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 16, + "d": 6, + "c": 2 + }, + { + "w": 1527984000, + "a": 1920, + "d": 1920, + "c": 1 + }, + { + "w": 1528588800, + "a": 5, + "d": 5, + "c": 1 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 41074, + "d": 41073, + "c": 1 + }, + { + "w": 1532217600, + "a": 41599, + "d": 41598, + "c": 1 + }, + { + "w": 1532822400, + "a": 41653, + "d": 41652, + "c": 1 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 777, + "d": 766, + "c": 2 + }, + { + "w": 1536451200, + "a": 26763, + "d": 26755, + "c": 5 + }, + { + "w": 1537056000, + "a": 8, + "d": 11, + "c": 1 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 1650, + "d": 1573, + "c": 7 + }, + { + "w": 1538870400, + "a": 120, + "d": 131, + "c": 2 + }, + { + "w": 1539475200, + "a": 406, + "d": 422, + "c": 4 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 147, + "d": 150, + "c": 1 + }, + { + "w": 1541289600, + "a": 16, + "d": 16, + "c": 1 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 22, + "d": 14, + "c": 1 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 360, + "d": 340, + "c": 4 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 111, + "d": 98, + "c": 2 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 50, + "d": 49, + "c": 1 + }, + { + "w": 1548547200, + "a": 675, + "d": 549, + "c": 1 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 931, + "d": 717, + "c": 1 + }, + { + "w": 1550966400, + "a": 5183, + "d": 5183, + "c": 1 + }, + { + "w": 1551571200, + "a": 1700, + "d": 1837, + "c": 3 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 594, + "d": 491, + "c": 2 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 7, + "d": 7, + "c": 2 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "a14n", + "id": 1206632, + "node_id": "MDQ6VXNlcjEyMDY2MzI=", + "avatar_url": "https://avatars1.githubusercontent.com/u/1206632?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/a14n", + "html_url": "https://github.com/a14n", + "followers_url": "https://api.github.com/users/a14n/followers", + "following_url": "https://api.github.com/users/a14n/following{/other_user}", + "gists_url": "https://api.github.com/users/a14n/gists{/gist_id}", + "starred_url": "https://api.github.com/users/a14n/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/a14n/subscriptions", + "organizations_url": "https://api.github.com/users/a14n/orgs", + "repos_url": "https://api.github.com/users/a14n/repos", + "events_url": "https://api.github.com/users/a14n/events{/privacy}", + "received_events_url": "https://api.github.com/users/a14n/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 159, + "weeks": [ + { + "w": 1413676800, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 56, + "d": 0, + "c": 1 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 7, + "d": 1, + "c": 1 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1424563200, + "a": 3186, + "d": 65, + "c": 6 + }, + { + "w": 1425168000, + "a": 17, + "d": 14, + "c": 3 + }, + { + "w": 1425772800, + "a": 32, + "d": 27, + "c": 3 + }, + { + "w": 1426377600, + "a": 468, + "d": 5717, + "c": 6 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 37, + "d": 20, + "c": 4 + }, + { + "w": 1428796800, + "a": 13, + "d": 64, + "c": 4 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 113, + "d": 5, + "c": 6 + }, + { + "w": 1430611200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1431216000, + "a": 18, + "d": 0, + "c": 1 + }, + { + "w": 1431820800, + "a": 52, + "d": 0, + "c": 2 + }, + { + "w": 1432425600, + "a": 153, + "d": 30, + "c": 4 + }, + { + "w": 1433030400, + "a": 67, + "d": 44, + "c": 6 + }, + { + "w": 1433635200, + "a": 39, + "d": 22, + "c": 3 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 26, + "d": 30, + "c": 3 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 14, + "d": 3, + "c": 1 + }, + { + "w": 1437264000, + "a": 250, + "d": 128, + "c": 4 + }, + { + "w": 1437868800, + "a": 6, + "d": 2, + "c": 2 + }, + { + "w": 1438473600, + "a": 67, + "d": 17, + "c": 5 + }, + { + "w": 1439078400, + "a": 290, + "d": 87, + "c": 10 + }, + { + "w": 1439683200, + "a": 97, + "d": 78, + "c": 8 + }, + { + "w": 1440288000, + "a": 394, + "d": 59, + "c": 8 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 16, + "d": 29696, + "c": 1 + }, + { + "w": 1447545600, + "a": 2, + "d": 1, + "c": 2 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 144, + "d": 8, + "c": 6 + }, + { + "w": 1449360000, + "a": 63, + "d": 26, + "c": 5 + }, + { + "w": 1449964800, + "a": 351, + "d": 216, + "c": 10 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 23, + "d": 9, + "c": 3 + }, + { + "w": 1452384000, + "a": 382, + "d": 5, + "c": 4 + }, + { + "w": 1452988800, + "a": 29, + "d": 28, + "c": 2 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 101, + "d": 1, + "c": 2 + }, + { + "w": 1454803200, + "a": 214, + "d": 371, + "c": 5 + }, + { + "w": 1455408000, + "a": 8, + "d": 6, + "c": 1 + }, + { + "w": 1456012800, + "a": 64, + "d": 1379, + "c": 2 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 18, + "d": 178, + "c": 2 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 117, + "d": 20, + "c": 5 + }, + { + "w": 1460851200, + "a": 22, + "d": 3, + "c": 1 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1462665600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1463270400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 3, + "d": 1, + "c": 1 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 74, + "c": 2 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "eseidelGoogle", + "id": 11857803, + "node_id": "MDQ6VXNlcjExODU3ODAz", + "avatar_url": "https://avatars2.githubusercontent.com/u/11857803?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/eseidelGoogle", + "html_url": "https://github.com/eseidelGoogle", + "followers_url": "https://api.github.com/users/eseidelGoogle/followers", + "following_url": "https://api.github.com/users/eseidelGoogle/following{/other_user}", + "gists_url": "https://api.github.com/users/eseidelGoogle/gists{/gist_id}", + "starred_url": "https://api.github.com/users/eseidelGoogle/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/eseidelGoogle/subscriptions", + "organizations_url": "https://api.github.com/users/eseidelGoogle/orgs", + "repos_url": "https://api.github.com/users/eseidelGoogle/repos", + "events_url": "https://api.github.com/users/eseidelGoogle/events{/privacy}", + "received_events_url": "https://api.github.com/users/eseidelGoogle/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 172, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 47, + "d": 10, + "c": 1 + }, + { + "w": 1432425600, + "a": 32, + "d": 5, + "c": 2 + }, + { + "w": 1433030400, + "a": 258, + "d": 102, + "c": 7 + }, + { + "w": 1433635200, + "a": 60, + "d": 6, + "c": 2 + }, + { + "w": 1434240000, + "a": 458, + "d": 257, + "c": 10 + }, + { + "w": 1434844800, + "a": 134, + "d": 83, + "c": 3 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 52397, + "d": 129, + "c": 18 + }, + { + "w": 1437264000, + "a": 897, + "d": 469, + "c": 7 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 30, + "d": 4, + "c": 3 + }, + { + "w": 1439078400, + "a": 417, + "d": 164, + "c": 7 + }, + { + "w": 1439683200, + "a": 572, + "d": 222, + "c": 14 + }, + { + "w": 1440288000, + "a": 631, + "d": 43, + "c": 7 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 14, + "d": 14, + "c": 1 + }, + { + "w": 1442102400, + "a": 1040, + "d": 357, + "c": 7 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 10, + "d": 7, + "c": 4 + }, + { + "w": 1443916800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1444521600, + "a": 70, + "d": 2, + "c": 2 + }, + { + "w": 1445126400, + "a": 181, + "d": 12, + "c": 1 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 249, + "d": 0, + "c": 1 + }, + { + "w": 1446940800, + "a": 152, + "d": 4, + "c": 3 + }, + { + "w": 1447545600, + "a": 111, + "d": 78, + "c": 6 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 288, + "d": 151, + "c": 2 + }, + { + "w": 1449360000, + "a": 84, + "d": 24, + "c": 1 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 53, + "c": 1 + }, + { + "w": 1452988800, + "a": 4, + "d": 4, + "c": 2 + }, + { + "w": 1453593600, + "a": 57, + "d": 29, + "c": 4 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 13, + "d": 13, + "c": 3 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 4, + "d": 6, + "c": 1 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1457827200, + "a": 51, + "d": 3, + "c": 2 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 6, + "d": 2, + "c": 3 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 11, + "d": 0, + "c": 1 + }, + { + "w": 1462665600, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1463270400, + "a": 30, + "d": 56, + "c": 2 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 72, + "d": 18, + "c": 4 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 646, + "d": 11, + "c": 1 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 174, + "d": 0, + "c": 2 + }, + { + "w": 1471737600, + "a": 159, + "d": 15, + "c": 2 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 27, + "d": 6, + "c": 2 + }, + { + "w": 1480809600, + "a": 26, + "d": 21, + "c": 1 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 129, + "d": 42, + "c": 2 + }, + { + "w": 1490486400, + "a": 18, + "d": 18, + "c": 6 + }, + { + "w": 1491091200, + "a": 4, + "d": 8, + "c": 2 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 27, + "d": 6, + "c": 3 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 8, + "d": 8, + "c": 2 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 3, + "d": 2, + "c": 1 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 42, + "d": 2, + "c": 1 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 4, + "d": 10, + "c": 2 + }, + { + "w": 1504396800, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1505001600, + "a": 47, + "d": 12, + "c": 3 + }, + { + "w": 1505606400, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 8, + "d": 8, + "c": 2 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 8, + "d": 8, + "c": 2 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "collinjackson", + "id": 394889, + "node_id": "MDQ6VXNlcjM5NDg4OQ==", + "avatar_url": "https://avatars2.githubusercontent.com/u/394889?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/collinjackson", + "html_url": "https://github.com/collinjackson", + "followers_url": "https://api.github.com/users/collinjackson/followers", + "following_url": "https://api.github.com/users/collinjackson/following{/other_user}", + "gists_url": "https://api.github.com/users/collinjackson/gists{/gist_id}", + "starred_url": "https://api.github.com/users/collinjackson/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/collinjackson/subscriptions", + "organizations_url": "https://api.github.com/users/collinjackson/orgs", + "repos_url": "https://api.github.com/users/collinjackson/repos", + "events_url": "https://api.github.com/users/collinjackson/events{/privacy}", + "received_events_url": "https://api.github.com/users/collinjackson/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 175, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 451, + "d": 2, + "c": 3 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 5, + "d": 5, + "c": 1 + }, + { + "w": 1519516800, + "a": 90, + "d": 48, + "c": 2 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 6, + "c": 1 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 781, + "d": 180, + "c": 28 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 86, + "d": 34, + "c": 8 + }, + { + "w": 1525564800, + "a": 50, + "d": 31, + "c": 9 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 115, + "d": 12, + "c": 2 + }, + { + "w": 1527379200, + "a": 241, + "d": 32, + "c": 6 + }, + { + "w": 1527984000, + "a": 69, + "d": 19, + "c": 4 + }, + { + "w": 1528588800, + "a": 931, + "d": 929, + "c": 5 + }, + { + "w": 1529193600, + "a": 3, + "d": 2, + "c": 1 + }, + { + "w": 1529798400, + "a": 1080, + "d": 340, + "c": 15 + }, + { + "w": 1530403200, + "a": 28, + "d": 20, + "c": 6 + }, + { + "w": 1531008000, + "a": 785, + "d": 597, + "c": 6 + }, + { + "w": 1531612800, + "a": 297, + "d": 95, + "c": 5 + }, + { + "w": 1532217600, + "a": 367, + "d": 299, + "c": 7 + }, + { + "w": 1532822400, + "a": 57, + "d": 74, + "c": 6 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 59, + "d": 17, + "c": 4 + }, + { + "w": 1535241600, + "a": 34, + "d": 15, + "c": 4 + }, + { + "w": 1535846400, + "a": 509, + "d": 361, + "c": 12 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 7, + "d": 13, + "c": 3 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 19, + "d": 14, + "c": 6 + }, + { + "w": 1538870400, + "a": 23, + "d": 11, + "c": 1 + }, + { + "w": 1539475200, + "a": 34, + "d": 24, + "c": 2 + }, + { + "w": 1540080000, + "a": 71, + "d": 27, + "c": 3 + }, + { + "w": 1540684800, + "a": 72, + "d": 2, + "c": 1 + }, + { + "w": 1541289600, + "a": 165, + "d": 95, + "c": 2 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 268, + "d": 250, + "c": 3 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 160, + "d": 26, + "c": 3 + }, + { + "w": 1544918400, + "a": 24, + "d": 11, + "c": 2 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 448, + "d": 33, + "c": 4 + }, + { + "w": 1547337600, + "a": 18, + "d": 14, + "c": 3 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 4, + "d": 2, + "c": 2 + }, + { + "w": 1549152000, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1549756800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 76, + "d": 4, + "c": 1 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "DanTup", + "id": 1078012, + "node_id": "MDQ6VXNlcjEwNzgwMTI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/1078012?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/DanTup", + "html_url": "https://github.com/DanTup", + "followers_url": "https://api.github.com/users/DanTup/followers", + "following_url": "https://api.github.com/users/DanTup/following{/other_user}", + "gists_url": "https://api.github.com/users/DanTup/gists{/gist_id}", + "starred_url": "https://api.github.com/users/DanTup/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/DanTup/subscriptions", + "organizations_url": "https://api.github.com/users/DanTup/orgs", + "repos_url": "https://api.github.com/users/DanTup/repos", + "events_url": "https://api.github.com/users/DanTup/events{/privacy}", + "received_events_url": "https://api.github.com/users/DanTup/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 188, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 2268, + "d": 563, + "c": 30 + }, + { + "w": 1435449600, + "a": 390, + "d": 34, + "c": 6 + }, + { + "w": 1436054400, + "a": 782, + "d": 402, + "c": 16 + }, + { + "w": 1436659200, + "a": 56, + "d": 17, + "c": 1 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 109638, + "d": 108841, + "c": 5 + }, + { + "w": 1438473600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1439078400, + "a": 63, + "d": 0, + "c": 1 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 732, + "d": 0, + "c": 1 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 13, + "d": 12, + "c": 1 + }, + { + "w": 1443312000, + "a": 140, + "d": 0, + "c": 1 + }, + { + "w": 1443916800, + "a": 91, + "d": 30, + "c": 9 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 14, + "d": 13, + "c": 3 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 162, + "d": 5, + "c": 2 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 6, + "d": 5, + "c": 1 + }, + { + "w": 1452384000, + "a": 176, + "d": 82, + "c": 8 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 9, + "d": 6, + "c": 4 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 324, + "d": 99, + "c": 9 + }, + { + "w": 1455408000, + "a": 1509, + "d": 1555, + "c": 16 + }, + { + "w": 1456012800, + "a": 867, + "d": 506, + "c": 8 + }, + { + "w": 1456617600, + "a": 61, + "d": 21, + "c": 1 + }, + { + "w": 1457222400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 133, + "d": 75, + "c": 6 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 27, + "d": 22, + "c": 3 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1462665600, + "a": 49, + "d": 23, + "c": 6 + }, + { + "w": 1463270400, + "a": 19, + "d": 3, + "c": 4 + }, + { + "w": 1463875200, + "a": 37, + "d": 24, + "c": 2 + }, + { + "w": 1464480000, + "a": 16, + "d": 7, + "c": 1 + }, + { + "w": 1465084800, + "a": 9, + "d": 3, + "c": 1 + }, + { + "w": 1465689600, + "a": 165, + "d": 16, + "c": 1 + }, + { + "w": 1466294400, + "a": 392, + "d": 8, + "c": 3 + }, + { + "w": 1466899200, + "a": 22, + "d": 55, + "c": 2 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 19, + "d": 19, + "c": 2 + }, + { + "w": 1470528000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1471132800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1471737600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 4, + "d": 3, + "c": 2 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 1695, + "d": 477, + "c": 1 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1478390400, + "a": 127, + "d": 14, + "c": 1 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 3, + "d": 2, + "c": 2 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 5, + "d": 3, + "c": 2 + }, + { + "w": 1481414400, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 17, + "d": 0, + "c": 1 + }, + { + "w": 1485648000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 4, + "d": 18, + "c": 1 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 19, + "d": 27, + "c": 5 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 3, + "d": 1, + "c": 1 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 1, + "c": 1 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 11, + "d": 0, + "c": 1 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "chinmaygarde", + "id": 44085, + "node_id": "MDQ6VXNlcjQ0MDg1", + "avatar_url": "https://avatars1.githubusercontent.com/u/44085?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/chinmaygarde", + "html_url": "https://github.com/chinmaygarde", + "followers_url": "https://api.github.com/users/chinmaygarde/followers", + "following_url": "https://api.github.com/users/chinmaygarde/following{/other_user}", + "gists_url": "https://api.github.com/users/chinmaygarde/gists{/gist_id}", + "starred_url": "https://api.github.com/users/chinmaygarde/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/chinmaygarde/subscriptions", + "organizations_url": "https://api.github.com/users/chinmaygarde/orgs", + "repos_url": "https://api.github.com/users/chinmaygarde/repos", + "events_url": "https://api.github.com/users/chinmaygarde/events{/privacy}", + "received_events_url": "https://api.github.com/users/chinmaygarde/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 189, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 712, + "d": 0, + "c": 1 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 1492, + "d": 111, + "c": 2 + }, + { + "w": 1488672000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1489276800, + "a": 693, + "d": 686, + "c": 3 + }, + { + "w": 1489881600, + "a": 83, + "d": 11, + "c": 3 + }, + { + "w": 1490486400, + "a": 131, + "d": 55, + "c": 1 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 196, + "d": 171, + "c": 4 + }, + { + "w": 1492905600, + "a": 2526, + "d": 259, + "c": 5 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 142, + "d": 111, + "c": 5 + }, + { + "w": 1494720000, + "a": 45, + "d": 83, + "c": 2 + }, + { + "w": 1495324800, + "a": 1711, + "d": 932, + "c": 6 + }, + { + "w": 1495929600, + "a": 530, + "d": 546, + "c": 4 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 575, + "d": 453, + "c": 5 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 178, + "d": 30, + "c": 3 + }, + { + "w": 1501977600, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1502582400, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1503187200, + "a": 4262, + "d": 2297, + "c": 6 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 561, + "d": 297, + "c": 4 + }, + { + "w": 1505001600, + "a": 138, + "d": 139, + "c": 1 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 187, + "d": 3, + "c": 1 + }, + { + "w": 1509840000, + "a": 167, + "d": 142, + "c": 8 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 1541, + "d": 159, + "c": 5 + }, + { + "w": 1511654400, + "a": 17, + "d": 10, + "c": 3 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 499, + "d": 472, + "c": 5 + }, + { + "w": 1513468800, + "a": 78, + "d": 38, + "c": 4 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 1059, + "d": 1092, + "c": 6 + }, + { + "w": 1515283200, + "a": 46, + "d": 87, + "c": 2 + }, + { + "w": 1515888000, + "a": 365, + "d": 30, + "c": 1 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1518307200, + "a": 1800, + "d": 1762, + "c": 10 + }, + { + "w": 1518912000, + "a": 68, + "d": 27, + "c": 3 + }, + { + "w": 1519516800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1520121600, + "a": 278, + "d": 175, + "c": 1 + }, + { + "w": 1520726400, + "a": 354, + "d": 83, + "c": 5 + }, + { + "w": 1521331200, + "a": 21, + "d": 12, + "c": 4 + }, + { + "w": 1521936000, + "a": 2, + "d": 1, + "c": 2 + }, + { + "w": 1522540800, + "a": 142, + "d": 11, + "c": 3 + }, + { + "w": 1523145600, + "a": 2, + "d": 9, + "c": 1 + }, + { + "w": 1523750400, + "a": 31, + "d": 21, + "c": 4 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 657, + "d": 657, + "c": 4 + }, + { + "w": 1525564800, + "a": 235, + "d": 118, + "c": 1 + }, + { + "w": 1526169600, + "a": 112, + "d": 79, + "c": 3 + }, + { + "w": 1526774400, + "a": 3, + "d": 17, + "c": 2 + }, + { + "w": 1527379200, + "a": 692, + "d": 704, + "c": 15 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 482, + "d": 460, + "c": 3 + }, + { + "w": 1529193600, + "a": 601, + "d": 73, + "c": 3 + }, + { + "w": 1529798400, + "a": 315, + "d": 72, + "c": 8 + }, + { + "w": 1530403200, + "a": 53, + "d": 18, + "c": 7 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 577, + "d": 499, + "c": 4 + }, + { + "w": 1533427200, + "a": 528, + "d": 320, + "c": 4 + }, + { + "w": 1534032000, + "a": 812, + "d": 483, + "c": 4 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 1307, + "d": 325, + "c": 2 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 994, + "d": 87, + "c": 1 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "mravn-google", + "id": 22935389, + "node_id": "MDQ6VXNlcjIyOTM1Mzg5", + "avatar_url": "https://avatars0.githubusercontent.com/u/22935389?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mravn-google", + "html_url": "https://github.com/mravn-google", + "followers_url": "https://api.github.com/users/mravn-google/followers", + "following_url": "https://api.github.com/users/mravn-google/following{/other_user}", + "gists_url": "https://api.github.com/users/mravn-google/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mravn-google/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mravn-google/subscriptions", + "organizations_url": "https://api.github.com/users/mravn-google/orgs", + "repos_url": "https://api.github.com/users/mravn-google/repos", + "events_url": "https://api.github.com/users/mravn-google/events{/privacy}", + "received_events_url": "https://api.github.com/users/mravn-google/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 209, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 84, + "d": 0, + "c": 1 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 15, + "d": 2, + "c": 2 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 600, + "d": 108, + "c": 3 + }, + { + "w": 1504396800, + "a": 11, + "d": 3, + "c": 2 + }, + { + "w": 1505001600, + "a": 110, + "d": 42, + "c": 3 + }, + { + "w": 1505606400, + "a": 12, + "d": 1, + "c": 2 + }, + { + "w": 1506211200, + "a": 74, + "d": 33, + "c": 1 + }, + { + "w": 1506816000, + "a": 223, + "d": 627, + "c": 5 + }, + { + "w": 1507420800, + "a": 217, + "d": 93, + "c": 11 + }, + { + "w": 1508025600, + "a": 20, + "d": 4, + "c": 3 + }, + { + "w": 1508630400, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 41, + "d": 15, + "c": 3 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 2, + "d": 1, + "c": 2 + }, + { + "w": 1511654400, + "a": 35, + "d": 22, + "c": 6 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 7, + "d": 6, + "c": 4 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 132, + "d": 112, + "c": 14 + }, + { + "w": 1515283200, + "a": 195, + "d": 83, + "c": 10 + }, + { + "w": 1515888000, + "a": 62, + "d": 23, + "c": 5 + }, + { + "w": 1516492800, + "a": 39, + "d": 12, + "c": 1 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 67, + "d": 49, + "c": 3 + }, + { + "w": 1518307200, + "a": 270, + "d": 291, + "c": 6 + }, + { + "w": 1518912000, + "a": 38, + "d": 38, + "c": 3 + }, + { + "w": 1519516800, + "a": 127, + "d": 42, + "c": 8 + }, + { + "w": 1520121600, + "a": 550, + "d": 409, + "c": 9 + }, + { + "w": 1520726400, + "a": 551, + "d": 421, + "c": 9 + }, + { + "w": 1521331200, + "a": 108, + "d": 82, + "c": 5 + }, + { + "w": 1521936000, + "a": 77, + "d": 76, + "c": 5 + }, + { + "w": 1522540800, + "a": 6, + "d": 5, + "c": 1 + }, + { + "w": 1523145600, + "a": 175, + "d": 90, + "c": 8 + }, + { + "w": 1523750400, + "a": 18, + "d": 12, + "c": 3 + }, + { + "w": 1524355200, + "a": 14, + "d": 12, + "c": 6 + }, + { + "w": 1524960000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 11, + "d": 3, + "c": 3 + }, + { + "w": 1526774400, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 186, + "d": 9, + "c": 4 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 191, + "d": 31, + "c": 3 + }, + { + "w": 1529798400, + "a": 5, + "d": 4, + "c": 3 + }, + { + "w": 1530403200, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1531008000, + "a": 738, + "d": 718, + "c": 7 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 124, + "d": 12, + "c": 1 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 645, + "d": 597, + "c": 7 + }, + { + "w": 1535846400, + "a": 31, + "d": 16, + "c": 1 + }, + { + "w": 1536451200, + "a": 17, + "d": 16, + "c": 2 + }, + { + "w": 1537056000, + "a": 8, + "d": 8, + "c": 3 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 2, + "d": 3, + "c": 2 + }, + { + "w": 1539475200, + "a": 14, + "d": 23, + "c": 4 + }, + { + "w": 1540080000, + "a": 288, + "d": 261, + "c": 3 + }, + { + "w": 1540684800, + "a": 144, + "d": 124, + "c": 2 + }, + { + "w": 1541289600, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1541894400, + "a": 17, + "d": 8, + "c": 3 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 108, + "d": 64, + "c": 2 + }, + { + "w": 1546128000, + "a": 22, + "d": 4, + "c": 1 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 15, + "d": 5, + "c": 2 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 80, + "d": 8, + "c": 1 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 9, + "c": 1 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 55, + "d": 370, + "c": 1 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "aam", + "id": 381137, + "node_id": "MDQ6VXNlcjM4MTEzNw==", + "avatar_url": "https://avatars0.githubusercontent.com/u/381137?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/aam", + "html_url": "https://github.com/aam", + "followers_url": "https://api.github.com/users/aam/followers", + "following_url": "https://api.github.com/users/aam/following{/other_user}", + "gists_url": "https://api.github.com/users/aam/gists{/gist_id}", + "starred_url": "https://api.github.com/users/aam/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/aam/subscriptions", + "organizations_url": "https://api.github.com/users/aam/orgs", + "repos_url": "https://api.github.com/users/aam/repos", + "events_url": "https://api.github.com/users/aam/events{/privacy}", + "received_events_url": "https://api.github.com/users/aam/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 209, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 760, + "d": 49, + "c": 4 + }, + { + "w": 1433635200, + "a": 703, + "d": 373, + "c": 2 + }, + { + "w": 1434240000, + "a": 1052, + "d": 683, + "c": 5 + }, + { + "w": 1434844800, + "a": 794, + "d": 147, + "c": 3 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 733, + "d": 134, + "c": 12 + }, + { + "w": 1437868800, + "a": 360, + "d": 204, + "c": 13 + }, + { + "w": 1438473600, + "a": 759, + "d": 58, + "c": 12 + }, + { + "w": 1439078400, + "a": 607, + "d": 110, + "c": 19 + }, + { + "w": 1439683200, + "a": 1176, + "d": 801, + "c": 7 + }, + { + "w": 1440288000, + "a": 5065, + "d": 4337, + "c": 22 + }, + { + "w": 1440892800, + "a": 462, + "d": 113, + "c": 18 + }, + { + "w": 1441497600, + "a": 599, + "d": 1, + "c": 1 + }, + { + "w": 1442102400, + "a": 244, + "d": 175, + "c": 1 + }, + { + "w": 1442707200, + "a": 135, + "d": 14, + "c": 4 + }, + { + "w": 1443312000, + "a": 743, + "d": 31, + "c": 2 + }, + { + "w": 1443916800, + "a": 553, + "d": 66, + "c": 12 + }, + { + "w": 1444521600, + "a": 661, + "d": 246, + "c": 21 + }, + { + "w": 1445126400, + "a": 798, + "d": 505, + "c": 8 + }, + { + "w": 1445731200, + "a": 596, + "d": 55, + "c": 12 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 4032, + "c": 1 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 574, + "d": 2, + "c": 2 + }, + { + "w": 1454198400, + "a": 501, + "d": 1, + "c": 1 + }, + { + "w": 1454803200, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1455408000, + "a": 89, + "d": 2, + "c": 2 + }, + { + "w": 1456012800, + "a": 940, + "d": 0, + "c": 1 + }, + { + "w": 1456617600, + "a": 552, + "d": 552, + "c": 1 + }, + { + "w": 1457222400, + "a": 178, + "d": 42, + "c": 2 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 326, + "d": 221, + "c": 1 + }, + { + "w": 1459036800, + "a": 1043, + "d": 3, + "c": 3 + }, + { + "w": 1459641600, + "a": 1036, + "d": 222, + "c": 4 + }, + { + "w": 1460246400, + "a": 252, + "d": 137, + "c": 2 + }, + { + "w": 1460851200, + "a": 625, + "d": 227, + "c": 3 + }, + { + "w": 1461456000, + "a": 513, + "d": 1, + "c": 1 + }, + { + "w": 1462060800, + "a": 65, + "d": 2324, + "c": 2 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 538, + "d": 59, + "c": 2 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 3, + "d": 1, + "c": 1 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "vlidholt", + "id": 1539812, + "node_id": "MDQ6VXNlcjE1Mzk4MTI=", + "avatar_url": "https://avatars3.githubusercontent.com/u/1539812?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/vlidholt", + "html_url": "https://github.com/vlidholt", + "followers_url": "https://api.github.com/users/vlidholt/followers", + "following_url": "https://api.github.com/users/vlidholt/following{/other_user}", + "gists_url": "https://api.github.com/users/vlidholt/gists{/gist_id}", + "starred_url": "https://api.github.com/users/vlidholt/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/vlidholt/subscriptions", + "organizations_url": "https://api.github.com/users/vlidholt/orgs", + "repos_url": "https://api.github.com/users/vlidholt/repos", + "events_url": "https://api.github.com/users/vlidholt/events{/privacy}", + "received_events_url": "https://api.github.com/users/vlidholt/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 216, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 348, + "d": 19, + "c": 1 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 44, + "d": 3, + "c": 1 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 69, + "d": 37, + "c": 2 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 225, + "d": 18, + "c": 3 + }, + { + "w": 1506816000, + "a": 720, + "d": 333, + "c": 2 + }, + { + "w": 1507420800, + "a": 552, + "d": 118, + "c": 5 + }, + { + "w": 1508025600, + "a": 439, + "d": 93, + "c": 2 + }, + { + "w": 1508630400, + "a": 1112, + "d": 534, + "c": 3 + }, + { + "w": 1509235200, + "a": 921, + "d": 83, + "c": 2 + }, + { + "w": 1509840000, + "a": 837, + "d": 156, + "c": 1 + }, + { + "w": 1510444800, + "a": 1570, + "d": 925, + "c": 8 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 166, + "d": 1197, + "c": 3 + }, + { + "w": 1512259200, + "a": 480, + "d": 275, + "c": 6 + }, + { + "w": 1512864000, + "a": 553, + "d": 30, + "c": 5 + }, + { + "w": 1513468800, + "a": 205, + "d": 196, + "c": 3 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 83, + "d": 83, + "c": 1 + }, + { + "w": 1515283200, + "a": 601, + "d": 601, + "c": 3 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 772, + "d": 44, + "c": 2 + }, + { + "w": 1517702400, + "a": 2301, + "d": 1021, + "c": 6 + }, + { + "w": 1518307200, + "a": 437, + "d": 118, + "c": 2 + }, + { + "w": 1518912000, + "a": 4, + "d": 5, + "c": 1 + }, + { + "w": 1519516800, + "a": 2075, + "d": 559, + "c": 3 + }, + { + "w": 1520121600, + "a": 1017, + "d": 500, + "c": 2 + }, + { + "w": 1520726400, + "a": 426, + "d": 155, + "c": 3 + }, + { + "w": 1521331200, + "a": 1403, + "d": 426, + "c": 7 + }, + { + "w": 1521936000, + "a": 48, + "d": 12, + "c": 1 + }, + { + "w": 1522540800, + "a": 2645, + "d": 376, + "c": 4 + }, + { + "w": 1523145600, + "a": 2056, + "d": 709, + "c": 6 + }, + { + "w": 1523750400, + "a": 918, + "d": 606, + "c": 5 + }, + { + "w": 1524355200, + "a": 107, + "d": 7, + "c": 2 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 291, + "d": 19, + "c": 2 + }, + { + "w": 1526169600, + "a": 1653, + "d": 811, + "c": 7 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 431, + "d": 431, + "c": 2 + }, + { + "w": 1527984000, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1528588800, + "a": 216, + "d": 371, + "c": 1 + }, + { + "w": 1529193600, + "a": 999, + "d": 394, + "c": 2 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 437, + "d": 235, + "c": 2 + }, + { + "w": 1531612800, + "a": 289, + "d": 157, + "c": 5 + }, + { + "w": 1532217600, + "a": 167, + "d": 107, + "c": 4 + }, + { + "w": 1532822400, + "a": 722, + "d": 704, + "c": 7 + }, + { + "w": 1533427200, + "a": 502, + "d": 341, + "c": 4 + }, + { + "w": 1534032000, + "a": 108, + "d": 11, + "c": 2 + }, + { + "w": 1534636800, + "a": 21, + "d": 19, + "c": 1 + }, + { + "w": 1535241600, + "a": 216, + "d": 26, + "c": 4 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 695, + "d": 257, + "c": 5 + }, + { + "w": 1537660800, + "a": 62, + "d": 23, + "c": 2 + }, + { + "w": 1538265600, + "a": 6799, + "d": 6222, + "c": 12 + }, + { + "w": 1538870400, + "a": 3885, + "d": 2714, + "c": 6 + }, + { + "w": 1539475200, + "a": 19, + "d": 54, + "c": 5 + }, + { + "w": 1540080000, + "a": 1384, + "d": 176, + "c": 2 + }, + { + "w": 1540684800, + "a": 404, + "d": 78, + "c": 3 + }, + { + "w": 1541289600, + "a": 1571, + "d": 696, + "c": 4 + }, + { + "w": 1541894400, + "a": 1345, + "d": 1231, + "c": 10 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 3, + "d": 55, + "c": 2 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 67, + "d": 15, + "c": 2 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 949, + "d": 106, + "c": 4 + }, + { + "w": 1547337600, + "a": 42, + "d": 47, + "c": 2 + }, + { + "w": 1547942400, + "a": 32, + "d": 2, + "c": 1 + }, + { + "w": 1548547200, + "a": 1385, + "d": 169, + "c": 1 + }, + { + "w": 1549152000, + "a": 11995, + "d": 145, + "c": 4 + }, + { + "w": 1549756800, + "a": 969, + "d": 587, + "c": 3 + }, + { + "w": 1550361600, + "a": 12, + "d": 3, + "c": 2 + }, + { + "w": 1550966400, + "a": 277, + "d": 182, + "c": 3 + }, + { + "w": 1551571200, + "a": 767, + "d": 305, + "c": 2 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "gspencergoog", + "id": 8867023, + "node_id": "MDQ6VXNlcjg4NjcwMjM=", + "avatar_url": "https://avatars2.githubusercontent.com/u/8867023?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/gspencergoog", + "html_url": "https://github.com/gspencergoog", + "followers_url": "https://api.github.com/users/gspencergoog/followers", + "following_url": "https://api.github.com/users/gspencergoog/following{/other_user}", + "gists_url": "https://api.github.com/users/gspencergoog/gists{/gist_id}", + "starred_url": "https://api.github.com/users/gspencergoog/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/gspencergoog/subscriptions", + "organizations_url": "https://api.github.com/users/gspencergoog/orgs", + "repos_url": "https://api.github.com/users/gspencergoog/repos", + "events_url": "https://api.github.com/users/gspencergoog/events{/privacy}", + "received_events_url": "https://api.github.com/users/gspencergoog/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 253, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 192, + "d": 1, + "c": 5 + }, + { + "w": 1454803200, + "a": 928, + "d": 0, + "c": 1 + }, + { + "w": 1455408000, + "a": 288, + "d": 36, + "c": 2 + }, + { + "w": 1456012800, + "a": 1135, + "d": 212, + "c": 7 + }, + { + "w": 1456617600, + "a": 564, + "d": 216, + "c": 8 + }, + { + "w": 1457222400, + "a": 301, + "d": 31, + "c": 4 + }, + { + "w": 1457827200, + "a": 36, + "d": 8, + "c": 2 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 576, + "d": 175, + "c": 4 + }, + { + "w": 1459641600, + "a": 287, + "d": 50, + "c": 3 + }, + { + "w": 1460246400, + "a": 1867, + "d": 1348, + "c": 6 + }, + { + "w": 1460851200, + "a": 111, + "d": 111, + "c": 3 + }, + { + "w": 1461456000, + "a": 167, + "d": 81, + "c": 4 + }, + { + "w": 1462060800, + "a": 7, + "d": 128, + "c": 2 + }, + { + "w": 1462665600, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 28, + "d": 13, + "c": 1 + }, + { + "w": 1464480000, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1468713600, + "a": 43, + "d": 11, + "c": 3 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 17, + "d": 7, + "c": 1 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 11, + "d": 21, + "c": 3 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 34, + "d": 27, + "c": 1 + }, + { + "w": 1473552000, + "a": 2625, + "d": 4, + "c": 2 + }, + { + "w": 1474156800, + "a": 38, + "d": 0, + "c": 1 + }, + { + "w": 1474761600, + "a": 325, + "d": 149, + "c": 1 + }, + { + "w": 1475366400, + "a": 65, + "d": 38, + "c": 4 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 84, + "d": 16, + "c": 6 + }, + { + "w": 1477180800, + "a": 65, + "d": 91, + "c": 2 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 256, + "d": 43, + "c": 3 + }, + { + "w": 1478995200, + "a": 82, + "d": 35, + "c": 2 + }, + { + "w": 1479600000, + "a": 4, + "d": 5, + "c": 1 + }, + { + "w": 1480204800, + "a": 37, + "d": 40, + "c": 2 + }, + { + "w": 1480809600, + "a": 248, + "d": 158, + "c": 3 + }, + { + "w": 1481414400, + "a": 80, + "d": 32, + "c": 2 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 410, + "d": 117, + "c": 6 + }, + { + "w": 1484438400, + "a": 0, + "d": 1, + "c": 1 + }, + { + "w": 1485043200, + "a": 39, + "d": 23, + "c": 2 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 68, + "d": 328, + "c": 2 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 762, + "d": 112, + "c": 3 + }, + { + "w": 1488672000, + "a": 103, + "d": 340, + "c": 4 + }, + { + "w": 1489276800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1489881600, + "a": 104, + "d": 5, + "c": 2 + }, + { + "w": 1490486400, + "a": 104, + "d": 61, + "c": 3 + }, + { + "w": 1491091200, + "a": 679, + "d": 47, + "c": 6 + }, + { + "w": 1491696000, + "a": 286, + "d": 73, + "c": 6 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 129, + "c": 1 + }, + { + "w": 1494115200, + "a": 55, + "d": 11, + "c": 4 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 57, + "d": 24, + "c": 2 + }, + { + "w": 1495929600, + "a": 66, + "d": 46, + "c": 3 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 108, + "d": 435, + "c": 3 + }, + { + "w": 1497744000, + "a": 9, + "d": 1, + "c": 2 + }, + { + "w": 1498348800, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 300, + "d": 3, + "c": 3 + }, + { + "w": 1500163200, + "a": 505, + "d": 221, + "c": 2 + }, + { + "w": 1500768000, + "a": 141, + "d": 3, + "c": 3 + }, + { + "w": 1501372800, + "a": 611, + "d": 4, + "c": 1 + }, + { + "w": 1501977600, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 29, + "d": 12, + "c": 2 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 1384, + "d": 438, + "c": 3 + }, + { + "w": 1505606400, + "a": 1449, + "d": 376, + "c": 4 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 780, + "d": 429, + "c": 3 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 392, + "d": 108, + "c": 2 + }, + { + "w": 1508630400, + "a": 5086, + "d": 1446, + "c": 6 + }, + { + "w": 1509235200, + "a": 645, + "d": 390, + "c": 5 + }, + { + "w": 1509840000, + "a": 357, + "d": 54, + "c": 5 + }, + { + "w": 1510444800, + "a": 24, + "d": 0, + "c": 1 + }, + { + "w": 1511049600, + "a": 97, + "d": 26, + "c": 1 + }, + { + "w": 1511654400, + "a": 24, + "d": 29, + "c": 2 + }, + { + "w": 1512259200, + "a": 3203, + "d": 1090, + "c": 7 + }, + { + "w": 1512864000, + "a": 690, + "d": 195, + "c": 4 + }, + { + "w": 1513468800, + "a": 854, + "d": 260, + "c": 4 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 120, + "d": 115, + "c": 8 + }, + { + "w": 1515283200, + "a": 11, + "d": 6, + "c": 2 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 11, + "d": 0, + "c": 1 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 89, + "d": 16, + "c": 1 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 31, + "d": 27, + "c": 2 + }, + { + "w": 1520121600, + "a": 27, + "d": 20, + "c": 2 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 453, + "d": 666, + "c": 2 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 49, + "d": 12, + "c": 1 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 54, + "d": 16, + "c": 3 + }, + { + "w": 1524355200, + "a": 1676, + "d": 1026, + "c": 3 + }, + { + "w": 1524960000, + "a": 77, + "d": 76, + "c": 3 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 17, + "d": 8, + "c": 1 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 3, + "d": 2, + "c": 1 + }, + { + "w": 1528588800, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1529193600, + "a": 85, + "d": 16, + "c": 2 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 62, + "d": 0, + "c": 1 + }, + { + "w": 1537660800, + "a": 61, + "d": 18, + "c": 1 + }, + { + "w": 1538265600, + "a": 726, + "d": 726, + "c": 3 + }, + { + "w": 1538870400, + "a": 30, + "d": 0, + "c": 1 + }, + { + "w": 1539475200, + "a": 125, + "d": 230, + "c": 3 + }, + { + "w": 1540080000, + "a": 15, + "d": 17, + "c": 4 + }, + { + "w": 1540684800, + "a": 6, + "d": 6, + "c": 2 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 120, + "d": 24, + "c": 1 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "yjbanov", + "id": 211513, + "node_id": "MDQ6VXNlcjIxMTUxMw==", + "avatar_url": "https://avatars1.githubusercontent.com/u/211513?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/yjbanov", + "html_url": "https://github.com/yjbanov", + "followers_url": "https://api.github.com/users/yjbanov/followers", + "following_url": "https://api.github.com/users/yjbanov/following{/other_user}", + "gists_url": "https://api.github.com/users/yjbanov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/yjbanov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/yjbanov/subscriptions", + "organizations_url": "https://api.github.com/users/yjbanov/orgs", + "repos_url": "https://api.github.com/users/yjbanov/repos", + "events_url": "https://api.github.com/users/yjbanov/events{/privacy}", + "received_events_url": "https://api.github.com/users/yjbanov/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 319, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 202, + "d": 10, + "c": 5 + }, + { + "w": 1520726400, + "a": 378, + "d": 220, + "c": 4 + }, + { + "w": 1521331200, + "a": 2117, + "d": 1672, + "c": 6 + }, + { + "w": 1521936000, + "a": 39, + "d": 2, + "c": 2 + }, + { + "w": 1522540800, + "a": 116, + "d": 59, + "c": 4 + }, + { + "w": 1523145600, + "a": 43, + "d": 12, + "c": 1 + }, + { + "w": 1523750400, + "a": 1135, + "d": 221, + "c": 4 + }, + { + "w": 1524355200, + "a": 41, + "d": 5, + "c": 2 + }, + { + "w": 1524960000, + "a": 300, + "d": 8, + "c": 2 + }, + { + "w": 1525564800, + "a": 163, + "d": 32, + "c": 3 + }, + { + "w": 1526169600, + "a": 22, + "d": 7, + "c": 1 + }, + { + "w": 1526774400, + "a": 289, + "d": 1, + "c": 1 + }, + { + "w": 1527379200, + "a": 149, + "d": 58, + "c": 2 + }, + { + "w": 1527984000, + "a": 428, + "d": 32, + "c": 3 + }, + { + "w": 1528588800, + "a": 572, + "d": 130, + "c": 7 + }, + { + "w": 1529193600, + "a": 758, + "d": 137, + "c": 5 + }, + { + "w": 1529798400, + "a": 118, + "d": 17, + "c": 2 + }, + { + "w": 1530403200, + "a": 1238, + "d": 1238, + "c": 2 + }, + { + "w": 1531008000, + "a": 2112, + "d": 233, + "c": 9 + }, + { + "w": 1531612800, + "a": 637, + "d": 114, + "c": 4 + }, + { + "w": 1532217600, + "a": 1060, + "d": 295, + "c": 12 + }, + { + "w": 1532822400, + "a": 1111, + "d": 199, + "c": 15 + }, + { + "w": 1533427200, + "a": 464, + "d": 80, + "c": 7 + }, + { + "w": 1534032000, + "a": 1316, + "d": 133, + "c": 9 + }, + { + "w": 1534636800, + "a": 1593, + "d": 108, + "c": 4 + }, + { + "w": 1535241600, + "a": 1375, + "d": 161, + "c": 7 + }, + { + "w": 1535846400, + "a": 1140, + "d": 2062, + "c": 9 + }, + { + "w": 1536451200, + "a": 1782, + "d": 412, + "c": 7 + }, + { + "w": 1537056000, + "a": 195, + "d": 205, + "c": 4 + }, + { + "w": 1537660800, + "a": 24, + "d": 5, + "c": 1 + }, + { + "w": 1538265600, + "a": 406, + "d": 90, + "c": 5 + }, + { + "w": 1538870400, + "a": 1052, + "d": 95, + "c": 3 + }, + { + "w": 1539475200, + "a": 99, + "d": 64, + "c": 1 + }, + { + "w": 1540080000, + "a": 266, + "d": 30, + "c": 4 + }, + { + "w": 1540684800, + "a": 2113, + "d": 2819, + "c": 4 + }, + { + "w": 1541289600, + "a": 3975, + "d": 4344, + "c": 19 + }, + { + "w": 1541894400, + "a": 415, + "d": 311, + "c": 5 + }, + { + "w": 1542499200, + "a": 154, + "d": 7, + "c": 2 + }, + { + "w": 1543104000, + "a": 146, + "d": 93, + "c": 1 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 800, + "d": 766, + "c": 10 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 102, + "d": 21, + "c": 1 + }, + { + "w": 1546732800, + "a": 635, + "d": 304, + "c": 8 + }, + { + "w": 1547337600, + "a": 1014, + "d": 27, + "c": 5 + }, + { + "w": 1547942400, + "a": 1375, + "d": 221, + "c": 9 + }, + { + "w": 1548547200, + "a": 242, + "d": 128, + "c": 5 + }, + { + "w": 1549152000, + "a": 1768, + "d": 1441, + "c": 7 + }, + { + "w": 1549756800, + "a": 2805, + "d": 1584, + "c": 10 + }, + { + "w": 1550361600, + "a": 79, + "d": 16, + "c": 4 + }, + { + "w": 1550966400, + "a": 690, + "d": 404, + "c": 9 + }, + { + "w": 1551571200, + "a": 2532, + "d": 2000, + "c": 10 + }, + { + "w": 1552176000, + "a": 544, + "d": 1341, + "c": 9 + }, + { + "w": 1552780800, + "a": 565, + "d": 186, + "c": 15 + }, + { + "w": 1553385600, + "a": 2998, + "d": 1866, + "c": 23 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 478, + "d": 23, + "c": 6 + } + ], + "author": { + "login": "jonahwilliams", + "id": 8975114, + "node_id": "MDQ6VXNlcjg5NzUxMTQ=", + "avatar_url": "https://avatars0.githubusercontent.com/u/8975114?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jonahwilliams", + "html_url": "https://github.com/jonahwilliams", + "followers_url": "https://api.github.com/users/jonahwilliams/followers", + "following_url": "https://api.github.com/users/jonahwilliams/following{/other_user}", + "gists_url": "https://api.github.com/users/jonahwilliams/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jonahwilliams/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jonahwilliams/subscriptions", + "organizations_url": "https://api.github.com/users/jonahwilliams/orgs", + "repos_url": "https://api.github.com/users/jonahwilliams/repos", + "events_url": "https://api.github.com/users/jonahwilliams/events{/privacy}", + "received_events_url": "https://api.github.com/users/jonahwilliams/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 325, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 12, + "d": 2, + "c": 1 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 4, + "d": 2, + "c": 2 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 77, + "d": 23, + "c": 2 + }, + { + "w": 1454803200, + "a": 9, + "d": 10, + "c": 5 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1463270400, + "a": 8, + "d": 4, + "c": 1 + }, + { + "w": 1463875200, + "a": 187, + "d": 104, + "c": 6 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 161, + "d": 96, + "c": 4 + }, + { + "w": 1465689600, + "a": 653, + "d": 97, + "c": 1 + }, + { + "w": 1466294400, + "a": 194, + "d": 139, + "c": 9 + }, + { + "w": 1466899200, + "a": 6, + "d": 19, + "c": 2 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 16, + "d": 2, + "c": 2 + }, + { + "w": 1470528000, + "a": 24, + "d": 14, + "c": 3 + }, + { + "w": 1471132800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1471737600, + "a": 23, + "d": 3, + "c": 2 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 197, + "d": 104, + "c": 3 + }, + { + "w": 1476576000, + "a": 36, + "d": 14, + "c": 1 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1478390400, + "a": 10, + "d": 9, + "c": 1 + }, + { + "w": 1478995200, + "a": 72, + "d": 47, + "c": 2 + }, + { + "w": 1479600000, + "a": 24, + "d": 15, + "c": 1 + }, + { + "w": 1480204800, + "a": 582, + "d": 43, + "c": 4 + }, + { + "w": 1480809600, + "a": 757, + "d": 99, + "c": 5 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 683, + "d": 632, + "c": 3 + }, + { + "w": 1483833600, + "a": 594, + "d": 369, + "c": 8 + }, + { + "w": 1484438400, + "a": 44, + "d": 8, + "c": 2 + }, + { + "w": 1485043200, + "a": 280, + "d": 1251, + "c": 5 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 307, + "d": 168, + "c": 12 + }, + { + "w": 1487462400, + "a": 385, + "d": 60, + "c": 3 + }, + { + "w": 1488067200, + "a": 233, + "d": 41, + "c": 3 + }, + { + "w": 1488672000, + "a": 12501, + "d": 320, + "c": 11 + }, + { + "w": 1489276800, + "a": 152, + "d": 103, + "c": 3 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 70, + "d": 23, + "c": 1 + }, + { + "w": 1491091200, + "a": 2, + "d": 1, + "c": 1 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1492905600, + "a": 308, + "d": 12205, + "c": 8 + }, + { + "w": 1493510400, + "a": 212, + "d": 340, + "c": 8 + }, + { + "w": 1494115200, + "a": 8, + "d": 3, + "c": 3 + }, + { + "w": 1494720000, + "a": 175, + "d": 126, + "c": 5 + }, + { + "w": 1495324800, + "a": 80, + "d": 3, + "c": 2 + }, + { + "w": 1495929600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1496534400, + "a": 103, + "d": 103, + "c": 3 + }, + { + "w": 1497139200, + "a": 137, + "d": 106, + "c": 9 + }, + { + "w": 1497744000, + "a": 13, + "d": 7, + "c": 3 + }, + { + "w": 1498348800, + "a": 23, + "d": 24, + "c": 4 + }, + { + "w": 1498953600, + "a": 6, + "d": 7, + "c": 1 + }, + { + "w": 1499558400, + "a": 49, + "d": 30, + "c": 9 + }, + { + "w": 1500163200, + "a": 22, + "d": 50, + "c": 6 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 38, + "d": 1, + "c": 1 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 253, + "d": 89, + "c": 6 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 58, + "d": 49, + "c": 1 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1509840000, + "a": 455, + "d": 442, + "c": 2 + }, + { + "w": 1510444800, + "a": 72, + "d": 54, + "c": 4 + }, + { + "w": 1511049600, + "a": 104, + "d": 42, + "c": 1 + }, + { + "w": 1511654400, + "a": 44, + "d": 65, + "c": 2 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 100, + "d": 219, + "c": 1 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 8, + "d": 9, + "c": 1 + }, + { + "w": 1517097600, + "a": 72, + "d": 32, + "c": 1 + }, + { + "w": 1517702400, + "a": 11, + "d": 4, + "c": 1 + }, + { + "w": 1518307200, + "a": 230, + "d": 522, + "c": 7 + }, + { + "w": 1518912000, + "a": 36, + "d": 8, + "c": 6 + }, + { + "w": 1519516800, + "a": 30, + "d": 15, + "c": 7 + }, + { + "w": 1520121600, + "a": 39, + "d": 23, + "c": 6 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 1137, + "d": 844, + "c": 6 + }, + { + "w": 1522540800, + "a": 66, + "d": 8, + "c": 3 + }, + { + "w": 1523145600, + "a": 191, + "d": 153, + "c": 2 + }, + { + "w": 1523750400, + "a": 140, + "d": 76, + "c": 14 + }, + { + "w": 1524355200, + "a": 337, + "d": 50, + "c": 6 + }, + { + "w": 1524960000, + "a": 1548, + "d": 246, + "c": 11 + }, + { + "w": 1525564800, + "a": 158, + "d": 31, + "c": 12 + }, + { + "w": 1526169600, + "a": 160, + "d": 48, + "c": 5 + }, + { + "w": 1526774400, + "a": 14, + "d": 5, + "c": 3 + }, + { + "w": 1527379200, + "a": 154, + "d": 55, + "c": 3 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 41662, + "d": 41660, + "c": 5 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 31, + "d": 15, + "c": 1 + }, + { + "w": 1534032000, + "a": 5, + "d": 0, + "c": 1 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 51, + "d": 51, + "c": 1 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 11, + "d": 5, + "c": 2 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 121, + "d": 85, + "c": 5 + }, + { + "w": 1541894400, + "a": 49, + "d": 2, + "c": 1 + }, + { + "w": 1542499200, + "a": 5, + "d": 5, + "c": 4 + }, + { + "w": 1543104000, + "a": 3, + "d": 2, + "c": 2 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 41, + "d": 15, + "c": 2 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 21, + "d": 6, + "c": 1 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 169, + "d": 193, + "c": 4 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 170, + "d": 774, + "c": 1 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 45, + "d": 23, + "c": 5 + } + ], + "author": { + "login": "tvolkert", + "id": 15253456, + "node_id": "MDQ6VXNlcjE1MjUzNDU2", + "avatar_url": "https://avatars0.githubusercontent.com/u/15253456?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tvolkert", + "html_url": "https://github.com/tvolkert", + "followers_url": "https://api.github.com/users/tvolkert/followers", + "following_url": "https://api.github.com/users/tvolkert/following{/other_user}", + "gists_url": "https://api.github.com/users/tvolkert/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tvolkert/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tvolkert/subscriptions", + "organizations_url": "https://api.github.com/users/tvolkert/orgs", + "repos_url": "https://api.github.com/users/tvolkert/repos", + "events_url": "https://api.github.com/users/tvolkert/events{/privacy}", + "received_events_url": "https://api.github.com/users/tvolkert/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 337, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 132, + "d": 19, + "c": 4 + }, + { + "w": 1485648000, + "a": 257, + "d": 48, + "c": 3 + }, + { + "w": 1486252800, + "a": 125, + "d": 22, + "c": 2 + }, + { + "w": 1486857600, + "a": 213, + "d": 60, + "c": 4 + }, + { + "w": 1487462400, + "a": 381, + "d": 153, + "c": 3 + }, + { + "w": 1488067200, + "a": 5379, + "d": 4706, + "c": 3 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 2, + "d": 16, + "c": 1 + }, + { + "w": 1490486400, + "a": 73, + "d": 37, + "c": 3 + }, + { + "w": 1491091200, + "a": 590, + "d": 356, + "c": 5 + }, + { + "w": 1491696000, + "a": 1569, + "d": 1289, + "c": 6 + }, + { + "w": 1492300800, + "a": 59, + "d": 44, + "c": 4 + }, + { + "w": 1492905600, + "a": 1020, + "d": 101, + "c": 5 + }, + { + "w": 1493510400, + "a": 180, + "d": 918, + "c": 9 + }, + { + "w": 1494115200, + "a": 188, + "d": 80, + "c": 4 + }, + { + "w": 1494720000, + "a": 948, + "d": 369, + "c": 4 + }, + { + "w": 1495324800, + "a": 2143, + "d": 1869, + "c": 4 + }, + { + "w": 1495929600, + "a": 487, + "d": 107, + "c": 4 + }, + { + "w": 1496534400, + "a": 247, + "d": 56, + "c": 1 + }, + { + "w": 1497139200, + "a": 968, + "d": 159, + "c": 6 + }, + { + "w": 1497744000, + "a": 101, + "d": 4, + "c": 1 + }, + { + "w": 1498348800, + "a": 175, + "d": 28, + "c": 4 + }, + { + "w": 1498953600, + "a": 127, + "d": 19, + "c": 2 + }, + { + "w": 1499558400, + "a": 486, + "d": 192, + "c": 3 + }, + { + "w": 1500163200, + "a": 426, + "d": 66, + "c": 1 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1501977600, + "a": 124, + "d": 16, + "c": 3 + }, + { + "w": 1502582400, + "a": 234, + "d": 40, + "c": 7 + }, + { + "w": 1503187200, + "a": 35, + "d": 7, + "c": 3 + }, + { + "w": 1503792000, + "a": 5, + "d": 2, + "c": 1 + }, + { + "w": 1504396800, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1505001600, + "a": 386, + "d": 66, + "c": 1 + }, + { + "w": 1505606400, + "a": 2092, + "d": 1164, + "c": 7 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 340, + "d": 127, + "c": 6 + }, + { + "w": 1508630400, + "a": 106, + "d": 3, + "c": 1 + }, + { + "w": 1509235200, + "a": 861, + "d": 28, + "c": 3 + }, + { + "w": 1509840000, + "a": 163, + "d": 5, + "c": 4 + }, + { + "w": 1510444800, + "a": 93, + "d": 36, + "c": 2 + }, + { + "w": 1511049600, + "a": 22, + "d": 0, + "c": 2 + }, + { + "w": 1511654400, + "a": 498, + "d": 293, + "c": 6 + }, + { + "w": 1512259200, + "a": 664, + "d": 163, + "c": 4 + }, + { + "w": 1512864000, + "a": 282, + "d": 24, + "c": 7 + }, + { + "w": 1513468800, + "a": 595, + "d": 162, + "c": 3 + }, + { + "w": 1514073600, + "a": 279, + "d": 254, + "c": 2 + }, + { + "w": 1514678400, + "a": 94, + "d": 3, + "c": 2 + }, + { + "w": 1515283200, + "a": 34, + "d": 6, + "c": 2 + }, + { + "w": 1515888000, + "a": 42, + "d": 20, + "c": 1 + }, + { + "w": 1516492800, + "a": 2862, + "d": 359, + "c": 2 + }, + { + "w": 1517097600, + "a": 530, + "d": 128, + "c": 3 + }, + { + "w": 1517702400, + "a": 205, + "d": 99, + "c": 8 + }, + { + "w": 1518307200, + "a": 360, + "d": 120, + "c": 9 + }, + { + "w": 1518912000, + "a": 243, + "d": 18, + "c": 4 + }, + { + "w": 1519516800, + "a": 96, + "d": 13, + "c": 4 + }, + { + "w": 1520121600, + "a": 119, + "d": 20, + "c": 2 + }, + { + "w": 1520726400, + "a": 75, + "d": 15, + "c": 3 + }, + { + "w": 1521331200, + "a": 2443, + "d": 216, + "c": 11 + }, + { + "w": 1521936000, + "a": 137, + "d": 83, + "c": 6 + }, + { + "w": 1522540800, + "a": 153, + "d": 43, + "c": 4 + }, + { + "w": 1523145600, + "a": 18, + "d": 18, + "c": 2 + }, + { + "w": 1523750400, + "a": 152, + "d": 52, + "c": 5 + }, + { + "w": 1524355200, + "a": 91, + "d": 79, + "c": 5 + }, + { + "w": 1524960000, + "a": 202, + "d": 153, + "c": 6 + }, + { + "w": 1525564800, + "a": 112, + "d": 30, + "c": 5 + }, + { + "w": 1526169600, + "a": 73, + "d": 17, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 10, + "d": 7, + "c": 1 + }, + { + "w": 1528588800, + "a": 75, + "d": 18, + "c": 3 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 896, + "d": 862, + "c": 1 + }, + { + "w": 1531008000, + "a": 291, + "d": 33, + "c": 5 + }, + { + "w": 1531612800, + "a": 6, + "d": 3, + "c": 3 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 868, + "d": 343, + "c": 3 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 487, + "d": 65, + "c": 7 + }, + { + "w": 1534636800, + "a": 780, + "d": 234, + "c": 1 + }, + { + "w": 1535241600, + "a": 2974, + "d": 429, + "c": 5 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 414, + "d": 109, + "c": 5 + }, + { + "w": 1538265600, + "a": 291, + "d": 154, + "c": 6 + }, + { + "w": 1538870400, + "a": 15, + "d": 1, + "c": 2 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 2486, + "d": 209, + "c": 5 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1541894400, + "a": 528, + "d": 18, + "c": 2 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 95, + "d": 3, + "c": 2 + }, + { + "w": 1544918400, + "a": 3510, + "d": 1076, + "c": 5 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 7, + "d": 7, + "c": 2 + }, + { + "w": 1546732800, + "a": 25, + "d": 11, + "c": 4 + }, + { + "w": 1547337600, + "a": 8, + "d": 317, + "c": 1 + }, + { + "w": 1547942400, + "a": 121, + "d": 8, + "c": 3 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 1520, + "d": 946, + "c": 7 + }, + { + "w": 1549756800, + "a": 1304, + "d": 200, + "c": 4 + }, + { + "w": 1550361600, + "a": 481, + "d": 1340, + "c": 4 + }, + { + "w": 1550966400, + "a": 1257, + "d": 329, + "c": 2 + }, + { + "w": 1551571200, + "a": 443, + "d": 57, + "c": 3 + }, + { + "w": 1552176000, + "a": 107, + "d": 4, + "c": 1 + }, + { + "w": 1552780800, + "a": 20, + "d": 5, + "c": 2 + }, + { + "w": 1553385600, + "a": 30152, + "d": 29852, + "c": 4 + }, + { + "w": 1553990400, + "a": 61, + "d": 40, + "c": 5 + }, + { + "w": 1554595200, + "a": 787, + "d": 254, + "c": 3 + } + ], + "author": { + "login": "xster", + "id": 156888, + "node_id": "MDQ6VXNlcjE1Njg4OA==", + "avatar_url": "https://avatars2.githubusercontent.com/u/156888?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/xster", + "html_url": "https://github.com/xster", + "followers_url": "https://api.github.com/users/xster/followers", + "following_url": "https://api.github.com/users/xster/following{/other_user}", + "gists_url": "https://api.github.com/users/xster/gists{/gist_id}", + "starred_url": "https://api.github.com/users/xster/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/xster/subscriptions", + "organizations_url": "https://api.github.com/users/xster/orgs", + "repos_url": "https://api.github.com/users/xster/repos", + "events_url": "https://api.github.com/users/xster/events{/privacy}", + "received_events_url": "https://api.github.com/users/xster/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 356, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 173, + "d": 18, + "c": 2 + }, + { + "w": 1442707200, + "a": 297, + "d": 100, + "c": 2 + }, + { + "w": 1443312000, + "a": 11, + "d": 9, + "c": 2 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 277, + "d": 227, + "c": 5 + }, + { + "w": 1445126400, + "a": 65, + "d": 6, + "c": 4 + }, + { + "w": 1445731200, + "a": 14, + "d": 20, + "c": 4 + }, + { + "w": 1446336000, + "a": 77, + "d": 49, + "c": 2 + }, + { + "w": 1446940800, + "a": 161, + "d": 91, + "c": 4 + }, + { + "w": 1447545600, + "a": 456, + "d": 88, + "c": 3 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 214, + "d": 4, + "c": 4 + }, + { + "w": 1449360000, + "a": 202, + "d": 142, + "c": 4 + }, + { + "w": 1449964800, + "a": 31, + "d": 17, + "c": 5 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 58, + "d": 8, + "c": 1 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 39, + "d": 60, + "c": 2 + }, + { + "w": 1454198400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1454803200, + "a": 395, + "d": 236, + "c": 3 + }, + { + "w": 1455408000, + "a": 6, + "d": 0, + "c": 1 + }, + { + "w": 1456012800, + "a": 10, + "d": 3, + "c": 1 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 285, + "d": 3, + "c": 3 + }, + { + "w": 1457827200, + "a": 60, + "d": 0, + "c": 1 + }, + { + "w": 1458432000, + "a": 464, + "d": 85, + "c": 9 + }, + { + "w": 1459036800, + "a": 33, + "d": 5, + "c": 1 + }, + { + "w": 1459641600, + "a": 148, + "d": 40, + "c": 3 + }, + { + "w": 1460246400, + "a": 99, + "d": 13, + "c": 2 + }, + { + "w": 1460851200, + "a": 89, + "d": 69, + "c": 2 + }, + { + "w": 1461456000, + "a": 197, + "d": 13, + "c": 2 + }, + { + "w": 1462060800, + "a": 143, + "d": 63, + "c": 3 + }, + { + "w": 1462665600, + "a": 338, + "d": 936, + "c": 7 + }, + { + "w": 1463270400, + "a": 23, + "d": 24, + "c": 3 + }, + { + "w": 1463875200, + "a": 28, + "d": 22, + "c": 2 + }, + { + "w": 1464480000, + "a": 105, + "d": 19, + "c": 3 + }, + { + "w": 1465084800, + "a": 29, + "d": 89, + "c": 3 + }, + { + "w": 1465689600, + "a": 2, + "d": 584, + "c": 1 + }, + { + "w": 1466294400, + "a": 18, + "d": 67, + "c": 5 + }, + { + "w": 1466899200, + "a": 6, + "d": 2, + "c": 1 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 3, + "d": 5, + "c": 3 + }, + { + "w": 1468713600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1469318400, + "a": 75, + "d": 11, + "c": 4 + }, + { + "w": 1469923200, + "a": 283, + "d": 45, + "c": 5 + }, + { + "w": 1470528000, + "a": 90, + "d": 24, + "c": 3 + }, + { + "w": 1471132800, + "a": 34, + "d": 39, + "c": 5 + }, + { + "w": 1471737600, + "a": 4, + "d": 4, + "c": 4 + }, + { + "w": 1472342400, + "a": 44, + "d": 3, + "c": 3 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 5, + "d": 5, + "c": 2 + }, + { + "w": 1475366400, + "a": 89, + "d": 45, + "c": 5 + }, + { + "w": 1475971200, + "a": 6, + "d": 6, + "c": 2 + }, + { + "w": 1476576000, + "a": 55, + "d": 148, + "c": 5 + }, + { + "w": 1477180800, + "a": 98, + "d": 46, + "c": 4 + }, + { + "w": 1477785600, + "a": 19, + "d": 17, + "c": 5 + }, + { + "w": 1478390400, + "a": 62, + "d": 45, + "c": 1 + }, + { + "w": 1478995200, + "a": 21, + "d": 1, + "c": 1 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 193, + "d": 81, + "c": 3 + }, + { + "w": 1480809600, + "a": 148, + "d": 2, + "c": 4 + }, + { + "w": 1481414400, + "a": 195, + "d": 2, + "c": 3 + }, + { + "w": 1482019200, + "a": 7, + "d": 1, + "c": 1 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 99, + "d": 6, + "c": 5 + }, + { + "w": 1484438400, + "a": 5, + "d": 2, + "c": 1 + }, + { + "w": 1485043200, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1485648000, + "a": 25, + "d": 18, + "c": 5 + }, + { + "w": 1486252800, + "a": 82, + "d": 0, + "c": 4 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1488067200, + "a": 241, + "d": 9, + "c": 3 + }, + { + "w": 1488672000, + "a": 2, + "d": 0, + "c": 1 + }, + { + "w": 1489276800, + "a": 74, + "d": 12, + "c": 4 + }, + { + "w": 1489881600, + "a": 53, + "d": 23, + "c": 2 + }, + { + "w": 1490486400, + "a": 28, + "d": 67, + "c": 4 + }, + { + "w": 1491091200, + "a": 121, + "d": 68, + "c": 6 + }, + { + "w": 1491696000, + "a": 74, + "d": 17, + "c": 4 + }, + { + "w": 1492300800, + "a": 19, + "d": 2, + "c": 2 + }, + { + "w": 1492905600, + "a": 6, + "d": 4, + "c": 4 + }, + { + "w": 1493510400, + "a": 201, + "d": 194, + "c": 4 + }, + { + "w": 1494115200, + "a": 172, + "d": 24, + "c": 7 + }, + { + "w": 1494720000, + "a": 2607, + "d": 1639, + "c": 2 + }, + { + "w": 1495324800, + "a": 54, + "d": 4, + "c": 2 + }, + { + "w": 1495929600, + "a": 183, + "d": 9, + "c": 3 + }, + { + "w": 1496534400, + "a": 53, + "d": 13, + "c": 1 + }, + { + "w": 1497139200, + "a": 11, + "d": 1, + "c": 1 + }, + { + "w": 1497744000, + "a": 144, + "d": 42, + "c": 8 + }, + { + "w": 1498348800, + "a": 23, + "d": 1, + "c": 1 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 87, + "d": 15, + "c": 4 + }, + { + "w": 1500163200, + "a": 51, + "d": 6, + "c": 3 + }, + { + "w": 1500768000, + "a": 97, + "d": 17, + "c": 4 + }, + { + "w": 1501372800, + "a": 73, + "d": 12, + "c": 5 + }, + { + "w": 1501977600, + "a": 32, + "d": 3, + "c": 3 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 2, + "d": 49, + "c": 3 + }, + { + "w": 1503792000, + "a": 48, + "d": 4, + "c": 2 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 4, + "d": 2, + "c": 1 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 12, + "d": 8, + "c": 5 + }, + { + "w": 1508630400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1509235200, + "a": 89, + "d": 195, + "c": 4 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 2, + "d": 3, + "c": 1 + }, + { + "w": 1512864000, + "a": 6, + "d": 4, + "c": 2 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 14, + "d": 15, + "c": 4 + }, + { + "w": 1515888000, + "a": 20, + "d": 37, + "c": 2 + }, + { + "w": 1516492800, + "a": 1, + "d": 2, + "c": 2 + }, + { + "w": 1517097600, + "a": 82, + "d": 14, + "c": 1 + }, + { + "w": 1517702400, + "a": 15, + "d": 10, + "c": 3 + }, + { + "w": 1518307200, + "a": 9, + "d": 1, + "c": 2 + }, + { + "w": 1518912000, + "a": 28, + "d": 27, + "c": 2 + }, + { + "w": 1519516800, + "a": 38, + "d": 14, + "c": 2 + }, + { + "w": 1520121600, + "a": 34, + "d": 9, + "c": 7 + }, + { + "w": 1520726400, + "a": 751, + "d": 715, + "c": 6 + }, + { + "w": 1521331200, + "a": 8, + "d": 2, + "c": 2 + }, + { + "w": 1521936000, + "a": 28, + "d": 3, + "c": 1 + }, + { + "w": 1522540800, + "a": 11, + "d": 11, + "c": 1 + }, + { + "w": 1523145600, + "a": 39, + "d": 7, + "c": 1 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 5, + "d": 30, + "c": 2 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 70, + "d": 8, + "c": 2 + }, + { + "w": 1526169600, + "a": 31, + "d": 6, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 4, + "d": 4, + "c": 3 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 3, + "d": 3, + "c": 2 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 8, + "d": 9, + "c": 3 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 37, + "d": 3, + "c": 1 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 85, + "d": 78, + "c": 3 + }, + { + "w": 1538870400, + "a": 19, + "d": 1, + "c": 3 + }, + { + "w": 1539475200, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1540080000, + "a": 97, + "d": 25, + "c": 3 + }, + { + "w": 1540684800, + "a": 120, + "d": 0, + "c": 2 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 23, + "d": 16, + "c": 1 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 7, + "d": 0, + "c": 1 + }, + { + "w": 1547337600, + "a": 43, + "d": 11, + "c": 2 + }, + { + "w": 1547942400, + "a": 61, + "d": 61, + "c": 2 + }, + { + "w": 1548547200, + "a": 11, + "d": 0, + "c": 1 + }, + { + "w": 1549152000, + "a": 0, + "d": 3, + "c": 1 + }, + { + "w": 1549756800, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1551571200, + "a": 18, + "d": 34, + "c": 3 + }, + { + "w": 1552176000, + "a": 6, + "d": 1, + "c": 1 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "jason-simmons", + "id": 14226037, + "node_id": "MDQ6VXNlcjE0MjI2MDM3", + "avatar_url": "https://avatars3.githubusercontent.com/u/14226037?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jason-simmons", + "html_url": "https://github.com/jason-simmons", + "followers_url": "https://api.github.com/users/jason-simmons/followers", + "following_url": "https://api.github.com/users/jason-simmons/following{/other_user}", + "gists_url": "https://api.github.com/users/jason-simmons/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jason-simmons/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jason-simmons/subscriptions", + "organizations_url": "https://api.github.com/users/jason-simmons/orgs", + "repos_url": "https://api.github.com/users/jason-simmons/repos", + "events_url": "https://api.github.com/users/jason-simmons/events{/privacy}", + "received_events_url": "https://api.github.com/users/jason-simmons/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 370, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485648000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486252800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1486857600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492905600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1493510400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494115200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1534032000, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1534636800, + "a": 8, + "d": 8, + "c": 8 + }, + { + "w": 1535241600, + "a": 10, + "d": 10, + "c": 10 + }, + { + "w": 1535846400, + "a": 8, + "d": 8, + "c": 8 + }, + { + "w": 1536451200, + "a": 9, + "d": 9, + "c": 9 + }, + { + "w": 1537056000, + "a": 8, + "d": 8, + "c": 8 + }, + { + "w": 1537660800, + "a": 6, + "d": 6, + "c": 6 + }, + { + "w": 1538265600, + "a": 9, + "d": 9, + "c": 9 + }, + { + "w": 1538870400, + "a": 16, + "d": 16, + "c": 16 + }, + { + "w": 1539475200, + "a": 8, + "d": 8, + "c": 8 + }, + { + "w": 1540080000, + "a": 7, + "d": 7, + "c": 7 + }, + { + "w": 1540684800, + "a": 9, + "d": 9, + "c": 9 + }, + { + "w": 1541289600, + "a": 15, + "d": 15, + "c": 15 + }, + { + "w": 1541894400, + "a": 41, + "d": 41, + "c": 41 + }, + { + "w": 1542499200, + "a": 8, + "d": 8, + "c": 8 + }, + { + "w": 1543104000, + "a": 7, + "d": 7, + "c": 7 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 7, + "d": 7, + "c": 7 + }, + { + "w": 1544918400, + "a": 9, + "d": 9, + "c": 9 + }, + { + "w": 1545523200, + "a": 5, + "d": 5, + "c": 5 + }, + { + "w": 1546128000, + "a": 8, + "d": 8, + "c": 8 + }, + { + "w": 1546732800, + "a": 3, + "d": 3, + "c": 3 + }, + { + "w": 1547337600, + "a": 9, + "d": 9, + "c": 9 + }, + { + "w": 1547942400, + "a": 11, + "d": 11, + "c": 11 + }, + { + "w": 1548547200, + "a": 4, + "d": 4, + "c": 4 + }, + { + "w": 1549152000, + "a": 15, + "d": 15, + "c": 15 + }, + { + "w": 1549756800, + "a": 11, + "d": 11, + "c": 11 + }, + { + "w": 1550361600, + "a": 6, + "d": 6, + "c": 6 + }, + { + "w": 1550966400, + "a": 15, + "d": 15, + "c": 15 + }, + { + "w": 1551571200, + "a": 15, + "d": 15, + "c": 15 + }, + { + "w": 1552176000, + "a": 14, + "d": 14, + "c": 14 + }, + { + "w": 1552780800, + "a": 18, + "d": 18, + "c": 18 + }, + { + "w": 1553385600, + "a": 17, + "d": 17, + "c": 17 + }, + { + "w": 1553990400, + "a": 25, + "d": 25, + "c": 25 + }, + { + "w": 1554595200, + "a": 7, + "d": 7, + "c": 7 + } + ], + "author": { + "login": "engine-flutter-autoroll", + "id": 42042535, + "node_id": "MDQ6VXNlcjQyMDQyNTM1", + "avatar_url": "https://avatars2.githubusercontent.com/u/42042535?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/engine-flutter-autoroll", + "html_url": "https://github.com/engine-flutter-autoroll", + "followers_url": "https://api.github.com/users/engine-flutter-autoroll/followers", + "following_url": "https://api.github.com/users/engine-flutter-autoroll/following{/other_user}", + "gists_url": "https://api.github.com/users/engine-flutter-autoroll/gists{/gist_id}", + "starred_url": "https://api.github.com/users/engine-flutter-autoroll/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/engine-flutter-autoroll/subscriptions", + "organizations_url": "https://api.github.com/users/engine-flutter-autoroll/orgs", + "repos_url": "https://api.github.com/users/engine-flutter-autoroll/repos", + "events_url": "https://api.github.com/users/engine-flutter-autoroll/events{/privacy}", + "received_events_url": "https://api.github.com/users/engine-flutter-autoroll/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 420, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474761600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475366400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1475971200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477785600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 24, + "d": 0, + "c": 1 + }, + { + "w": 1483833600, + "a": 282, + "d": 158, + "c": 5 + }, + { + "w": 1484438400, + "a": 58, + "d": 19, + "c": 3 + }, + { + "w": 1485043200, + "a": 340, + "d": 315, + "c": 9 + }, + { + "w": 1485648000, + "a": 45, + "d": 42, + "c": 7 + }, + { + "w": 1486252800, + "a": 599, + "d": 256, + "c": 13 + }, + { + "w": 1486857600, + "a": 1327, + "d": 1100, + "c": 13 + }, + { + "w": 1487462400, + "a": 117, + "d": 108, + "c": 12 + }, + { + "w": 1488067200, + "a": 589, + "d": 410, + "c": 15 + }, + { + "w": 1488672000, + "a": 116, + "d": 215, + "c": 12 + }, + { + "w": 1489276800, + "a": 220, + "d": 163, + "c": 11 + }, + { + "w": 1489881600, + "a": 335, + "d": 78, + "c": 8 + }, + { + "w": 1490486400, + "a": 19, + "d": 11, + "c": 2 + }, + { + "w": 1491091200, + "a": 36, + "d": 41, + "c": 3 + }, + { + "w": 1491696000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1492300800, + "a": 3, + "d": 1, + "c": 1 + }, + { + "w": 1492905600, + "a": 135, + "d": 27, + "c": 5 + }, + { + "w": 1493510400, + "a": 31, + "d": 4, + "c": 2 + }, + { + "w": 1494115200, + "a": 138, + "d": 121, + "c": 2 + }, + { + "w": 1494720000, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1495324800, + "a": 6, + "d": 4, + "c": 4 + }, + { + "w": 1495929600, + "a": 569, + "d": 187, + "c": 4 + }, + { + "w": 1496534400, + "a": 17, + "d": 4, + "c": 5 + }, + { + "w": 1497139200, + "a": 420, + "d": 96, + "c": 9 + }, + { + "w": 1497744000, + "a": 765, + "d": 168, + "c": 4 + }, + { + "w": 1498348800, + "a": 3, + "d": 1, + "c": 1 + }, + { + "w": 1498953600, + "a": 150, + "d": 39, + "c": 5 + }, + { + "w": 1499558400, + "a": 292, + "d": 103, + "c": 9 + }, + { + "w": 1500163200, + "a": 371, + "d": 22, + "c": 6 + }, + { + "w": 1500768000, + "a": 2, + "d": 7, + "c": 1 + }, + { + "w": 1501372800, + "a": 419, + "d": 60, + "c": 6 + }, + { + "w": 1501977600, + "a": 353, + "d": 72, + "c": 5 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 913, + "d": 133, + "c": 4 + }, + { + "w": 1503792000, + "a": 826, + "d": 56, + "c": 6 + }, + { + "w": 1504396800, + "a": 377, + "d": 27, + "c": 6 + }, + { + "w": 1505001600, + "a": 57, + "d": 22, + "c": 2 + }, + { + "w": 1505606400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1506211200, + "a": 134, + "d": 18, + "c": 4 + }, + { + "w": 1506816000, + "a": 564, + "d": 145, + "c": 5 + }, + { + "w": 1507420800, + "a": 106, + "d": 33, + "c": 2 + }, + { + "w": 1508025600, + "a": 2528, + "d": 1784, + "c": 5 + }, + { + "w": 1508630400, + "a": 1102, + "d": 297, + "c": 4 + }, + { + "w": 1509235200, + "a": 721, + "d": 39, + "c": 7 + }, + { + "w": 1509840000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1510444800, + "a": 50, + "d": 1, + "c": 1 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 176, + "d": 154, + "c": 1 + }, + { + "w": 1512259200, + "a": 341, + "d": 16, + "c": 2 + }, + { + "w": 1512864000, + "a": 1062, + "d": 224, + "c": 4 + }, + { + "w": 1513468800, + "a": 239, + "d": 32, + "c": 5 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 635, + "d": 139, + "c": 4 + }, + { + "w": 1515283200, + "a": 1195, + "d": 803, + "c": 6 + }, + { + "w": 1515888000, + "a": 280, + "d": 34, + "c": 5 + }, + { + "w": 1516492800, + "a": 932, + "d": 164, + "c": 7 + }, + { + "w": 1517097600, + "a": 589, + "d": 192, + "c": 2 + }, + { + "w": 1517702400, + "a": 562, + "d": 217, + "c": 3 + }, + { + "w": 1518307200, + "a": 186, + "d": 13, + "c": 2 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 600, + "d": 93, + "c": 6 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 99, + "d": 5, + "c": 1 + }, + { + "w": 1521936000, + "a": 45, + "d": 7, + "c": 1 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 106, + "d": 20, + "c": 1 + }, + { + "w": 1523750400, + "a": 296, + "d": 21, + "c": 4 + }, + { + "w": 1524355200, + "a": 5, + "d": 4, + "c": 2 + }, + { + "w": 1524960000, + "a": 3807, + "d": 891, + "c": 1 + }, + { + "w": 1525564800, + "a": 94, + "d": 21, + "c": 3 + }, + { + "w": 1526169600, + "a": 14, + "d": 28, + "c": 3 + }, + { + "w": 1526774400, + "a": 1749, + "d": 102, + "c": 4 + }, + { + "w": 1527379200, + "a": 112, + "d": 2, + "c": 4 + }, + { + "w": 1527984000, + "a": 1539, + "d": 105, + "c": 3 + }, + { + "w": 1528588800, + "a": 521, + "d": 23, + "c": 4 + }, + { + "w": 1529193600, + "a": 103, + "d": 10, + "c": 2 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 16, + "d": 2, + "c": 1 + }, + { + "w": 1531008000, + "a": 81, + "d": 2, + "c": 1 + }, + { + "w": 1531612800, + "a": 10, + "d": 8, + "c": 3 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 254, + "d": 76, + "c": 9 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 448, + "d": 394, + "c": 3 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 18, + "d": 261, + "c": 3 + }, + { + "w": 1535846400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1536451200, + "a": 11, + "d": 11, + "c": 7 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 20, + "d": 1, + "c": 1 + }, + { + "w": 1538265600, + "a": 8, + "d": 2, + "c": 2 + }, + { + "w": 1538870400, + "a": 208, + "d": 186, + "c": 1 + }, + { + "w": 1539475200, + "a": 179, + "d": 201, + "c": 2 + }, + { + "w": 1540080000, + "a": 235, + "d": 155, + "c": 3 + }, + { + "w": 1540684800, + "a": 8, + "d": 8, + "c": 2 + }, + { + "w": 1541289600, + "a": 6, + "d": 3, + "c": 3 + }, + { + "w": 1541894400, + "a": 4, + "d": 4, + "c": 4 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 70, + "d": 0, + "c": 1 + }, + { + "w": 1544313600, + "a": 381, + "d": 38, + "c": 9 + }, + { + "w": 1544918400, + "a": 130, + "d": 77, + "c": 1 + }, + { + "w": 1545523200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1546128000, + "a": 39, + "d": 5, + "c": 3 + }, + { + "w": 1546732800, + "a": 370, + "d": 526, + "c": 3 + }, + { + "w": 1547337600, + "a": 1292, + "d": 341, + "c": 7 + }, + { + "w": 1547942400, + "a": 16, + "d": 4, + "c": 2 + }, + { + "w": 1548547200, + "a": 454, + "d": 164, + "c": 6 + }, + { + "w": 1549152000, + "a": 84, + "d": 11, + "c": 3 + }, + { + "w": 1549756800, + "a": 219, + "d": 80, + "c": 4 + }, + { + "w": 1550361600, + "a": 226, + "d": 37, + "c": 5 + }, + { + "w": 1550966400, + "a": 443, + "d": 317, + "c": 4 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 486, + "d": 105, + "c": 4 + }, + { + "w": 1552780800, + "a": 4, + "d": 4, + "c": 2 + }, + { + "w": 1553385600, + "a": 561, + "d": 9, + "c": 1 + }, + { + "w": 1553990400, + "a": 196, + "d": 27, + "c": 7 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "goderbauer", + "id": 1227763, + "node_id": "MDQ6VXNlcjEyMjc3NjM=", + "avatar_url": "https://avatars2.githubusercontent.com/u/1227763?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/goderbauer", + "html_url": "https://github.com/goderbauer", + "followers_url": "https://api.github.com/users/goderbauer/followers", + "following_url": "https://api.github.com/users/goderbauer/following{/other_user}", + "gists_url": "https://api.github.com/users/goderbauer/gists{/gist_id}", + "starred_url": "https://api.github.com/users/goderbauer/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/goderbauer/subscriptions", + "organizations_url": "https://api.github.com/users/goderbauer/orgs", + "repos_url": "https://api.github.com/users/goderbauer/repos", + "events_url": "https://api.github.com/users/goderbauer/events{/privacy}", + "received_events_url": "https://api.github.com/users/goderbauer/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 496, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 395, + "d": 27, + "c": 9 + }, + { + "w": 1439078400, + "a": 40, + "d": 26, + "c": 5 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 17, + "d": 7, + "c": 1 + }, + { + "w": 1441497600, + "a": 26, + "d": 29, + "c": 1 + }, + { + "w": 1442102400, + "a": 3, + "d": 5, + "c": 1 + }, + { + "w": 1442707200, + "a": 48, + "d": 23, + "c": 3 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 37, + "d": 27, + "c": 8 + }, + { + "w": 1444521600, + "a": 55, + "d": 43, + "c": 1 + }, + { + "w": 1445126400, + "a": 59, + "d": 22, + "c": 6 + }, + { + "w": 1445731200, + "a": 105, + "d": 53, + "c": 8 + }, + { + "w": 1446336000, + "a": 588, + "d": 101, + "c": 5 + }, + { + "w": 1446940800, + "a": 57, + "d": 22, + "c": 6 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 42, + "d": 41, + "c": 3 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 154, + "d": 73, + "c": 7 + }, + { + "w": 1450569600, + "a": 29, + "d": 30, + "c": 3 + }, + { + "w": 1451174400, + "a": 213, + "d": 206, + "c": 2 + }, + { + "w": 1451779200, + "a": 27, + "d": 12, + "c": 2 + }, + { + "w": 1452384000, + "a": 7, + "d": 248, + "c": 3 + }, + { + "w": 1452988800, + "a": 2182, + "d": 1558, + "c": 12 + }, + { + "w": 1453593600, + "a": 1288, + "d": 931, + "c": 14 + }, + { + "w": 1454198400, + "a": 1132, + "d": 684, + "c": 14 + }, + { + "w": 1454803200, + "a": 986, + "d": 685, + "c": 12 + }, + { + "w": 1455408000, + "a": 2266, + "d": 1742, + "c": 6 + }, + { + "w": 1456012800, + "a": 817, + "d": 436, + "c": 20 + }, + { + "w": 1456617600, + "a": 263, + "d": 184, + "c": 10 + }, + { + "w": 1457222400, + "a": 766, + "d": 570, + "c": 17 + }, + { + "w": 1457827200, + "a": 842, + "d": 656, + "c": 9 + }, + { + "w": 1458432000, + "a": 884, + "d": 623, + "c": 11 + }, + { + "w": 1459036800, + "a": 272, + "d": 105, + "c": 9 + }, + { + "w": 1459641600, + "a": 724, + "d": 526, + "c": 13 + }, + { + "w": 1460246400, + "a": 1600, + "d": 1231, + "c": 10 + }, + { + "w": 1460851200, + "a": 304, + "d": 221, + "c": 11 + }, + { + "w": 1461456000, + "a": 1649, + "d": 902, + "c": 15 + }, + { + "w": 1462060800, + "a": 803, + "d": 533, + "c": 12 + }, + { + "w": 1462665600, + "a": 1056, + "d": 629, + "c": 10 + }, + { + "w": 1463270400, + "a": 115, + "d": 31, + "c": 6 + }, + { + "w": 1463875200, + "a": 1158, + "d": 809, + "c": 12 + }, + { + "w": 1464480000, + "a": 102, + "d": 55, + "c": 6 + }, + { + "w": 1465084800, + "a": 912, + "d": 499, + "c": 9 + }, + { + "w": 1465689600, + "a": 244, + "d": 72, + "c": 3 + }, + { + "w": 1466294400, + "a": 54, + "d": 15, + "c": 4 + }, + { + "w": 1466899200, + "a": 26, + "d": 18, + "c": 2 + }, + { + "w": 1467504000, + "a": 69, + "d": 13, + "c": 1 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 488, + "d": 76, + "c": 3 + }, + { + "w": 1469318400, + "a": 58, + "d": 50, + "c": 3 + }, + { + "w": 1469923200, + "a": 62, + "d": 64, + "c": 2 + }, + { + "w": 1470528000, + "a": 299, + "d": 166, + "c": 6 + }, + { + "w": 1471132800, + "a": 77, + "d": 53, + "c": 6 + }, + { + "w": 1471737600, + "a": 7, + "d": 3, + "c": 1 + }, + { + "w": 1472342400, + "a": 4, + "d": 3, + "c": 1 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 3, + "d": 0, + "c": 1 + }, + { + "w": 1474156800, + "a": 35, + "d": 20, + "c": 2 + }, + { + "w": 1474761600, + "a": 359, + "d": 65, + "c": 7 + }, + { + "w": 1475366400, + "a": 192, + "d": 15, + "c": 4 + }, + { + "w": 1475971200, + "a": 5, + "d": 5, + "c": 1 + }, + { + "w": 1476576000, + "a": 98, + "d": 36, + "c": 3 + }, + { + "w": 1477180800, + "a": 16, + "d": 0, + "c": 1 + }, + { + "w": 1477785600, + "a": 84, + "d": 231, + "c": 3 + }, + { + "w": 1478390400, + "a": 3, + "d": 4, + "c": 2 + }, + { + "w": 1478995200, + "a": 16, + "d": 10, + "c": 1 + }, + { + "w": 1479600000, + "a": 21, + "d": 10, + "c": 3 + }, + { + "w": 1480204800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1480809600, + "a": 79, + "d": 28, + "c": 2 + }, + { + "w": 1481414400, + "a": 20, + "d": 6, + "c": 2 + }, + { + "w": 1482019200, + "a": 9, + "d": 12, + "c": 1 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 37, + "d": 19, + "c": 3 + }, + { + "w": 1484438400, + "a": 49, + "d": 0, + "c": 1 + }, + { + "w": 1485043200, + "a": 0, + "d": 69, + "c": 1 + }, + { + "w": 1485648000, + "a": 12, + "d": 2, + "c": 2 + }, + { + "w": 1486252800, + "a": 75, + "d": 156, + "c": 5 + }, + { + "w": 1486857600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1487462400, + "a": 8, + "d": 3, + "c": 1 + }, + { + "w": 1488067200, + "a": 13, + "d": 5, + "c": 2 + }, + { + "w": 1488672000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1489276800, + "a": 52, + "d": 9, + "c": 2 + }, + { + "w": 1489881600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1490486400, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1491091200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1491696000, + "a": 69, + "d": 46, + "c": 5 + }, + { + "w": 1492300800, + "a": 108, + "d": 44, + "c": 4 + }, + { + "w": 1492905600, + "a": 82, + "d": 20, + "c": 2 + }, + { + "w": 1493510400, + "a": 43, + "d": 32, + "c": 2 + }, + { + "w": 1494115200, + "a": 7, + "d": 8, + "c": 1 + }, + { + "w": 1494720000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495324800, + "a": 70, + "d": 18, + "c": 2 + }, + { + "w": 1495929600, + "a": 89, + "d": 44, + "c": 2 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 9, + "d": 15, + "c": 2 + }, + { + "w": 1498348800, + "a": 11, + "d": 18, + "c": 2 + }, + { + "w": 1498953600, + "a": 71, + "d": 17, + "c": 2 + }, + { + "w": 1499558400, + "a": 7, + "d": 10, + "c": 2 + }, + { + "w": 1500163200, + "a": 96, + "d": 34, + "c": 3 + }, + { + "w": 1500768000, + "a": 252, + "d": 244, + "c": 4 + }, + { + "w": 1501372800, + "a": 183, + "d": 44, + "c": 4 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 160, + "d": 160, + "c": 3 + }, + { + "w": 1503792000, + "a": 209, + "d": 23, + "c": 2 + }, + { + "w": 1504396800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505001600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1505606400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 23, + "d": 17, + "c": 1 + }, + { + "w": 1507420800, + "a": 16, + "d": 4, + "c": 1 + }, + { + "w": 1508025600, + "a": 9, + "d": 10, + "c": 2 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509840000, + "a": 26, + "d": 30, + "c": 3 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 21, + "c": 1 + }, + { + "w": 1511654400, + "a": 53, + "d": 14, + "c": 3 + }, + { + "w": 1512259200, + "a": 102, + "d": 38, + "c": 2 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 52, + "d": 10, + "c": 1 + }, + { + "w": 1515888000, + "a": 15, + "d": 2, + "c": 1 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 4, + "d": 7, + "c": 2 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 106, + "d": 6, + "c": 1 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 51, + "d": 75, + "c": 1 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 7, + "d": 2, + "c": 2 + }, + { + "w": 1523145600, + "a": 1395, + "d": 1386, + "c": 3 + }, + { + "w": 1523750400, + "a": 46, + "d": 24, + "c": 4 + }, + { + "w": 1524355200, + "a": 8, + "d": 3, + "c": 1 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 888, + "d": 1249, + "c": 5 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 13, + "d": 9, + "c": 3 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 10, + "d": 11, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 104, + "d": 213, + "c": 2 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 15, + "d": 79, + "c": 3 + }, + { + "w": 1533427200, + "a": 29, + "d": 11, + "c": 1 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 32, + "d": 2, + "c": 1 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 1, + "d": 32, + "c": 2 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 48, + "d": 2, + "c": 1 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 46, + "d": 12, + "c": 2 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 13, + "d": 26, + "c": 3 + }, + { + "w": 1541289600, + "a": 11, + "d": 0, + "c": 1 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 144, + "d": 60, + "c": 2 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 4, + "d": 4, + "c": 1 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "devoncarew", + "id": 1269969, + "node_id": "MDQ6VXNlcjEyNjk5Njk=", + "avatar_url": "https://avatars0.githubusercontent.com/u/1269969?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/devoncarew", + "html_url": "https://github.com/devoncarew", + "followers_url": "https://api.github.com/users/devoncarew/followers", + "following_url": "https://api.github.com/users/devoncarew/following{/other_user}", + "gists_url": "https://api.github.com/users/devoncarew/gists{/gist_id}", + "starred_url": "https://api.github.com/users/devoncarew/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/devoncarew/subscriptions", + "organizations_url": "https://api.github.com/users/devoncarew/orgs", + "repos_url": "https://api.github.com/users/devoncarew/repos", + "events_url": "https://api.github.com/users/devoncarew/events{/privacy}", + "received_events_url": "https://api.github.com/users/devoncarew/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 537, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434240000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437264000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440288000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1440892800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1441497600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443312000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1443916800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1444521600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445126400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1445731200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446336000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1446940800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1447545600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448150400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1448755200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449360000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1449964800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452384000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1452988800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454803200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1455408000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456012800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1456617600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457222400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1457827200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1458432000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459036800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1459641600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460246400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1460851200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1461456000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462060800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463875200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1464480000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466294400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469318400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1469923200, + "a": 42, + "d": 10, + "c": 4 + }, + { + "w": 1470528000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471132800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1471737600, + "a": 125, + "d": 49, + "c": 3 + }, + { + "w": 1472342400, + "a": 3, + "d": 3, + "c": 2 + }, + { + "w": 1472947200, + "a": 19, + "d": 26, + "c": 3 + }, + { + "w": 1473552000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1474156800, + "a": 5, + "d": 2, + "c": 1 + }, + { + "w": 1474761600, + "a": 594, + "d": 522, + "c": 5 + }, + { + "w": 1475366400, + "a": 316, + "d": 9, + "c": 4 + }, + { + "w": 1475971200, + "a": 17, + "d": 4, + "c": 2 + }, + { + "w": 1476576000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1477180800, + "a": 59, + "d": 47, + "c": 7 + }, + { + "w": 1477785600, + "a": 182, + "d": 68, + "c": 8 + }, + { + "w": 1478390400, + "a": 88, + "d": 10, + "c": 1 + }, + { + "w": 1478995200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1479600000, + "a": 22, + "d": 9, + "c": 3 + }, + { + "w": 1480204800, + "a": 51, + "d": 0, + "c": 1 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483833600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1484438400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1485043200, + "a": 67, + "d": 7, + "c": 5 + }, + { + "w": 1485648000, + "a": 368, + "d": 114, + "c": 18 + }, + { + "w": 1486252800, + "a": 330, + "d": 129, + "c": 17 + }, + { + "w": 1486857600, + "a": 30, + "d": 13, + "c": 4 + }, + { + "w": 1487462400, + "a": 219, + "d": 32, + "c": 9 + }, + { + "w": 1488067200, + "a": 4715, + "d": 4348, + "c": 20 + }, + { + "w": 1488672000, + "a": 2971, + "d": 2941, + "c": 19 + }, + { + "w": 1489276800, + "a": 24, + "d": 8, + "c": 6 + }, + { + "w": 1489881600, + "a": 315, + "d": 56, + "c": 2 + }, + { + "w": 1490486400, + "a": 135, + "d": 90, + "c": 5 + }, + { + "w": 1491091200, + "a": 37, + "d": 17, + "c": 4 + }, + { + "w": 1491696000, + "a": 13, + "d": 4, + "c": 3 + }, + { + "w": 1492300800, + "a": 3, + "d": 1, + "c": 1 + }, + { + "w": 1492905600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1493510400, + "a": 1322, + "d": 1041, + "c": 16 + }, + { + "w": 1494115200, + "a": 1380, + "d": 93, + "c": 14 + }, + { + "w": 1494720000, + "a": 123, + "d": 28, + "c": 8 + }, + { + "w": 1495324800, + "a": 466, + "d": 403, + "c": 9 + }, + { + "w": 1495929600, + "a": 35, + "d": 8, + "c": 7 + }, + { + "w": 1496534400, + "a": 13, + "d": 7, + "c": 7 + }, + { + "w": 1497139200, + "a": 508, + "d": 493, + "c": 12 + }, + { + "w": 1497744000, + "a": 353, + "d": 218, + "c": 10 + }, + { + "w": 1498348800, + "a": 299, + "d": 42, + "c": 6 + }, + { + "w": 1498953600, + "a": 131, + "d": 41, + "c": 2 + }, + { + "w": 1499558400, + "a": 93, + "d": 6, + "c": 3 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 463, + "d": 172, + "c": 4 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 161, + "d": 31, + "c": 7 + }, + { + "w": 1503792000, + "a": 752, + "d": 309, + "c": 6 + }, + { + "w": 1504396800, + "a": 83, + "d": 44, + "c": 7 + }, + { + "w": 1505001600, + "a": 122, + "d": 34, + "c": 1 + }, + { + "w": 1505606400, + "a": 35, + "d": 7, + "c": 3 + }, + { + "w": 1506211200, + "a": 458, + "d": 52, + "c": 5 + }, + { + "w": 1506816000, + "a": 3, + "d": 5, + "c": 3 + }, + { + "w": 1507420800, + "a": 10, + "d": 10, + "c": 3 + }, + { + "w": 1508025600, + "a": 4, + "d": 2, + "c": 1 + }, + { + "w": 1508630400, + "a": 21, + "d": 7, + "c": 2 + }, + { + "w": 1509235200, + "a": 74, + "d": 74, + "c": 6 + }, + { + "w": 1509840000, + "a": 120, + "d": 75, + "c": 6 + }, + { + "w": 1510444800, + "a": 49, + "d": 0, + "c": 1 + }, + { + "w": 1511049600, + "a": 2321, + "d": 2630, + "c": 8 + }, + { + "w": 1511654400, + "a": 201, + "d": 128, + "c": 7 + }, + { + "w": 1512259200, + "a": 284, + "d": 150, + "c": 12 + }, + { + "w": 1512864000, + "a": 992, + "d": 787, + "c": 16 + }, + { + "w": 1513468800, + "a": 83, + "d": 88, + "c": 3 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 29, + "d": 17, + "c": 3 + }, + { + "w": 1515283200, + "a": 70, + "d": 1, + "c": 1 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 462, + "d": 263, + "c": 6 + }, + { + "w": 1517097600, + "a": 172, + "d": 23, + "c": 6 + }, + { + "w": 1517702400, + "a": 383, + "d": 102, + "c": 14 + }, + { + "w": 1518307200, + "a": 2, + "d": 2, + "c": 2 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 15, + "d": 22, + "c": 4 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 4, + "d": 4, + "c": 4 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 18, + "d": 18, + "c": 8 + }, + { + "w": 1522540800, + "a": 3, + "d": 3, + "c": 3 + }, + { + "w": 1523145600, + "a": 47, + "d": 44, + "c": 3 + }, + { + "w": 1523750400, + "a": 35, + "d": 4, + "c": 1 + }, + { + "w": 1524355200, + "a": 914, + "d": 742, + "c": 19 + }, + { + "w": 1524960000, + "a": 1421, + "d": 1681, + "c": 21 + }, + { + "w": 1525564800, + "a": 635, + "d": 246, + "c": 17 + }, + { + "w": 1526169600, + "a": 96, + "d": 56, + "c": 8 + }, + { + "w": 1526774400, + "a": 76, + "d": 1, + "c": 2 + }, + { + "w": 1527379200, + "a": 1, + "d": 79, + "c": 2 + }, + { + "w": 1527984000, + "a": 1419, + "d": 1999, + "c": 7 + }, + { + "w": 1528588800, + "a": 1251, + "d": 441, + "c": 2 + }, + { + "w": 1529193600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529798400, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 88, + "d": 237, + "c": 5 + }, + { + "w": 1531612800, + "a": 4, + "d": 115, + "c": 2 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 221, + "d": 114, + "c": 7 + }, + { + "w": 1535846400, + "a": 224, + "d": 760, + "c": 12 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 334, + "d": 160, + "c": 3 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 9, + "d": 17, + "c": 1 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 6, + "d": 6, + "c": 1 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 41, + "d": 8, + "c": 3 + }, + { + "w": 1547337600, + "a": 51, + "d": 26, + "c": 3 + }, + { + "w": 1547942400, + "a": 7, + "d": 13, + "c": 2 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 4, + "d": 4, + "c": 4 + }, + { + "w": 1550966400, + "a": 4, + "d": 4, + "c": 4 + }, + { + "w": 1551571200, + "a": 27, + "d": 5, + "c": 2 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "cbracken", + "id": 351029, + "node_id": "MDQ6VXNlcjM1MTAyOQ==", + "avatar_url": "https://avatars3.githubusercontent.com/u/351029?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/cbracken", + "html_url": "https://github.com/cbracken", + "followers_url": "https://api.github.com/users/cbracken/followers", + "following_url": "https://api.github.com/users/cbracken/following{/other_user}", + "gists_url": "https://api.github.com/users/cbracken/gists{/gist_id}", + "starred_url": "https://api.github.com/users/cbracken/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/cbracken/subscriptions", + "organizations_url": "https://api.github.com/users/cbracken/orgs", + "repos_url": "https://api.github.com/users/cbracken/repos", + "events_url": "https://api.github.com/users/cbracken/events{/privacy}", + "received_events_url": "https://api.github.com/users/cbracken/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 699, + "weeks": [ + { + "w": 1413676800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420934400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 213, + "d": 13, + "c": 2 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 96, + "d": 0, + "c": 1 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1432425600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433030400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1433635200, + "a": 86, + "d": 111, + "c": 3 + }, + { + "w": 1434240000, + "a": 144, + "d": 16, + "c": 5 + }, + { + "w": 1434844800, + "a": 64, + "d": 2, + "c": 2 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 328, + "d": 135, + "c": 1 + }, + { + "w": 1437264000, + "a": 164, + "d": 142, + "c": 8 + }, + { + "w": 1437868800, + "a": 281, + "d": 89, + "c": 4 + }, + { + "w": 1438473600, + "a": 110, + "d": 25, + "c": 4 + }, + { + "w": 1439078400, + "a": 707, + "d": 60, + "c": 4 + }, + { + "w": 1439683200, + "a": 631, + "d": 320, + "c": 5 + }, + { + "w": 1440288000, + "a": 152, + "d": 61, + "c": 5 + }, + { + "w": 1440892800, + "a": 873, + "d": 384, + "c": 9 + }, + { + "w": 1441497600, + "a": 146, + "d": 57, + "c": 3 + }, + { + "w": 1442102400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1442707200, + "a": 108, + "d": 33, + "c": 7 + }, + { + "w": 1443312000, + "a": 657, + "d": 139, + "c": 10 + }, + { + "w": 1443916800, + "a": 409, + "d": 79, + "c": 5 + }, + { + "w": 1444521600, + "a": 450, + "d": 27, + "c": 6 + }, + { + "w": 1445126400, + "a": 474, + "d": 75, + "c": 4 + }, + { + "w": 1445731200, + "a": 249, + "d": 7, + "c": 3 + }, + { + "w": 1446336000, + "a": 623, + "d": 288, + "c": 13 + }, + { + "w": 1446940800, + "a": 495, + "d": 241, + "c": 9 + }, + { + "w": 1447545600, + "a": 157, + "d": 121, + "c": 3 + }, + { + "w": 1448150400, + "a": 347, + "d": 278, + "c": 1 + }, + { + "w": 1448755200, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1449360000, + "a": 665, + "d": 343, + "c": 8 + }, + { + "w": 1449964800, + "a": 310, + "d": 98, + "c": 5 + }, + { + "w": 1450569600, + "a": 328, + "d": 217, + "c": 2 + }, + { + "w": 1451174400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451779200, + "a": 282, + "d": 131, + "c": 4 + }, + { + "w": 1452384000, + "a": 753, + "d": 640, + "c": 4 + }, + { + "w": 1452988800, + "a": 416, + "d": 82, + "c": 3 + }, + { + "w": 1453593600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1454198400, + "a": 1078, + "d": 280, + "c": 7 + }, + { + "w": 1454803200, + "a": 1674, + "d": 632, + "c": 7 + }, + { + "w": 1455408000, + "a": 736, + "d": 101, + "c": 6 + }, + { + "w": 1456012800, + "a": 1019, + "d": 356, + "c": 8 + }, + { + "w": 1456617600, + "a": 276, + "d": 116, + "c": 8 + }, + { + "w": 1457222400, + "a": 529, + "d": 258, + "c": 6 + }, + { + "w": 1457827200, + "a": 402, + "d": 172, + "c": 3 + }, + { + "w": 1458432000, + "a": 135, + "d": 72, + "c": 5 + }, + { + "w": 1459036800, + "a": 65, + "d": 50, + "c": 5 + }, + { + "w": 1459641600, + "a": 802, + "d": 586, + "c": 7 + }, + { + "w": 1460246400, + "a": 674, + "d": 90, + "c": 3 + }, + { + "w": 1460851200, + "a": 451, + "d": 127, + "c": 7 + }, + { + "w": 1461456000, + "a": 188, + "d": 43, + "c": 3 + }, + { + "w": 1462060800, + "a": 599, + "d": 53, + "c": 3 + }, + { + "w": 1462665600, + "a": 14974, + "d": 14869, + "c": 9 + }, + { + "w": 1463270400, + "a": 806, + "d": 363, + "c": 6 + }, + { + "w": 1463875200, + "a": 110, + "d": 65, + "c": 8 + }, + { + "w": 1464480000, + "a": 2617, + "d": 1698, + "c": 5 + }, + { + "w": 1465084800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1465689600, + "a": 660, + "d": 330, + "c": 10 + }, + { + "w": 1466294400, + "a": 1239, + "d": 465, + "c": 9 + }, + { + "w": 1466899200, + "a": 429, + "d": 193, + "c": 4 + }, + { + "w": 1467504000, + "a": 78, + "d": 51, + "c": 1 + }, + { + "w": 1468108800, + "a": 185, + "d": 104, + "c": 4 + }, + { + "w": 1468713600, + "a": 1120, + "d": 70, + "c": 5 + }, + { + "w": 1469318400, + "a": 259, + "d": 175, + "c": 9 + }, + { + "w": 1469923200, + "a": 382, + "d": 124, + "c": 7 + }, + { + "w": 1470528000, + "a": 5, + "d": 5, + "c": 2 + }, + { + "w": 1471132800, + "a": 502, + "d": 339, + "c": 10 + }, + { + "w": 1471737600, + "a": 364, + "d": 213, + "c": 5 + }, + { + "w": 1472342400, + "a": 267, + "d": 35, + "c": 6 + }, + { + "w": 1472947200, + "a": 233, + "d": 40, + "c": 2 + }, + { + "w": 1473552000, + "a": 303, + "d": 79, + "c": 3 + }, + { + "w": 1474156800, + "a": 365, + "d": 91, + "c": 4 + }, + { + "w": 1474761600, + "a": 243, + "d": 15, + "c": 1 + }, + { + "w": 1475366400, + "a": 304, + "d": 84, + "c": 4 + }, + { + "w": 1475971200, + "a": 178, + "d": 121, + "c": 1 + }, + { + "w": 1476576000, + "a": 139, + "d": 33, + "c": 4 + }, + { + "w": 1477180800, + "a": 38, + "d": 30, + "c": 2 + }, + { + "w": 1477785600, + "a": 292, + "d": 230, + "c": 3 + }, + { + "w": 1478390400, + "a": 242, + "d": 59, + "c": 1 + }, + { + "w": 1478995200, + "a": 478, + "d": 253, + "c": 6 + }, + { + "w": 1479600000, + "a": 502, + "d": 184, + "c": 5 + }, + { + "w": 1480204800, + "a": 387, + "d": 199, + "c": 8 + }, + { + "w": 1480809600, + "a": 132, + "d": 19, + "c": 2 + }, + { + "w": 1481414400, + "a": 63, + "d": 3, + "c": 2 + }, + { + "w": 1482019200, + "a": 548, + "d": 104, + "c": 2 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 46, + "d": 1, + "c": 1 + }, + { + "w": 1483833600, + "a": 1529, + "d": 1414, + "c": 3 + }, + { + "w": 1484438400, + "a": 572, + "d": 11, + "c": 2 + }, + { + "w": 1485043200, + "a": 449, + "d": 95, + "c": 5 + }, + { + "w": 1485648000, + "a": 11, + "d": 0, + "c": 1 + }, + { + "w": 1486252800, + "a": 322, + "d": 141, + "c": 3 + }, + { + "w": 1486857600, + "a": 565, + "d": 447, + "c": 3 + }, + { + "w": 1487462400, + "a": 273, + "d": 32, + "c": 2 + }, + { + "w": 1488067200, + "a": 866, + "d": 4, + "c": 2 + }, + { + "w": 1488672000, + "a": 303, + "d": 47, + "c": 4 + }, + { + "w": 1489276800, + "a": 1515, + "d": 930, + "c": 6 + }, + { + "w": 1489881600, + "a": 1235, + "d": 946, + "c": 4 + }, + { + "w": 1490486400, + "a": 303, + "d": 200, + "c": 2 + }, + { + "w": 1491091200, + "a": 166, + "d": 55, + "c": 2 + }, + { + "w": 1491696000, + "a": 1211, + "d": 429, + "c": 6 + }, + { + "w": 1492300800, + "a": 773, + "d": 47, + "c": 7 + }, + { + "w": 1492905600, + "a": 655, + "d": 1, + "c": 2 + }, + { + "w": 1493510400, + "a": 2552, + "d": 200, + "c": 5 + }, + { + "w": 1494115200, + "a": 533, + "d": 143, + "c": 4 + }, + { + "w": 1494720000, + "a": 972, + "d": 192, + "c": 3 + }, + { + "w": 1495324800, + "a": 1332, + "d": 462, + "c": 9 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 805, + "d": 187, + "c": 6 + }, + { + "w": 1497139200, + "a": 459, + "d": 238, + "c": 4 + }, + { + "w": 1497744000, + "a": 271, + "d": 57, + "c": 3 + }, + { + "w": 1498348800, + "a": 193, + "d": 65, + "c": 3 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 212, + "d": 18, + "c": 2 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 202, + "d": 14, + "c": 2 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 1061, + "d": 237, + "c": 1 + }, + { + "w": 1503792000, + "a": 805, + "d": 261, + "c": 5 + }, + { + "w": 1504396800, + "a": 807, + "d": 94, + "c": 2 + }, + { + "w": 1505001600, + "a": 1089, + "d": 107, + "c": 4 + }, + { + "w": 1505606400, + "a": 378, + "d": 14, + "c": 2 + }, + { + "w": 1506211200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1506816000, + "a": 184, + "d": 0, + "c": 1 + }, + { + "w": 1507420800, + "a": 3620, + "d": 3060, + "c": 1 + }, + { + "w": 1508025600, + "a": 202, + "d": 11, + "c": 3 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 109, + "d": 72, + "c": 2 + }, + { + "w": 1509840000, + "a": 123, + "d": 20, + "c": 1 + }, + { + "w": 1510444800, + "a": 64, + "d": 6, + "c": 2 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 954, + "d": 287, + "c": 2 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 3735, + "d": 2009, + "c": 3 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 4071, + "d": 984, + "c": 3 + }, + { + "w": 1515888000, + "a": 676, + "d": 97, + "c": 5 + }, + { + "w": 1516492800, + "a": 42, + "d": 1, + "c": 1 + }, + { + "w": 1517097600, + "a": 40, + "d": 4, + "c": 1 + }, + { + "w": 1517702400, + "a": 1197, + "d": 389, + "c": 4 + }, + { + "w": 1518307200, + "a": 275, + "d": 71, + "c": 1 + }, + { + "w": 1518912000, + "a": 650, + "d": 185, + "c": 2 + }, + { + "w": 1519516800, + "a": 833, + "d": 18, + "c": 2 + }, + { + "w": 1520121600, + "a": 554, + "d": 175, + "c": 4 + }, + { + "w": 1520726400, + "a": 1031, + "d": 247, + "c": 9 + }, + { + "w": 1521331200, + "a": 654, + "d": 149, + "c": 6 + }, + { + "w": 1521936000, + "a": 304, + "d": 223, + "c": 5 + }, + { + "w": 1522540800, + "a": 717, + "d": 164, + "c": 7 + }, + { + "w": 1523145600, + "a": 33, + "d": 25, + "c": 3 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 6149, + "d": 5162, + "c": 9 + }, + { + "w": 1524960000, + "a": 681, + "d": 462, + "c": 8 + }, + { + "w": 1525564800, + "a": 199, + "d": 114, + "c": 5 + }, + { + "w": 1526169600, + "a": 1743, + "d": 1015, + "c": 6 + }, + { + "w": 1526774400, + "a": 315, + "d": 146, + "c": 6 + }, + { + "w": 1527379200, + "a": 16, + "d": 2, + "c": 1 + }, + { + "w": 1527984000, + "a": 330, + "d": 21, + "c": 3 + }, + { + "w": 1528588800, + "a": 244, + "d": 75, + "c": 2 + }, + { + "w": 1529193600, + "a": 130, + "d": 0, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 12, + "d": 7, + "c": 1 + }, + { + "w": 1531008000, + "a": 1111, + "d": 127, + "c": 4 + }, + { + "w": 1531612800, + "a": 9003, + "d": 2379, + "c": 3 + }, + { + "w": 1532217600, + "a": 506, + "d": 51, + "c": 1 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 262, + "d": 1, + "c": 2 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 8368, + "d": 4901, + "c": 4 + }, + { + "w": 1535241600, + "a": 14, + "d": 0, + "c": 1 + }, + { + "w": 1535846400, + "a": 1627, + "d": 39, + "c": 3 + }, + { + "w": 1536451200, + "a": 2319, + "d": 1179, + "c": 2 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 877, + "d": 426, + "c": 4 + }, + { + "w": 1538265600, + "a": 2332, + "d": 1105, + "c": 4 + }, + { + "w": 1538870400, + "a": 1358, + "d": 694, + "c": 5 + }, + { + "w": 1539475200, + "a": 913, + "d": 140, + "c": 5 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 7170, + "d": 5808, + "c": 7 + }, + { + "w": 1541289600, + "a": 259, + "d": 8, + "c": 2 + }, + { + "w": 1541894400, + "a": 487, + "d": 6, + "c": 2 + }, + { + "w": 1542499200, + "a": 4831, + "d": 4242, + "c": 5 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 204, + "d": 83, + "c": 6 + }, + { + "w": 1544918400, + "a": 3311, + "d": 1985, + "c": 6 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 98, + "d": 26, + "c": 2 + }, + { + "w": 1546732800, + "a": 162, + "d": 2, + "c": 3 + }, + { + "w": 1547337600, + "a": 256, + "d": 41, + "c": 4 + }, + { + "w": 1547942400, + "a": 310, + "d": 24, + "c": 3 + }, + { + "w": 1548547200, + "a": 96, + "d": 5, + "c": 2 + }, + { + "w": 1549152000, + "a": 1656, + "d": 301, + "c": 9 + }, + { + "w": 1549756800, + "a": 837, + "d": 11, + "c": 3 + }, + { + "w": 1550361600, + "a": 856, + "d": 208, + "c": 1 + }, + { + "w": 1550966400, + "a": 180, + "d": 19, + "c": 4 + }, + { + "w": 1551571200, + "a": 91, + "d": 20, + "c": 1 + }, + { + "w": 1552176000, + "a": 1383, + "d": 1326, + "c": 4 + }, + { + "w": 1552780800, + "a": 34, + "d": 26, + "c": 1 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 398, + "d": 11, + "c": 4 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "HansMuller", + "id": 1377460, + "node_id": "MDQ6VXNlcjEzNzc0NjA=", + "avatar_url": "https://avatars2.githubusercontent.com/u/1377460?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/HansMuller", + "html_url": "https://github.com/HansMuller", + "followers_url": "https://api.github.com/users/HansMuller/followers", + "following_url": "https://api.github.com/users/HansMuller/following{/other_user}", + "gists_url": "https://api.github.com/users/HansMuller/gists{/gist_id}", + "starred_url": "https://api.github.com/users/HansMuller/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/HansMuller/subscriptions", + "organizations_url": "https://api.github.com/users/HansMuller/orgs", + "repos_url": "https://api.github.com/users/HansMuller/repos", + "events_url": "https://api.github.com/users/HansMuller/events{/privacy}", + "received_events_url": "https://api.github.com/users/HansMuller/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 1486, + "weeks": [ + { + "w": 1413676800, + "a": 8, + "d": 1, + "c": 2 + }, + { + "w": 1414281600, + "a": 73, + "d": 61, + "c": 4 + }, + { + "w": 1414886400, + "a": 292, + "d": 21, + "c": 6 + }, + { + "w": 1415491200, + "a": 612, + "d": 11, + "c": 2 + }, + { + "w": 1416096000, + "a": 65, + "d": 86, + "c": 9 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 71, + "d": 75, + "c": 3 + }, + { + "w": 1420934400, + "a": 14, + "d": 5, + "c": 1 + }, + { + "w": 1421539200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422144000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423958400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1424563200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1425168000, + "a": 120, + "d": 62, + "c": 2 + }, + { + "w": 1425772800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426377600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1426982400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1427587200, + "a": 12, + "d": 12, + "c": 1 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 10, + "d": 12, + "c": 4 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 137, + "d": 37, + "c": 6 + }, + { + "w": 1431820800, + "a": 7, + "d": 7, + "c": 1 + }, + { + "w": 1432425600, + "a": 31067, + "d": 268, + "c": 13 + }, + { + "w": 1433030400, + "a": 172, + "d": 92, + "c": 9 + }, + { + "w": 1433635200, + "a": 1322, + "d": 1188, + "c": 16 + }, + { + "w": 1434240000, + "a": 389, + "d": 219, + "c": 12 + }, + { + "w": 1434844800, + "a": 257, + "d": 63, + "c": 10 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436659200, + "a": 7, + "d": 7, + "c": 1 + }, + { + "w": 1437264000, + "a": 1262, + "d": 641, + "c": 25 + }, + { + "w": 1437868800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1438473600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439078400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1439683200, + "a": 840, + "d": 456, + "c": 21 + }, + { + "w": 1440288000, + "a": 1799, + "d": 766, + "c": 21 + }, + { + "w": 1440892800, + "a": 491, + "d": 129, + "c": 6 + }, + { + "w": 1441497600, + "a": 309, + "d": 131, + "c": 7 + }, + { + "w": 1442102400, + "a": 1510, + "d": 1107, + "c": 21 + }, + { + "w": 1442707200, + "a": 3347, + "d": 1310, + "c": 19 + }, + { + "w": 1443312000, + "a": 3205, + "d": 1088, + "c": 29 + }, + { + "w": 1443916800, + "a": 2176, + "d": 1618, + "c": 12 + }, + { + "w": 1444521600, + "a": 732, + "d": 76, + "c": 8 + }, + { + "w": 1445126400, + "a": 1769, + "d": 1052, + "c": 19 + }, + { + "w": 1445731200, + "a": 1354, + "d": 668, + "c": 22 + }, + { + "w": 1446336000, + "a": 1795, + "d": 1388, + "c": 20 + }, + { + "w": 1446940800, + "a": 919, + "d": 377, + "c": 7 + }, + { + "w": 1447545600, + "a": 2569, + "d": 1289, + "c": 18 + }, + { + "w": 1448150400, + "a": 511, + "d": 258, + "c": 11 + }, + { + "w": 1448755200, + "a": 6011, + "d": 5828, + "c": 21 + }, + { + "w": 1449360000, + "a": 867, + "d": 639, + "c": 20 + }, + { + "w": 1449964800, + "a": 17722, + "d": 13872, + "c": 16 + }, + { + "w": 1450569600, + "a": 250, + "d": 113, + "c": 3 + }, + { + "w": 1451174400, + "a": 274, + "d": 225, + "c": 5 + }, + { + "w": 1451779200, + "a": 1191, + "d": 646, + "c": 14 + }, + { + "w": 1452384000, + "a": 1823, + "d": 698, + "c": 21 + }, + { + "w": 1452988800, + "a": 352, + "d": 176, + "c": 7 + }, + { + "w": 1453593600, + "a": 182, + "d": 153, + "c": 5 + }, + { + "w": 1454198400, + "a": 213, + "d": 107, + "c": 3 + }, + { + "w": 1454803200, + "a": 3295, + "d": 1334, + "c": 25 + }, + { + "w": 1455408000, + "a": 127, + "d": 19, + "c": 4 + }, + { + "w": 1456012800, + "a": 1197, + "d": 1212, + "c": 22 + }, + { + "w": 1456617600, + "a": 954, + "d": 512, + "c": 12 + }, + { + "w": 1457222400, + "a": 3522, + "d": 3086, + "c": 22 + }, + { + "w": 1457827200, + "a": 9110, + "d": 4283, + "c": 26 + }, + { + "w": 1458432000, + "a": 2500, + "d": 577, + "c": 18 + }, + { + "w": 1459036800, + "a": 1649, + "d": 507, + "c": 13 + }, + { + "w": 1459641600, + "a": 977, + "d": 446, + "c": 11 + }, + { + "w": 1460246400, + "a": 2869, + "d": 930, + "c": 15 + }, + { + "w": 1460851200, + "a": 2654, + "d": 1949, + "c": 17 + }, + { + "w": 1461456000, + "a": 10573, + "d": 10640, + "c": 6 + }, + { + "w": 1462060800, + "a": 683, + "d": 105, + "c": 8 + }, + { + "w": 1462665600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1463270400, + "a": 5233, + "d": 3567, + "c": 16 + }, + { + "w": 1463875200, + "a": 2124, + "d": 1754, + "c": 4 + }, + { + "w": 1464480000, + "a": 2121, + "d": 1124, + "c": 8 + }, + { + "w": 1465084800, + "a": 1128, + "d": 407, + "c": 5 + }, + { + "w": 1465689600, + "a": 2609, + "d": 2074, + "c": 10 + }, + { + "w": 1466294400, + "a": 1641, + "d": 292, + "c": 6 + }, + { + "w": 1466899200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1467504000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468108800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1468713600, + "a": 135, + "d": 69, + "c": 1 + }, + { + "w": 1469318400, + "a": 1062, + "d": 473, + "c": 8 + }, + { + "w": 1469923200, + "a": 353, + "d": 125, + "c": 5 + }, + { + "w": 1470528000, + "a": 598, + "d": 150, + "c": 5 + }, + { + "w": 1471132800, + "a": 1111, + "d": 109, + "c": 6 + }, + { + "w": 1471737600, + "a": 243, + "d": 58, + "c": 4 + }, + { + "w": 1472342400, + "a": 1682, + "d": 366, + "c": 7 + }, + { + "w": 1472947200, + "a": 1032, + "d": 362, + "c": 6 + }, + { + "w": 1473552000, + "a": 726, + "d": 319, + "c": 8 + }, + { + "w": 1474156800, + "a": 492, + "d": 113, + "c": 8 + }, + { + "w": 1474761600, + "a": 1711, + "d": 537, + "c": 5 + }, + { + "w": 1475366400, + "a": 21, + "d": 7, + "c": 2 + }, + { + "w": 1475971200, + "a": 102, + "d": 22, + "c": 3 + }, + { + "w": 1476576000, + "a": 320, + "d": 30, + "c": 7 + }, + { + "w": 1477180800, + "a": 1154, + "d": 135, + "c": 11 + }, + { + "w": 1477785600, + "a": 610, + "d": 227, + "c": 23 + }, + { + "w": 1478390400, + "a": 18652, + "d": 17591, + "c": 18 + }, + { + "w": 1478995200, + "a": 779, + "d": 444, + "c": 16 + }, + { + "w": 1479600000, + "a": 3, + "d": 3, + "c": 1 + }, + { + "w": 1480204800, + "a": 90, + "d": 36, + "c": 3 + }, + { + "w": 1480809600, + "a": 14, + "d": 3, + "c": 2 + }, + { + "w": 1481414400, + "a": 514, + "d": 245, + "c": 5 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 448, + "d": 143, + "c": 10 + }, + { + "w": 1483833600, + "a": 2248, + "d": 19, + "c": 4 + }, + { + "w": 1484438400, + "a": 4307, + "d": 571, + "c": 12 + }, + { + "w": 1485043200, + "a": 6235, + "d": 1783, + "c": 32 + }, + { + "w": 1485648000, + "a": 3330, + "d": 2557, + "c": 19 + }, + { + "w": 1486252800, + "a": 2085, + "d": 1765, + "c": 15 + }, + { + "w": 1486857600, + "a": 1244, + "d": 517, + "c": 11 + }, + { + "w": 1487462400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1488067200, + "a": 7, + "d": 1, + "c": 1 + }, + { + "w": 1488672000, + "a": 1076, + "d": 1939, + "c": 3 + }, + { + "w": 1489276800, + "a": 406, + "d": 304, + "c": 3 + }, + { + "w": 1489881600, + "a": 15, + "d": 0, + "c": 2 + }, + { + "w": 1490486400, + "a": 148, + "d": 90, + "c": 6 + }, + { + "w": 1491091200, + "a": 4162, + "d": 2001, + "c": 22 + }, + { + "w": 1491696000, + "a": 3477, + "d": 2850, + "c": 6 + }, + { + "w": 1492300800, + "a": 998, + "d": 631, + "c": 6 + }, + { + "w": 1492905600, + "a": 1949, + "d": 1544, + "c": 8 + }, + { + "w": 1493510400, + "a": 1834, + "d": 903, + "c": 16 + }, + { + "w": 1494115200, + "a": 5208, + "d": 4038, + "c": 22 + }, + { + "w": 1494720000, + "a": 1777, + "d": 465, + "c": 16 + }, + { + "w": 1495324800, + "a": 1541, + "d": 210, + "c": 11 + }, + { + "w": 1495929600, + "a": 755, + "d": 172, + "c": 4 + }, + { + "w": 1496534400, + "a": 1814, + "d": 718, + "c": 10 + }, + { + "w": 1497139200, + "a": 1519, + "d": 393, + "c": 8 + }, + { + "w": 1497744000, + "a": 2576, + "d": 419, + "c": 12 + }, + { + "w": 1498348800, + "a": 2441, + "d": 198, + "c": 7 + }, + { + "w": 1498953600, + "a": 11, + "d": 9, + "c": 2 + }, + { + "w": 1499558400, + "a": 5, + "d": 3, + "c": 1 + }, + { + "w": 1500163200, + "a": 1589, + "d": 401, + "c": 8 + }, + { + "w": 1500768000, + "a": 1270, + "d": 510, + "c": 4 + }, + { + "w": 1501372800, + "a": 496, + "d": 340, + "c": 6 + }, + { + "w": 1501977600, + "a": 509, + "d": 298, + "c": 2 + }, + { + "w": 1502582400, + "a": 668, + "d": 1015, + "c": 3 + }, + { + "w": 1503187200, + "a": 337, + "d": 116, + "c": 4 + }, + { + "w": 1503792000, + "a": 5393, + "d": 1757, + "c": 6 + }, + { + "w": 1504396800, + "a": 2520, + "d": 1167, + "c": 4 + }, + { + "w": 1505001600, + "a": 1566, + "d": 1070, + "c": 12 + }, + { + "w": 1505606400, + "a": 6444, + "d": 2876, + "c": 13 + }, + { + "w": 1506211200, + "a": 3530, + "d": 726, + "c": 13 + }, + { + "w": 1506816000, + "a": 2862, + "d": 585, + "c": 7 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 2019, + "d": 432, + "c": 6 + }, + { + "w": 1508630400, + "a": 297, + "d": 21, + "c": 3 + }, + { + "w": 1509235200, + "a": 3541, + "d": 229, + "c": 7 + }, + { + "w": 1509840000, + "a": 209, + "d": 115, + "c": 1 + }, + { + "w": 1510444800, + "a": 477, + "d": 130, + "c": 4 + }, + { + "w": 1511049600, + "a": 1280, + "d": 301, + "c": 5 + }, + { + "w": 1511654400, + "a": 679, + "d": 361, + "c": 7 + }, + { + "w": 1512259200, + "a": 3769, + "d": 3131, + "c": 7 + }, + { + "w": 1512864000, + "a": 693, + "d": 81, + "c": 7 + }, + { + "w": 1513468800, + "a": 1215, + "d": 260, + "c": 6 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 1334, + "d": 199, + "c": 18 + }, + { + "w": 1515888000, + "a": 2305, + "d": 571, + "c": 14 + }, + { + "w": 1516492800, + "a": 256, + "d": 92, + "c": 6 + }, + { + "w": 1517097600, + "a": 787, + "d": 271, + "c": 9 + }, + { + "w": 1517702400, + "a": 182, + "d": 25, + "c": 2 + }, + { + "w": 1518307200, + "a": 221, + "d": 73, + "c": 3 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 66, + "d": 9, + "c": 2 + }, + { + "w": 1520121600, + "a": 9, + "d": 3, + "c": 1 + }, + { + "w": 1520726400, + "a": 148, + "d": 51, + "c": 2 + }, + { + "w": 1521331200, + "a": 2269, + "d": 928, + "c": 10 + }, + { + "w": 1521936000, + "a": 2, + "d": 2, + "c": 1 + }, + { + "w": 1522540800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523145600, + "a": 80, + "d": 21, + "c": 4 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 266, + "d": 6, + "c": 4 + }, + { + "w": 1524960000, + "a": 389, + "d": 280, + "c": 4 + }, + { + "w": 1525564800, + "a": 349, + "d": 34, + "c": 1 + }, + { + "w": 1526169600, + "a": 80, + "d": 18, + "c": 1 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 749, + "d": 214, + "c": 5 + }, + { + "w": 1527984000, + "a": 456, + "d": 367, + "c": 7 + }, + { + "w": 1528588800, + "a": 931, + "d": 649, + "c": 4 + }, + { + "w": 1529193600, + "a": 232, + "d": 225, + "c": 1 + }, + { + "w": 1529798400, + "a": 659, + "d": 312, + "c": 6 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 263, + "d": 255, + "c": 3 + }, + { + "w": 1531612800, + "a": 41131, + "d": 41133, + "c": 3 + }, + { + "w": 1532217600, + "a": 1069, + "d": 1033, + "c": 3 + }, + { + "w": 1532822400, + "a": 11320, + "d": 4475, + "c": 7 + }, + { + "w": 1533427200, + "a": 1644, + "d": 448, + "c": 6 + }, + { + "w": 1534032000, + "a": 1355, + "d": 1191, + "c": 13 + }, + { + "w": 1534636800, + "a": 346, + "d": 225, + "c": 5 + }, + { + "w": 1535241600, + "a": 205, + "d": 107, + "c": 3 + }, + { + "w": 1535846400, + "a": 6, + "d": 6, + "c": 1 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 94, + "d": 71, + "c": 2 + }, + { + "w": 1537660800, + "a": 758, + "d": 427, + "c": 1 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 209, + "d": 133, + "c": 1 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 696, + "d": 75, + "c": 6 + }, + { + "w": 1540684800, + "a": 192, + "d": 334, + "c": 4 + }, + { + "w": 1541289600, + "a": 443, + "d": 414, + "c": 3 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 110, + "d": 45, + "c": 7 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 3056, + "d": 3056, + "c": 3 + }, + { + "w": 1545523200, + "a": 620, + "d": 163, + "c": 6 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 2, + "c": 1 + }, + { + "w": 1547337600, + "a": 2760, + "d": 1514, + "c": 6 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 245, + "d": 22, + "c": 2 + }, + { + "w": 1549152000, + "a": 253, + "d": 24, + "c": 2 + }, + { + "w": 1549756800, + "a": 249, + "d": 146, + "c": 2 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 41, + "d": 7, + "c": 1 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 11, + "d": 5, + "c": 1 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "Hixie", + "id": 551196, + "node_id": "MDQ6VXNlcjU1MTE5Ng==", + "avatar_url": "https://avatars2.githubusercontent.com/u/551196?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Hixie", + "html_url": "https://github.com/Hixie", + "followers_url": "https://api.github.com/users/Hixie/followers", + "following_url": "https://api.github.com/users/Hixie/following{/other_user}", + "gists_url": "https://api.github.com/users/Hixie/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Hixie/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Hixie/subscriptions", + "organizations_url": "https://api.github.com/users/Hixie/orgs", + "repos_url": "https://api.github.com/users/Hixie/repos", + "events_url": "https://api.github.com/users/Hixie/events{/privacy}", + "received_events_url": "https://api.github.com/users/Hixie/received_events", + "type": "User", + "site_admin": false + } + }, + { + "total": 1830, + "weeks": [ + { + "w": 1413676800, + "a": 80, + "d": 0, + "c": 1 + }, + { + "w": 1414281600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1414886400, + "a": 47, + "d": 8, + "c": 3 + }, + { + "w": 1415491200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1416096000, + "a": 26, + "d": 0, + "c": 1 + }, + { + "w": 1416700800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417305600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1417910400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1418515200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419120000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1419724800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1420329600, + "a": 8, + "d": 2, + "c": 2 + }, + { + "w": 1420934400, + "a": 109, + "d": 13, + "c": 4 + }, + { + "w": 1421539200, + "a": 10, + "d": 13, + "c": 2 + }, + { + "w": 1422144000, + "a": 5163, + "d": 5010, + "c": 6 + }, + { + "w": 1422748800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1423353600, + "a": 122, + "d": 15, + "c": 2 + }, + { + "w": 1423958400, + "a": 104, + "d": 61, + "c": 4 + }, + { + "w": 1424563200, + "a": 521, + "d": 607, + "c": 15 + }, + { + "w": 1425168000, + "a": 8398, + "d": 14677, + "c": 17 + }, + { + "w": 1425772800, + "a": 631, + "d": 1533, + "c": 14 + }, + { + "w": 1426377600, + "a": 3576, + "d": 3463, + "c": 13 + }, + { + "w": 1426982400, + "a": 63217, + "d": 37295, + "c": 15 + }, + { + "w": 1427587200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428192000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1428796800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1429401600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430006400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1430611200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431216000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1431820800, + "a": 254, + "d": 26, + "c": 10 + }, + { + "w": 1432425600, + "a": 624, + "d": 718, + "c": 6 + }, + { + "w": 1433030400, + "a": 173, + "d": 118, + "c": 10 + }, + { + "w": 1433635200, + "a": 303, + "d": 297, + "c": 16 + }, + { + "w": 1434240000, + "a": 41, + "d": 43, + "c": 6 + }, + { + "w": 1434844800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1435449600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1436054400, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1436659200, + "a": 330, + "d": 68, + "c": 2 + }, + { + "w": 1437264000, + "a": 411, + "d": 417, + "c": 32 + }, + { + "w": 1437868800, + "a": 507, + "d": 486, + "c": 28 + }, + { + "w": 1438473600, + "a": 2409, + "d": 1755, + "c": 29 + }, + { + "w": 1439078400, + "a": 890, + "d": 474, + "c": 31 + }, + { + "w": 1439683200, + "a": 4617, + "d": 3081, + "c": 43 + }, + { + "w": 1440288000, + "a": 3040, + "d": 1204, + "c": 40 + }, + { + "w": 1440892800, + "a": 17925, + "d": 16635, + "c": 30 + }, + { + "w": 1441497600, + "a": 4284, + "d": 5311, + "c": 19 + }, + { + "w": 1442102400, + "a": 3499, + "d": 2938, + "c": 35 + }, + { + "w": 1442707200, + "a": 9523, + "d": 1838, + "c": 57 + }, + { + "w": 1443312000, + "a": 9016, + "d": 18483, + "c": 31 + }, + { + "w": 1443916800, + "a": 7980, + "d": 8116, + "c": 35 + }, + { + "w": 1444521600, + "a": 7565, + "d": 7505, + "c": 33 + }, + { + "w": 1445126400, + "a": 1126, + "d": 1808, + "c": 27 + }, + { + "w": 1445731200, + "a": 2621, + "d": 3555, + "c": 28 + }, + { + "w": 1446336000, + "a": 1668, + "d": 1219, + "c": 44 + }, + { + "w": 1446940800, + "a": 744, + "d": 280, + "c": 23 + }, + { + "w": 1447545600, + "a": 1595, + "d": 903, + "c": 34 + }, + { + "w": 1448150400, + "a": 3458, + "d": 2437, + "c": 26 + }, + { + "w": 1448755200, + "a": 1246, + "d": 776, + "c": 33 + }, + { + "w": 1449360000, + "a": 1390, + "d": 586, + "c": 23 + }, + { + "w": 1449964800, + "a": 1, + "d": 0, + "c": 1 + }, + { + "w": 1450569600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1451174400, + "a": 589, + "d": 160, + "c": 5 + }, + { + "w": 1451779200, + "a": 1601, + "d": 764, + "c": 18 + }, + { + "w": 1452384000, + "a": 1843, + "d": 1517, + "c": 19 + }, + { + "w": 1452988800, + "a": 4072, + "d": 3895, + "c": 29 + }, + { + "w": 1453593600, + "a": 1206, + "d": 649, + "c": 15 + }, + { + "w": 1454198400, + "a": 1749, + "d": 1090, + "c": 16 + }, + { + "w": 1454803200, + "a": 3950, + "d": 4369, + "c": 32 + }, + { + "w": 1455408000, + "a": 1717, + "d": 1911, + "c": 25 + }, + { + "w": 1456012800, + "a": 1076, + "d": 1040, + "c": 15 + }, + { + "w": 1456617600, + "a": 1986, + "d": 1089, + "c": 24 + }, + { + "w": 1457222400, + "a": 4820, + "d": 4482, + "c": 37 + }, + { + "w": 1457827200, + "a": 448, + "d": 108, + "c": 15 + }, + { + "w": 1458432000, + "a": 404, + "d": 183, + "c": 13 + }, + { + "w": 1459036800, + "a": 1622, + "d": 762, + "c": 29 + }, + { + "w": 1459641600, + "a": 3189, + "d": 2345, + "c": 24 + }, + { + "w": 1460246400, + "a": 522, + "d": 100, + "c": 4 + }, + { + "w": 1460851200, + "a": 1131, + "d": 279, + "c": 13 + }, + { + "w": 1461456000, + "a": 5508, + "d": 4788, + "c": 28 + }, + { + "w": 1462060800, + "a": 1179, + "d": 809, + "c": 18 + }, + { + "w": 1462665600, + "a": 203, + "d": 27, + "c": 4 + }, + { + "w": 1463270400, + "a": 1562, + "d": 516, + "c": 14 + }, + { + "w": 1463875200, + "a": 1638, + "d": 608, + "c": 23 + }, + { + "w": 1464480000, + "a": 3424, + "d": 1902, + "c": 26 + }, + { + "w": 1465084800, + "a": 1648, + "d": 712, + "c": 23 + }, + { + "w": 1465689600, + "a": 888, + "d": 749, + "c": 12 + }, + { + "w": 1466294400, + "a": 1666, + "d": 408, + "c": 24 + }, + { + "w": 1466899200, + "a": 2197, + "d": 1455, + "c": 13 + }, + { + "w": 1467504000, + "a": 5269, + "d": 2969, + "c": 17 + }, + { + "w": 1468108800, + "a": 4, + "d": 3, + "c": 2 + }, + { + "w": 1468713600, + "a": 132, + "d": 41, + "c": 5 + }, + { + "w": 1469318400, + "a": 737, + "d": 346, + "c": 11 + }, + { + "w": 1469923200, + "a": 307, + "d": 188, + "c": 11 + }, + { + "w": 1470528000, + "a": 6, + "d": 6, + "c": 5 + }, + { + "w": 1471132800, + "a": 173, + "d": 3, + "c": 2 + }, + { + "w": 1471737600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1472342400, + "a": 39, + "d": 25, + "c": 1 + }, + { + "w": 1472947200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1473552000, + "a": 265, + "d": 422, + "c": 8 + }, + { + "w": 1474156800, + "a": 1538, + "d": 7145, + "c": 18 + }, + { + "w": 1474761600, + "a": 1397, + "d": 1077, + "c": 16 + }, + { + "w": 1475366400, + "a": 981, + "d": 1554, + "c": 9 + }, + { + "w": 1475971200, + "a": 1718, + "d": 1425, + "c": 26 + }, + { + "w": 1476576000, + "a": 1583, + "d": 1140, + "c": 23 + }, + { + "w": 1477180800, + "a": 143, + "d": 250, + "c": 10 + }, + { + "w": 1477785600, + "a": 201, + "d": 61, + "c": 6 + }, + { + "w": 1478390400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1478995200, + "a": 311, + "d": 56, + "c": 6 + }, + { + "w": 1479600000, + "a": 460, + "d": 243, + "c": 4 + }, + { + "w": 1480204800, + "a": 71, + "d": 62, + "c": 5 + }, + { + "w": 1480809600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1481414400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482019200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1482624000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1483228800, + "a": 1451, + "d": 3016, + "c": 14 + }, + { + "w": 1483833600, + "a": 3080, + "d": 768, + "c": 21 + }, + { + "w": 1484438400, + "a": 2482, + "d": 1620, + "c": 16 + }, + { + "w": 1485043200, + "a": 2061, + "d": 936, + "c": 13 + }, + { + "w": 1485648000, + "a": 6910, + "d": 3816, + "c": 26 + }, + { + "w": 1486252800, + "a": 4188, + "d": 5064, + "c": 28 + }, + { + "w": 1486857600, + "a": 2870, + "d": 7835, + "c": 29 + }, + { + "w": 1487462400, + "a": 2062, + "d": 1437, + "c": 16 + }, + { + "w": 1488067200, + "a": 791, + "d": 169, + "c": 9 + }, + { + "w": 1488672000, + "a": 0, + "d": 2, + "c": 1 + }, + { + "w": 1489276800, + "a": 1323, + "d": 520, + "c": 18 + }, + { + "w": 1489881600, + "a": 1408, + "d": 1155, + "c": 10 + }, + { + "w": 1490486400, + "a": 4303, + "d": 3904, + "c": 10 + }, + { + "w": 1491091200, + "a": 2275, + "d": 326, + "c": 13 + }, + { + "w": 1491696000, + "a": 37, + "d": 10, + "c": 1 + }, + { + "w": 1492300800, + "a": 406, + "d": 143, + "c": 7 + }, + { + "w": 1492905600, + "a": 3533, + "d": 1287, + "c": 7 + }, + { + "w": 1493510400, + "a": 1038, + "d": 235, + "c": 8 + }, + { + "w": 1494115200, + "a": 246, + "d": 1033, + "c": 6 + }, + { + "w": 1494720000, + "a": 171, + "d": 53, + "c": 4 + }, + { + "w": 1495324800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1495929600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1496534400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497139200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1497744000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498348800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1498953600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1499558400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500163200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1500768000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501372800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1501977600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1502582400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503187200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1503792000, + "a": 3351, + "d": 973, + "c": 14 + }, + { + "w": 1504396800, + "a": 5016, + "d": 3100, + "c": 6 + }, + { + "w": 1505001600, + "a": 53, + "d": 34, + "c": 1 + }, + { + "w": 1505606400, + "a": 995, + "d": 438, + "c": 3 + }, + { + "w": 1506211200, + "a": 43, + "d": 40, + "c": 2 + }, + { + "w": 1506816000, + "a": 2242, + "d": 1744, + "c": 5 + }, + { + "w": 1507420800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1508025600, + "a": 67, + "d": 14, + "c": 2 + }, + { + "w": 1508630400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1509235200, + "a": 62, + "d": 8, + "c": 2 + }, + { + "w": 1509840000, + "a": 9, + "d": 1, + "c": 1 + }, + { + "w": 1510444800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511049600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1511654400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512259200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1512864000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1513468800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514073600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1514678400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515283200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1515888000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1516492800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517097600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1517702400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518307200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1518912000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1519516800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520121600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1520726400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521331200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1521936000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1522540800, + "a": 5, + "d": 0, + "c": 1 + }, + { + "w": 1523145600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1523750400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524355200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1524960000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1525564800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526169600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1526774400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527379200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1527984000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1528588800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1529193600, + "a": 1, + "d": 1, + "c": 1 + }, + { + "w": 1529798400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1530403200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531008000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1531612800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532217600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1532822400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1533427200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534032000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1534636800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535241600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1535846400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1536451200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537056000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1537660800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538265600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1538870400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1539475200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540080000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1540684800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541289600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1541894400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1542499200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543104000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1543708800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544313600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1544918400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1545523200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546128000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1546732800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547337600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1547942400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1548547200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549152000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1549756800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550361600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1550966400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1551571200, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552176000, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1552780800, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553385600, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1553990400, + "a": 0, + "d": 0, + "c": 0 + }, + { + "w": 1554595200, + "a": 0, + "d": 0, + "c": 0 + } + ], + "author": { + "login": "abarth", + "id": 112007, + "node_id": "MDQ6VXNlcjExMjAwNw==", + "avatar_url": "https://avatars3.githubusercontent.com/u/112007?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/abarth", + "html_url": "https://github.com/abarth", + "followers_url": "https://api.github.com/users/abarth/followers", + "following_url": "https://api.github.com/users/abarth/following{/other_user}", + "gists_url": "https://api.github.com/users/abarth/gists{/gist_id}", + "starred_url": "https://api.github.com/users/abarth/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/abarth/subscriptions", + "organizations_url": "https://api.github.com/users/abarth/orgs", + "repos_url": "https://api.github.com/users/abarth/repos", + "events_url": "https://api.github.com/users/abarth/events{/privacy}", + "received_events_url": "https://api.github.com/users/abarth/received_events", + "type": "User", + "site_admin": false + } + } +] diff --git a/web/github_dataviz/web/github_data/forks.tsv b/web/github_dataviz/web/github_data/forks.tsv new file mode 100644 index 000000000..f65d01de3 --- /dev/null +++ b/web/github_dataviz/web/github_data/forks.tsv @@ -0,0 +1,175 @@ +51 1 +54 14 +55 19 +56 11 +57 7 +58 2 +59 2 +60 4 +61 2 +62 3 +63 6 +64 2 +65 6 +66 2 +67 4 +68 5 +69 3 +70 2 +71 3 +72 3 +73 6 +75 3 +76 2 +77 6 +78 1 +79 2 +80 5 +81 4 +82 7 +83 6 +85 2 +86 1 +87 4 +88 2 +89 3 +90 1 +91 3 +92 1 +93 3 +94 4 +95 15 +96 11 +97 6 +98 1 +99 2 +100 2 +101 3 +102 2 +103 2 +104 1 +105 5 +106 6 +107 8 +108 7 +109 7 +110 4 +111 5 +112 3 +113 5 +114 2 +115 8 +116 7 +117 5 +118 5 +119 8 +120 6 +121 6 +122 5 +123 6 +124 6 +125 4 +126 5 +127 7 +128 6 +129 4 +130 4 +131 3 +132 5 +133 18 +134 9 +135 16 +136 14 +137 5 +138 6 +139 12 +140 8 +141 9 +142 9 +143 2 +144 9 +145 11 +146 7 +147 7 +148 2 +149 10 +150 13 +151 12 +152 61 +153 35 +154 15 +155 20 +156 18 +157 10 +158 8 +159 17 +160 12 +161 14 +162 21 +163 12 +164 21 +165 21 +166 10 +167 3 +171 9 +172 17 +173 14 +174 33 +175 274 +176 154 +177 111 +178 96 +179 65 +180 50 +181 65 +182 67 +183 68 +184 46 +185 87 +186 68 +187 85 +188 65 +189 73 +190 66 +191 176 +192 156 +193 109 +194 95 +195 87 +196 68 +197 98 +198 65 +199 82 +200 51 +201 68 +202 54 +203 66 +204 96 +205 62 +206 68 +207 60 +208 71 +209 74 +210 69 +211 78 +212 72 +213 66 +214 68 +215 237 +216 193 +217 117 +218 96 +219 137 +220 116 +221 133 +222 122 +223 85 +224 69 +225 115 +226 138 +227 139 +228 168 +229 145 +230 152 +231 139 +232 8 diff --git a/web/github_dataviz/web/github_data/pull_requests.tsv b/web/github_dataviz/web/github_data/pull_requests.tsv new file mode 100644 index 000000000..2ab33cabb --- /dev/null +++ b/web/github_dataviz/web/github_data/pull_requests.tsv @@ -0,0 +1,177 @@ +54 25 +55 122 +56 146 +57 90 +58 146 +59 130 +60 103 +61 21 +62 22 +63 88 +64 108 +65 92 +66 94 +67 116 +68 174 +69 124 +70 173 +71 146 +72 179 +73 142 +74 138 +75 165 +76 160 +77 114 +78 122 +79 133 +80 124 +81 110 +82 141 +83 128 +84 119 +85 117 +86 90 +87 124 +88 71 +89 37 +90 38 +91 71 +92 92 +93 106 +94 98 +95 117 +96 90 +97 80 +98 55 +99 104 +100 131 +101 101 +102 92 +103 98 +104 137 +105 113 +106 134 +107 103 +108 105 +109 75 +110 100 +111 70 +112 47 +113 10 +115 73 +116 137 +117 78 +118 178 +119 191 +120 206 +121 205 +122 138 +123 160 +124 149 +125 167 +126 130 +127 126 +128 175 +129 87 +130 148 +131 129 +132 181 +133 194 +134 124 +135 114 +136 101 +137 107 +138 157 +139 127 +140 100 +141 39 +142 106 +143 105 +144 52 +145 77 +146 60 +147 43 +148 103 +149 127 +150 88 +151 98 +152 84 +153 104 +154 93 +155 74 +156 119 +157 72 +158 101 +159 110 +160 72 +161 95 +162 129 +163 139 +164 148 +165 105 +166 9 +167 4 +171 44 +172 160 +173 146 +174 92 +175 117 +176 156 +177 151 +178 140 +179 110 +180 48 +181 99 +182 128 +183 140 +184 149 +185 127 +186 100 +187 76 +188 131 +189 103 +190 125 +191 108 +192 120 +193 82 +194 125 +195 123 +196 166 +197 226 +198 178 +199 216 +200 148 +201 201 +202 219 +203 166 +204 157 +205 217 +206 246 +207 190 +208 221 +209 209 +210 172 +211 221 +212 183 +213 91 +214 104 +215 52 +216 165 +217 157 +218 99 +219 213 +220 246 +221 260 +222 169 +223 174 +224 199 +225 233 +226 190 +227 233 +228 181 +229 257 +230 240 +231 284 +232 214 +233 271 +234 48 diff --git a/web/github_dataviz/web/github_data/stars.tsv b/web/github_dataviz/web/github_data/stars.tsv new file mode 100644 index 000000000..e9237339f --- /dev/null +++ b/web/github_dataviz/web/github_data/stars.tsv @@ -0,0 +1,206 @@ +22 8 +23 4 +24 3 +25 2 +26 1 +27 356 +28 1108 +29 118 +30 36 +31 28 +32 19 +33 24 +34 11 +35 6 +36 20 +37 14 +38 17 +39 1 +40 3 +43 1 +44 2 +47 2 +48 1 +50 1 +51 1 +52 1 +53 5 +54 85 +55 68 +56 54 +57 116 +58 43 +59 36 +60 24 +61 25 +62 26 +63 31 +64 28 +65 23 +66 19 +67 15 +68 12 +69 22 +70 15 +71 21 +72 18 +73 19 +74 11 +75 15 +76 8 +77 7 +78 10 +79 3 +80 10 +81 9 +82 30 +83 17 +84 13 +85 12 +86 4 +87 25 +88 58 +89 7 +90 13 +91 8 +92 10 +93 11 +94 38 +95 112 +96 44 +97 22 +98 36 +99 25 +100 10 +101 32 +102 11 +103 12 +104 20 +105 107 +106 38 +107 33 +108 41 +109 21 +110 27 +111 17 +112 22 +113 10 +114 9 +115 54 +116 32 +117 35 +118 26 +119 19 +120 30 +121 83 +122 30 +123 33 +124 31 +125 28 +126 13 +127 77 +128 41 +129 27 +130 36 +131 26 +132 26 +133 248 +134 166 +135 138 +136 91 +137 63 +138 61 +139 169 +140 144 +141 69 +142 45 +143 45 +144 43 +145 59 +146 50 +147 86 +148 114 +149 96 +150 93 +151 74 +152 944 +153 352 +154 130 +155 159 +156 111 +157 105 +158 64 +159 113 +160 121 +161 150 +162 167 +163 120 +164 208 +165 151 +166 92 +167 96 +168 116 +169 108 +170 139 +171 156 +172 162 +173 108 +174 353 +175 4210 +176 2601 +177 1227 +178 798 +179 594 +180 464 +181 633 +182 514 +183 400 +184 388 +185 717 +186 644 +187 646 +188 526 +189 756 +190 457 +191 2248 +192 1649 +193 906 +194 794 +195 702 +196 591 +197 725 +198 584 +199 502 +200 477 +201 506 +202 488 +203 398 +204 738 +205 549 +206 339 +207 455 +208 398 +209 452 +210 424 +211 485 +212 415 +213 411 +214 493 +215 2359 +216 1458 +217 860 +218 764 +219 716 +220 741 +221 999 +222 904 +223 669 +224 430 +225 729 +226 793 +227 976 +228 1032 +229 973 +230 998 +231 1032 +232 87 diff --git a/web/github_dataviz/web/index.html b/web/github_dataviz/web/index.html new file mode 100644 index 000000000..b54ed98d8 --- /dev/null +++ b/web/github_dataviz/web/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/github_dataviz/web/main.dart b/web/github_dataviz/web/main.dart new file mode 100644 index 000000000..1eaf7e4f2 --- /dev/null +++ b/web/github_dataviz/web/main.dart @@ -0,0 +1,10 @@ +// 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 'package:flutter_web_ui/ui.dart' as ui; +import 'package:github_dataviz/main.dart' as app; + +main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/github_dataviz/web/preview.png b/web/github_dataviz/web/preview.png new file mode 100644 index 000000000..f74999be4 Binary files /dev/null and b/web/github_dataviz/web/preview.png differ diff --git a/web/particle_background/LICENSE b/web/particle_background/LICENSE new file mode 100644 index 000000000..f1e7e2f51 --- /dev/null +++ b/web/particle_background/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Felix Blaschke + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/web/particle_background/README.md b/web/particle_background/README.md new file mode 100644 index 000000000..143b22253 --- /dev/null +++ b/web/particle_background/README.md @@ -0,0 +1,7 @@ +Flutter app demonstrating +[simple_animations](https://pub.dev/packages/simple_animations) in action. + +Contributed by Felix Blaschke. + +One of a series of samples +[published here](https://github.com/felixblaschke/simple_animations_example_app). diff --git a/web/particle_background/lib/main.dart b/web/particle_background/lib/main.dart new file mode 100644 index 000000000..18b8fd44d --- /dev/null +++ b/web/particle_background/lib/main.dart @@ -0,0 +1,171 @@ +import 'dart:math'; + +import 'package:flutter_web/material.dart'; +import 'package:particle_background/simple_animations_package.dart'; + +void main() => runApp(ParticleApp()); + +class ParticleApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: ParticleBackgroundPage(), + ), + ); + } +} + +class ParticleBackgroundPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned.fill(child: AnimatedBackground()), + Positioned.fill(child: Particles(30)), + Positioned.fill(child: CenteredText()), + ], + ); + } +} + +class Particles extends StatefulWidget { + final int numberOfParticles; + + Particles(this.numberOfParticles); + + @override + _ParticlesState createState() => _ParticlesState(); +} + +class _ParticlesState extends State { + final Random random = Random(); + + final List particles = []; + + @override + void initState() { + List.generate(widget.numberOfParticles, (index) { + particles.add(ParticleModel(random)); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Rendering( + startTime: Duration(seconds: 30), + onTick: _simulateParticles, + builder: (context, time) { + return CustomPaint( + painter: ParticlePainter(particles, time), + ); + }, + ); + } + + _simulateParticles(Duration time) { + particles.forEach((particle) => particle.maintainRestart(time)); + } +} + +class ParticleModel { + Animatable tween; + double size; + AnimationProgress animationProgress; + Random random; + + ParticleModel(this.random) { + restart(); + } + + restart({Duration time = Duration.zero}) { + final startPosition = Offset(-0.2 + 1.4 * random.nextDouble(), 1.2); + final endPosition = Offset(-0.2 + 1.4 * random.nextDouble(), -0.2); + final duration = Duration(milliseconds: 3000 + random.nextInt(6000)); + + tween = MultiTrackTween([ + Track("x").add( + duration, Tween(begin: startPosition.dx, end: endPosition.dx), + curve: Curves.easeInOutSine), + Track("y").add( + duration, Tween(begin: startPosition.dy, end: endPosition.dy), + curve: Curves.easeIn), + ]); + animationProgress = AnimationProgress(duration: duration, startTime: time); + size = 0.2 + random.nextDouble() * 0.4; + } + + maintainRestart(Duration time) { + if (animationProgress.progress(time) == 1.0) { + restart(time: time); + } + } +} + +class ParticlePainter extends CustomPainter { + List particles; + Duration time; + + ParticlePainter(this.particles, this.time); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = Colors.white.withAlpha(50); + + particles.forEach((particle) { + var progress = particle.animationProgress.progress(time); + final animation = particle.tween.transform(progress); + final position = + Offset(animation["x"] * size.width, animation["y"] * size.height); + canvas.drawCircle(position, size.width * 0.2 * particle.size, paint); + }); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => true; +} + +class AnimatedBackground extends StatelessWidget { + @override + Widget build(BuildContext context) { + final tween = MultiTrackTween([ + Track("color1").add(Duration(seconds: 3), + ColorTween(begin: Color(0xff8a113a), end: Colors.lightBlue.shade900)), + Track("color2").add(Duration(seconds: 3), + ColorTween(begin: Color(0xff440216), end: Colors.blue.shade600)) + ]); + + return ControlledAnimation( + playback: Playback.MIRROR, + tween: tween, + duration: tween.duration, + builder: (context, animation) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [animation["color1"], animation["color2"]])), + ); + }, + ); + } +} + +class CenteredText extends StatelessWidget { + const CenteredText({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + "Welcome to Flutter for web", + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w200), + textScaleFactor: 4, + ), + ); + } +} diff --git a/web/particle_background/lib/simple_animations_package.dart b/web/particle_background/lib/simple_animations_package.dart new file mode 100644 index 000000000..09a5d094b --- /dev/null +++ b/web/particle_background/lib/simple_animations_package.dart @@ -0,0 +1,465 @@ +// Package simple_animations: +// https://pub.dev/packages/simple_animations + +import 'dart:math'; +import 'package:flutter_web/widgets.dart'; +import 'package:flutter_web/scheduler.dart'; + +/// Widget to easily create a continuous animation. +/// +/// You need to specify a [builder] function that gets the build context passed +/// along with the [timeElapsed] (as a [Duration]) since the rendering started. +/// You can use this time to specify custom animations on it. +/// +/// The [builder] rebuilds all sub-widgets every frame. +/// +/// You define an [onTick] function that is called before builder to update +/// you rendered scene. It's also utilized during fast-forwarding the animation. +/// +/// Specify a [startTime] to fast-forward your animation in the beginning. +/// The widget will interpolate the animation by calling the [onTick] function +/// multiple times. (Default value is `20`. You can tune it by setting the +/// [startTimeSimulationTicks] property.) +class Rendering extends StatefulWidget { + final Widget Function(BuildContext context, Duration timeElapsed) builder; + final Function(Duration timeElapsed) onTick; + final Duration startTime; + final int startTimeSimulationTicks; + + Rendering( + {this.builder, + this.onTick, + this.startTime = Duration.zero, + this.startTimeSimulationTicks = 20}) + : assert(builder != null, "Builder needs to defined."); + + @override + _RenderingState createState() => _RenderingState(); +} + +class _RenderingState extends State + with SingleTickerProviderStateMixin { + Ticker _ticker; + Duration _timeElapsed = Duration(milliseconds: 0); + + @override + void initState() { + if (widget.startTime > Duration.zero) { + _simulateStartTimeTicks(); + } + + _ticker = createTicker((elapsed) { + _onRender(elapsed + widget.startTime); + }); + _ticker.start(); + super.initState(); + } + + void _onRender(Duration effectiveElapsed) { + if (widget.onTick != null) { + widget.onTick(effectiveElapsed); + } + setState(() { + _timeElapsed = effectiveElapsed; + }); + } + + void _simulateStartTimeTicks() { + if (widget.onTick != null) { + Iterable.generate(widget.startTimeSimulationTicks + 1).forEach((i) { + final simulatedTime = Duration( + milliseconds: (widget.startTime.inMilliseconds * + i / + widget.startTimeSimulationTicks) + .round()); + widget.onTick(simulatedTime); + }); + } + } + + @override + void dispose() { + _ticker.stop(canceled: true); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.builder(context, _timeElapsed); + } +} + +/// Animatable that tweens multiple parallel properties (called [Track]s). +/// --- +/// The constructor of [MultiTrackTween] expects a list of [Track] objects. +/// You can fetch the specified total duration via [duration] getter. +/// --- +/// Example: +/// +/// ```dart +/// final tween = MultiTrackTween([ +/// Track("color") +/// .add(Duration(seconds: 1), ColorTween(begin: Colors.red, end: Colors.blue)) +/// .add(Duration(seconds: 1), ColorTween(begin: Colors.blue, end: Colors.yellow)), +/// Track("width") +/// .add(Duration(milliseconds: 500), ConstantTween(200.0)) +/// .add(Duration(milliseconds: 1500), Tween(begin: 200.0, end: 400.0), +/// curve: Curves.easeIn) +/// ]); +/// +/// return ControlledAnimation( +/// duration: tween.duration, +/// tween: tween, +/// builder: (context, values) { +/// ... +/// } +/// ); +/// ``` +class MultiTrackTween extends Animatable> { + final _tracksToTween = Map(); + var _maxDuration = 0; + + MultiTrackTween(List tracks) + : assert(tracks != null && tracks.length > 0, + "Add a List to configure the tween."), + assert(tracks.where((track) => track.items.length == 0).length == 0, + "Each Track needs at least one added Tween by using the add()-method.") { + _computeMaxDuration(tracks); + _computeTrackTweens(tracks); + } + + void _computeMaxDuration(List tracks) { + tracks.forEach((track) { + final trackDuration = track.items + .map((item) => item.duration.inMilliseconds) + .reduce((sum, item) => sum + item); + _maxDuration = max(_maxDuration, trackDuration); + }); + } + + void _computeTrackTweens(List tracks) { + tracks.forEach((track) { + final trackDuration = track.items + .map((item) => item.duration.inMilliseconds) + .reduce((sum, item) => sum + item); + + final sequenceItems = track.items + .map((item) => TweenSequenceItem( + tween: item.tween, + weight: item.duration.inMilliseconds / _maxDuration)) + .toList(); + + if (trackDuration < _maxDuration) { + sequenceItems.add(TweenSequenceItem( + tween: ConstantTween(null), + weight: (_maxDuration - trackDuration) / _maxDuration)); + } + + final sequence = TweenSequence(sequenceItems); + + _tracksToTween[track.name] = + _TweenData(tween: sequence, maxTime: trackDuration / _maxDuration); + }); + } + + /// Returns the highest duration specified by [Track]s. + /// --- + /// Use it to pass it into an [ControlledAnimation]. + /// + /// You can also scale it by multiplying a double value. + /// + /// Example: + /// ```dart + /// final tween = MultiTrackTween(listOfTracks); + /// + /// return ControlledAnimation( + /// duration: tween.duration * 1.25, // stretch animation by 25% + /// tween: tween, + /// builder: (context, values) { + /// ... + /// } + /// ); + /// ``` + Duration get duration { + return Duration(milliseconds: _maxDuration); + } + + @override + Map transform(double t) { + final Map result = Map(); + _tracksToTween.forEach((name, tweenData) { + final double tTween = max(0, min(t, tweenData.maxTime - 0.0001)); + result[name] = tweenData.tween.transform(tTween); + }); + return result; + } +} + +/// Single property to tween. Used by [MultiTrackTween]. +class Track { + final String name; + final List<_TrackItem> items = []; + + Track(this.name) : assert(name != null, "Track name must not be null."); + + /// Adds a "piece of animation" to a [Track]. + /// + /// You need to pass a [duration] and a [tween]. It will return the track, so + /// you can specify a track in a builder's style. + /// + /// Optionally you can set a named parameter [curve] that applies an easing + /// curve to the tween. + Track add(Duration duration, Animatable tween, {Curve curve}) { + items.add(_TrackItem(duration, tween, curve: curve)); + return this; + } +} + +class _TrackItem { + final Duration duration; + Animatable tween; + + _TrackItem(this.duration, Animatable _tween, {Curve curve}) + : assert(duration != null, "Please set a duration."), + assert(_tween != null, "Please set a tween.") { + if (curve != null) { + tween = _tween.chain(CurveTween(curve: curve)); + } else { + tween = _tween; + } + } +} + +class _TweenData { + final Animatable tween; + final double maxTime; + + _TweenData({this.tween, this.maxTime}); +} + +/// Playback tell the controller of the animation what to do. +enum Playback { + /// Animation stands still. + PAUSE, + + /// Animation plays forwards and stops at the end. + PLAY_FORWARD, + + /// Animation plays backwards and stops at the beginning. + PLAY_REVERSE, + + /// Animation will reset to the beginning and start playing forward. + START_OVER_FORWARD, + + /// Animation will reset to the end and start play backward. + START_OVER_REVERSE, + + /// Animation will play forwards and start over at the beginning when it + /// reaches the end. + LOOP, + + /// Animation will play forward until the end and will reverse playing until + /// it reaches the beginning. Then it starts over playing forward. And so on. + MIRROR +} + +/// Widget to create custom, managed, tween-based animations in a very simple way. +/// +/// --- +/// +/// An internal [AnimationController] will do everything you tell him by +/// dynamically assigning the one [Playback] to [playback] property. +/// By default the animation will start playing forward and stops at the end. +/// +/// A minimum set of properties are [duration] (time span of the animation), +/// [tween] (values to interpolate among the animation) and a [builder] function +/// (defines the animated scene). +/// +/// Instead of using [builder] as building function you can use for performance +/// critical scenarios [builderWithChild] along with a prebuild [child]. +/// +/// --- +/// +/// The following properties are optional: +/// +/// - You can apply a [delay] that forces the animation to pause a +/// specified time before the animation will perform the defined [playback] +/// instruction. +/// +/// - You can specify a [curve] that modifies the [tween] by applying a +/// non-linear animation function. You can find curves in [Curves], for +/// example [Curves.easeOut] or [Curves.easeIn]. +/// +/// - You can track the animation by setting an [AnimationStatusListener] to +/// the property [animationControllerStatusListener]. The internal [AnimationController] then +/// will route out any events that occur. [ControlledAnimation] doesn't filter +/// or modifies these events. These events are currently only reliable for the +/// [playback]-types [Playback.PLAY_FORWARD] and [Playback.PLAY_REVERSE]. +/// +/// - You can set the start position of animation by specifying [startPosition] +/// with a value between *0.0* and *1.0*. The [startPosition] is only +/// evaluated once during the initialization of the widget. +/// +class ControlledAnimation extends StatefulWidget { + final Playback playback; + final Animatable tween; + final Curve curve; + final Duration duration; + final Duration delay; + final Widget Function(BuildContext buildContext, T animatedValue) builder; + final Widget Function(BuildContext, Widget child, T animatedValue) + builderWithChild; + final Widget child; + final AnimationStatusListener animationControllerStatusListener; + final double startPosition; + + ControlledAnimation( + {this.playback = Playback.PLAY_FORWARD, + this.tween, + this.curve = Curves.linear, + this.duration, + this.delay, + this.builder, + this.builderWithChild, + this.child, + this.animationControllerStatusListener, + this.startPosition = 0.0, + Key key}) + : assert(duration != null, + "Please set property duration. Example: Duration(milliseconds: 500)"), + assert(tween != null, + "Please set property tween. Example: Tween(from: 0.0, to: 100.0)"), + assert( + (builderWithChild != null && child != null && builder == null) || + (builder != null && builderWithChild == null && child == null), + "Either use just builder and keep buildWithChild and child null. " + "Or keep builder null and set a builderWithChild and a child."), + assert( + startPosition >= 0 && startPosition <= 1, + "The property startPosition " + "must have a value between 0.0 and 1.0."), + super(key: key); + + @override + _ControlledAnimationState createState() => _ControlledAnimationState(); +} + +class _ControlledAnimationState extends State + with SingleTickerProviderStateMixin { + AnimationController _controller; + Animation _animation; + bool _isDisposed = false; + bool _waitForDelay = true; + bool _isCurrentlyMirroring = false; + + @override + void initState() { + _controller = AnimationController(vsync: this, duration: widget.duration) + ..addListener(() { + setState(() {}); + }) + ..value = widget.startPosition; + + _animation = widget.tween + .chain(CurveTween(curve: widget.curve)) + .animate(_controller); + + if (widget.animationControllerStatusListener != null) { + _controller.addStatusListener(widget.animationControllerStatusListener); + } + + initialize(); + super.initState(); + } + + void initialize() async { + if (widget.delay != null) { + await Future.delayed(widget.delay); + } + _waitForDelay = false; + executeInstruction(); + } + + @override + void didUpdateWidget(ControlledAnimation oldWidget) { + _controller.duration = widget.duration; + executeInstruction(); + super.didUpdateWidget(oldWidget); + } + + void executeInstruction() async { + if (_isDisposed || _waitForDelay) { + return; + } + + if (widget.playback == Playback.PAUSE) { + _controller.stop(); + } + if (widget.playback == Playback.PLAY_FORWARD) { + _controller.forward(); + } + if (widget.playback == Playback.PLAY_REVERSE) { + _controller.reverse(); + } + if (widget.playback == Playback.START_OVER_FORWARD) { + _controller.forward(from: 0.0); + } + if (widget.playback == Playback.START_OVER_REVERSE) { + _controller.reverse(from: 1.0); + } + if (widget.playback == Playback.LOOP) { + _controller.repeat(); + } + if (widget.playback == Playback.MIRROR && !_isCurrentlyMirroring) { + _isCurrentlyMirroring = true; + _controller.repeat(reverse: true); + } + + if (widget.playback != Playback.MIRROR) { + _isCurrentlyMirroring = false; + } + } + + @override + Widget build(BuildContext context) { + if (widget.builder != null) { + return widget.builder(context, _animation.value); + } else if (widget.builderWithChild != null && widget.child != null) { + return widget.builderWithChild(context, widget.child, _animation.value); + } + _controller.stop(canceled: true); + throw FlutterError( + "I don't know how to build the animation. Make sure to either specify " + "a builder or a builderWithChild (along with a child)."); + } + + @override + void dispose() { + _isDisposed = true; + _controller.dispose(); + super.dispose(); + } +} + +/// Utility class to compute an animation progress between two points in time. +/// +/// On creation you specify a [startTime] and a [duration]. +/// +/// You can query the progress value - a value between `0.0` and `11.0` - by +/// calling [progress] and passing the current time. +class AnimationProgress { + final Duration duration; + final Duration startTime; + + /// Creates an [AnimationProgress]. + AnimationProgress({this.duration, this.startTime}) + : assert(duration != null, "Please specify an animation duration."), + assert( + startTime != null, "Please specify a start time of the animation."); + + /// Queries the current progress value based on the specified [startTime] and + /// [duration] as a value between `0.0` and `1.0`. It will automatically + /// clamp values this interval to fit in. + double progress(Duration time) => max(0.0, + min((time - startTime).inMilliseconds / duration.inMilliseconds, 1.0)); +} diff --git a/web/particle_background/pubspec.lock b/web/particle_background/pubspec.lock new file mode 100644 index 000000000..2e9964fa1 --- /dev/null +++ b/web/particle_background/pubspec.lock @@ -0,0 +1,471 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct main" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/particle_background/pubspec.yaml b/web/particle_background/pubspec.yaml new file mode 100644 index 000000000..aaf13687f --- /dev/null +++ b/web/particle_background/pubspec.yaml @@ -0,0 +1,27 @@ +name: particle_background +description: Example for the simple_animations package +author: Felix Blaschke +homepage: https://github.com/felixblaschke/simple_animations + +environment: + sdk: ">=2.2.0 <3.0.0" + +dependencies: + flutter_web: any + flutter_web_ui: any + +dev_dependencies: + build_runner: any + build_web_compilers: any + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/particle_background/web/index.html b/web/particle_background/web/index.html new file mode 100644 index 000000000..b54ed98d8 --- /dev/null +++ b/web/particle_background/web/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/particle_background/web/main.dart b/web/particle_background/web/main.dart new file mode 100644 index 000000000..bd55290c7 --- /dev/null +++ b/web/particle_background/web/main.dart @@ -0,0 +1,10 @@ +// 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 'package:flutter_web_ui/ui.dart' as ui; +import 'package:particle_background/main.dart' as app; + +main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/particle_background/web/preview.png b/web/particle_background/web/preview.png new file mode 100644 index 000000000..7a63a7991 Binary files /dev/null and b/web/particle_background/web/preview.png differ diff --git a/web/peanut.yaml b/web/peanut.yaml new file mode 100644 index 000000000..a98fb7957 --- /dev/null +++ b/web/peanut.yaml @@ -0,0 +1,30 @@ +# Configuration for https://pub.dartlang.org/packages/peanut + +directories: +- charts/example/web +- dad_jokes/web +- filipino_cuisine/web +- gallery/web +- github_dataviz/web +- particle_background/web +- slide_puzzle/web +- spinning_square/web +- timeflow/web +- vision_challenge/web + +builder-options: + build_web_compilers|entrypoint: + dart2js_args: + # Generate `.info.json` files for compiled libraries. + - --dump-info + # Reduces diffs between builds, but increases JS output size slightly. + - --no-frequency-based-minification + # Deployed example does not include Dart source. Not needed. + - --no-source-maps + # Enable all dart2js optimizations. Not recommended without thorough testing. + - -O4 + build_web_compilers|dart2js_archive_extractor: + # Ensures .info.json file is preserved. Useful for tracking output details. + filter_outputs: false + +post-build-dart-script: _tool/peanut_post_build.dart diff --git a/web/readme.md b/web/readme.md new file mode 100644 index 000000000..a8875a81b --- /dev/null +++ b/web/readme.md @@ -0,0 +1 @@ +Samples for [Flutter for web](https://flutter.dev/web). diff --git a/web/slide_puzzle/README.md b/web/slide_puzzle/README.md new file mode 100644 index 000000000..998c2fa12 --- /dev/null +++ b/web/slide_puzzle/README.md @@ -0,0 +1,4 @@ +Get the numbers in order by clicking tiles next to the open space. + +Created by [Kevin Moore](https://twitter.com/kevmoo). Original source at +[github.com/kevmoo/slide_puzzle](https://github.com/kevmoo/slide_puzzle). diff --git a/web/slide_puzzle/analysis_options.yaml b/web/slide_puzzle/analysis_options.yaml new file mode 100644 index 000000000..d7de72eb6 --- /dev/null +++ b/web/slide_puzzle/analysis_options.yaml @@ -0,0 +1,93 @@ +include: package:pedantic/analysis_options.yaml +analyzer: + strong-mode: + implicit-casts: false +linter: + rules: + - always_declare_return_types + - annotate_overrides + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_empty_else + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_types_as_parameter_names + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - comment_references + - constant_identifier_names + - control_flow_in_finally + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - file_names + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - join_return_with_assignment + - library_names + - library_prefixes + - list_remove_unrelated_type + - literal_only_boolean_expressions + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_final_locals + - prefer_generic_function_type_aliases + - prefer_initializing_formals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_null_aware_operators + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_function_type_syntax_for_parameters + - use_rethrow_when_possible + - valid_regexps + - void_checks diff --git a/web/slide_puzzle/lib/main.dart b/web/slide_puzzle/lib/main.dart new file mode 100644 index 000000000..261c3d06a --- /dev/null +++ b/web/slide_puzzle/lib/main.dart @@ -0,0 +1,33 @@ +// 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 'src/core/puzzle_animator.dart'; +import 'src/flutter.dart'; +import 'src/puzzle_home_state.dart'; + +void main() => runApp(PuzzleApp()); + +class PuzzleApp extends StatelessWidget { + final int rows, columns; + + PuzzleApp({int columns = 4, int rows = 4}) + : columns = columns ?? 4, + rows = rows ?? 4; + + @override + Widget build(BuildContext context) => MaterialApp( + title: 'Slide Puzzle', + home: _PuzzleHome(rows, columns), + ); +} + +class _PuzzleHome extends StatefulWidget { + final int _rows, _columns; + + const _PuzzleHome(this._rows, this._columns, {Key key}) : super(key: key); + + @override + PuzzleHomeState createState() => + PuzzleHomeState(PuzzleAnimator(_columns, _rows)); +} diff --git a/web/slide_puzzle/lib/src/app_state.dart b/web/slide_puzzle/lib/src/app_state.dart new file mode 100644 index 000000000..a94c31c94 --- /dev/null +++ b/web/slide_puzzle/lib/src/app_state.dart @@ -0,0 +1,31 @@ +// 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 'core/puzzle_animator.dart'; +import 'flutter.dart'; +import 'shared_theme.dart'; + +abstract class AppState { + TabController get tabController; + + PuzzleProxy get puzzle; + + bool get autoPlay; + + void setAutoPlay(bool newValue); + + AnimationNotifier get animationNotifier; + + Iterable get themeData; + + SharedTheme get currentTheme; + + set currentTheme(SharedTheme theme); +} + +abstract class AnimationNotifier implements Listenable { + void animate(); + + void dispose(); +} diff --git a/web/slide_puzzle/lib/src/core/body.dart b/web/slide_puzzle/lib/src/core/body.dart new file mode 100644 index 000000000..76fd7ffe4 --- /dev/null +++ b/web/slide_puzzle/lib/src/core/body.dart @@ -0,0 +1,113 @@ +// 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:math' show Point; + +const zeroPoint = Point(0, 0); + +const _epsilon = 0.0001; + +/// Represents a point object with a location and velocity. +class Body { + Point _velocity; + Point _location; + + Point get velocity => _velocity; + + Point get location => _location; + + Body({Point location = zeroPoint, Point velocity = zeroPoint}) + : assert(location.magnitude.isFinite), + _location = location, + assert(velocity.magnitude.isFinite), + _velocity = velocity; + + factory Body.raw(double x, double y, double vx, double vy) => + Body(location: Point(x, y), velocity: Point(vx, vy)); + + Body clone() => Body(location: _location, velocity: _velocity); + + /// Add the velocity specified in [delta] to `this`. + void kick(Point delta) { + assert(delta.magnitude.isFinite); + _velocity = delta; + } + + /// [drag] must be greater than or equal to zero. It defines the percent of + /// the previous velocity that is lost every second. + bool animate(double seconds, + {Point force = zeroPoint, + double drag = 0, + double maxVelocity, + Point snapTo}) { + assert(seconds.isFinite && seconds > 0, + 'milliseconds must be finite and > 0 (was $seconds)'); + + force ??= zeroPoint; + assert(force.x.isFinite && force.y.isFinite, 'force must be finite'); + + drag ??= 0; + assert(drag.isFinite && drag >= 0, 'drag must be finiate and >= 0'); + + maxVelocity ??= double.infinity; + assert(maxVelocity > 0, 'maxVelocity must be null or > 0'); + + final dragVelocity = _velocity * (1 - drag * seconds); + + if (_sameDirection(_velocity, dragVelocity)) { + assert(dragVelocity.magnitude <= _velocity.magnitude, + 'Huh? $dragVelocity $_velocity'); + _velocity = dragVelocity; + } else { + _velocity = zeroPoint; + } + + // apply force to velocity + _velocity += force * seconds; + + // apply terminal velocity + if (_velocity.magnitude > maxVelocity) { + _velocity = _unitPoint(_velocity) * maxVelocity; + } + + // update location + final locationDelta = _velocity * seconds; + if (locationDelta.magnitude > _epsilon || + (force.magnitude * seconds) > _epsilon) { + _location += locationDelta; + return true; + } else { + if (snapTo != null && (_location.distanceTo(snapTo) < _epsilon * 2)) { + _location = snapTo; + } + _velocity = zeroPoint; + return false; + } + } + + @override + String toString() => + 'Body @(${_location.x},${_location.y}) ↕(${_velocity.x},${_velocity.y})'; + + @override + bool operator ==(Object other) => + other is Body && + other._location == _location && + other._velocity == _velocity; + + // Since this is a mutable class, a constant value is returned for `hashCode` + // This ensures values don't get lost in a Hashing data structure. + // Note: this means you shouldn't use this type in most Map/Set impls. + @override + int get hashCode => 199; +} + +Point _unitPoint(Point source) { + final result = source * (1 / source.magnitude); + return Point(result.x.isNaN ? 0 : result.x, result.y.isNaN ? 0 : result.y); +} + +bool _sameDirection(Point a, Point b) { + return a.x.sign == b.x.sign && a.y.sign == b.y.sign; +} diff --git a/web/slide_puzzle/lib/src/core/point_int.dart b/web/slide_puzzle/lib/src/core/point_int.dart new file mode 100644 index 000000000..9854ebe90 --- /dev/null +++ b/web/slide_puzzle/lib/src/core/point_int.dart @@ -0,0 +1,12 @@ +// 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:math' as math; + +class Point extends math.Point { + Point(int x, int y) : super(x, y); + + @override + Point operator +(math.Point other) => Point(x + other.x, y + other.y); +} diff --git a/web/slide_puzzle/lib/src/core/puzzle.dart b/web/slide_puzzle/lib/src/core/puzzle.dart new file mode 100644 index 000000000..059d1f21d --- /dev/null +++ b/web/slide_puzzle/lib/src/core/puzzle.dart @@ -0,0 +1,275 @@ +// 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:collection'; +import 'dart:convert'; +import 'dart:math' show Random, max; +import 'dart:typed_data'; + +import 'point_int.dart'; +import 'util.dart'; + +part 'puzzle_simple.dart'; + +part 'puzzle_smart.dart'; + +final _rnd = Random(); + +final _spacesRegexp = RegExp(' +'); + +abstract class Puzzle { + int get width; + + int get length; + + int operator [](int index); + + int indexOf(int value); + + List get _intView; + + List _copyData(); + + Puzzle _newWithValues(List values); + + Puzzle clone(); + + int get height => length ~/ width; + + Puzzle._(); + + factory Puzzle._raw(int width, List source) { + if (source.length <= 16) { + return _PuzzleSmart(width, source); + } + return _PuzzleSimple(width, source); + } + + factory Puzzle.raw(int width, List source) { + requireArgument(width >= 3, 'width', 'Must be at least 3.'); + requireArgument(source.length >= 6, 'source', 'Must be at least 6 items'); + _validate(source); + + return Puzzle._raw(width, source); + } + + factory Puzzle(int width, int height) => + Puzzle.raw(width, _randomList(width, height)); + + factory Puzzle.parse(String input) { + final rows = LineSplitter.split(input).map((line) { + final splits = line.trim().split(_spacesRegexp); + return splits.map(int.parse).toList(); + }).toList(); + + return Puzzle.raw(rows.first.length, rows.expand((row) => row).toList()); + } + + int valueAt(int x, int y) { + final i = _getIndex(x, y); + return this[i]; + } + + int get tileCount => length - 1; + + bool isCorrectPosition(int cellValue) => cellValue == this[cellValue]; + + bool get solvable => isSolvable(width, _intView); + + Puzzle reset({List source}) { + final data = (source == null) + ? _randomizeList(width, _intView) + : Uint8List.fromList(source); + + if (data.length != length) { + throw ArgumentError.value(source, 'source', 'Cannot change the size!'); + } + _validate(data); + if (!isSolvable(width, data)) { + throw ArgumentError.value(source, 'source', 'Not a solvable puzzle.'); + } + + return _newWithValues(data); + } + + int get incorrectTiles { + var count = tileCount; + for (var i = 0; i < tileCount; i++) { + if (isCorrectPosition(i)) { + count--; + } + } + return count; + } + + Point openPosition() => coordinatesOf(tileCount); + + /// A measure of how close the puzzle is to being solved. + /// + /// The sum of all of the distances squared `(x + y)^2 ` each tile has to move + /// to be in the correct position. + /// + /// `0` - you've won! + int get fitness { + var value = 0; + for (var i = 0; i < tileCount; i++) { + if (!isCorrectPosition(i)) { + final correctColumn = i % width; + final correctRow = i ~/ width; + + final index = indexOf(i); + final x = index % width; + final y = index ~/ width; + + final delta = (correctColumn - x).abs() + (correctRow - y).abs(); + + value += delta * delta; + } + } + return value * incorrectTiles; + } + + Puzzle clickRandom({bool vertical}) { + final clickable = clickableValues(vertical: vertical).toList(); + return clickValue(clickable[_rnd.nextInt(clickable.length)]); + } + + Iterable allMovable() => + (clickableValues()..shuffle(_rnd)).map(_clickValue); + + List clickableValues({bool vertical}) { + final open = openPosition(); + final doRow = vertical == null || vertical == false; + final doColumn = vertical == null || vertical; + + final values = + Uint8List((doRow ? (width - 1) : 0) + (doColumn ? (height - 1) : 0)); + + var index = 0; + + if (doRow) { + for (var x = 0; x < width; x++) { + if (x != open.x) { + values[index++] = valueAt(x, open.y); + } + } + } + if (doColumn) { + for (var y = 0; y < height; y++) { + if (y != open.y) { + values[index++] = valueAt(open.x, y); + } + } + } + + return values; + } + + bool _movable(int tileValue) { + if (tileValue == tileCount) { + return false; + } + + final target = coordinatesOf(tileValue); + final lastCoord = openPosition(); + if (lastCoord.x != target.x && lastCoord.y != target.y) { + return false; + } + return true; + } + + Puzzle clickValue(int tileValue) { + if (!_movable(tileValue)) { + return null; + } + return _clickValue(tileValue); + } + + Puzzle _clickValue(int tileValue) { + assert(_movable(tileValue)); + final target = coordinatesOf(tileValue); + + final newStore = _copyData(); + + _shift(newStore, target.x, target.y); + return _newWithValues(newStore); + } + + void _shift(List source, int targetX, int targetY) { + final lastCoord = openPosition(); + final deltaX = lastCoord.x - targetX; + final deltaY = lastCoord.y - targetY; + + if ((deltaX.abs() + deltaY.abs()) > 1) { + final shiftPointX = targetX + deltaX.sign; + final shiftPointY = targetY + deltaY.sign; + _shift(source, shiftPointX, shiftPointY); + _staticSwap(source, targetX, targetY, shiftPointX, shiftPointY); + } else { + _staticSwap(source, lastCoord.x, lastCoord.y, targetX, targetY); + } + } + + void _staticSwap(List source, int ax, int ay, int bx, int by) { + final aIndex = ax + ay * width; + final aValue = source[aIndex]; + final bIndex = bx + by * width; + + source[aIndex] = source[bIndex]; + source[bIndex] = aValue; + } + + Point coordinatesOf(int value) { + final index = indexOf(value); + final x = index % width; + final y = index ~/ width; + assert(_getIndex(x, y) == index); + return Point(x, y); + } + + int _getIndex(int x, int y) { + assert(x >= 0 && x < width); + assert(y >= 0 && y < height); + return x + y * width; + } + + @override + String toString() => _toString(); + + String _toString() { + final grid = List>.generate( + height, + (row) => List.generate( + width, (col) => valueAt(col, row).toString())); + + final longestLength = + grid.expand((r) => r).fold(0, (int l, cell) => max(l, cell.length)); + + return grid + .map((r) => r.map((v) => v.padLeft(longestLength)).join(' ')) + .join('\n'); + } +} + +Uint8List _randomList(int width, int height) => _randomizeList( + width, List.generate(width * height, (i) => i, growable: false)); + +Uint8List _randomizeList(int width, List existing) { + final copy = Uint8List.fromList(existing); + do { + copy.shuffle(_rnd); + } while (!isSolvable(width, copy) || + copy.any((v) => copy[v] == v || copy[v] == existing[v])); + return copy; +} + +void _validate(List source) { + for (var i = 0; i < source.length; i++) { + requireArgument( + source.contains(i), + 'source', + 'Must contain each number from 0 to `length - 1` ' + 'once and only once.'); + } +} diff --git a/web/slide_puzzle/lib/src/core/puzzle_animator.dart b/web/slide_puzzle/lib/src/core/puzzle_animator.dart new file mode 100644 index 000000000..897f05c3c --- /dev/null +++ b/web/slide_puzzle/lib/src/core/puzzle_animator.dart @@ -0,0 +1,205 @@ +// 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:async'; +import 'dart:math' show Point, Random; + +import 'body.dart'; +import 'puzzle.dart'; + +enum PuzzleEvent { click, reset, noop } + +abstract class PuzzleProxy { + int get width; + + int get height; + + int get length; + + bool get solved; + + void reset(); + + void clickOrShake(int tileValue); + + int get tileCount; + + int get clickCount; + + int get incorrectTiles; + + Point location(int index); + + bool isCorrectPosition(int value); +} + +class PuzzleAnimator implements PuzzleProxy { + final _rnd = Random(); + final List _locations; + final _controller = StreamController(); + bool _nextRandomVertical = true; + Puzzle _puzzle; + int _clickCount = 0; + + bool _stable; + + bool get stable => _stable; + + @override + bool get solved => _puzzle.incorrectTiles == 0; + + @override + int get width => _puzzle.width; + + @override + int get height => _puzzle.height; + + @override + int get length => _puzzle.length; + + @override + int get tileCount => _puzzle.tileCount; + + @override + int get incorrectTiles => _puzzle.incorrectTiles; + + @override + int get clickCount => _clickCount; + + @override + void reset() => _resetCore(); + + Stream get onEvent => _controller.stream; + + @override + bool isCorrectPosition(int value) => _puzzle.isCorrectPosition(value); + + @override + Point location(int index) => _locations[index].location; + + int _lastBadClick; + int _badClickCount = 0; + + PuzzleAnimator(int width, int height) : this._(Puzzle(width, height)); + + PuzzleAnimator._(this._puzzle) + : _locations = List.generate(_puzzle.length, (i) { + return Body.raw( + (_puzzle.width - 1.0) / 2, (_puzzle.height - 1.0) / 2, 0, 0); + }); + + void playRandom() { + if (_puzzle.fitness == 0) { + return; + } + + _puzzle = _puzzle.clickRandom(vertical: _nextRandomVertical); + _nextRandomVertical = !_nextRandomVertical; + _clickCount++; + _controller.add(PuzzleEvent.click); + } + + @override + void clickOrShake(int tileValue) { + if (solved) { + _controller.add(PuzzleEvent.noop); + _shake(tileValue); + _lastBadClick = null; + _badClickCount = 0; + return; + } + + _controller.add(PuzzleEvent.click); + if (!_clickValue(tileValue)) { + _shake(tileValue); + + // This is logic to allow a user to skip to the end – useful for testing + // click on 5 un-movable tiles in a row, but not the same tile twice + // in a row + if (tileValue != _lastBadClick) { + _badClickCount++; + if (_badClickCount >= 5) { + // Do the reset! + final newValues = List.generate(_puzzle.length, (i) { + if (i == _puzzle.tileCount) { + return _puzzle.tileCount - 1; + } else if (i == (_puzzle.tileCount - 1)) { + return _puzzle.tileCount; + } + return i; + }); + _resetCore(source: newValues); + _clickCount = 999; + } + } else { + _badClickCount = 0; + } + _lastBadClick = tileValue; + } else { + _lastBadClick = null; + _badClickCount = 0; + } + } + + void _resetCore({List source}) { + _puzzle = _puzzle.reset(source: source); + _clickCount = 0; + _lastBadClick = null; + _badClickCount = 0; + _controller.add(PuzzleEvent.reset); + } + + bool _clickValue(int value) { + final newPuzzle = _puzzle.clickValue(value); + if (newPuzzle == null) { + return false; + } else { + _clickCount++; + _puzzle = newPuzzle; + return true; + } + } + + void _shake(int tileValue) { + Point deltaDouble; + if (solved) { + deltaDouble = Point(_rnd.nextDouble() - 0.5, _rnd.nextDouble() - 0.5); + } else { + final delta = _puzzle.openPosition() - _puzzle.coordinatesOf(tileValue); + deltaDouble = Point(delta.x.toDouble(), delta.y.toDouble()); + } + deltaDouble *= 0.5 / deltaDouble.magnitude; + + _locations[tileValue].kick(deltaDouble); + } + + void update(Duration timeDelta) { + assert(!timeDelta.isNegative); + assert(timeDelta != Duration.zero); + + var animationSeconds = timeDelta.inMilliseconds / 60.0; + if (animationSeconds == 0) { + animationSeconds = 0.1; + } + assert(animationSeconds > 0); + + _stable = true; + for (var i = 0; i < _puzzle.length; i++) { + final target = _target(i); + final body = _locations[i]; + + _stable = !body.animate(animationSeconds, + force: target - body.location, + drag: .9, + maxVelocity: 1.0, + snapTo: target) && + _stable; + } + } + + Point _target(int item) { + final target = _puzzle.coordinatesOf(item); + return Point(target.x.toDouble(), target.y.toDouble()); + } +} diff --git a/web/slide_puzzle/lib/src/core/puzzle_simple.dart b/web/slide_puzzle/lib/src/core/puzzle_simple.dart new file mode 100644 index 000000000..ee87c0e52 --- /dev/null +++ b/web/slide_puzzle/lib/src/core/puzzle_simple.dart @@ -0,0 +1,63 @@ +// 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. + +part of 'puzzle.dart'; + +class _PuzzleSimple extends Puzzle { + @override + final int width; + final Uint8List _source; + + _PuzzleSimple(this.width, List source) + : _source = UnmodifiableUint8ListView(Uint8List.fromList(source)), + super._(); + + @override + int indexOf(int value) => _source.indexOf(value); + + @override + List get _intView => _source; + + @override + List _copyData() => Uint8List.fromList(_source); + + @override + Puzzle _newWithValues(List values) => _PuzzleSimple(width, values); + + @override + int operator [](int index) => _source[index]; + + @override + Puzzle clone() => _PuzzleSimple(width, _source); + + @override + int get length => _source.length; + + @override + bool operator ==(other) { + if (other is Puzzle && + other.width == width && + other.length == _source.length) { + for (var i = 0; i < _source.length; i++) { + if (other[i] != _source[i]) { + return false; + } + } + return true; + } + return false; + } + + @override + int get hashCode { + var v = 0; + for (var i = 0; i < _source.length; i++) { + v = (v << 2) + _source[i]; + } + v += v << 3; + v ^= v >> 11; + v += v << 15; + return v; + } +} diff --git a/web/slide_puzzle/lib/src/core/puzzle_smart.dart b/web/slide_puzzle/lib/src/core/puzzle_smart.dart new file mode 100644 index 000000000..7bbae8562 --- /dev/null +++ b/web/slide_puzzle/lib/src/core/puzzle_smart.dart @@ -0,0 +1,188 @@ +// 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. + +part of 'puzzle.dart'; + +mixin _SliceListMixin on ListMixin { + static const _bitsPerValue = 4; + static const _maxShift = _valuesPerCell - 1; + + static const _bitsPerCell = 32; + static const _valuesPerCell = _bitsPerCell ~/ _bitsPerValue; + static const _valueMask = (1 << _bitsPerValue) - 1; + + Uint32List get _data; + + int _cellValue(int index) => _data[index ~/ _valuesPerCell]; + + @override + int operator [](int index) { + /* + final bigValue = _data[index ~/ _valuesPerCell]; + final shiftedValue = + bigValue >> (_maxShift - (index % _valuesPerCell)) * _bitsPerValue; + final flattenedValue = shiftedValue & _valueMask; + return flattenedValue; + */ + return (_cellValue(index) >> + (_maxShift - (index % _valuesPerCell)) * _bitsPerValue) & + _valueMask; + } + + @override + int indexOf(Object value, [int start]) { + for (var i = 0; i < _data.length; i++) { + final cellValue = _data[i]; + for (var j = 0; j < _valuesPerCell; j++) { + final option = + (cellValue >> (_maxShift - j) * _bitsPerValue) & _valueMask; + + if (value == option) { + final k = i * _valuesPerCell + j; + if (k < length && (start == null || k >= start)) { + return k; + } + } + } + } + return -1; + } + + @override + set length(int value) => throw UnsupportedError('immutable, yo!'); +} + +class _SliceList with ListMixin, _SliceListMixin { + @override + final Uint32List _data; + + @override + final int length; + + _SliceList(this.length, this._data); + + @override + void operator []=(int index, int value) { + var cellValue = _cellValue(index); + + // clear out the target bits in `cellValue` + + final sharedShift = + (_SliceListMixin._maxShift - (index % _SliceListMixin._valuesPerCell)) * + _SliceListMixin._bitsPerValue; + + final wipeout = _SliceListMixin._valueMask << sharedShift; + + cellValue &= ~wipeout; + + final newShiftedValue = value << sharedShift; + + cellValue |= newShiftedValue; + + _data[index ~/ _SliceListMixin._valuesPerCell] = cellValue; + } +} + +class _PuzzleSmart extends Puzzle with ListMixin, _SliceListMixin { + static const _bitsPerValue = 4; + static const _maxShift = _valuesPerCell - 1; + + static const _bitsPerCell = 32; + static const _valuesPerCell = _bitsPerCell ~/ _bitsPerValue; + static const _valueMask = (1 << _bitsPerValue) - 1; + + @override + final Uint32List _data; + + @override + final int width; + + @override + final int length; + + int _fitnessCache; + + @override + int get fitness => _fitnessCache ??= super.fitness; + + _PuzzleSmart(this.width, List source) + : length = source.length, + _data = _create(source), + super._(); + + @override + void operator []=(int index, int value) => + throw UnsupportedError('immutable, yo!'); + + @override + List get _intView => this; + + @override + List _copyData() => _SliceList(length, Uint32List.fromList(_data)); + + @override + Puzzle _newWithValues(List values) => _PuzzleSmart(width, values); + + @override + Puzzle clone() => _PuzzleSmart(width, this); + + @override + String toString() => _toString(); + + @override + bool operator ==(other) { + if (other is _PuzzleSmart && + other.width == width && + other._data.length == _data.length) { + for (var i = 0; i < _data.length; i++) { + if (other._data[i] != _data[i]) { + return false; + } + } + return true; + } + if (other is Puzzle && other.width == width && other.length == length) { + for (var i = 0; i < length; i++) { + if (other[i] != this[i]) { + return false; + } + } + return true; + } + return false; + } + + @override + int get hashCode { + var v = 0; + for (var i = 0; i < _data.length; i++) { + v = (v << 2) + _data[i]; + } + v += v << 3; + v ^= v >> 11; + v += v << 15; + return v; + } + + static Uint32List _create(List source) { + if (source is _SliceList) { + return source._data; + } + + final data = Uint32List((source.length / _valuesPerCell).ceil()); + for (var i = 0; i < data.length; i++) { + var value = 0; + for (var j = 0; j < _valuesPerCell; j++) { + final k = i * _valuesPerCell + j; + if (k < source.length) { + // shift the value over 4 bits for item 0, 3 for item 2, etc + final sourceValue = source[k] << ((_maxShift - j) * _bitsPerValue); + value |= sourceValue; + } + } + data[i] = value; + } + return data; + } +} diff --git a/web/slide_puzzle/lib/src/core/util.dart b/web/slide_puzzle/lib/src/core/util.dart new file mode 100644 index 000000000..992b31c26 --- /dev/null +++ b/web/slide_puzzle/lib/src/core/util.dart @@ -0,0 +1,58 @@ +// 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. + +void requireArgument(bool truth, String argName, [String message]) { + if (!truth) { + if (message == null || message.isEmpty) { + message = 'value was invalid'; + } + throw ArgumentError('`$argName` - $message'); + } +} + +void requireArgumentNotNull(argument, String argName) { + if (argument == null) { + throw ArgumentError.notNull(argName); + } +} + +// Logic from +// https://www.cs.bham.ac.uk/~mdr/teaching/modules04/java2/TilesSolvability.html +// Used with gratitude! +bool isSolvable(int width, List list) { + final height = list.length ~/ width; + assert(width * height == list.length); + final inversions = _countInversions(list); + + if (width.isOdd) { + return inversions.isEven; + } + + final blankRow = list.indexOf(list.length - 1) ~/ width; + + if ((height - blankRow).isEven) { + return inversions.isOdd; + } else { + return inversions.isEven; + } +} + +int _countInversions(List items) { + final tileCount = items.length - 1; + var score = 0; + for (var i = 0; i < items.length; i++) { + final value = items[i]; + if (value == tileCount) { + continue; + } + + for (var j = i + 1; j < items.length; j++) { + final v = items[j]; + if (v != tileCount && v < value) { + score++; + } + } + } + return score; +} diff --git a/web/slide_puzzle/lib/src/flutter.dart b/web/slide_puzzle/lib/src/flutter.dart new file mode 100644 index 000000000..01f8b1cc4 --- /dev/null +++ b/web/slide_puzzle/lib/src/flutter.dart @@ -0,0 +1,7 @@ +// 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. + +export 'package:flutter_web/material.dart'; + +export 'package:flutter_web/scheduler.dart' show Ticker; diff --git a/web/slide_puzzle/lib/src/puzzle_flow_delegate.dart b/web/slide_puzzle/lib/src/puzzle_flow_delegate.dart new file mode 100644 index 000000000..5e8aea630 --- /dev/null +++ b/web/slide_puzzle/lib/src/puzzle_flow_delegate.dart @@ -0,0 +1,43 @@ +// 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 'core/puzzle_animator.dart'; +import 'flutter.dart'; + +class PuzzleFlowDelegate extends FlowDelegate { + final Size _tileSize; + final PuzzleProxy _puzzleProxy; + + PuzzleFlowDelegate(this._tileSize, this._puzzleProxy, Listenable repaint) + : super(repaint: repaint); + + @override + Size getSize(BoxConstraints constraints) => Size( + _tileSize.width * _puzzleProxy.width, + _tileSize.height * _puzzleProxy.height); + + @override + BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) => + BoxConstraints.tight(_tileSize); + + @override + void paintChildren(FlowPaintingContext context) { + for (var i = 0; i < _puzzleProxy.length; i++) { + final tileLocation = _puzzleProxy.location(i); + context.paintChild( + i, + transform: Matrix4.translationValues( + tileLocation.x * _tileSize.width, + tileLocation.y * _tileSize.height, + i.toDouble(), + ), + ); + } + } + + @override + bool shouldRepaint(covariant PuzzleFlowDelegate oldDelegate) => + _tileSize != oldDelegate._tileSize || + !identical(oldDelegate._puzzleProxy, _puzzleProxy); +} diff --git a/web/slide_puzzle/lib/src/puzzle_home_state.dart b/web/slide_puzzle/lib/src/puzzle_home_state.dart new file mode 100644 index 000000000..a0abdb53b --- /dev/null +++ b/web/slide_puzzle/lib/src/puzzle_home_state.dart @@ -0,0 +1,177 @@ +// 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:async'; + +import 'app_state.dart'; +import 'core/puzzle_animator.dart'; +import 'flutter.dart'; +import 'shared_theme.dart'; +import 'theme_plaster.dart'; +import 'theme_seattle.dart'; +import 'theme_simple.dart'; + +class PuzzleHomeState extends State + with TickerProviderStateMixin + implements AppState { + TabController _tabController; + AnimationController _controller; + + @override + final PuzzleAnimator puzzle; + + @override + final animationNotifier = _AnimationNotifier(); + + @override + TabController get tabController => _tabController; + + SharedTheme _currentTheme; + + @override + SharedTheme get currentTheme => _currentTheme; + + @override + set currentTheme(SharedTheme theme) { + setState(() { + _currentTheme = theme; + }); + } + + Duration _tickerTimeSinceLastEvent = Duration.zero; + Ticker _ticker; + Duration _lastElapsed; + StreamSubscription sub; + + @override + bool autoPlay = false; + + PuzzleHomeState(this.puzzle) { + sub = puzzle.onEvent.listen(_onPuzzleEvent); + + _themeDataCache = List.unmodifiable([ + ThemeSimple(this), + ThemeSeattle(this), + ThemePlaster(this), + ]); + + _currentTheme = themeData.first; + } + + @override + void initState() { + super.initState(); + _ticker ??= createTicker(_onTick); + _ensureTicking(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + + _tabController = TabController(vsync: this, length: _themeDataCache.length); + + _tabController.addListener(() { + currentTheme = _themeDataCache[_tabController.index]; + }); + } + + List _themeDataCache; + + @override + Iterable get themeData => _themeDataCache; + + @override + void setAutoPlay(bool newValue) { + if (newValue != autoPlay) { + setState(() { + // Only allow enabling autoPlay if the puzzle is not solved + autoPlay = newValue && !puzzle.solved; + if (autoPlay) { + _ensureTicking(); + } + }); + } + } + + @override + Widget build(BuildContext context) => + LayoutBuilder(builder: _currentTheme.build); + + @override + void dispose() { + animationNotifier.dispose(); + _tabController.dispose(); + _controller?.dispose(); + _ticker?.dispose(); + sub.cancel(); + super.dispose(); + } + + void _onPuzzleEvent(PuzzleEvent e) { + _tickerTimeSinceLastEvent = Duration.zero; + _ensureTicking(); + if (e == PuzzleEvent.noop) { + assert(e == PuzzleEvent.noop); + _controller + ..reset() + ..forward(); + } + setState(() { + // noop + }); + } + + void _ensureTicking() { + if (!_ticker.isTicking) { + _ticker.start(); + } + } + + void _onTick(Duration elapsed) { + if (elapsed == Duration.zero) { + _lastElapsed = elapsed; + } + final delta = elapsed - _lastElapsed; + _lastElapsed = elapsed; + + if (delta.inMilliseconds <= 0) { + // `_delta` may be negative or zero if `elapsed` is zero (first tick) + // or during a restart. Just ignore this case. + return; + } + + _tickerTimeSinceLastEvent += delta; + puzzle.update(delta > _maxFrameDuration ? _maxFrameDuration : delta); + + if (!puzzle.stable) { + animationNotifier.animate(); + } else { + if (!autoPlay) { + _ticker.stop(); + _lastElapsed = null; + } + } + + if (autoPlay && + _tickerTimeSinceLastEvent > const Duration(milliseconds: 200)) { + puzzle.playRandom(); + + if (puzzle.solved) { + setAutoPlay(false); + } + } + } +} + +class _AnimationNotifier extends ChangeNotifier implements AnimationNotifier { + _AnimationNotifier(); + + @override + void animate() { + notifyListeners(); + } +} + +const _maxFrameDuration = Duration(milliseconds: 34); diff --git a/web/slide_puzzle/lib/src/shared_theme.dart b/web/slide_puzzle/lib/src/shared_theme.dart new file mode 100644 index 000000000..0dca32bb9 --- /dev/null +++ b/web/slide_puzzle/lib/src/shared_theme.dart @@ -0,0 +1,247 @@ +// 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 'app_state.dart'; +import 'core/puzzle_animator.dart'; +import 'flutter.dart'; +import 'puzzle_flow_delegate.dart'; +import 'widgets/material_interior_alt.dart'; + +abstract class SharedTheme { + SharedTheme(this._appState); + + final AppState _appState; + + PuzzleProxy get puzzle => _appState.puzzle; + + String get name; + + Color get puzzleThemeBackground; + + RoundedRectangleBorder get puzzleBorder; + + Color get puzzleBackgroundColor; + + Color get puzzleAccentColor; + + EdgeInsetsGeometry get tilePadding => const EdgeInsets.all(6); + + Widget tileButton(int i); + + Ink createInk( + Widget child, { + DecorationImage image, + EdgeInsetsGeometry padding, + }) => + Ink( + padding: padding, + decoration: BoxDecoration( + image: image, + ), + child: child, + ); + + Widget createButton( + int tileValue, + Widget content, { + Color color, + RoundedRectangleBorder shape, + }) => + AnimatedContainer( + duration: _puzzleAnimationDuration, + padding: tilePadding, + child: RaisedButton( + elevation: 4, + clipBehavior: Clip.hardEdge, + animationDuration: _puzzleAnimationDuration, + onPressed: () => _tilePress(tileValue), + shape: shape ?? puzzleBorder, + padding: const EdgeInsets.symmetric(), + child: content, + color: color, + ), + ); + + double _previousConstraintWidth; + bool _small; + + bool get small => _small; + + void _updateConstraints(BoxConstraints constraints) { + const _smallWidth = 580; + + final constraintWidth = + constraints.hasBoundedWidth ? constraints.maxWidth : 1000.0; + + if (constraintWidth == _previousConstraintWidth) { + assert(_small != null); + return; + } + + _previousConstraintWidth = constraintWidth; + + if (_previousConstraintWidth < _smallWidth) { + _small = true; + } else { + _small = false; + } + } + + Widget build(BuildContext context, BoxConstraints constraints) { + _updateConstraints(constraints); + return Material( + child: Stack( + children: [ + const SizedBox.expand( + child: FittedBox( + fit: BoxFit.cover, + child: Image( + image: AssetImage('seattle.jpg'), + ), + ), + ), + AnimatedContainer( + duration: _puzzleAnimationDuration, + color: puzzleThemeBackground, + child: Center( + child: _styledWrapper( + SizedBox( + width: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.black26, + width: 1, + ), + ), + ), + margin: const EdgeInsets.symmetric(horizontal: 20), + child: TabBar( + controller: _appState.tabController, + labelPadding: const EdgeInsets.fromLTRB(0, 20, 0, 12), + labelColor: puzzleAccentColor, + indicatorColor: puzzleAccentColor, + indicatorWeight: 1.5, + unselectedLabelColor: Colors.black.withOpacity(0.6), + tabs: _appState.themeData + .map((st) => Text( + st.name.toUpperCase(), + style: const TextStyle( + letterSpacing: 0.5, + ), + )) + .toList(), + ), + ), + Container( + constraints: const BoxConstraints.tightForFinite(), + padding: const EdgeInsets.all(10), + child: Flow( + delegate: PuzzleFlowDelegate( + small ? const Size(90, 90) : const Size(140, 140), + puzzle, + _appState.animationNotifier, + ), + children: List.generate( + puzzle.length, + _tileButton, + ), + ), + ), + Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: Colors.black26, width: 1), + ), + ), + padding: const EdgeInsets.only( + left: 10, + bottom: 6, + top: 2, + right: 10, + ), + child: Row(children: _bottomControls(context)), + ) + ], + ), + ), + ), + ), + ) + ], + )); + } + + Duration get _puzzleAnimationDuration => kThemeAnimationDuration * 3; + + // Thought about using AnimatedContainer here, but it causes some weird + // resizing behavior + Widget _styledWrapper(Widget child) => MaterialInterior( + duration: _puzzleAnimationDuration, + shape: puzzleBorder, + color: puzzleBackgroundColor, + child: child, + ); + + void Function(bool newValue) get _setAutoPlay { + if (puzzle.solved) { + return null; + } + return _appState.setAutoPlay; + } + + void _tilePress(int tileValue) { + _appState.setAutoPlay(false); + _appState.puzzle.clickOrShake(tileValue); + } + + TextStyle get _infoStyle => TextStyle( + color: puzzleAccentColor, + fontWeight: FontWeight.bold, + ); + + List _bottomControls(BuildContext context) => [ + IconButton( + onPressed: puzzle.reset, + icon: Icon(Icons.refresh, color: puzzleAccentColor), + //Icons.refresh, + ), + Checkbox( + value: _appState.autoPlay, + onChanged: _setAutoPlay, + activeColor: puzzleAccentColor, + ), + Expanded( + child: Container(), + ), + Text( + puzzle.clickCount.toString(), + textAlign: TextAlign.right, + style: _infoStyle, + ), + const Text(' Moves'), + SizedBox( + width: 28, + child: Text( + puzzle.incorrectTiles.toString(), + textAlign: TextAlign.right, + style: _infoStyle, + ), + ), + const Text(' Tiles left ') + ]; + + Widget _tileButton(int i) { + if (i == puzzle.tileCount && !puzzle.solved) { + return const Center(); + } + + return tileButton(i); + } +} diff --git a/web/slide_puzzle/lib/src/theme_plaster.dart b/web/slide_puzzle/lib/src/theme_plaster.dart new file mode 100644 index 000000000..ca2921b9f --- /dev/null +++ b/web/slide_puzzle/lib/src/theme_plaster.dart @@ -0,0 +1,76 @@ +// 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 'app_state.dart'; +import 'flutter.dart'; +import 'shared_theme.dart'; + +const _yellowIsh = Color.fromARGB(255, 248, 244, 233); +const _chocolate = Color.fromARGB(255, 66, 66, 68); +const _orangeIsh = Color.fromARGB(255, 224, 107, 83); + +class ThemePlaster extends SharedTheme { + @override + String get name => 'Plaster'; + + ThemePlaster(AppState baseTheme) : super(baseTheme); + + @override + Color get puzzleThemeBackground => _chocolate; + + @override + Color get puzzleBackgroundColor => _yellowIsh; + + @override + Color get puzzleAccentColor => _orangeIsh; + + @override + RoundedRectangleBorder get puzzleBorder => RoundedRectangleBorder( + side: const BorderSide( + color: Color.fromARGB(255, 103, 103, 105), + width: 8, + ), + borderRadius: BorderRadius.all( + Radius.circular(small ? 10 : 18), + ), + ); + + @override + Widget tileButton(int i) { + final correctColumn = i % puzzle.width; + final correctRow = i ~/ puzzle.width; + + final primary = (correctColumn + correctRow).isEven; + + if (i == puzzle.tileCount) { + assert(puzzle.solved); + return Center( + child: Icon( + Icons.thumb_up, + size: small ? 50 : 72, + color: _orangeIsh, + ), + ); + } + + final content = Text( + (i + 1).toString(), + style: TextStyle( + color: primary ? _yellowIsh : _chocolate, + fontFamily: 'Plaster', + fontSize: small ? 40 : 77, + ), + ); + + return createButton( + i, + content, + color: primary ? _orangeIsh : _yellowIsh, + shape: RoundedRectangleBorder( + side: BorderSide(color: primary ? _chocolate : _orangeIsh, width: 5), + borderRadius: BorderRadius.circular(5), + ), + ); + } +} diff --git a/web/slide_puzzle/lib/src/theme_seattle.dart b/web/slide_puzzle/lib/src/theme_seattle.dart new file mode 100644 index 000000000..f834c1bd6 --- /dev/null +++ b/web/slide_puzzle/lib/src/theme_seattle.dart @@ -0,0 +1,74 @@ +// 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 'app_state.dart'; +import 'flutter.dart'; +import 'shared_theme.dart'; +import 'widgets/decoration_image_plus.dart'; + +class ThemeSeattle extends SharedTheme { + @override + String get name => 'Seattle'; + + ThemeSeattle(AppState proxy) : super(proxy); + + @override + Color get puzzleThemeBackground => const Color.fromARGB(153, 90, 135, 170); + + @override + Color get puzzleBackgroundColor => Colors.white70; + + @override + Color get puzzleAccentColor => const Color(0xff000579f); + + @override + RoundedRectangleBorder get puzzleBorder => const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(1), + ), + ); + + @override + EdgeInsetsGeometry get tilePadding => + puzzle.solved ? const EdgeInsets.all(1) : const EdgeInsets.all(4); + + @override + Widget tileButton(int i) { + if (i == puzzle.tileCount && !puzzle.solved) { + assert(puzzle.solved); + } + + final decorationImage = DecorationImagePlus( + puzzleWidth: puzzle.width, + puzzleHeight: puzzle.height, + pieceIndex: i, + fit: BoxFit.cover, + image: const AssetImage('seattle.jpg')); + + final correctPosition = puzzle.isCorrectPosition(i); + final content = createInk( + puzzle.solved + ? const Center() + : Container( + decoration: ShapeDecoration( + shape: const CircleBorder(), + color: correctPosition ? Colors.black38 : Colors.white54, + ), + alignment: Alignment.center, + child: Text( + (i + 1).toString(), + style: TextStyle( + fontWeight: FontWeight.normal, + color: correctPosition ? Colors.white : Colors.black, + fontSize: small ? 25 : 42, + ), + ), + ), + image: decorationImage, + padding: EdgeInsets.all(small ? 20 : 32), + ); + + return createButton(i, content); + } +} diff --git a/web/slide_puzzle/lib/src/theme_simple.dart b/web/slide_puzzle/lib/src/theme_simple.dart new file mode 100644 index 000000000..8246a50c2 --- /dev/null +++ b/web/slide_puzzle/lib/src/theme_simple.dart @@ -0,0 +1,68 @@ +// 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 'app_state.dart'; +import 'flutter.dart'; +import 'shared_theme.dart'; + +const _accentBlue = Color(0xff000579e); + +class ThemeSimple extends SharedTheme { + @override + String get name => 'Simple'; + + ThemeSimple(AppState proxy) : super(proxy); + + @override + Color get puzzleThemeBackground => Colors.white; + + @override + Color get puzzleBackgroundColor => Colors.white70; + + @override + Color get puzzleAccentColor => _accentBlue; + + @override + RoundedRectangleBorder get puzzleBorder => const RoundedRectangleBorder( + side: BorderSide(color: Colors.black26, width: 1), + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ); + + @override + Widget tileButton(int i) { + if (i == puzzle.tileCount) { + assert(puzzle.solved); + return const Center( + child: Icon( + Icons.thumb_up, + size: 72, + color: _accentBlue, + ), + ); + } + + final correctPosition = puzzle.isCorrectPosition(i); + + final content = createInk( + Center( + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Colors.white, + fontWeight: correctPosition ? FontWeight.bold : FontWeight.normal, + fontSize: small ? 30 : 49, + ), + ), + ), + ); + + return createButton( + i, + content, + color: const Color.fromARGB(255, 13, 87, 155), + ); + } +} diff --git a/web/slide_puzzle/lib/src/widgets/decoration_image_plus.dart b/web/slide_puzzle/lib/src/widgets/decoration_image_plus.dart new file mode 100644 index 000000000..280e30e7e --- /dev/null +++ b/web/slide_puzzle/lib/src/widgets/decoration_image_plus.dart @@ -0,0 +1,322 @@ +// 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. + +// ignore_for_file: omit_local_variable_types, annotate_overrides + +import 'package:flutter_web_ui/ui.dart' as ui show Image; + +import '../flutter.dart'; + +// A model on top of DecorationImage that supports slicing up the source image +// efficiently to draw it as tiles in the puzzle game +@immutable +class DecorationImagePlus implements DecorationImage { + final int puzzleWidth, puzzleHeight, pieceIndex; + + /// Creates an image to show in a [BoxDecoration]. + /// + /// The [image], [alignment], [repeat], and [matchTextDirection] arguments + /// must not be null. + const DecorationImagePlus({ + @required this.image, + @required this.puzzleWidth, + @required this.puzzleHeight, + @required this.pieceIndex, + this.colorFilter, + this.fit, + this.alignment = Alignment.center, + this.centerSlice, + this.repeat = ImageRepeat.noRepeat, + this.matchTextDirection = false, + }) : assert(image != null), + assert(alignment != null), + assert(repeat != null), + assert(matchTextDirection != null), + assert(puzzleHeight > 1 && + puzzleHeight > 1 && + pieceIndex >= 0 && + pieceIndex < (puzzleHeight * puzzleWidth)); + + /// The image to be painted into the decoration. + /// + /// Typically this will be an [AssetImage] (for an image shipped with the + /// application) or a [NetworkImage] (for an image obtained from the network). + final ImageProvider image; + + /// A color filter to apply to the image before painting it. + final ColorFilter colorFilter; + + /// How the image should be inscribed into the box. + /// + /// The default is [BoxFit.scaleDown] if [centerSlice] is null, and + /// [BoxFit.fill] if [centerSlice] is not null. + /// + /// See the discussion at [_paintImage] for more details. + final BoxFit fit; + + /// How to align the image within its bounds. + /// + /// The alignment aligns the given position in the image to the given position + /// in the layout bounds. For example, an [Alignment] alignment of (-1.0, + /// -1.0) aligns the image to the top-left corner of its layout bounds, while a + /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the + /// image with the bottom right corner of its layout bounds. Similarly, an + /// alignment of (0.0, 1.0) aligns the bottom middle of the image with the + /// middle of the bottom edge of its layout bounds. + /// + /// To display a subpart of an image, consider using a [CustomPainter] and + /// [Canvas.drawImageRect]. + /// + /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a + /// [AlignmentDirectional]), then a [TextDirection] must be available + /// when the image is painted. + /// + /// Defaults to [Alignment.center]. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry alignment; + + /// The center slice for a nine-patch image. + /// + /// The region of the image inside the center slice will be stretched both + /// horizontally and vertically to fit the image into its destination. The + /// region of the image above and below the center slice will be stretched + /// only horizontally and the region of the image to the left and right of + /// the center slice will be stretched only vertically. + /// + /// The stretching will be applied in order to make the image fit into the box + /// specified by [fit]. When [centerSlice] is not null, [fit] defaults to + /// [BoxFit.fill], which distorts the destination image size relative to the + /// image's original aspect ratio. Values of [BoxFit] which do not distort the + /// destination image size will result in [centerSlice] having no effect + /// (since the nine regions of the image will be rendered with the same + /// scaling, as if it wasn't specified). + final Rect centerSlice; + + /// How to paint any portions of the box that would not otherwise be covered + /// by the image. + final ImageRepeat repeat; + + /// Whether to paint the image in the direction of the [TextDirection]. + /// + /// If this is true, then in [TextDirection.ltr] contexts, the image will be + /// drawn with its origin in the top left (the "normal" painting direction for + /// images); and in [TextDirection.rtl] contexts, the image will be drawn with + /// a scaling factor of -1 in the horizontal direction so that the origin is + /// in the top right. + final bool matchTextDirection; + + /// Creates a [DecorationImagePainterPlus] for this [DecorationImagePlus]. + /// + /// The `onChanged` argument must not be null. It will be called whenever the + /// image needs to be repainted, e.g. because it is loading incrementally or + /// because it is animated. + DecorationImagePainterPlus createPainter(VoidCallback onChanged) { + assert(onChanged != null); + return DecorationImagePainterPlus._(this, onChanged); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + return other is DecorationImagePlus && + other.runtimeType == runtimeType && + image == other.image && + colorFilter == other.colorFilter && + fit == other.fit && + alignment == other.alignment && + centerSlice == other.centerSlice && + repeat == other.repeat && + matchTextDirection == other.matchTextDirection; + } + + @override + int get hashCode => hashValues(image, colorFilter, fit, alignment, + centerSlice, repeat, matchTextDirection); + + @override + String toString() { + final List properties = ['$image']; + if (colorFilter != null) properties.add('$colorFilter'); + if (fit != null && + !(fit == BoxFit.fill && centerSlice != null) && + !(fit == BoxFit.scaleDown && centerSlice == null)) { + properties.add('$fit'); + } + properties.add('$alignment'); + if (centerSlice != null) properties.add('centerSlice: $centerSlice'); + if (repeat != ImageRepeat.noRepeat) properties.add('$repeat'); + if (matchTextDirection) properties.add('match text direction'); + return '$runtimeType(${properties.join(", ")})'; + } +} + +/// The painter for a [DecorationImagePlus]. +/// +/// To obtain a painter, call [DecorationImagePlus.createPainter]. +/// +/// To paint, call [paint]. The `onChanged` callback passed to +/// [DecorationImagePlus.createPainter] will be called if the image needs to paint +/// again (e.g. because it is animated or because it had not yet loaded the +/// first time the [paint] method was called). +/// +/// This object should be disposed using the [dispose] method when it is no +/// longer needed. +class DecorationImagePainterPlus implements DecorationImagePainter { + DecorationImagePainterPlus._(this._details, this._onChanged) + : assert(_details != null); + + final DecorationImagePlus _details; + final VoidCallback _onChanged; + + ImageStream _imageStream; + ImageInfo _image; + + /// Draw the image onto the given canvas. + /// + /// The image is drawn at the position and size given by the `rect` argument. + /// + /// The image is clipped to the given `clipPath`, if any. + /// + /// The `configuration` object is used to resolve the image (e.g. to pick + /// resolution-specific assets), and to implement the + /// [DecorationImagePlus.matchTextDirection] feature. + /// + /// If the image needs to be painted again, e.g. because it is animated or + /// because it had not yet been loaded the first time this method was called, + /// then the `onChanged` callback passed to [DecorationImagePlus.createPainter] + /// will be called. + void paint(Canvas canvas, Rect rect, Path clipPath, + ImageConfiguration configuration) { + assert(canvas != null); + assert(rect != null); + assert(configuration != null); + + if (_details.matchTextDirection) { + assert(() { + // We check this first so that the assert will fire immediately, not just + // when the image is ready. + if (configuration.textDirection == null) { + throw FlutterError( + 'ImageDecoration.matchTextDirection can only be used when a TextDirection is available.\n' + 'When DecorationImagePainter.paint() was called, there was no text direction provided ' + 'in the ImageConfiguration object to match.\n' + 'The DecorationImage was:\n' + ' $_details\n' + 'The ImageConfiguration was:\n' + ' $configuration'); + } + return true; + }()); + } + + final ImageStream newImageStream = _details.image.resolve(configuration); + if (newImageStream.key != _imageStream?.key) { + _imageStream?.removeListener(_imageListener); + _imageStream = newImageStream..addListener(_imageListener); + } + if (_image == null) return; + + if (clipPath != null) { + canvas + ..save() + ..clipPath(clipPath); + } + + _paintImage( + canvas: canvas, + puzzleWidth: _details.puzzleWidth, + puzzleHeight: _details.puzzleHeight, + pieceIndex: _details.pieceIndex, + rect: rect, + image: _image.image, + scale: _image.scale, + colorFilter: _details.colorFilter, + fit: _details.fit, + alignment: _details.alignment.resolve(configuration.textDirection), + ); + + if (clipPath != null) canvas.restore(); + } + + void _imageListener(ImageInfo value, bool synchronousCall) { + if (_image == value) return; + _image = value; + assert(_onChanged != null); + if (!synchronousCall) _onChanged(); + } + + /// Releases the resources used by this painter. + /// + /// This should be called whenever the painter is no longer needed. + /// + /// After this method has been called, the object is no longer usable. + @mustCallSuper + void dispose() { + _imageStream?.removeListener(_imageListener); + } + + @override + String toString() { + return '$runtimeType(stream: $_imageStream, image: $_image) for $_details'; + } +} + +void _paintImage( + {@required Canvas canvas, + @required Rect rect, + @required ui.Image image, + @required int puzzleWidth, + @required int puzzleHeight, + @required int pieceIndex, + double scale = 1.0, + ColorFilter colorFilter, + BoxFit fit, + Alignment alignment = Alignment.center}) { + assert(canvas != null); + assert(image != null); + assert(alignment != null); + + if (rect.isEmpty) return; + final outputSize = rect.size; + final inputSize = Size(image.width.toDouble(), image.height.toDouble()); + fit ??= BoxFit.scaleDown; + final FittedSizes fittedSizes = + applyBoxFit(fit, inputSize / scale, outputSize); + final Size sourceSize = fittedSizes.source * scale; + final destinationSize = fittedSizes.destination; + final Paint paint = Paint() + ..isAntiAlias = false + ..filterQuality = FilterQuality.medium; + if (colorFilter != null) paint.colorFilter = colorFilter; + final double halfWidthDelta = + (outputSize.width - destinationSize.width) / 2.0; + final double halfHeightDelta = + (outputSize.height - destinationSize.height) / 2.0; + final double dx = halfWidthDelta + (alignment.x) * halfWidthDelta; + final double dy = halfHeightDelta + alignment.y * halfHeightDelta; + final Offset destinationPosition = rect.topLeft.translate(dx, dy); + final Rect destinationRect = destinationPosition & destinationSize; + final Rect sourceRect = + alignment.inscribe(sourceSize, Offset.zero & inputSize); + + final sliceSize = + Size(sourceRect.width / puzzleWidth, sourceRect.height / puzzleHeight); + + final col = pieceIndex % puzzleWidth; + final row = pieceIndex ~/ puzzleWidth; + + final sliceRect = Rect.fromLTWH( + sourceRect.left + col * sliceSize.width, + sourceRect.top + row * sliceSize.height, + sliceSize.width, + sliceSize.height); + + canvas.drawImageRect(image, sliceRect, destinationRect, paint); +} diff --git a/web/slide_puzzle/lib/src/widgets/material_interior_alt.dart b/web/slide_puzzle/lib/src/widgets/material_interior_alt.dart new file mode 100644 index 000000000..8b2640ce0 --- /dev/null +++ b/web/slide_puzzle/lib/src/widgets/material_interior_alt.dart @@ -0,0 +1,106 @@ +// 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 '../flutter.dart'; + +// Copied from +// https://github.com/flutter/flutter/blob/f5b02e3c05ed1ab31e890add84fb56e35de2d392/packages/flutter/lib/src/material/material.dart#L593-L715 +// So I could have animated color! +// TODO(kevmoo): file a feature request for this? +class MaterialInterior extends ImplicitlyAnimatedWidget { + const MaterialInterior({ + Key key, + @required this.child, + @required this.shape, + @required this.color, + Curve curve = Curves.linear, + @required Duration duration, + }) : assert(child != null), + assert(shape != null), + assert(color != null), + super(key: key, curve: curve, duration: duration); + + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// The border of the widget. + /// + /// This border will be painted, and in addition the outer path of the border + /// determines the physical shape. + final ShapeBorder shape; + + /// The target background color. + final Color color; + + @override + _MaterialInteriorState createState() => _MaterialInteriorState(); +} + +class _MaterialInteriorState extends AnimatedWidgetBaseState { + ShapeBorderTween _border; + ColorTween _color; + + @override + void forEachTween(TweenVisitor visitor) { + _border = visitor(_border, widget.shape, + (value) => ShapeBorderTween(begin: value as ShapeBorder)) + as ShapeBorderTween; + _color = visitor( + _color, widget.color, (value) => ColorTween(begin: value as Color)) + as ColorTween; + } + + @override + Widget build(BuildContext context) { + final shape = _border.evaluate(animation); + return PhysicalShape( + child: _ShapeBorderPaint( + child: widget.child, + shape: shape, + ), + clipper: ShapeBorderClipper( + shape: shape, + textDirection: Directionality.of(context), + ), + color: _color.evaluate(animation), + ); + } +} + +class _ShapeBorderPaint extends StatelessWidget { + const _ShapeBorderPaint({ + @required this.child, + @required this.shape, + }); + + final Widget child; + final ShapeBorder shape; + + @override + Widget build(BuildContext context) { + return CustomPaint( + child: child, + foregroundPainter: _ShapeBorderPainter(shape, Directionality.of(context)), + ); + } +} + +class _ShapeBorderPainter extends CustomPainter { + _ShapeBorderPainter(this.border, this.textDirection); + + final ShapeBorder border; + final TextDirection textDirection; + + @override + void paint(Canvas canvas, Size size) { + border.paint(canvas, Offset.zero & size, textDirection: textDirection); + } + + @override + bool shouldRepaint(_ShapeBorderPainter oldDelegate) { + return oldDelegate.border != border; + } +} diff --git a/web/slide_puzzle/pubspec.lock b/web/slide_puzzle/pubspec.lock new file mode 100644 index 000000000..d1288b168 --- /dev/null +++ b/web/slide_puzzle/pubspec.lock @@ -0,0 +1,471 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct main" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/slide_puzzle/pubspec.yaml b/web/slide_puzzle/pubspec.yaml new file mode 100644 index 000000000..e9feb5cac --- /dev/null +++ b/web/slide_puzzle/pubspec.yaml @@ -0,0 +1,26 @@ +name: flutter_web.examples.slide_puzzle + +environment: + sdk: ">=2.2.0 <3.0.0" + +dependencies: + flutter_web: any + flutter_web_ui: any + +dev_dependencies: + pedantic: ^1.3.0 + + build_runner: any + build_web_compilers: any + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/slide_puzzle/web/assets/FontManifest.json b/web/slide_puzzle/web/assets/FontManifest.json new file mode 100644 index 000000000..68ad82100 --- /dev/null +++ b/web/slide_puzzle/web/assets/FontManifest.json @@ -0,0 +1,18 @@ +[ + { + "family": "MaterialIcons", + "fonts": [ + { + "asset": "https://fonts.gstatic.com/s/materialicons/v47/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" + } + ] + }, + { + "family": "Plaster", + "fonts": [ + { + "asset": "plaster.woff2" + } + ] + } +] diff --git a/web/slide_puzzle/web/assets/plaster.woff2 b/web/slide_puzzle/web/assets/plaster.woff2 new file mode 100644 index 000000000..3eafd899b Binary files /dev/null and b/web/slide_puzzle/web/assets/plaster.woff2 differ diff --git a/web/slide_puzzle/web/assets/seattle.jpg b/web/slide_puzzle/web/assets/seattle.jpg new file mode 100644 index 000000000..c2942becd Binary files /dev/null and b/web/slide_puzzle/web/assets/seattle.jpg differ diff --git a/web/slide_puzzle/web/index.html b/web/slide_puzzle/web/index.html new file mode 100644 index 000000000..b54ed98d8 --- /dev/null +++ b/web/slide_puzzle/web/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/slide_puzzle/web/main.dart b/web/slide_puzzle/web/main.dart new file mode 100644 index 000000000..3c8ee9114 --- /dev/null +++ b/web/slide_puzzle/web/main.dart @@ -0,0 +1,11 @@ +// 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 'package:flutter_web_ui/ui.dart' as ui; +import 'package:flutter_web.examples.slide_puzzle/main.dart' as app; + +void main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/slide_puzzle/web/preview.png b/web/slide_puzzle/web/preview.png new file mode 100644 index 000000000..0f55f737a Binary files /dev/null and b/web/slide_puzzle/web/preview.png differ diff --git a/web/spinning_square/README.md b/web/spinning_square/README.md new file mode 100644 index 000000000..36d5575d6 --- /dev/null +++ b/web/spinning_square/README.md @@ -0,0 +1 @@ +Just a spinning square. That's it. Super exciting. diff --git a/web/spinning_square/lib/main.dart b/web/spinning_square/lib/main.dart new file mode 100644 index 000000000..3bc3c485f --- /dev/null +++ b/web/spinning_square/lib/main.dart @@ -0,0 +1,48 @@ +// Copyright 2018 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 'package:flutter_web/material.dart'; + +class SpinningSquare extends StatefulWidget { + @override + _SpinningSquareState createState() => new _SpinningSquareState(); +} + +class _SpinningSquareState extends State + with SingleTickerProviderStateMixin { + AnimationController _animation; + + @override + void initState() { + super.initState(); + // We use 3600 milliseconds instead of 1800 milliseconds because 0.0 -> 1.0 + // represents an entire turn of the square whereas in the other examples + // we used 0.0 -> math.pi, which is only half a turn. + _animation = new AnimationController( + duration: const Duration(milliseconds: 3600), + vsync: this, + )..repeat(); + } + + @override + Widget build(BuildContext context) { + return new RotationTransition( + turns: _animation, + child: new Container( + width: 200.0, + height: 200.0, + color: const Color(0xFF00FF00), + )); + } + + @override + void dispose() { + _animation.dispose(); + super.dispose(); + } +} + +void main() { + runApp(new Center(child: new SpinningSquare())); +} diff --git a/web/spinning_square/pubspec.lock b/web/spinning_square/pubspec.lock new file mode 100644 index 000000000..cdf5d8c14 --- /dev/null +++ b/web/spinning_square/pubspec.lock @@ -0,0 +1,471 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct overridden" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/spinning_square/pubspec.yaml b/web/spinning_square/pubspec.yaml new file mode 100644 index 000000000..d031400b5 --- /dev/null +++ b/web/spinning_square/pubspec.yaml @@ -0,0 +1,23 @@ +name: flutter_web.examples.spinning_square + +environment: + sdk: ">=2.2.0 <3.0.0" + +dependencies: + flutter_web: any + +dev_dependencies: + build_runner: any + build_web_compilers: any + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/spinning_square/web/index.html b/web/spinning_square/web/index.html new file mode 100644 index 000000000..b54ed98d8 --- /dev/null +++ b/web/spinning_square/web/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/spinning_square/web/main.dart b/web/spinning_square/web/main.dart new file mode 100644 index 000000000..6ba47e2d6 --- /dev/null +++ b/web/spinning_square/web/main.dart @@ -0,0 +1,10 @@ +// 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 'package:flutter_web_ui/ui.dart' as ui; +import 'package:flutter_web.examples.spinning_square/main.dart' as app; + +main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/spinning_square/web/preview.png b/web/spinning_square/web/preview.png new file mode 100644 index 000000000..6e14fb569 Binary files /dev/null and b/web/spinning_square/web/preview.png differ diff --git a/web/timeflow/LICENSE b/web/timeflow/LICENSE new file mode 100644 index 000000000..b6d835594 --- /dev/null +++ b/web/timeflow/LICENSE @@ -0,0 +1,13 @@ +Copyright 2019 Fabian Stein + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/web/timeflow/README.md b/web/timeflow/README.md new file mode 100644 index 000000000..150616b8b --- /dev/null +++ b/web/timeflow/README.md @@ -0,0 +1,101 @@ +A gentle animation that provides a calming experience to stressed developers. + +Contributed as part of the Flutter Create 5K challenge by Fabian Stein. + +Timers and stopwatches aren’t the most peaceful gadgets, often reminding us of +urgent tasks, deadlines and unpleasant appointments. Not in this case, Timeflow +is the epitome of pure tranquility, ideal for mindful activities: mediation, +yoga or exercise. The slow, breath like animation is free of sudden, abrupt +jumps and builds up to a Zen finish. + +## Use + +Tap the screen to start/pause the timer + +when paused: + + 1. red button reset the timer and the animation + + 2. green button: resume the timer + +when finished/startscreen: + + 1. blue button choose the desired timeframe + + 2. orange button randomize a new triangle mesh/color scheme + +## Code description + +Please run dartfmt for readability. + +Some of the variable names are short and I have not used comments, because of the character limit, so here is an explanation. + +### globals + +triangles: the list of triangles that are animated + +percent: how much of the timer is completed (from 0.0 to 1.0) + +cTime: the time that is already gone by since the start of the timer (paused time is excluded) + +dur: how long is the timer in Milliseconds + +rng: the random number generator that is used throughout the program + +rebuild: is an indicator that the triangles destination points should be rebuild + +### class TM + The timer class that manages the state of the app + + SI cState: tracks the change of the apps state: is the timer stopped, playing or paused + pTime: tracks when the ticker was paused + Ticker t: the ticker that calls the update function up every frame + up: function that updates the current time or stops the timer, when the duration is reached + + press, pause, play, stop: callback functions, for the button presses + openDialog: callback function, opens the numberPickerDialog, which is used to pick the timer duration + build: returns the app, mainly the custom painter P is called + +### class P + The custom painter, which draws the triangles + + paint: + d = diameter of the circle is 2/3 of the width of the screen + 1. if the triangles are not setup completely (rebuild == true) calculate the outer points of for every triangle setupdP this happens here, because the ratio of the screens has got be known + 2. paint all triangles + shouldRepaint: every frame should be repainted + +### class T + The triangle class + + sP: the list of the starting points of the triangle (these are the points you see at the start of the animation) + dP: the list of destination points (the outer points, where the triangles wander to first, before they circle back to the starting point) + + constructor: p1,p2,p3 are the starting points, c is the overall color scheme (blue, red, green etc.) + for the triangle a random color out of the color scheme is chosen: p.color = c[100 * (rng.nextInt(9) + 1)]; + the rest of the function determines, if the triangle is in the circle, if it is, it is added to the triangles list, otherwise it is forgotten and should be freed by the garbage collector + + setupdP: setup the destination points, choose a random x and y position on the screen + + cP: gives back the current points of the triangle with respect to the timerstate, some trigonometry and interpolations happen here + this is responsible for the animations + 1. alter the alpha repetitively: + 2. alter the distance to the starting points, use a linear interpolation between the starting points sP and the destination points dP with respect to the percentage already done + 3. alter the angle with respect to the starting points + 4. alter the size of the triangles repetitively + +### function setupT + setup the Triangles (starting positions + color scheme) + + dim: dimensions of the “net” + 1. make a net of points in the following manner: + . . . . . + . . . . + . . . . . + . . . . + . . . . . + . . . . + 2. alter the points a little bit by randomization, so that the net is a little more intresting + 3. connect the points to form triangles + 4. randomize a color scheme for the triangles + diff --git a/web/timeflow/lib/infinite_listview.dart b/web/timeflow/lib/infinite_listview.dart new file mode 100644 index 000000000..bb9342ff3 --- /dev/null +++ b/web/timeflow/lib/infinite_listview.dart @@ -0,0 +1,264 @@ +// Package infinite_listview: +// https://pub.dartlang.org/packages/infinite_listview + +import 'dart:math' as math; + +import 'package:flutter_web/rendering.dart'; +import 'package:flutter_web/widgets.dart'; + +/// Infinite ListView +/// +/// ListView that builds its children with to an infinite extent. +/// +class InfiniteListView extends StatelessWidget { + /// See [ListView.builder] + InfiniteListView.builder({ + Key key, + this.scrollDirection = Axis.vertical, + this.reverse = false, + InfiniteScrollController controller, + this.physics, + this.padding, + this.itemExtent, + @required IndexedWidgetBuilder itemBuilder, + int itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + this.cacheExtent, + }) : positiveChildrenDelegate = SliverChildBuilderDelegate( + itemBuilder, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + negativeChildrenDelegate = SliverChildBuilderDelegate( + (BuildContext context, int index) => itemBuilder(context, -1 - index), + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + controller = controller ?? InfiniteScrollController(), + super(key: key); + + /// See [ListView.separated] + InfiniteListView.separated({ + Key key, + this.scrollDirection = Axis.vertical, + this.reverse = false, + InfiniteScrollController controller, + this.physics, + this.padding, + @required IndexedWidgetBuilder itemBuilder, + @required IndexedWidgetBuilder separatorBuilder, + int itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + this.cacheExtent, + }) : assert(itemBuilder != null), + assert(separatorBuilder != null), + itemExtent = null, + positiveChildrenDelegate = SliverChildBuilderDelegate( + (BuildContext context, int index) { + final itemIndex = index ~/ 2; + return index.isEven + ? itemBuilder(context, itemIndex) + : separatorBuilder(context, itemIndex); + }, + childCount: itemCount != null ? math.max(0, itemCount * 2 - 1) : null, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + negativeChildrenDelegate = SliverChildBuilderDelegate( + (BuildContext context, int index) { + final itemIndex = (-1 - index) ~/ 2; + return index.isOdd + ? itemBuilder(context, itemIndex) + : separatorBuilder(context, itemIndex); + }, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + controller = controller ?? InfiniteScrollController(), + super(key: key); + + /// See: [ScrollView.scrollDirection] + final Axis scrollDirection; + + /// See: [ScrollView.reverse] + final bool reverse; + + /// See: [ScrollView.controller] + final InfiniteScrollController controller; + + /// See: [ScrollView.physics] + final ScrollPhysics physics; + + /// See: [BoxScrollView.padding] + final EdgeInsets padding; + + /// See: [ListView.itemExtent] + final double itemExtent; + + /// See: [ScrollView.cacheExtent] + final double cacheExtent; + + /// See: [ListView.childrenDelegate] + final SliverChildDelegate negativeChildrenDelegate; + + /// See: [ListView.childrenDelegate] + final SliverChildDelegate positiveChildrenDelegate; + + @override + Widget build(BuildContext context) { + final List slivers = _buildSlivers(context, negative: false); + final List negativeSlivers = _buildSlivers(context, negative: true); + final AxisDirection axisDirection = _getDirection(context); + final scrollPhysics = AlwaysScrollableScrollPhysics(parent: physics); + return Scrollable( + axisDirection: axisDirection, + controller: controller, + physics: scrollPhysics, + viewportBuilder: (BuildContext context, ViewportOffset offset) { + return Builder(builder: (BuildContext context) { + /// Build negative [ScrollPosition] for the negative scrolling [Viewport]. + final state = Scrollable.of(context); + final negativeOffset = _InfiniteScrollPosition( + physics: scrollPhysics, + context: state, + initialPixels: -offset.pixels, + keepScrollOffset: controller.keepScrollOffset, + ); + + /// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition]. + offset.addListener(() { + negativeOffset._forceNegativePixels(offset.pixels); + }); + + /// Stack the two [Viewport]s on top of each other so they move in sync. + return Stack( + children: [ + Viewport( + axisDirection: flipAxisDirection(axisDirection), + anchor: 1.0, + offset: negativeOffset, + slivers: negativeSlivers, + cacheExtent: cacheExtent, + ), + Viewport( + axisDirection: axisDirection, + offset: offset, + slivers: slivers, + cacheExtent: cacheExtent, + ), + ], + ); + }); + }, + ); + } + + AxisDirection _getDirection(BuildContext context) { + return getAxisDirectionFromAxisReverseAndDirectionality( + context, scrollDirection, reverse); + } + + List _buildSlivers(BuildContext context, {bool negative = false}) { + Widget sliver; + if (itemExtent != null) { + sliver = SliverFixedExtentList( + delegate: + negative ? negativeChildrenDelegate : positiveChildrenDelegate, + itemExtent: itemExtent, + ); + } else { + sliver = SliverList( + delegate: + negative ? negativeChildrenDelegate : positiveChildrenDelegate); + } + if (padding != null) { + sliver = new SliverPadding( + padding: negative + ? padding - EdgeInsets.only(bottom: padding.bottom) + : padding - EdgeInsets.only(top: padding.top), + sliver: sliver, + ); + } + return [sliver]; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(new EnumProperty('scrollDirection', scrollDirection)); + properties.add(new FlagProperty('reverse', + value: reverse, ifTrue: 'reversed', showName: true)); + properties.add(new DiagnosticsProperty( + 'controller', controller, + showName: false, defaultValue: null)); + properties.add(new DiagnosticsProperty('physics', physics, + showName: false, defaultValue: null)); + properties.add(new DiagnosticsProperty( + 'padding', padding, + defaultValue: null)); + properties + .add(new DoubleProperty('itemExtent', itemExtent, defaultValue: null)); + properties.add( + new DoubleProperty('cacheExtent', cacheExtent, defaultValue: null)); + } +} + +/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds. +class InfiniteScrollController extends ScrollController { + /// Creates a new [InfiniteScrollController] + InfiniteScrollController({ + double initialScrollOffset = 0.0, + bool keepScrollOffset = true, + String debugLabel, + }) : super( + initialScrollOffset: initialScrollOffset, + keepScrollOffset: keepScrollOffset, + debugLabel: debugLabel, + ); + + @override + ScrollPosition createScrollPosition(ScrollPhysics physics, + ScrollContext context, ScrollPosition oldPosition) { + return new _InfiniteScrollPosition( + physics: physics, + context: context, + initialPixels: initialScrollOffset, + keepScrollOffset: keepScrollOffset, + oldPosition: oldPosition, + debugLabel: debugLabel, + ); + } +} + +class _InfiniteScrollPosition extends ScrollPositionWithSingleContext { + _InfiniteScrollPosition({ + @required ScrollPhysics physics, + @required ScrollContext context, + double initialPixels = 0.0, + bool keepScrollOffset = true, + ScrollPosition oldPosition, + String debugLabel, + }) : super( + physics: physics, + context: context, + initialPixels: initialPixels, + keepScrollOffset: keepScrollOffset, + oldPosition: oldPosition, + debugLabel: debugLabel, + ); + + void _forceNegativePixels(double value) { + super.forcePixels(-value); + } + + @override + double get minScrollExtent => double.negativeInfinity; + + @override + double get maxScrollExtent => double.infinity; +} diff --git a/web/timeflow/lib/main.dart b/web/timeflow/lib/main.dart new file mode 100644 index 000000000..f32d65e0c --- /dev/null +++ b/web/timeflow/lib/main.dart @@ -0,0 +1,260 @@ +import 'dart:core'; +import 'dart:math'; +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/scheduler.dart'; + +import 'numberpicker.dart'; + +main() => runApp(MaterialApp(home: App(), debugShowCheckedModeBanner: false)); + +class App extends StatefulWidget { + @override + State createState() => TM(); +} + +enum SI { pause, play, stop } +List triangles; +var percent = 0.0, cTime = 0.0, dur = 120000.0, rng = Random(), rebuild = true; + +class TM extends State { + SI cState = SI.stop; + Ticker t; + var pTime = 0.0; + + @override + initState() { +// Screen.keepOn(true); + t = Ticker(up); + super.initState(); + } + + up(Duration d) { + if (cState == SI.play) { + setState(() { + if (cTime >= dur) + stop(); + else { + cTime = d.inMilliseconds.toDouble() + pTime; + percent = cTime / dur; + } + }); + } + } + + press() { + if (cState == SI.play) + pause(); + else if (cState == SI.pause) + play(); + else { + cState = SI.play; + t.start(); + } + } + + pause() { + setState(() { + cState = SI.pause; + t.stop(); + }); + } + + play() { + setState(() { + cState = SI.play; + t.start(); + pTime = cTime; + }); + } + + stop() { + setState(() { + cState = SI.stop; + t.stop(); + pTime = 0.0; + cTime = 0.0; + percent = 0.0; + }); + } + + openDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return NumberPickerDialog.integer( + initialIntegerValue: (dur + 1.0) ~/ 60000, + maxValue: 20, + minValue: 1, + title: Text('Minutes')); + }).then((num v) { + if (v != null) dur = 60000.0 * v; + }); + } + + @override + Widget build(BuildContext context) { + List w = List(); + + if (cState == SI.pause) { + w.add(fab(Colors.green, play, Icons.play_arrow)); + w.add(SizedBox(height: 10)); + w.add(fab(Colors.red, stop, Icons.close)); + w.add(SizedBox(height: 20)); + } + + if (cState == SI.stop) { + w.add(fab(Colors.lightBlue, openDialog, Icons.timer)); + w.add(SizedBox(height: 10)); + w.add(fab(Colors.yellow[900], () { + rebuild = true; + }, Icons.loop)); + w.add(SizedBox(height: 20)); + } + + Column r = Column(mainAxisAlignment: MainAxisAlignment.end, children: w); + + return Scaffold( + backgroundColor: Colors.black, + body: SizedBox.expand( + child: Container( + child: CustomPaint( + painter: P(), + child: FlatButton( + onPressed: press, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [r])))))); + } +} + +FloatingActionButton fab(Color c, VoidCallback f, IconData ic) => + FloatingActionButton(backgroundColor: c, onPressed: f, child: Icon(ic)); + +class P extends CustomPainter { + @override + paint(Canvas canvas, Size size) { + var w = size.width, h = size.height, d = 2 / 3 * w; + if (w > 0.1 && h > 0.1) { + if (rebuild) { + rebuild = false; + setupT(); + for (var t in triangles) t.setupdP(w / d, h / d); + } + + for (var t in triangles) { + var cP = t.cP(), p = Path(); + p.moveTo(cP[0].x * d + w / 2, cP[0].y * d + h / 2); + for (i = 1; i < 3; i++) + p.lineTo(cP[i].x * d + w / 2, cP[i].y * d + h / 2); + p.close(); + canvas.drawPath(p, t.p); + } + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => true; +} + +int i; + +class T { + List dP = List(3), sP = List(3); + Paint p; + + T(Point p1, p2, p3, var c) { + p = Paint()..style = PaintingStyle.fill; + sP[0] = p1; + sP[1] = p2; + sP[2] = p3; + p.color = c[100 * (rng.nextInt(9) + 1)]; + + double x = 0, y = 0; + for (i = 0; i < 3; i++) { + x += sP[i].x; + y += sP[i].y; + } + + x = 2 * x / 3; + y = 2 * y / 3; + if (x * x + y * y < 1) triangles.add(this); + } + + setupdP(double wR, hR) { + var x = (rng.nextDouble() - 0.5) * (wR - 0.1), + y = (rng.nextDouble() - 0.5) * (hR - 0.1); + dP[0] = Point(x, y); + for (i = 1; i < 3; i++) + dP[i] = Point(sP[i].x + x - sP[0].x, sP[i].y + y - sP[0].y); + } + + List cP() { + List res = List(3); + var p, k, o = 6000, r; + if (cTime < o) + p = 1 - cTime / o; + else + p = (cTime - o) / (dur - o); + k = 2 * ((cTime.toInt() % o) - o / 2).abs() / o; + r = min(min(1, (dur - cTime) / o), cTime / o); + this.p.color = this.p.color.withAlpha(255 - (200 * k * r).toInt()); + + for (i = 0; i < 3; i++) + res[i] = Point( + sP[i].x * p + dP[i].x * (1 - p), sP[i].y * p + dP[i].y * (1 - p)); + + if (cTime > o) { + var d = res[0].distanceTo(sP[0]); + var a = acos((sP[0].x - res[0].x) / d); + if (sP[0].y > res[0].y) a = 2 * pi - a; + var b = pi - a + p * pi * dur / 120000; + var dX = cos(b) * d, dY = sin(b) * d; + for (i = 0; i < 3; i++) res[i] = Point(sP[i].x + dX, sP[i].y + dY); + } + + double mx = 0, my = 0; + for (i = 0; i < 3; i++) { + mx += res[i].x; + my += res[i].y; + } + mx /= 3; + my /= 3; + for (i = 0; i < 3; i++) + res[i] = Point(res[i].x + (res[i].x - mx) * (1 - k) * r / 2, + res[i].y + (res[i].y - my) * (1 - k) * r / 2); + + return res; + } +} + +setupT() { + int dim = 20, x, y; + List tri = List(dim * dim); + + for (x = 0; x < dim; x++) { + for (y = 0; y < dim; y++) { + var dx = rng.nextDouble() - 0.5, dy = rng.nextDouble() - 0.5, off; + if (x % 2 == 0) + off = 0; + else + off = 0.5; + tri[x * dim + y] = + Point((x + dx) / (dim - 1) - 0.5, (y + off + dy) / (dim - 1) - 0.5); + } + } + triangles = List(); + var r = rng.nextInt(5), c; + if (r == 0) c = Colors.lightBlue; + if (r == 1) c = Colors.yellow; + if (r == 2) c = Colors.lightGreen; + if (r == 3) c = Colors.red; + if (r == 4) c = Colors.pink; + + for (x = 0; x < dim - 1; x++) { + for (y = 0; y < dim - 1; y++) { + int off = x * dim; + T(tri[y + off], tri[y + 1 + off], tri[y + off + dim], c); + T(tri[y + off + dim], tri[y + 1 + off], tri[y + 1 + off + dim], c); + } + } +} diff --git a/web/timeflow/lib/numberpicker.dart b/web/timeflow/lib/numberpicker.dart new file mode 100644 index 000000000..2cbff7255 --- /dev/null +++ b/web/timeflow/lib/numberpicker.dart @@ -0,0 +1,527 @@ +// Package numberpicker: +// https://pub.dartlang.org/packages/numberpicker + +import 'dart:math' as math; + +import 'package:flutter_web/foundation.dart'; +import 'package:flutter_web/material.dart'; +import 'package:flutter_web/rendering.dart'; + +import 'infinite_listview.dart'; + +/// Created by Marcin Szałek + +///NumberPicker is a widget designed to pick a number between #minValue and #maxValue +class NumberPicker extends StatelessWidget { + ///height of every list element + static const double DEFAULT_ITEM_EXTENT = 50.0; + + ///width of list view + static const double DEFAULT_LISTVIEW_WIDTH = 100.0; + + ///constructor for integer number picker + NumberPicker.integer({ + Key key, + @required int initialValue, + @required this.minValue, + @required this.maxValue, + @required this.onChanged, + this.itemExtent = DEFAULT_ITEM_EXTENT, + this.listViewWidth = DEFAULT_LISTVIEW_WIDTH, + this.step = 1, + this.infiniteLoop = false, + }) : assert(initialValue != null), + assert(minValue != null), + assert(maxValue != null), + assert(maxValue > minValue), + assert(initialValue >= minValue && initialValue <= maxValue), + assert(step > 0), + selectedIntValue = initialValue, + selectedDecimalValue = -1, + decimalPlaces = 0, + intScrollController = infiniteLoop + ? new InfiniteScrollController( + initialScrollOffset: + (initialValue - minValue) ~/ step * itemExtent, + ) + : new ScrollController( + initialScrollOffset: + (initialValue - minValue) ~/ step * itemExtent, + ), + decimalScrollController = null, + _listViewHeight = 3 * itemExtent, + integerItemCount = (maxValue - minValue) ~/ step + 1, + super(key: key); + + ///constructor for decimal number picker + NumberPicker.decimal({ + Key key, + @required double initialValue, + @required this.minValue, + @required this.maxValue, + @required this.onChanged, + this.decimalPlaces = 1, + this.itemExtent = DEFAULT_ITEM_EXTENT, + this.listViewWidth = DEFAULT_LISTVIEW_WIDTH, + }) : assert(initialValue != null), + assert(minValue != null), + assert(maxValue != null), + assert(decimalPlaces != null && decimalPlaces > 0), + assert(maxValue > minValue), + assert(initialValue >= minValue && initialValue <= maxValue), + selectedIntValue = initialValue.floor(), + selectedDecimalValue = ((initialValue - initialValue.floorToDouble()) * + math.pow(10, decimalPlaces)) + .round(), + intScrollController = new ScrollController( + initialScrollOffset: (initialValue.floor() - minValue) * itemExtent, + ), + decimalScrollController = new ScrollController( + initialScrollOffset: ((initialValue - initialValue.floorToDouble()) * + math.pow(10, decimalPlaces)) + .roundToDouble() * + itemExtent, + ), + _listViewHeight = 3 * itemExtent, + step = 1, + integerItemCount = maxValue.floor() - minValue.floor() + 1, + infiniteLoop = false, + super(key: key); + + ///called when selected value changes + final ValueChanged onChanged; + + ///min value user can pick + final int minValue; + + ///max value user can pick + final int maxValue; + + ///inidcates how many decimal places to show + /// e.g. 0=>[1,2,3...], 1=>[1.0, 1.1, 1.2...] 2=>[1.00, 1.01, 1.02...] + final int decimalPlaces; + + ///height of every list element in pixels + final double itemExtent; + + ///view will always contain only 3 elements of list in pixels + final double _listViewHeight; + + ///width of list view in pixels + final double listViewWidth; + + ///ScrollController used for integer list + final ScrollController intScrollController; + + ///ScrollController used for decimal list + final ScrollController decimalScrollController; + + ///Currently selected integer value + final int selectedIntValue; + + ///Currently selected decimal value + final int selectedDecimalValue; + + ///Step between elements. Only for integer datePicker + ///Examples: + /// if step is 100 the following elements may be 100, 200, 300... + /// if min=0, max=6, step=3, then items will be 0, 3 and 6 + /// if min=0, max=5, step=3, then items will be 0 and 3. + final int step; + + ///Repeat values infinitely + final bool infiniteLoop; + + ///Amount of items + final int integerItemCount; + + // + //----------------------------- PUBLIC ------------------------------ + // + + animateInt(int valueToSelect) { + int diff = valueToSelect - minValue; + int index = diff ~/ step; + animateIntToIndex(index); + } + + animateIntToIndex(int index) { + _animate(intScrollController, index * itemExtent); + } + + animateDecimal(int decimalValue) { + _animate(decimalScrollController, decimalValue * itemExtent); + } + + animateDecimalAndInteger(double valueToSelect) { + animateInt(valueToSelect.floor()); + animateDecimal(((valueToSelect - valueToSelect.floorToDouble()) * + math.pow(10, decimalPlaces)) + .round()); + } + + // + //----------------------------- VIEWS ----------------------------- + // + + ///main widget + @override + Widget build(BuildContext context) { + final ThemeData themeData = Theme.of(context); + + if (infiniteLoop) { + return _integerInfiniteListView(themeData); + } + if (decimalPlaces == 0) { + return _integerListView(themeData); + } else { + return new Row( + children: [ + _integerListView(themeData), + _decimalListView(themeData), + ], + mainAxisAlignment: MainAxisAlignment.center, + ); + } + } + + Widget _integerListView(ThemeData themeData) { + TextStyle defaultStyle = themeData.textTheme.body1; + TextStyle selectedStyle = + themeData.textTheme.headline.copyWith(color: themeData.accentColor); + + var listItemCount = integerItemCount + 2; + + return new NotificationListener( + child: new Container( + height: _listViewHeight, + width: listViewWidth, + child: new ListView.builder( + controller: intScrollController, + itemExtent: itemExtent, + itemCount: listItemCount, + cacheExtent: _calculateCacheExtent(listItemCount), + itemBuilder: (BuildContext context, int index) { + final int value = _intValueFromIndex(index); + + //define special style for selected (middle) element + final TextStyle itemStyle = + value == selectedIntValue ? selectedStyle : defaultStyle; + + bool isExtra = index == 0 || index == listItemCount - 1; + + return isExtra + ? new Container() //empty first and last element + : new Center( + child: new Text(value.toString(), style: itemStyle), + ); + }, + ), + ), + onNotification: _onIntegerNotification, + ); + } + + Widget _decimalListView(ThemeData themeData) { + TextStyle defaultStyle = themeData.textTheme.body1; + TextStyle selectedStyle = + themeData.textTheme.headline.copyWith(color: themeData.accentColor); + + int decimalItemCount = + selectedIntValue == maxValue ? 3 : math.pow(10, decimalPlaces) + 2; + + return new NotificationListener( + child: new Container( + height: _listViewHeight, + width: listViewWidth, + child: new ListView.builder( + controller: decimalScrollController, + itemExtent: itemExtent, + itemCount: decimalItemCount, + itemBuilder: (BuildContext context, int index) { + final int value = index - 1; + + //define special style for selected (middle) element + final TextStyle itemStyle = + value == selectedDecimalValue ? selectedStyle : defaultStyle; + + bool isExtra = index == 0 || index == decimalItemCount - 1; + + return isExtra + ? new Container() //empty first and last element + : new Center( + child: new Text( + value.toString().padLeft(decimalPlaces, '0'), + style: itemStyle), + ); + }, + ), + ), + onNotification: _onDecimalNotification, + ); + } + + Widget _integerInfiniteListView(ThemeData themeData) { + TextStyle defaultStyle = themeData.textTheme.body1; + TextStyle selectedStyle = + themeData.textTheme.headline.copyWith(color: themeData.accentColor); + + return new NotificationListener( + child: new Container( + height: _listViewHeight, + width: listViewWidth, + child: new InfiniteListView.builder( + controller: intScrollController, + itemExtent: itemExtent, + itemBuilder: (BuildContext context, int index) { + final int value = _intValueFromIndex(index); + + //define special style for selected (middle) element + final TextStyle itemStyle = + value == selectedIntValue ? selectedStyle : defaultStyle; + + return new Center( + child: new Text(value.toString(), style: itemStyle), + ); + }, + ), + ), + onNotification: _onIntegerNotification, + ); + } + + // + // ----------------------------- LOGIC ----------------------------- + // + + int _intValueFromIndex(int index) { + index--; + index %= integerItemCount; + return minValue + index * step; + } + + bool _onIntegerNotification(Notification notification) { + if (notification is ScrollNotification) { + //calculate + int intIndexOfMiddleElement = + (notification.metrics.pixels / itemExtent).round(); + if (!infiniteLoop) { + intIndexOfMiddleElement = + intIndexOfMiddleElement.clamp(0, integerItemCount - 1); + } + int intValueInTheMiddle = _intValueFromIndex(intIndexOfMiddleElement + 1); + intValueInTheMiddle = _normalizeIntegerMiddleValue(intValueInTheMiddle); + + if (_userStoppedScrolling(notification, intScrollController)) { + //center selected value + animateIntToIndex(intIndexOfMiddleElement); + } + + //update selection + if (intValueInTheMiddle != selectedIntValue) { + num newValue; + if (decimalPlaces == 0) { + //return integer value + newValue = (intValueInTheMiddle); + } else { + if (intValueInTheMiddle == maxValue) { + //if new value is maxValue, then return that value and ignore decimal + newValue = (intValueInTheMiddle.toDouble()); + animateDecimal(0); + } else { + //return integer+decimal + double decimalPart = _toDecimal(selectedDecimalValue); + newValue = ((intValueInTheMiddle + decimalPart).toDouble()); + } + } + onChanged(newValue); + } + } + return true; + } + + bool _onDecimalNotification(Notification notification) { + if (notification is ScrollNotification) { + //calculate middle value + int indexOfMiddleElement = + (notification.metrics.pixels + _listViewHeight / 2) ~/ itemExtent; + int decimalValueInTheMiddle = indexOfMiddleElement - 1; + decimalValueInTheMiddle = + _normalizeDecimalMiddleValue(decimalValueInTheMiddle); + + if (_userStoppedScrolling(notification, decimalScrollController)) { + //center selected value + animateDecimal(decimalValueInTheMiddle); + } + + //update selection + if (selectedIntValue != maxValue && + decimalValueInTheMiddle != selectedDecimalValue) { + double decimalPart = _toDecimal(decimalValueInTheMiddle); + double newValue = ((selectedIntValue + decimalPart).toDouble()); + onChanged(newValue); + } + } + return true; + } + + ///There was a bug, when if there was small integer range, e.g. from 1 to 5, + ///When user scrolled to the top, whole listview got displayed. + ///To prevent this we are calculating cacheExtent by our own so it gets smaller if number of items is smaller + double _calculateCacheExtent(int itemCount) { + double cacheExtent = 250.0; //default cache extent + if ((itemCount - 2) * DEFAULT_ITEM_EXTENT <= cacheExtent) { + cacheExtent = ((itemCount - 3) * DEFAULT_ITEM_EXTENT); + } + return cacheExtent; + } + + ///When overscroll occurs on iOS, + ///we can end up with value not in the range between [minValue] and [maxValue] + ///To avoid going out of range, we change values out of range to border values. + int _normalizeMiddleValue(int valueInTheMiddle, int min, int max) { + return math.max(math.min(valueInTheMiddle, max), min); + } + + int _normalizeIntegerMiddleValue(int integerValueInTheMiddle) { + //make sure that max is a multiple of step + int max = (maxValue ~/ step) * step; + return _normalizeMiddleValue(integerValueInTheMiddle, minValue, max); + } + + int _normalizeDecimalMiddleValue(int decimalValueInTheMiddle) { + return _normalizeMiddleValue( + decimalValueInTheMiddle, 0, math.pow(10, decimalPlaces) - 1); + } + + ///indicates if user has stopped scrolling so we can center value in the middle + bool _userStoppedScrolling( + Notification notification, ScrollController scrollController) { + return notification is UserScrollNotification && + notification.direction == ScrollDirection.idle && + // ignore: invalid_use_of_protected_member + scrollController.position.activity is! HoldScrollActivity; + } + + ///converts integer indicator of decimal value to double + ///e.g. decimalPlaces = 1, value = 4 >>> result = 0.4 + /// decimalPlaces = 2, value = 12 >>> result = 0.12 + double _toDecimal(int decimalValueAsInteger) { + return double.parse((decimalValueAsInteger * math.pow(10, -decimalPlaces)) + .toStringAsFixed(decimalPlaces)); + } + + ///scroll to selected value + _animate(ScrollController scrollController, double value) { + scrollController.animateTo(value, + duration: new Duration(seconds: 1), curve: new ElasticOutCurve()); + } +} + +///Returns AlertDialog as a Widget so it is designed to be used in showDialog method +class NumberPickerDialog extends StatefulWidget { + final int minValue; + final int maxValue; + final int initialIntegerValue; + final double initialDoubleValue; + final int decimalPlaces; + final Widget title; + final EdgeInsets titlePadding; + final Widget confirmWidget; + final Widget cancelWidget; + final int step; + final bool infiniteLoop; + + ///constructor for integer values + NumberPickerDialog.integer({ + @required this.minValue, + @required this.maxValue, + @required this.initialIntegerValue, + this.title, + this.titlePadding, + this.step = 1, + this.infiniteLoop = false, + Widget confirmWidget, + Widget cancelWidget, + }) : confirmWidget = confirmWidget ?? new Text("OK"), + cancelWidget = cancelWidget ?? new Text("CANCEL"), + decimalPlaces = 0, + initialDoubleValue = -1.0; + + ///constructor for decimal values + NumberPickerDialog.decimal({ + @required this.minValue, + @required this.maxValue, + @required this.initialDoubleValue, + this.decimalPlaces = 1, + this.title, + this.titlePadding, + Widget confirmWidget, + Widget cancelWidget, + }) : confirmWidget = confirmWidget ?? new Text("OK"), + cancelWidget = cancelWidget ?? new Text("CANCEL"), + initialIntegerValue = -1, + step = 1, + infiniteLoop = false; + + @override + State createState() => + new _NumberPickerDialogControllerState( + initialIntegerValue, initialDoubleValue); +} + +class _NumberPickerDialogControllerState extends State { + int selectedIntValue; + double selectedDoubleValue; + + _NumberPickerDialogControllerState( + this.selectedIntValue, this.selectedDoubleValue); + + _handleValueChanged(num value) { + if (value is int) { + setState(() => selectedIntValue = value); + } else { + setState(() => selectedDoubleValue = value); + } + } + + NumberPicker _buildNumberPicker() { + if (widget.decimalPlaces > 0) { + return new NumberPicker.decimal( + initialValue: selectedDoubleValue, + minValue: widget.minValue, + maxValue: widget.maxValue, + decimalPlaces: widget.decimalPlaces, + onChanged: _handleValueChanged); + } else { + return new NumberPicker.integer( + initialValue: selectedIntValue, + minValue: widget.minValue, + maxValue: widget.maxValue, + step: widget.step, + infiniteLoop: widget.infiniteLoop, + onChanged: _handleValueChanged, + ); + } + } + + @override + Widget build(BuildContext context) { + return new AlertDialog( + title: widget.title, + titlePadding: widget.titlePadding, + content: _buildNumberPicker(), + actions: [ + new FlatButton( + onPressed: () => Navigator.of(context).pop(), + child: widget.cancelWidget, + ), + new FlatButton( + onPressed: () => Navigator.of(context).pop(widget.decimalPlaces > 0 + ? selectedDoubleValue + : selectedIntValue), + child: widget.confirmWidget), + ], + ); + } +} diff --git a/web/timeflow/pubspec.lock b/web/timeflow/pubspec.lock new file mode 100644 index 000000000..2e9964fa1 --- /dev/null +++ b/web/timeflow/pubspec.lock @@ -0,0 +1,471 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct main" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/timeflow/pubspec.yaml b/web/timeflow/pubspec.yaml new file mode 100644 index 000000000..1e2ea6ece --- /dev/null +++ b/web/timeflow/pubspec.yaml @@ -0,0 +1,23 @@ +name: timeflow + +environment: + sdk: ">=2.2.0 <3.0.0" + +dependencies: + flutter_web: any + flutter_web_ui: any + +dev_dependencies: + build_runner: any + build_web_compilers: any +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/timeflow/web/assets/FontManifest.json b/web/timeflow/web/assets/FontManifest.json new file mode 100644 index 000000000..01fc0852f --- /dev/null +++ b/web/timeflow/web/assets/FontManifest.json @@ -0,0 +1,10 @@ +[ + { + "family": "MaterialIcons", + "fonts": [ + { + "asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" + } + ] + } + ] \ No newline at end of file diff --git a/web/timeflow/web/index.html b/web/timeflow/web/index.html new file mode 100644 index 000000000..b54ed98d8 --- /dev/null +++ b/web/timeflow/web/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/timeflow/web/main.dart b/web/timeflow/web/main.dart new file mode 100644 index 000000000..e8472a48c --- /dev/null +++ b/web/timeflow/web/main.dart @@ -0,0 +1,10 @@ +// 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 'package:flutter_web_ui/ui.dart' as ui; +import 'package:timeflow/main.dart' as app; + +main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/timeflow/web/preview.png b/web/timeflow/web/preview.png new file mode 100644 index 000000000..62f052388 Binary files /dev/null and b/web/timeflow/web/preview.png differ diff --git a/web/vision_challenge/LICENSE b/web/vision_challenge/LICENSE new file mode 100644 index 000000000..39ac907e4 --- /dev/null +++ b/web/vision_challenge/LICENSE @@ -0,0 +1,25 @@ +Copyright 2019 Yukkei Choi + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/web/vision_challenge/README.md b/web/vision_challenge/README.md new file mode 100644 index 000000000..a24f7b660 --- /dev/null +++ b/web/vision_challenge/README.md @@ -0,0 +1,55 @@ +A fun game to test your color perception abilities. + +Contributed as part of the Flutter Create 5K challenge by Yukkei Choi. + +## How to play + +Tap the unique color block as fast as possible. + +## Features + +1. Each round when user taps the unique color block, score will be increased by one. +2. Timer: 30 seconds countdown. +3. Color difference will be stepwise reduced when user reached a higher score. +4. If it is difficult to distinguish the unique color block, user can "SHAKE" the device to shift to another theme color, while the position of the unique color block still keep the same. +5. Provide a restart button at the end, user can infinitely play again without relaunching the app. +6. After each replay, game board's theme color will be different from the previous play. +7. Give user a grade based on the final score: + +| score range | grade | +|-------------|-------| +| 0 - 9 | Fail | +| 10 - 19 | D | +| 20 - 29 | C | +| 30 - 34 | B | +| 35 - 39 | B+ | +| 40 - 44 | A | +| 45 or above | A+ | + +## Graphics + +1. I created all graphics used on the app by using Photoshop. +2. Flutter is great and now I'm able to demonstrate my artwork on the app into practice. + +## Techniques used + +1. Use stateful widget to run the timer countdown animation. +2. Since only 5kb is allowed, the grade is calculated by using math, instead of writing if-else statement. +3. Use redux to store the game states: + +| state | description | data type | +|-------|----------------------------------------------------------|-------------------| +| score | Store the player score | int | +| board | Locate the position of unique color block | [[int],[int],...] | +| count | Count the no. of replay, for switching the theme color | int | +| page | Current page / game status | int | + +| page | description | +|------|----------------------------------------------------------------| +| -1 | First launch the app, show the welcome screen with instruction | +| 0 | Game in progress | +| 1 | Game end, show result | + +## Limitation + +Limited to portrait view. diff --git a/web/vision_challenge/lib/game.dart b/web/vision_challenge/lib/game.dart new file mode 100644 index 000000000..7833ad82c --- /dev/null +++ b/web/vision_challenge/lib/game.dart @@ -0,0 +1,207 @@ +import 'dart:math'; + +import 'package:flutter_web/material.dart'; +import 'package:vision_challenge/packages/flutter_redux.dart'; +import 'package:vision_challenge/packages/redux.dart'; + +setText(text, size, color) => Text(text, + style: TextStyle( + fontSize: size, + color: color, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none)); + +pad(double left, double top) => EdgeInsets.fromLTRB(left, top, 0, 0); + +setBg(name) => BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + alignment: Alignment.topLeft, + image: AssetImage(name))); + +class Game extends StatelessWidget { + final Store store; + Game(this.store); + _grade(int score) => [10, 20, 30, 35, 40, 45, 99] + .where((i) => i > score) + .reduce(min) + .toString(); + + _createBoard(double size, List> blocks, int depth, + MaterialColor color) => + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: blocks + .map((cols) => Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: cols + .map((item) => Flexible( + child: GestureDetector( + onTap: () { + if (item == 1) store.dispatch(Action.next); + }, + child: Container( + width: size, + height: size, + color: item > 0 ? color[depth] : color)), + )) + .toList())) + .toList()); + + @override + Widget build(BuildContext context) => StoreConnector( + // onInit: (state) => ShakeDetector.autoStart( + // onPhoneShake: () => store.dispatch(Action.shake)), + converter: (store) => store.state, + builder: (context, state) { + var w = MediaQuery.of(context).size.height / 16 * 9, + size = w / (state.board.length + 1), + depth = [1 + state.score ~/ 5, 4].reduce(min) * 100, + colors = [ + Colors.blue, + Colors.orange, + Colors.pink, + Colors.purple, + Colors.cyan + ]; + + return Scaffold( + backgroundColor: Color(0xFFBCE1F6), + body: Center( + child: SizedBox( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.height / 16 * 9, + child: Container( + decoration: setBg(state.page < 0 ? 'p0.jpg' : 'p1.jpg'), + child: state.page >= 0 + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: w * 0.325, + padding: pad(0, w * 0.145), + child: setText(state.score.toString(), + w * 0.2, Colors.white)), + Container( + height: w * 0.35, + padding: pad(w * 0.69, state.page * 7.0), + child: state.page < 1 + ? Timer( + onEnd: () => + store.dispatch(Action.end), + width: w) + : setText('End', w * 0.08, Colors.red)), + state.page < 1 + ? Container( + width: w, + height: w * 1.05, + padding: pad(0, w * 0.05), + child: _createBoard( + size, + state.board, + depth, + colors[ + state.count % colors.length])) + : Container( + width: w, + height: w, + decoration: + setBg(_grade(state.score) + '.png')) + ]) + : Container()), + ), + ), + floatingActionButton: state.page != 0 + ? Container( + // width: w * 0.2, + // height: w * 0.2, + child: FloatingActionButton( + child: Icon( + state.page < 1 ? Icons.play_arrow : Icons.refresh), + onPressed: () => store.dispatch(Action.start))) + : Container()); + }); +} + +class Timer extends StatefulWidget { + Timer({this.onEnd, this.width}); + final VoidCallback onEnd; + final double width; + @override + _TimerState createState() => _TimerState(); +} + +class _TimerState extends State with TickerProviderStateMixin { + Animation _animate; + int _sec = 31; + + @override + void initState() { + super.initState(); + _animate = StepTween(begin: _sec, end: 0).animate( + AnimationController(duration: Duration(seconds: _sec), vsync: this) + ..forward(from: 0.0)) + ..addStatusListener((AnimationStatus s) { + if (s == AnimationStatus.completed) widget.onEnd(); + }); + } + + @override + Widget build(BuildContext context) => AnimatedBuilder( + animation: _animate, + builder: (BuildContext context, Widget child) => setText( + _animate.value.toString().padLeft(2, '0'), + widget.width * 0.12, + Colors.green)); +} + +//REDUX +@immutable +class AppState { + final int score, page, count; + final List> board; + AppState({this.score, this.page, this.board, this.count}); + AppState.init() + : score = 0, + page = -1, + count = 0, + board = newBoard(0); +} + +enum Action { next, end, start, shake } + +AppState reducer(AppState s, act) { + switch (act) { + case Action.next: + return AppState( + score: s.score + 1, + page: s.page, + count: s.count, + board: newBoard(s.score + 1)); + case Action.end: + return AppState( + score: s.score, page: 1, count: s.count + 1, board: s.board); + case Action.start: + return AppState(score: 0, page: 0, count: s.count, board: newBoard(0)); + case Action.shake: + return AppState( + score: s.score, page: s.page, count: s.count + 1, board: s.board); + default: + return s; + } +} + +List> newBoard(score) { + var size = score < 7 ? score + 3 : 10, + rng = Random(), + bingoRow = rng.nextInt(size), + bingoCol = rng.nextInt(size); + List> board = []; + for (var i = 0; i < size; i++) { + List row = []; + for (var j = 0; j < size; j++) + row.add(i == bingoRow && j == bingoCol ? 1 : 0); + board.add(row); + } + return board; +} diff --git a/web/vision_challenge/lib/main.dart b/web/vision_challenge/lib/main.dart new file mode 100644 index 000000000..c7b45ab46 --- /dev/null +++ b/web/vision_challenge/lib/main.dart @@ -0,0 +1,20 @@ +import 'package:flutter_web/material.dart'; +import 'package:vision_challenge/packages/flutter_redux.dart'; +import 'package:vision_challenge/packages/redux.dart'; +import 'game.dart'; + +void main() { + final store = Store( + reducer, + initialState: AppState.init(), + ); + + runApp( + StoreProvider( + store: store, + child: MaterialApp( + home: Game(store), + ), + ), + ); +} diff --git a/web/vision_challenge/lib/packages/flutter_redux.dart b/web/vision_challenge/lib/packages/flutter_redux.dart new file mode 100644 index 000000000..c2a4c8836 --- /dev/null +++ b/web/vision_challenge/lib/packages/flutter_redux.dart @@ -0,0 +1,513 @@ +// Package flutter_redux: +// https://pub.dev/packages/flutter_redux + +import 'dart:async'; + +import 'package:flutter_web/widgets.dart'; +import 'package:meta/meta.dart'; +import 'redux.dart'; + +/// Provides a Redux [Store] to all descendants of this Widget. This should +/// generally be a root widget in your App. Connect to the Store provided +/// by this Widget using a [StoreConnector] or [StoreBuilder]. +class StoreProvider extends InheritedWidget { + final Store _store; + + /// Create a [StoreProvider] by passing in the required [store] and [child] + /// parameters. + const StoreProvider({ + Key key, + @required Store store, + @required Widget child, + }) : assert(store != null), + assert(child != null), + _store = store, + super(key: key, child: child); + + /// A method that can be called by descendant Widgets to retrieve the Store + /// from the StoreProvider. + /// + /// Important: When using this method, pass through complete type information + /// or Flutter will be unable to find the correct StoreProvider! + /// + /// ### Example + /// + /// ``` + /// class MyWidget extends StatelessWidget { + /// @override + /// Widget build(BuildContext context) { + /// final store = StoreProvider.of(context); + /// + /// return Text('${store.state}'); + /// } + /// } + /// ``` + static Store of(BuildContext context) { + final type = _typeOf>(); + final provider = + context.inheritFromWidgetOfExactType(type) as StoreProvider; + + if (provider == null) throw StoreProviderError(type); + + return provider._store; + } + + // Workaround to capture generics + static Type _typeOf() => T; + + @override + bool updateShouldNotify(StoreProvider oldWidget) => + _store != oldWidget._store; +} + +/// Build a Widget using the [BuildContext] and [ViewModel]. The [ViewModel] is +/// derived from the [Store] using a [StoreConverter]. +typedef ViewModelBuilder = Widget Function( + BuildContext context, + ViewModel vm, +); + +/// Convert the entire [Store] into a [ViewModel]. The [ViewModel] will be used +/// to build a Widget using the [ViewModelBuilder]. +typedef StoreConverter = ViewModel Function( + Store store, +); + +/// A function that will be run when the [StoreConnector] is initialized (using +/// the [State.initState] method). This can be useful for dispatching actions +/// that fetch data for your Widget when it is first displayed. +typedef OnInitCallback = void Function( + Store store, +); + +/// A function that will be run when the StoreConnector is removed from the +/// Widget Tree. +/// +/// It is run in the [State.dispose] method. +/// +/// This can be useful for dispatching actions that remove stale data from +/// your State tree. +typedef OnDisposeCallback = void Function( + Store store, +); + +/// A test of whether or not your `converter` function should run in response +/// to a State change. For advanced use only. +/// +/// Some changes to the State of your application will mean your `converter` +/// function can't produce a useful ViewModel. In these cases, such as when +/// performing exit animations on data that has been removed from your Store, +/// it can be best to ignore the State change while your animation completes. +/// +/// To ignore a change, provide a function that returns true or false. If the +/// returned value is true, the change will be ignored. +/// +/// If you ignore a change, and the framework needs to rebuild the Widget, the +/// `builder` function will be called with the latest `ViewModel` produced by +/// your `converter` function. +typedef IgnoreChangeTest = bool Function(S state); + +/// A function that will be run on State change, before the build method. +/// +/// This function is passed the `ViewModel`, and if `distinct` is `true`, +/// it will only be called if the `ViewModel` changes. +/// +/// This can be useful for imperative calls to things like Navigator, +/// TabController, etc +typedef OnWillChangeCallback = void Function(ViewModel viewModel); + +/// A function that will be run on State change, after the build method. +/// +/// This function is passed the `ViewModel`, and if `distinct` is `true`, +/// it will only be called if the `ViewModel` changes. +/// +/// This can be useful for running certain animations after the build is +/// complete. +/// +/// Note: Using a [BuildContext] inside this callback can cause problems if +/// the callback performs navigation. For navigation purposes, please use +/// an [OnWillChangeCallback]. +typedef OnDidChangeCallback = void Function(ViewModel viewModel); + +/// A function that will be run after the Widget is built the first time. +/// +/// This function is passed the initial `ViewModel` created by the `converter` +/// function. +/// +/// This can be useful for starting certain animations, such as showing +/// Snackbars, after the Widget is built the first time. +typedef OnInitialBuildCallback = void Function(ViewModel viewModel); + +/// Build a widget based on the state of the [Store]. +/// +/// Before the [builder] is run, the [converter] will convert the store into a +/// more specific `ViewModel` tailored to the Widget being built. +/// +/// Every time the store changes, the Widget will be rebuilt. As a performance +/// optimization, the Widget can be rebuilt only when the [ViewModel] changes. +/// In order for this to work correctly, you must implement [==] and [hashCode] +/// for the [ViewModel], and set the [distinct] option to true when creating +/// your StoreConnector. +class StoreConnector extends StatelessWidget { + /// Build a Widget using the [BuildContext] and [ViewModel]. The [ViewModel] + /// is created by the [converter] function. + final ViewModelBuilder builder; + + /// Convert the [Store] into a [ViewModel]. The resulting [ViewModel] will be + /// passed to the [builder] function. + final StoreConverter converter; + + /// As a performance optimization, the Widget can be rebuilt only when the + /// [ViewModel] changes. In order for this to work correctly, you must + /// implement [==] and [hashCode] for the [ViewModel], and set the [distinct] + /// option to true when creating your StoreConnector. + final bool distinct; + + /// A function that will be run when the StoreConnector is initially created. + /// It is run in the [State.initState] method. + /// + /// This can be useful for dispatching actions that fetch data for your Widget + /// when it is first displayed. + final OnInitCallback onInit; + + /// A function that will be run when the StoreConnector is removed from the + /// Widget Tree. + /// + /// It is run in the [State.dispose] method. + /// + /// This can be useful for dispatching actions that remove stale data from + /// your State tree. + final OnDisposeCallback onDispose; + + /// Determines whether the Widget should be rebuilt when the Store emits an + /// onChange event. + final bool rebuildOnChange; + + /// A test of whether or not your [converter] function should run in response + /// to a State change. For advanced use only. + /// + /// Some changes to the State of your application will mean your [converter] + /// function can't produce a useful ViewModel. In these cases, such as when + /// performing exit animations on data that has been removed from your Store, + /// it can be best to ignore the State change while your animation completes. + /// + /// To ignore a change, provide a function that returns true or false. If the + /// returned value is true, the change will be ignored. + /// + /// If you ignore a change, and the framework needs to rebuild the Widget, the + /// [builder] function will be called with the latest [ViewModel] produced by + /// your [converter] function. + final IgnoreChangeTest ignoreChange; + + /// A function that will be run on State change, before the Widget is built. + /// + /// This function is passed the `ViewModel`, and if `distinct` is `true`, + /// it will only be called if the `ViewModel` changes. + /// + /// This can be useful for imperative calls to things like Navigator, + /// TabController, etc + final OnWillChangeCallback onWillChange; + + /// A function that will be run on State change, after the Widget is built. + /// + /// This function is passed the `ViewModel`, and if `distinct` is `true`, + /// it will only be called if the `ViewModel` changes. + /// + /// This can be useful for running certain animations after the build is + /// complete. + /// + /// Note: Using a [BuildContext] inside this callback can cause problems if + /// the callback performs navigation. For navigation purposes, please use + /// [onWillChange]. + final OnDidChangeCallback onDidChange; + + /// A function that will be run after the Widget is built the first time. + /// + /// This function is passed the initial `ViewModel` created by the [converter] + /// function. + /// + /// This can be useful for starting certain animations, such as showing + /// Snackbars, after the Widget is built the first time. + final OnInitialBuildCallback onInitialBuild; + + /// Create a [StoreConnector] by passing in the required [converter] and + /// [builder] functions. + /// + /// You can also specify a number of additional parameters that allow you to + /// modify the behavior of the StoreConnector. Please see the documentation + /// for each option for more info. + StoreConnector({ + Key key, + @required this.builder, + @required this.converter, + this.distinct = false, + this.onInit, + this.onDispose, + this.rebuildOnChange = true, + this.ignoreChange, + this.onWillChange, + this.onDidChange, + this.onInitialBuild, + }) : assert(builder != null), + assert(converter != null), + super(key: key); + + @override + Widget build(BuildContext context) { + return _StoreStreamListener( + store: StoreProvider.of(context), + builder: builder, + converter: converter, + distinct: distinct, + onInit: onInit, + onDispose: onDispose, + rebuildOnChange: rebuildOnChange, + ignoreChange: ignoreChange, + onWillChange: onWillChange, + onDidChange: onDidChange, + onInitialBuild: onInitialBuild, + ); + } +} + +/// Build a Widget by passing the [Store] directly to the build function. +/// +/// Generally, it's considered best practice to use the [StoreConnector] and to +/// build a `ViewModel` specifically for your Widget rather than passing through +/// the entire [Store], but this is provided for convenience when that isn't +/// necessary. +class StoreBuilder extends StatelessWidget { + static Store _identity(Store store) => store; + + /// Builds a Widget using the [BuildContext] and your [Store]. + final ViewModelBuilder> builder; + + /// Indicates whether or not the Widget should rebuild when the [Store] emits + /// an `onChange` event. + final bool rebuildOnChange; + + /// A function that will be run when the StoreConnector is initially created. + /// It is run in the [State.initState] method. + /// + /// This can be useful for dispatching actions that fetch data for your Widget + /// when it is first displayed. + final OnInitCallback onInit; + + /// A function that will be run when the StoreBuilder is removed from the + /// Widget Tree. + /// + /// It is run in the [State.dispose] method. + /// + /// This can be useful for dispatching actions that remove stale data from + /// your State tree. + final OnDisposeCallback onDispose; + + /// A function that will be run on State change, before the Widget is built. + /// + /// This can be useful for imperative calls to things like Navigator, + /// TabController, etc + final OnWillChangeCallback> onWillChange; + + /// A function that will be run on State change, after the Widget is built. + /// + /// This can be useful for running certain animations after the build is + /// complete + /// + /// Note: Using a [BuildContext] inside this callback can cause problems if + /// the callback performs navigation. For navigation purposes, please use + /// [onWillChange]. + final OnDidChangeCallback> onDidChange; + + /// A function that will be run after the Widget is built the first time. + /// + /// This can be useful for starting certain animations, such as showing + /// Snackbars, after the Widget is built the first time. + final OnInitialBuildCallback> onInitialBuild; + + /// Create's a Widget based on the Store. + StoreBuilder({ + Key key, + @required this.builder, + this.onInit, + this.onDispose, + this.rebuildOnChange = true, + this.onWillChange, + this.onDidChange, + this.onInitialBuild, + }) : assert(builder != null), + super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector>( + builder: builder, + converter: _identity, + rebuildOnChange: rebuildOnChange, + onInit: onInit, + onDispose: onDispose, + onWillChange: onWillChange, + onDidChange: onDidChange, + onInitialBuild: onInitialBuild, + ); + } +} + +/// Listens to the [Store] and calls [builder] whenever [store] changes. +class _StoreStreamListener extends StatefulWidget { + final ViewModelBuilder builder; + final StoreConverter converter; + final Store store; + final bool rebuildOnChange; + final bool distinct; + final OnInitCallback onInit; + final OnDisposeCallback onDispose; + final IgnoreChangeTest ignoreChange; + final OnWillChangeCallback onWillChange; + final OnDidChangeCallback onDidChange; + final OnInitialBuildCallback onInitialBuild; + + _StoreStreamListener({ + Key key, + @required this.builder, + @required this.store, + @required this.converter, + this.distinct = false, + this.onInit, + this.onDispose, + this.rebuildOnChange = true, + this.ignoreChange, + this.onWillChange, + this.onDidChange, + this.onInitialBuild, + }) : super(key: key); + + @override + State createState() { + return _StoreStreamListenerState(); + } +} + +class _StoreStreamListenerState + extends State<_StoreStreamListener> { + Stream stream; + ViewModel latestValue; + + @override + void initState() { + _init(); + + super.initState(); + } + + @override + void dispose() { + if (widget.onDispose != null) { + widget.onDispose(widget.store); + } + + super.dispose(); + } + + @override + void didUpdateWidget(_StoreStreamListener oldWidget) { + if (widget.store != oldWidget.store) { + _init(); + } + + super.didUpdateWidget(oldWidget); + } + + void _init() { + if (widget.onInit != null) { + widget.onInit(widget.store); + } + + latestValue = widget.converter(widget.store); + + if (widget.onInitialBuild != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onInitialBuild(latestValue); + }); + } + + var _stream = widget.store.onChange; + + if (widget.ignoreChange != null) { + _stream = _stream.where((state) => !widget.ignoreChange(state)); + } + + stream = _stream.map((_) => widget.converter(widget.store)); + + // Don't use `Stream.distinct` because it cannot capture the initial + // ViewModel produced by the `converter`. + if (widget.distinct) { + stream = stream.where((vm) { + final isDistinct = vm != latestValue; + + return isDistinct; + }); + } + + // After each ViewModel is emitted from the Stream, we update the + // latestValue. Important: This must be done after all other optional + // transformations, such as ignoreChange. + stream = + stream.transform(StreamTransformer.fromHandlers(handleData: (vm, sink) { + latestValue = vm; + + if (widget.onWillChange != null) { + widget.onWillChange(latestValue); + } + + if (widget.onDidChange != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onDidChange(latestValue); + }); + } + + sink.add(vm); + })); + } + + @override + Widget build(BuildContext context) { + return widget.rebuildOnChange + ? StreamBuilder( + stream: stream, + builder: (context, snapshot) => widget.builder( + context, + snapshot.hasData ? snapshot.data : latestValue, + ), + ) + : widget.builder(context, latestValue); + } +} + +/// If the StoreProvider.of method fails, this error will be thrown. +/// +/// Often, when the `of` method fails, it is difficult to understand why since +/// there can be multiple causes. This error explains those causes so the user +/// can understand and fix the issue. +class StoreProviderError extends Error { + /// The type of the class the user tried to retrieve + Type type; + + /// Creates a StoreProviderError + StoreProviderError(this.type); + + @override + String toString() { + return '''Error: No $type found. To fix, please try: + + * Wrapping your MaterialApp with the StoreProvider, + rather than an individual Route + * Providing full type information to your Store, + StoreProvider and StoreConnector + * Ensure you are using consistent and complete imports. + E.g. always use `import 'package:my_app/app_state.dart'; + +If none of these solutions work, please file a bug at: +https://github.com/brianegan/flutter_redux/issues/new + '''; + } +} diff --git a/web/vision_challenge/lib/packages/redux.dart b/web/vision_challenge/lib/packages/redux.dart new file mode 100644 index 000000000..75ffc8121 --- /dev/null +++ b/web/vision_challenge/lib/packages/redux.dart @@ -0,0 +1,519 @@ +// Package redux: +// https://pub.dev/packages/redux + +import 'dart:async'; + +/// Defines an application's state change +/// +/// Implement this typedef to modify your app state in response to a given +/// action. +/// +/// ### Example +/// +/// int counterReducer(int state, action) { +/// switch (action) { +/// case 'INCREMENT': +/// return state + 1; +/// case 'DECREMENT': +/// return state - 1; +/// default: +/// return state; +/// } +/// } +/// +/// final store = new Store(counterReducer); +typedef Reducer = State Function(State state, dynamic action); + +/// Defines a [Reducer] using a class interface. +/// +/// Implement this class to modify your app state in response to a given action. +/// +/// For some use cases, a class may be preferred to a function. In these +/// instances, a ReducerClass can be used. +/// +/// ### Example +/// +/// class CounterReducer extends ReducerClass { +/// int call(int state, action) { +/// switch (action) { +/// case 'INCREMENT': +/// return state + 1; +/// case 'DECREMENT': +/// return state - 1; +/// default: +/// return state; +/// } +/// } +/// } +/// +/// final store = new Store(new CounterReducer()); +abstract class ReducerClass { + State call(State state, dynamic action); +} + +/// A function that intercepts actions and potentially transform actions before +/// they reach the reducer. +/// +/// Middleware intercept actions before they reach the reducer. This gives them +/// the ability to produce side-effects or modify the passed in action before +/// they reach the reducer. +/// +/// ### Example +/// +/// loggingMiddleware(Store store, action, NextDispatcher next) { +/// print('${new DateTime.now()}: $action'); +/// +/// next(action); +/// } +/// +/// // Create your store with the loggingMiddleware +/// final store = new Store( +/// counterReducer, +/// middleware: [loggingMiddleware], +/// ); +typedef Middleware = void Function( + Store store, + dynamic action, + NextDispatcher next, +); + +/// Defines a [Middleware] using a Class interface. +/// +/// Middleware intercept actions before they reach the reducer. This gives them +/// the ability to produce side-effects or modify the passed in action before +/// they reach the reducer. +/// +/// For some use cases, a class may be preferred to a function. In these +/// instances, a MiddlewareClass can be used. +/// +/// ### Example +/// class LoggingMiddleware extends MiddlewareClass { +/// call(Store store, action, NextDispatcher next) { +/// print('${new DateTime.now()}: $action'); +/// +/// next(action); +/// } +/// } +/// +/// // Create your store with the loggingMiddleware +/// final store = new Store( +/// counterReducer, +/// middleware: [new LoggingMiddleware()], +/// ); +abstract class MiddlewareClass { + void call(Store store, dynamic action, NextDispatcher next); +} + +/// The contract between one piece of middleware and the next in the chain. Use +/// it to send the current action in your [Middleware] to the next piece of +/// [Middleware] in the chain. +/// +/// Middleware can optionally pass the original action or a modified action to +/// the next piece of middleware, or never call the next piece of middleware at +/// all. +typedef NextDispatcher = void Function(dynamic action); + +/// Creates a Redux store that holds the app state tree. +/// +/// The only way to change the state tree in the store is to [dispatch] an +/// action. the action will then be intercepted by any provided [Middleware]. +/// After running through the middleware, the action will be sent to the given +/// [Reducer] to update the state tree. +/// +/// To access the state tree, call the [state] getter or listen to the +/// [onChange] stream. +/// +/// ### Basic Example +/// +/// // Create a reducer +/// final increment = 'INCREMENT'; +/// final decrement = 'DECREMENT'; +/// +/// int counterReducer(int state, action) { +/// switch (action) { +/// case increment: +/// return state + 1; +/// case decrement: +/// return state - 1; +/// default: +/// return state; +/// } +/// } +/// +/// // Create the store +/// final store = new Store(counterReducer, initialState: 0); +/// +/// // Print the Store's state. +/// print(store.state); // prints "0" +/// +/// // Dispatch an action. This will be sent to the reducer to update the +/// // state. +/// store.dispatch(increment); +/// +/// // Print the updated state. As an alternative, you can use the +/// // `store.onChange.listen` to respond to all state change events. +/// print(store.state); // prints "1" +class Store { + /// The [Reducer] for your Store. Allows you to get the current reducer or + /// replace it with a new one if need be. + Reducer reducer; + + final StreamController _changeController; + State _state; + List _dispatchers; + + Store( + this.reducer, { + State initialState, + List> middleware = const [], + bool syncStream = false, + + /// If set to true, the Store will not emit onChange events if the new State + /// that is returned from your [reducer] in response to an Action is equal + /// to the previous state. + /// + /// Under the hood, it will use the `==` method from your State class to + /// determine whether or not the two States are equal. + bool distinct = false, + }) : _changeController = StreamController.broadcast(sync: syncStream) { + _state = initialState; + _dispatchers = _createDispatchers( + middleware, + _createReduceAndNotify(distinct), + ); + } + + /// Returns the current state of the app + State get state => _state; + + /// A stream that emits the current state when it changes. + /// + /// ### Example + /// + /// // First, create the Store + /// final store = new Store(counterReducer, 0); + /// + /// // Next, listen to the Store's onChange stream, and print the latest + /// // state to your console whenever the reducer produces a new State. + /// // + /// // We'll store the StreamSubscription as a variable so we can stop + /// // listening later. + /// final subscription = store.onChange.listen(print); + /// + /// // Dispatch some actions, and see the printing magic! + /// store.dispatch("INCREMENT"); // prints 1 + /// store.dispatch("INCREMENT"); // prints 2 + /// store.dispatch("DECREMENT"); // prints 1 + /// + /// // When you want to stop printing the state to the console, simply + /// `cancel` your `subscription`. + /// subscription.cancel(); + Stream get onChange => _changeController.stream; + + // Creates the base [NextDispatcher]. + // + // The base NextDispatcher will be called after all other middleware provided + // by the user have been run. Its job is simple: Run the current state through + // the reducer, save the result, and notify any subscribers. + NextDispatcher _createReduceAndNotify(bool distinct) { + return (dynamic action) { + final state = reducer(_state, action); + + if (distinct && state == _state) return; + + _state = state; + _changeController.add(state); + }; + } + + List _createDispatchers( + List> middleware, + NextDispatcher reduceAndNotify, + ) { + final dispatchers = []..add(reduceAndNotify); + + // Convert each [Middleware] into a [NextDispatcher] + for (var nextMiddleware in middleware.reversed) { + final next = dispatchers.last; + + dispatchers.add( + (dynamic action) => nextMiddleware(this, action, next), + ); + } + + return dispatchers.reversed.toList(); + } + + /// Runs the action through all provided [Middleware], then applies an action + /// to the state using the given [Reducer]. Please note: [Middleware] can + /// intercept actions, and can modify actions or stop them from passing + /// through to the reducer. + void dispatch(dynamic action) { + _dispatchers[0](action); + } + + /// Closes down the Store so it will no longer be operational. Only use this + /// if you want to destroy the Store while your app is running. Do not use + /// this method as a way to stop listening to [onChange] state changes. For + /// that purpose, view the [onChange] documentation. + Future teardown() async { + _state = null; + return _changeController.close(); + } +} + +/// A convenience class for binding Reducers to Actions of a given Type. This +/// allows for type safe [Reducer]s and reduces boilerplate. +/// +/// ### Example +/// +/// In order to see what this utility function does, let's take a look at a +/// regular example of using reducers based on the Type of an action. +/// +/// ``` +/// // We define out State and Action classes. +/// class AppState { +/// final List items; +/// +/// AppState(this.items); +/// } +/// +/// class LoadItemsAction {} +/// class UpdateItemsAction {} +/// class AddItemAction{} +/// class RemoveItemAction {} +/// class ShuffleItemsAction {} +/// class ReverseItemsAction {} +/// class ItemsLoadedAction { +/// final List items; +/// +/// ItemsLoadedAction(this.items); +/// } +/// +/// // Then we define our reducer. Since we handle different actions in our +/// // reducer, we need to determine what kind of action we're working with +/// // using if statements, and then run some computation in response. +/// // +/// // This isn't a big deal if we have relatively few cases to handle, but your +/// // reducer function can quickly grow large and take on too many +/// // responsibilities as demonstrated here with pseudo-code. +/// final appReducer = (AppState state, action) { +/// if (action is ItemsLoadedAction) { +/// return new AppState(action.items); +/// } else if (action is UpdateItemsAction) { +/// return ...; +/// } else if (action is AddItemAction) { +/// return ...; +/// } else if (action is RemoveItemAction) { +/// return ...; +/// } else if (action is ShuffleItemsAction) { +/// return ...; +/// } else if (action is ReverseItemsAction) { +/// return ...; +/// } else { +/// return state; +/// } +/// }; +/// ``` +/// +/// What would be nice would be to break our big reducer up into smaller +/// reducers. It would also be nice to bind specific Types of Actions to +/// specific reducers so we can ensure type safety for our reducers while +/// avoiding large trees of `if` statements. +/// +/// ``` +/// // First, we'll break out all of our individual State Changes into +/// // individual reducers. These can be easily tested or composed! +/// final loadItemsReducer = (AppState state, LoadTodosAction action) => +/// return new AppState(action.items); +/// +/// final updateItemsReducer = (AppState state, UpdateItemsAction action) { +/// return ...; +/// } +/// +/// final addItemReducer = (AppState state, AddItemAction action) { +/// return ...; +/// } +/// +/// final removeItemReducer = (AppState state, RemoveItemAction action) { +/// return ...; +/// } +/// +/// final shuffleItemsReducer = (AppState state, ShuffleItemAction action) { +/// return ...; +/// } +/// +/// final reverseItemsReducer = (AppState state, ReverseItemAction action) { +/// return ...; +/// } +/// +/// // We will then wire up specific types of actions to our reducer functions +/// // above. This will return a new Reducer which puts everything +/// // together!. +/// final Reducer appReducer = combineReducers([ +/// new TypedReducer(loadItemsReducer), +/// new TypedReducer(updateItemsReducer), +/// new TypedReducer(addItemReducer), +/// new TypedReducer(removeItemReducer), +/// new TypedReducer(shuffleItemsReducer), +/// new TypedReducer(reverseItemsReducer), +/// ]); +/// ``` +class TypedReducer implements ReducerClass { + final State Function(State state, Action action) reducer; + + TypedReducer(this.reducer); + + @override + State call(State state, dynamic action) { + if (action is Action) { + return reducer(state, action); + } + + return state; + } +} + +/// A convenience type for binding a piece of Middleware to an Action +/// of a specific type. Allows for Type Safe Middleware and reduces boilerplate. +/// +/// ### Example +/// +/// In order to see what this utility function does, let's take a look at a +/// regular example of running Middleware based on the Type of an action. +/// +/// ``` +/// class AppState { +/// final List items; +/// +/// AppState(this.items); +/// } +/// class LoadItemsAction {} +/// class UpdateItemsAction {} +/// class AddItemAction{} +/// class RemoveItemAction {} +/// class ShuffleItemsAction {} +/// class ReverseItemsAction {} +/// class ItemsLoadedAction { +/// final List items; +/// +/// ItemsLoadedAction(this.items); +/// } +/// +/// final loadItems = () { /* Function that loads a Future> */} +/// final saveItems = (List items) { /* Function that persists items */} +/// +/// final middleware = (Store store, action, NextDispatcher next) { +/// if (action is LoadItemsAction) { +/// loadItems() +/// .then((items) => store.dispatch(new ItemsLoaded(items)) +/// .catchError((_) => store.dispatch(new ItemsNotLoaded()); +/// +/// next(action); +/// } else if (action is UpdateItemsAction || +/// action is AddItemAction || +/// action is RemoveItemAction || +/// action is ShuffleItemsAction || +/// action is ReverseItemsAction) { +/// next(action); +/// +/// saveItems(store.state.items); +/// } else { +/// next(action); +/// } +/// }; +/// ``` +/// +/// This works fine if you have one or two actions to handle, but you might +/// notice it's getting a bit messy already. Let's see how this lib helps clean +/// it up. +/// +/// ``` +/// // First, let's start by breaking up our functionality into two middleware +/// // functions. +/// // +/// // The loadItemsMiddleware will only handle the `LoadItemsAction`s that +/// // are dispatched, so we can annotate the Type of action. +/// final loadItemsMiddleware = ( +/// Store store, +/// LoadItemsAction action, +/// NextDispatcher next, +/// ) { +/// loadItems() +/// .then((items) => store.dispatch(new ItemsLoaded(items)) +/// .catchError((_) => store.dispatch(new ItemsNotLoaded()); +/// +/// next(action); +/// } +/// +/// // The saveItemsMiddleware handles all actions that change the Items, but +/// // does not depend on the payload of the action. Therefore, `action` will +/// // remain dynamic. +/// final saveItemsMiddleware = ( +/// Store store, +/// dynamic action, +/// NextDispatcher next, +/// ) { +/// next(action); +/// +/// saveItems(store.state.items); +/// } +/// +/// // We will then wire up specific types of actions to a List of Middleware +/// // that handle those actions. +/// final List> middleware = [ +/// new TypedMiddleware(loadItemsMiddleware), +/// new TypedMiddleware(saveItemsMiddleware), +/// new TypedMiddleware(saveItemsMiddleware), +/// new TypedMiddleware(saveItemsMiddleware), +/// new TypedMiddleware(saveItemsMiddleware), +/// new TypedMiddleware(saveItemsMiddleware), +/// ]; +/// ``` +class TypedMiddleware implements MiddlewareClass { + final void Function( + Store store, + Action action, + NextDispatcher next, + ) middleware; + + TypedMiddleware(this.middleware); + + @override + void call(Store store, dynamic action, NextDispatcher next) { + if (action is Action) { + middleware(store, action, next); + } else { + next(action); + } + } +} + +/// Defines a utility function that combines several reducers. +/// +/// In order to prevent having one large, monolithic reducer in your app, it can +/// be convenient to break reducers up into smaller parts that handle more +/// specific functionality that can be decoupled and easily tested. +/// +/// ### Example +/// +/// helloReducer(state, action) { +/// return "hello"; +/// } +/// +/// friendReducer(state, action) { +/// return state + " friend"; +/// } +/// +/// final helloFriendReducer = combineReducers( +/// helloReducer, +/// friendReducer, +/// ); +Reducer combineReducers(Iterable> reducers) { + return (State state, dynamic action) { + for (final reducer in reducers) { + state = reducer(state, action); + } + return state; + }; +} diff --git a/web/vision_challenge/pubspec.lock b/web/vision_challenge/pubspec.lock new file mode 100644 index 000000000..cdf5d8c14 --- /dev/null +++ b/web/vision_challenge/pubspec.lock @@ -0,0 +1,471 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.20" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + build_modules: + dependency: transitive + description: + name: build_modules + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + flutter_web: + dependency: "direct main" + description: + path: "packages/flutter_web" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + flutter_web_ui: + dependency: "direct overridden" + description: + path: "packages/flutter_web_ui" + ref: HEAD + resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4" + url: "https://github.com/flutter/flutter_web" + source: git + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.18" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.8" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.18" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.11" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + scratch_space: + dependency: transitive + description: + name: scratch_space + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3+2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.8" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.19" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.12" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.3.0-dev.0.1 <3.0.0" diff --git a/web/vision_challenge/pubspec.yaml b/web/vision_challenge/pubspec.yaml new file mode 100644 index 000000000..28770c8a4 --- /dev/null +++ b/web/vision_challenge/pubspec.yaml @@ -0,0 +1,24 @@ +name: vision_challenge +author: Yukkei Choi + +environment: + sdk: ">=2.2.0 <3.0.0" + +dependencies: + flutter_web: any + +dev_dependencies: + build_runner: any + build_web_compilers: any + +# flutter_web packages are not published to pub.dartlang.org +# These overrides tell the package tools to get them from GitHub +dependency_overrides: + flutter_web: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web + path: packages/flutter_web_ui diff --git a/web/vision_challenge/web/assets/10.png b/web/vision_challenge/web/assets/10.png new file mode 100644 index 000000000..35513ce15 Binary files /dev/null and b/web/vision_challenge/web/assets/10.png differ diff --git a/web/vision_challenge/web/assets/20.png b/web/vision_challenge/web/assets/20.png new file mode 100644 index 000000000..d0aeebaf2 Binary files /dev/null and b/web/vision_challenge/web/assets/20.png differ diff --git a/web/vision_challenge/web/assets/30.png b/web/vision_challenge/web/assets/30.png new file mode 100644 index 000000000..4f6d6b5b7 Binary files /dev/null and b/web/vision_challenge/web/assets/30.png differ diff --git a/web/vision_challenge/web/assets/35.png b/web/vision_challenge/web/assets/35.png new file mode 100644 index 000000000..4cf8bfa29 Binary files /dev/null and b/web/vision_challenge/web/assets/35.png differ diff --git a/web/vision_challenge/web/assets/40.png b/web/vision_challenge/web/assets/40.png new file mode 100644 index 000000000..ac35e3ab8 Binary files /dev/null and b/web/vision_challenge/web/assets/40.png differ diff --git a/web/vision_challenge/web/assets/45.png b/web/vision_challenge/web/assets/45.png new file mode 100644 index 000000000..b0be94893 Binary files /dev/null and b/web/vision_challenge/web/assets/45.png differ diff --git a/web/vision_challenge/web/assets/99.png b/web/vision_challenge/web/assets/99.png new file mode 100644 index 000000000..0e27ee554 Binary files /dev/null and b/web/vision_challenge/web/assets/99.png differ diff --git a/web/vision_challenge/web/assets/FontManifest.json b/web/vision_challenge/web/assets/FontManifest.json new file mode 100644 index 000000000..5921ca028 --- /dev/null +++ b/web/vision_challenge/web/assets/FontManifest.json @@ -0,0 +1,10 @@ +[ + { + "family": "MaterialIcons", + "fonts": [ + { + "asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" + } + ] + } +] \ No newline at end of file diff --git a/web/vision_challenge/web/assets/p0.jpg b/web/vision_challenge/web/assets/p0.jpg new file mode 100644 index 000000000..b4dabad62 Binary files /dev/null and b/web/vision_challenge/web/assets/p0.jpg differ diff --git a/web/vision_challenge/web/assets/p1.jpg b/web/vision_challenge/web/assets/p1.jpg new file mode 100644 index 000000000..c688ec887 Binary files /dev/null and b/web/vision_challenge/web/assets/p1.jpg differ diff --git a/web/vision_challenge/web/index.html b/web/vision_challenge/web/index.html new file mode 100644 index 000000000..b54ed98d8 --- /dev/null +++ b/web/vision_challenge/web/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/vision_challenge/web/main.dart b/web/vision_challenge/web/main.dart new file mode 100644 index 000000000..cd4683039 --- /dev/null +++ b/web/vision_challenge/web/main.dart @@ -0,0 +1,10 @@ +// 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 'package:flutter_web_ui/ui.dart' as ui; +import 'package:vision_challenge/main.dart' as app; + +main() async { + await ui.webOnlyInitializePlatform(); + app.main(); +} diff --git a/web/vision_challenge/web/preview.png b/web/vision_challenge/web/preview.png new file mode 100644 index 000000000..7b2b98ed1 Binary files /dev/null and b/web/vision_challenge/web/preview.png differ