diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4ad8801 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# 0.12.0 +* Add functions to draw horizontal or vertical links onto the dart charts web canvas. +* Add "preserveSelection" functionality to InitialSelection chart Behavior. +* Bug fixes. + +# 0.11.0 +* Null support +* Update to latest from internal repo + +# 0.10.0 +* Internal bug fixes +* Bump versions of intl due to pull request + +# 0.9.0 +* Internal bug fixes +* Bump versions in Gemlock file due to security alerts + +# 0.8.1 +* Update intl version. + +# 0.8.0 +* Bug fixes from open source. + +# 0.7.0 +* Added vertical bar label + +# 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/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/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/charts_flutter.gwsq b/charts_flutter.gwsq new file mode 100644 index 0000000..56a60ed --- /dev/null +++ b/charts_flutter.gwsq @@ -0,0 +1,6 @@ +send_cls_to('dart-charts-team+reviews'); +send_cls_to('dart-charts-team'); + +define Main { + reassign_to_list(from_owners_file('third_party/dart/charts_common/OWNERS')); +} diff --git a/example/android.iml b/example/android.iml new file mode 100644 index 0000000..462b903 --- /dev/null +++ b/example/android.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/example/android/Android_Charts.xml b/example/android/Android_Charts.xml new file mode 100644 index 0000000..fd24220 --- /dev/null +++ b/example/android/Android_Charts.xml @@ -0,0 +1,7 @@ + + diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..ca84ef1 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,51 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.examples" + minSdkVersion 16 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9144147 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/java/com/example/examples/MainActivity.java b/example/android/app/src/main/java/com/example/examples/MainActivity.java new file mode 100644 index 0000000..89a83b2 --- /dev/null +++ b/example/android/app/src/main/java/com/example/examples/MainActivity.java @@ -0,0 +1,17 @@ +package com.example.examples; + +import android.os.Bundle; + +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +/** + * FlutterActivity + */ +public class MainActivity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..6de3728 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.3' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..38c8d45 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.jar b/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..13372ae Binary files /dev/null and b/example/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6bb8b52 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Feb 19 15:38:12 AWST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/example/android/gradlew b/example/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/example/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/example/android/gradlew.bat b/example/android/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/example/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/example/examples.iml b/example/examples.iml new file mode 100644 index 0000000..4881df8 --- /dev/null +++ b/example/examples.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/example/examples_android.iml b/example/examples_android.iml new file mode 100644 index 0000000..0ca70ed --- /dev/null +++ b/example/examples_android.iml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..6c2de80 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 8.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Ios_Charts.xml b/example/ios/Ios_Charts.xml new file mode 100644 index 0000000..29fdd55 --- /dev/null +++ b/example/ios/Ios_Charts.xml @@ -0,0 +1,7 @@ + + diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0c51fe5 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,436 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.examples; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.examples; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..89550f6 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h new file mode 100644 index 0000000..3fbf7a5 --- /dev/null +++ b/example/ios/Runner/AppDelegate.h @@ -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 + +@interface AppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m new file mode 100644 index 0000000..b87ca49 --- /dev/null +++ b/example/ios/Runner/AppDelegate.m @@ -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. + +#include "AppDelegate.h" +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for + // certain types of temporary interruptions (such as an incoming phone call or SMS message) or + // when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame + // rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store + // enough application state information to restore your application to its current state in case + // it is terminated later. + // If your application supports background execution, this method is called instead of + // applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo + // many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. + // If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also + // applicationDidEnterBackground:. +} + +@end diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..033a072 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,82 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-Small-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-Small-40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-83.5@2x.png", + "scale" : "2x" + } + ] +} \ No newline at end of file diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 0000000..c767e73 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png new file mode 100644 index 0000000..b41356d Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png new file mode 100644 index 0000000..de0a6df Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png new file mode 100644 index 0000000..7c39194 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png new file mode 100644 index 0000000..2f594d0 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png new file mode 100644 index 0000000..31693a3 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png new file mode 100644 index 0000000..fec316a Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png new file mode 100644 index 0000000..c767e73 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png new file mode 100644 index 0000000..7abec8c Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png new file mode 100644 index 0000000..426123b Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png new file mode 100644 index 0000000..77f04fa Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..ebf48f6 --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..677c2dc --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Charts Flutter Gallery + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m new file mode 100644 index 0000000..8759ed8 --- /dev/null +++ b/example/ios/Runner/main.m @@ -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 +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/example/lib/a11y/a11y_gallery.dart b/example/lib/a11y/a11y_gallery.dart new file mode 100644 index 0000000..c5d4b5f --- /dev/null +++ b/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/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/example/lib/a11y/domain_a11y_explore_bar_chart.dart b/example/lib/a11y/domain_a11y_explore_bar_chart.dart new file mode 100644 index 0000000..d02f2e8 --- /dev/null +++ b/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/material.dart'; + +class DomainA11yExploreBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DomainA11yExploreBarChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/app_config.dart b/example/lib/app_config.dart new file mode 100644 index 0000000..a0a91e9 --- /dev/null +++ b/example/lib/app_config.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 'package:flutter/material.dart'; + +/// A particular configuration of the app. +class AppConfig { + final String appName; + final String appLink; + final ThemeData theme; + final bool showPerformanceOverlay; + + AppConfig({ + required this.appName, + required this.appLink, + required this.theme, + required 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/example/lib/axes/axes_gallery.dart b/example/lib/axes/axes_gallery.dart new file mode 100644 index 0000000..f192e2c --- /dev/null +++ b/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/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/example/lib/axes/bar_secondary_axis.dart b/example/lib/axes/bar_secondary_axis.dart new file mode 100644 index 0000000..085acd2 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/axes/bar_secondary_axis_only.dart b/example/lib/axes/bar_secondary_axis_only.dart new file mode 100644 index 0000000..b205b1d --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/axes/custom_axis_tick_formatters.dart b/example/lib/axes/custom_axis_tick_formatters.dart new file mode 100644 index 0000000..285a17c --- /dev/null +++ b/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/material.dart'; +import 'package:intl/intl.dart'; + +class CustomAxisTickFormatters extends StatelessWidget { + final List> seriesList; + final bool animate; + + CustomAxisTickFormatters(this.seriesList, {this.animate = false}); + + /// 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/example/lib/axes/custom_font_size_and_color.dart b/example/lib/axes/custom_font_size_and_color.dart new file mode 100644 index 0000000..e63d192 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/axes/custom_measure_tick_count.dart b/example/lib/axes/custom_measure_tick_count.dart new file mode 100644 index 0000000..dfa7bdd --- /dev/null +++ b/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/material.dart'; + +class CustomMeasureTickCount extends StatelessWidget { + final List> seriesList; + final bool animate; + + CustomMeasureTickCount(this.seriesList, {this.animate = false}); + + /// 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/example/lib/axes/flipped_vertical_axis.dart b/example/lib/axes/flipped_vertical_axis.dart new file mode 100644 index 0000000..4c11d14 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/axes/gridline_dash_pattern.dart b/example/lib/axes/gridline_dash_pattern.dart new file mode 100644 index 0000000..1190cb5 --- /dev/null +++ b/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/material.dart'; + +class GridlineDashPattern extends StatelessWidget { + final List> seriesList; + final bool animate; + + GridlineDashPattern(this.seriesList, {this.animate = false}); + + /// 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/example/lib/axes/hidden_ticks_and_labels_axis.dart b/example/lib/axes/hidden_ticks_and_labels_axis.dart new file mode 100644 index 0000000..a268207 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/axes/horizontal_bar_secondary_axis.dart b/example/lib/axes/horizontal_bar_secondary_axis.dart new file mode 100644 index 0000000..a1c3be2 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/axes/integer_only_measure_axis.dart b/example/lib/axes/integer_only_measure_axis.dart new file mode 100644 index 0000000..37e3e24 --- /dev/null +++ b/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/material.dart'; + +class IntegerOnlyMeasureAxis extends StatelessWidget { + final List> seriesList; + final bool animate; + + IntegerOnlyMeasureAxis(this.seriesList, {this.animate = false}); + + /// 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/example/lib/axes/line_disjoint_axis.dart b/example/lib/axes/line_disjoint_axis.dart new file mode 100644 index 0000000..879f151 --- /dev/null +++ b/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/material.dart'; + +class DisjointMeasureAxisLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DisjointMeasureAxisLineChart(this.seriesList, {this.animate = false}); + + /// 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, this.clickRate}); +} diff --git a/example/lib/axes/measure_axis_label_alignment.dart b/example/lib/axes/measure_axis_label_alignment.dart new file mode 100644 index 0000000..fe3251e --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/axes/nonzero_bound_measure_axis.dart b/example/lib/axes/nonzero_bound_measure_axis.dart new file mode 100644 index 0000000..da136b7 --- /dev/null +++ b/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/material.dart'; + +class NonzeroBoundMeasureAxis extends StatelessWidget { + final List> seriesList; + final bool animate; + + NonzeroBoundMeasureAxis(this.seriesList, {this.animate = false}); + + /// 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/example/lib/axes/numeric_initial_viewport.dart b/example/lib/axes/numeric_initial_viewport.dart new file mode 100644 index 0000000..6a3826e --- /dev/null +++ b/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/material.dart'; + +class NumericInitialViewport extends StatelessWidget { + final List> seriesList; + final bool animate; + + NumericInitialViewport(this.seriesList, {this.animate = false}); + + /// 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/example/lib/axes/ordinal_initial_viewport.dart b/example/lib/axes/ordinal_initial_viewport.dart new file mode 100644 index 0000000..157c5fe --- /dev/null +++ b/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/material.dart'; + +class OrdinalInitialViewport extends StatelessWidget { + final List> seriesList; + final bool animate; + + OrdinalInitialViewport(this.seriesList, {this.animate = false}); + + /// 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/example/lib/axes/short_tick_length_axis.dart b/example/lib/axes/short_tick_length_axis.dart new file mode 100644 index 0000000..b720736 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/axes/statically_provided_ticks.dart b/example/lib/axes/statically_provided_ticks.dart new file mode 100644 index 0000000..5990b7b --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/bar_gallery.dart b/example/lib/bar_chart/bar_gallery.dart new file mode 100644 index 0000000..ca656c4 --- /dev/null +++ b/example/lib/bar_chart/bar_gallery.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. + +import 'package:flutter/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'; +import 'vertical_bar_label.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 Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Vertical Bar Chart with Bar Labels', + subtitle: 'Vertical bar chart with a single series and bar labels', + childBuilder: () => new VerticalBarLabelChart.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/example/lib/bar_chart/custom_rounded_bars.dart b/example/lib/bar_chart/custom_rounded_bars.dart new file mode 100644 index 0000000..c773501 --- /dev/null +++ b/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/material.dart'; + +class CustomRoundedBars extends StatelessWidget { + final List> seriesList; + final bool animate; + + CustomRoundedBars(this.seriesList, {this.animate = false}); + + /// 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/example/lib/bar_chart/grouped.dart b/example/lib/bar_chart/grouped.dart new file mode 100644 index 0000000..f61282e --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/grouped_fill_color.dart b/example/lib/bar_chart/grouped_fill_color.dart new file mode 100644 index 0000000..14a921b --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/grouped_single_target_line.dart b/example/lib/bar_chart/grouped_single_target_line.dart new file mode 100644 index 0000000..1d6012a --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/grouped_stacked.dart b/example/lib/bar_chart/grouped_stacked.dart new file mode 100644 index 0000000..32551c7 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/grouped_stacked_weight_pattern.dart b/example/lib/bar_chart/grouped_stacked_weight_pattern.dart new file mode 100644 index 0000000..ddccb56 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/grouped_target_line.dart b/example/lib/bar_chart/grouped_target_line.dart new file mode 100644 index 0000000..d4fb620 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/horizontal.dart b/example/lib/bar_chart/horizontal.dart new file mode 100644 index 0000000..0d6260a --- /dev/null +++ b/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/material.dart'; + +class HorizontalBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + HorizontalBarChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/bar_chart/horizontal_bar_label.dart b/example/lib/bar_chart/horizontal_bar_label.dart new file mode 100644 index 0000000..e2e9d16 --- /dev/null +++ b/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/material.dart'; + +class HorizontalBarLabelChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + HorizontalBarLabelChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/bar_chart/horizontal_bar_label_custom.dart b/example/lib/bar_chart/horizontal_bar_label_custom.dart new file mode 100644 index 0000000..54583d8 --- /dev/null +++ b/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/material.dart'; + +class HorizontalBarLabelCustomChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + HorizontalBarLabelCustomChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/bar_chart/horizontal_pattern_forward_hatch.dart b/example/lib/bar_chart/horizontal_pattern_forward_hatch.dart new file mode 100644 index 0000000..68a7dd8 --- /dev/null +++ b/example/lib/bar_chart/horizontal_pattern_forward_hatch.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. + +/// Forward pattern hatch bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/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 = false}); + + 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/example/lib/bar_chart/pattern_forward_hatch.dart b/example/lib/bar_chart/pattern_forward_hatch.dart new file mode 100644 index 0000000..a86c975 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/simple.dart b/example/lib/bar_chart/simple.dart new file mode 100644 index 0000000..2a99b2b --- /dev/null +++ b/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/material.dart'; + +class SimpleBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleBarChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/bar_chart/spark_bar.dart b/example/lib/bar_chart/spark_bar.dart new file mode 100644 index 0000000..a3e9fc6 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/stacked.dart b/example/lib/bar_chart/stacked.dart new file mode 100644 index 0000000..c598e9f --- /dev/null +++ b/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/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 = false}); + + /// 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/example/lib/bar_chart/stacked_fill_color.dart b/example/lib/bar_chart/stacked_fill_color.dart new file mode 100644 index 0000000..cd4e596 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/bar_chart/stacked_horizontal.dart b/example/lib/bar_chart/stacked_horizontal.dart new file mode 100644 index 0000000..0f20b9a --- /dev/null +++ b/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/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 = false}); + + /// 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/example/lib/bar_chart/stacked_target_line.dart b/example/lib/bar_chart/stacked_target_line.dart new file mode 100644 index 0000000..01dc9e8 --- /dev/null +++ b/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/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 = false}); + + /// 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/example/lib/bar_chart/vertical_bar_label.dart b/example/lib/bar_chart/vertical_bar_label.dart new file mode 100644 index 0000000..3c53ca8 --- /dev/null +++ b/example/lib/bar_chart/vertical_bar_label.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. + +/// Vertical bar chart with bar label renderer 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/material.dart'; + +class VerticalBarLabelChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + VerticalBarLabelChart(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory VerticalBarLabelChart.withSampleData() { + return new VerticalBarLabelChart( + _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 VerticalBarLabelChart.withRandomData() { + return new VerticalBarLabelChart(_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.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, + // 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, + // 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(), + domainAxis: new charts.OrdinalAxisSpec(), + ); + } + + /// 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.sales.toString()}') + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/behaviors_gallery.dart b/example/lib/behaviors/behaviors_gallery.dart new file mode 100644 index 0000000..36d25e9 --- /dev/null +++ b/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/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/example/lib/behaviors/chart_title.dart b/example/lib/behaviors/chart_title.dart new file mode 100644 index 0000000..ad652f5 --- /dev/null +++ b/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/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 = false}); + + /// 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/example/lib/behaviors/initial_hint_animation.dart b/example/lib/behaviors/initial_hint_animation.dart new file mode 100644 index 0000000..804a006 --- /dev/null +++ b/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/material.dart'; + +class InitialHintAnimation extends StatelessWidget { + final List> seriesList; + final bool animate; + + InitialHintAnimation(this.seriesList, {this.animate = false}); + + /// 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/example/lib/behaviors/initial_selection.dart b/example/lib/behaviors/initial_selection.dart new file mode 100644 index 0000000..8793bee --- /dev/null +++ b/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/material.dart'; + +class InitialSelection extends StatelessWidget { + final List> seriesList; + final bool animate; + + InitialSelection(this.seriesList, {this.animate = false}); + + /// 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/example/lib/behaviors/percent_of_domain.dart b/example/lib/behaviors/percent_of_domain.dart new file mode 100644 index 0000000..2cf836c --- /dev/null +++ b/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/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 = false}); + + /// 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/example/lib/behaviors/percent_of_domain_by_category.dart b/example/lib/behaviors/percent_of_domain_by_category.dart new file mode 100644 index 0000000..5380c46 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/behaviors/percent_of_series.dart b/example/lib/behaviors/percent_of_series.dart new file mode 100644 index 0000000..92c9461 --- /dev/null +++ b/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/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 = false}); + + /// 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/example/lib/behaviors/selection_bar_highlight.dart b/example/lib/behaviors/selection_bar_highlight.dart new file mode 100644 index 0000000..86594a5 --- /dev/null +++ b/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/material.dart'; + +class SelectionBarHighlight extends StatelessWidget { + final List> seriesList; + final bool animate; + + SelectionBarHighlight(this.seriesList, {this.animate = false}); + + /// 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/example/lib/behaviors/selection_callback_example.dart b/example/lib/behaviors/selection_callback_example.dart new file mode 100644 index 0000000..d46112b --- /dev/null +++ b/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/material.dart'; + +class SelectionCallbackExample extends StatefulWidget { + final List> seriesList; + final bool animate; + + SelectionCallbackExample(this.seriesList, {this.animate = false}); + + /// 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/example/lib/behaviors/selection_line_highlight.dart b/example/lib/behaviors/selection_line_highlight.dart new file mode 100644 index 0000000..89ae68b --- /dev/null +++ b/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/material.dart'; + +class SelectionLineHighlight extends StatelessWidget { + final List> seriesList; + final bool animate; + + SelectionLineHighlight(this.seriesList, {this.animate = false}); + + /// 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/example/lib/behaviors/selection_line_highlight_custom_shape.dart b/example/lib/behaviors/selection_line_highlight_custom_shape.dart new file mode 100644 index 0000000..dcf7ae4 --- /dev/null +++ b/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/material.dart'; + +class SelectionLineHighlightCustomShape extends StatelessWidget { + final List> seriesList; + final bool animate; + + SelectionLineHighlightCustomShape(this.seriesList, {this.animate = false}); + + /// 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/example/lib/behaviors/selection_scatter_plot_highlight.dart b/example/lib/behaviors/selection_scatter_plot_highlight.dart new file mode 100644 index 0000000..abf471e --- /dev/null +++ b/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/material.dart'; + +class SelectionScatterPlotHighlight extends StatelessWidget { + final List> seriesList; + final bool animate; + + SelectionScatterPlotHighlight(this.seriesList, {this.animate = false}); + + /// 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/example/lib/behaviors/selection_user_managed.dart b/example/lib/behaviors/selection_user_managed.dart new file mode 100644 index 0000000..f378915 --- /dev/null +++ b/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/material.dart'; + +class SelectionUserManaged extends StatefulWidget { + final List> seriesList; + final bool animate; + + SelectionUserManaged(this.seriesList, {this.animate = false}); + + /// 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(height: 150.0, child: chart), 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/example/lib/behaviors/slider.dart b/example/lib/behaviors/slider.dart new file mode 100644 index 0000000..df45c6b --- /dev/null +++ b/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/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/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 = false}); + + /// 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/example/lib/behaviors/sliding_viewport_on_selection.dart b/example/lib/behaviors/sliding_viewport_on_selection.dart new file mode 100644 index 0000000..679c73a --- /dev/null +++ b/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/material.dart'; + +class SlidingViewportOnSelection extends StatelessWidget { + final List> seriesList; + final bool animate; + + SlidingViewportOnSelection(this.seriesList, {this.animate = false}); + + /// 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/example/lib/combo_chart/combo_gallery.dart b/example/lib/combo_chart/combo_gallery.dart new file mode 100644 index 0000000..e566551 --- /dev/null +++ b/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/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/example/lib/combo_chart/date_time_line_point.dart b/example/lib/combo_chart/date_time_line_point.dart new file mode 100644 index 0000000..a279588 --- /dev/null +++ b/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/material.dart'; + +class DateTimeComboLinePointChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DateTimeComboLinePointChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/combo_chart/numeric_line_bar.dart b/example/lib/combo_chart/numeric_line_bar.dart new file mode 100644 index 0000000..4dcbf36 --- /dev/null +++ b/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/material.dart'; + +class NumericComboLineBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + NumericComboLineBarChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/combo_chart/numeric_line_point.dart b/example/lib/combo_chart/numeric_line_point.dart new file mode 100644 index 0000000..31ae957 --- /dev/null +++ b/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/material.dart'; + +class NumericComboLinePointChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + NumericComboLinePointChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/combo_chart/ordinal_bar_line.dart b/example/lib/combo_chart/ordinal_bar_line.dart new file mode 100644 index 0000000..d9390f0 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/combo_chart/scatter_plot_line.dart b/example/lib/combo_chart/scatter_plot_line.dart new file mode 100644 index 0000000..54ff376 --- /dev/null +++ b/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/material.dart'; + +class ScatterPlotComboLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + ScatterPlotComboLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/drawer.dart b/example/lib/drawer.dart new file mode 100644 index 0000000..9d85a8f --- /dev/null +++ b/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/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 = false, + required 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/example/lib/gallery_scaffold.dart b/example/lib/gallery_scaffold.dart new file mode 100644 index 0000000..2a13c55 --- /dev/null +++ b/example/lib/gallery_scaffold.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 'package:flutter/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({ + required this.listTileIcon, + required this.title, + required this.subtitle, + required 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( + onPressed: _handleButtonPress, child: new Icon(Icons.refresh)), + ); + } +} diff --git a/example/lib/home.dart b/example/lib/home.dart new file mode 100644 index 0000000..3d96b96 --- /dev/null +++ b/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/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 = false, + required 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/example/lib/i18n/i18n_gallery.dart b/example/lib/i18n/i18n_gallery.dart new file mode 100644 index 0000000..cab9066 --- /dev/null +++ b/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/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/example/lib/i18n/rtl_bar_chart.dart b/example/lib/i18n/rtl_bar_chart.dart new file mode 100644 index 0000000..50637d8 --- /dev/null +++ b/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/material.dart'; + +class RTLBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + RTLBarChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/i18n/rtl_line_chart.dart b/example/lib/i18n/rtl_line_chart.dart new file mode 100644 index 0000000..7bc4e9e --- /dev/null +++ b/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/material.dart'; + +class RTLLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + RTLLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/i18n/rtl_line_segments.dart b/example/lib/i18n/rtl_line_segments.dart new file mode 100644 index 0000000..2e2af9e --- /dev/null +++ b/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/material.dart'; + +class RTLLineSegments extends StatelessWidget { + final List> seriesList; + final bool animate; + + RTLLineSegments(this.seriesList, {this.animate = false}); + + /// 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/example/lib/i18n/rtl_series_legend.dart b/example/lib/i18n/rtl_series_legend.dart new file mode 100644 index 0000000..a4006b9 --- /dev/null +++ b/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/material.dart'; + +class RTLSeriesLegend extends StatelessWidget { + final List> seriesList; + final bool animate; + + RTLSeriesLegend(this.seriesList, {this.animate = false}); + + /// 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/example/lib/legends/datum_legend_options.dart b/example/lib/legends/datum_legend_options.dart new file mode 100644 index 0000000..54aee0e --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/legends/datum_legend_with_measures.dart b/example/lib/legends/datum_legend_with_measures.dart new file mode 100644 index 0000000..1b2ce31 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/legends/default_hidden_series_legend.dart b/example/lib/legends/default_hidden_series_legend.dart new file mode 100644 index 0000000..e1a4dcf --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/legends/legend_custom_symbol.dart b/example/lib/legends/legend_custom_symbol.dart new file mode 100644 index 0000000..b5c7fcd --- /dev/null +++ b/example/lib/legends/legend_custom_symbol.dart @@ -0,0 +1,210 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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/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 = true}) { + // Lighten the color if the symbol is not enabled + // Example: If user has tapped on a Series deselecting it. + if (color != null && !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 = false}); + + 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/example/lib/legends/legends_gallery.dart b/example/lib/legends/legends_gallery.dart new file mode 100644 index 0000000..82be628 --- /dev/null +++ b/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/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/example/lib/legends/series_legend_options.dart b/example/lib/legends/series_legend_options.dart new file mode 100644 index 0000000..f5e32d4 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/legends/series_legend_with_measures.dart b/example/lib/legends/series_legend_with_measures.dart new file mode 100644 index 0000000..21a7678 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/legends/simple_datum_legend.dart b/example/lib/legends/simple_datum_legend.dart new file mode 100644 index 0000000..fe130a5 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/legends/simple_series_legend.dart b/example/lib/legends/simple_series_legend.dart new file mode 100644 index 0000000..1480793 --- /dev/null +++ b/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/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 = false}); + + 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/example/lib/line_chart/animation_zoom.dart b/example/lib/line_chart/animation_zoom.dart new file mode 100644 index 0000000..5e5a327 --- /dev/null +++ b/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/material.dart'; + +class LineAnimationZoomChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + LineAnimationZoomChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/area_and_line.dart b/example/lib/line_chart/area_and_line.dart new file mode 100644 index 0000000..cf69493 --- /dev/null +++ b/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/material.dart'; + +class AreaAndLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + AreaAndLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/dash_pattern.dart b/example/lib/line_chart/dash_pattern.dart new file mode 100644 index 0000000..f5da3be --- /dev/null +++ b/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/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 = false}); + + /// 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/example/lib/line_chart/line_annotation.dart b/example/lib/line_chart/line_annotation.dart new file mode 100644 index 0000000..2fd1f48 --- /dev/null +++ b/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/material.dart'; + +class LineLineAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + LineLineAnnotationChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/line_gallery.dart b/example/lib/line_chart/line_gallery.dart new file mode 100644 index 0000000..edd29a4 --- /dev/null +++ b/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/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/example/lib/line_chart/points.dart b/example/lib/line_chart/points.dart new file mode 100644 index 0000000..02291dd --- /dev/null +++ b/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/material.dart'; + +class PointsLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PointsLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/range_annotation.dart b/example/lib/line_chart/range_annotation.dart new file mode 100644 index 0000000..e9a8702 --- /dev/null +++ b/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/material.dart'; + +class LineRangeAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + LineRangeAnnotationChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/range_annotation_margin.dart b/example/lib/line_chart/range_annotation_margin.dart new file mode 100644 index 0000000..eaa5d75 --- /dev/null +++ b/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/material.dart'; + +class LineRangeAnnotationMarginChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + LineRangeAnnotationMarginChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/segments.dart b/example/lib/line_chart/segments.dart new file mode 100644 index 0000000..245778f --- /dev/null +++ b/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/material.dart'; + +class SegmentsLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SegmentsLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/simple.dart b/example/lib/line_chart/simple.dart new file mode 100644 index 0000000..f8b79cc --- /dev/null +++ b/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/material.dart'; + +class SimpleLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/simple_nulls.dart b/example/lib/line_chart/simple_nulls.dart new file mode 100644 index 0000000..2873477 --- /dev/null +++ b/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/material.dart'; + +class SimpleNullsLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleNullsLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/stacked_area.dart b/example/lib/line_chart/stacked_area.dart new file mode 100644 index 0000000..5a7ae58 --- /dev/null +++ b/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/material.dart'; + +class StackedAreaLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedAreaLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/stacked_area_custom_color.dart b/example/lib/line_chart/stacked_area_custom_color.dart new file mode 100644 index 0000000..ce6401a --- /dev/null +++ b/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/material.dart'; + +class StackedAreaCustomColorLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedAreaCustomColorLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/line_chart/stacked_area_nulls.dart b/example/lib/line_chart/stacked_area_nulls.dart new file mode 100644 index 0000000..89e24a1 --- /dev/null +++ b/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/material.dart'; + +class StackedAreaNullsLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedAreaNullsLineChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..13fd48f --- /dev/null +++ b/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/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/example/lib/pie_chart/auto_label.dart b/example/lib/pie_chart/auto_label.dart new file mode 100644 index 0000000..60cafdf --- /dev/null +++ b/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/material.dart'; + +class DonutAutoLabelChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DonutAutoLabelChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/pie_chart/donut.dart b/example/lib/pie_chart/donut.dart new file mode 100644 index 0000000..40c9051 --- /dev/null +++ b/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/material.dart'; + +class DonutPieChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DonutPieChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/pie_chart/gauge.dart b/example/lib/pie_chart/gauge.dart new file mode 100644 index 0000000..5ed4873 --- /dev/null +++ b/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/material.dart'; + +class GaugeChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + GaugeChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/pie_chart/outside_label.dart b/example/lib/pie_chart/outside_label.dart new file mode 100644 index 0000000..1db0ad4 --- /dev/null +++ b/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/material.dart'; + +class PieOutsideLabelChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PieOutsideLabelChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/pie_chart/partial_pie.dart b/example/lib/pie_chart/partial_pie.dart new file mode 100644 index 0000000..1b32f71 --- /dev/null +++ b/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/material.dart'; + +class PartialPieChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PartialPieChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/pie_chart/pie_gallery.dart b/example/lib/pie_chart/pie_gallery.dart new file mode 100644 index 0000000..56c9178 --- /dev/null +++ b/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/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/example/lib/pie_chart/simple.dart b/example/lib/pie_chart/simple.dart new file mode 100644 index 0000000..51b30b1 --- /dev/null +++ b/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/material.dart'; + +class SimplePieChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimplePieChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/scatter_plot_chart/animation_zoom.dart b/example/lib/scatter_plot_chart/animation_zoom.dart new file mode 100644 index 0000000..4adea61 --- /dev/null +++ b/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/material.dart'; + +class ScatterPlotAnimationZoomChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + ScatterPlotAnimationZoomChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/scatter_plot_chart/bucketing_axis.dart b/example/lib/scatter_plot_chart/bucketing_axis.dart new file mode 100644 index 0000000..0ea045d --- /dev/null +++ b/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/material.dart'; + +class BucketingAxisScatterPlotChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + BucketingAxisScatterPlotChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/scatter_plot_chart/comparison_points.dart b/example/lib/scatter_plot_chart/comparison_points.dart new file mode 100644 index 0000000..2412d54 --- /dev/null +++ b/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/material.dart'; + +class ComparisonPointsScatterPlotChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + ComparisonPointsScatterPlotChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/scatter_plot_chart/scatter_plot_gallery.dart b/example/lib/scatter_plot_chart/scatter_plot_gallery.dart new file mode 100644 index 0000000..3c1b3f7 --- /dev/null +++ b/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/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/example/lib/scatter_plot_chart/shapes.dart b/example/lib/scatter_plot_chart/shapes.dart new file mode 100644 index 0000000..1c0ed7f --- /dev/null +++ b/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/material.dart'; + +class ShapesScatterPlotChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + ShapesScatterPlotChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/scatter_plot_chart/simple.dart b/example/lib/scatter_plot_chart/simple.dart new file mode 100644 index 0000000..6faf10b --- /dev/null +++ b/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/material.dart'; + +class SimpleScatterPlotChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleScatterPlotChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/time_series_chart/confidence_interval.dart b/example/lib/time_series_chart/confidence_interval.dart new file mode 100644 index 0000000..32b0722 --- /dev/null +++ b/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/material.dart'; + +class TimeSeriesConfidenceInterval extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesConfidenceInterval(this.seriesList, {this.animate = false}); + + /// 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/example/lib/time_series_chart/end_points_axis.dart b/example/lib/time_series_chart/end_points_axis.dart new file mode 100644 index 0000000..50f9e76 --- /dev/null +++ b/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/material.dart'; + +class EndPointsAxisTimeSeriesChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + EndPointsAxisTimeSeriesChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/time_series_chart/line_annotation.dart b/example/lib/time_series_chart/line_annotation.dart new file mode 100644 index 0000000..9ab4a80 --- /dev/null +++ b/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/material.dart'; + +class TimeSeriesLineAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesLineAnnotationChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/time_series_chart/range_annotation.dart b/example/lib/time_series_chart/range_annotation.dart new file mode 100644 index 0000000..6501ff7 --- /dev/null +++ b/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/material.dart'; + +class TimeSeriesRangeAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesRangeAnnotationChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/time_series_chart/range_annotation_margin.dart b/example/lib/time_series_chart/range_annotation_margin.dart new file mode 100644 index 0000000..c05d5a1 --- /dev/null +++ b/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/material.dart'; + +class TimeSeriesRangeAnnotationMarginChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesRangeAnnotationMarginChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/time_series_chart/simple.dart b/example/lib/time_series_chart/simple.dart new file mode 100644 index 0000000..bf66db0 --- /dev/null +++ b/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/material.dart'; + +class SimpleTimeSeriesChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleTimeSeriesChart(this.seriesList, {this.animate = false}); + + /// 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/example/lib/time_series_chart/symbol_annotation.dart b/example/lib/time_series_chart/symbol_annotation.dart new file mode 100644 index 0000000..e57f584 --- /dev/null +++ b/example/lib/time_series_chart/symbol_annotation.dart @@ -0,0 +1,298 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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/material.dart'; + +class TimeSeriesSymbolAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesSymbolAnnotationChart(this.seriesList, {this.animate = false}); + + /// 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({ + required this.timeCurrent, + this.timePrevious, + this.timeTarget, + this.sales, + }); +} diff --git a/example/lib/time_series_chart/time_series_gallery.dart b/example/lib/time_series_chart/time_series_gallery.dart new file mode 100644 index 0000000..f60d6dd --- /dev/null +++ b/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/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/example/lib/time_series_chart/with_bar_renderer.dart b/example/lib/time_series_chart/with_bar_renderer.dart new file mode 100644 index 0000000..c2f5ae5 --- /dev/null +++ b/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/material.dart'; + +class TimeSeriesBar extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesBar(this.seriesList, {this.animate = false}); + + /// 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/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..8910916 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,18 @@ +name: example +description: Charts-Flutter Demo +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: '>=2.5.0' + +dependencies: + charts_flutter: + path: ../ + cupertino_icons: ^0.1.0 + flutter: + sdk: flutter + flutter_test: + sdk: flutter + meta: ^1.1.1 + intl: ">=0.15.2 < 0.18.0" +flutter: + uses-material-design: true diff --git a/lib/flutter.dart b/lib/flutter.dart new file mode 100644 index 0000000..609e7de --- /dev/null +++ b/lib/flutter.dart @@ -0,0 +1,199 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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, + BasicDateTimeTickFormatterSpec, + 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, + PointRendererElement, + PointSymbolRenderer, + QuantumPalette, + 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, + TextDirection, + TextElement, + TextStyle, + 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_entry_layout.dart' + show LegendEntryLayout, SimpleLegendEntryLayout; +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/lib/src/bar_chart.dart b/lib/src/bar_chart.dart new file mode 100644 index 0000000..db154d0 --- /dev/null +++ b/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.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? 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/lib/src/base_chart.dart b/lib/src/base_chart.dart new file mode 100644 index 0000000..aafe57a --- /dev/null +++ b/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; +import 'behaviors/chart_behavior.dart' + show ChartBehavior, ChartStateBehavior, GestureType; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'package:flutter/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 = 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 = 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, + selectClosestSeries: true)); + } + + bool _notACustomBehavior(ChartBehavior behavior) { + return behaviors == null || + !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)) { + if (prevChangedListener != null) { + selectionModel.removeSelectionChangedListener(prevChangedListener); + } + selectionModel.addSelectionChangedListener(model.changedListener!); + chartState.addedSelectionChangedListenersByType[model.type] = + model.changedListener!; + } + + final prevUpdatedListener = + chartState.addedSelectionUpdatedListenersByType[model.type]; + if (!identical(model.updatedListener, prevUpdatedListener)) { + if (prevUpdatedListener != null) { + 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/lib/src/base_chart_state.dart b/lib/src/base_chart_state.dart new file mode 100644 index 0000000..1b85334 --- /dev/null +++ b/lib/src/base_chart_state.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:ui' show TextDirection; +import 'package:flutter/material.dart' + show + AnimationController, + BuildContext, + State, + TickerProviderStateMixin, + Widget; +import 'package:charts_common/common.dart' as common; +import 'package:flutter/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 + late AnimationController _animationController; + double _animationValue = 0.0; + + BaseChart? _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; + + @override + void setState(fn) { + if (mounted) { + super.setState(fn); + } + } + + /// 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/lib/src/behaviors/a11y/domain_a11y_explore_behavior.dart b/lib/src/behaviors/a11y/domain_a11y_explore_behavior.dart new file mode 100644 index 0000000..aeeebea --- /dev/null +++ b/lib/src/behaviors/a11y/domain_a11y_explore_behavior.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:charts_common/common.dart' as common + show + ChartBehavior, + DomainA11yExploreBehavior, + VocalizationCallback, + ExploreModeTrigger; +import 'package:flutter/widgets.dart' ; +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, + required 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.ChartBehavior 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 Object.hash(minimumWidth, vocalizationCallback, exploreModeTrigger, + exploreModeEnabledAnnouncement, exploreModeDisabledAnnouncement); + } +} diff --git a/lib/src/behaviors/calculation/percent_injector.dart b/lib/src/behaviors/calculation/percent_injector.dart new file mode 100644 index 0000000..f2038f7 --- /dev/null +++ b/lib/src/behaviors/calculation/percent_injector.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/common.dart' as common + show ChartBehavior, 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; + + /// Constructs a [PercentInjector]. + /// + /// [totalType] configures the type of data total to be calculated. + PercentInjector({this.totalType = common.PercentInjectorTotalType.domain}); + + @override + common.PercentInjector createCommonBehavior() => + new common.PercentInjector(totalType: totalType); + + @override + void updateCommonBehavior(common.ChartBehavior 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/lib/src/behaviors/chart_behavior.dart b/lib/src/behaviors/chart_behavior.dart new file mode 100644 index 0000000..c580416 --- /dev/null +++ b/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/widgets.dart' show BuildContext, Widget; + +import '../base_chart_state.dart' show BaseChartState; + +/// Flutter wrapper for chart behaviors. +@immutable +abstract class ChartBehavior { + Set get desiredGestures; + + common.ChartBehavior createCommonBehavior(); + + void updateCommonBehavior(common.ChartBehavior 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/lib/src/behaviors/chart_title/chart_title.dart b/lib/src/behaviors/chart_title/chart_title.dart new file mode 100644 index 0000000..014cee0 --- /dev/null +++ b/lib/src/behaviors/chart_title/chart_title.dart @@ -0,0 +1,202 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, + ChartBehavior, + ChartTitle, + ChartTitleDirection, + MaxWidthStrategy, + OutsideJustification, + TextStyleSpec; +import 'package:flutter/widgets.dart' ; +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.ChartBehavior 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 Object.hash( + behaviorPosition, + layoutMinSize, + layoutPreferredSize, + maxWidthStrategy, + title, + titleDirection, + titleOutsideJustification, + titleStyleSpec, + subTitle, + subTitleStyleSpec, + innerPadding, + titlePadding, + outerPadding); + } +} diff --git a/lib/src/behaviors/domain_highlighter.dart b/lib/src/behaviors/domain_highlighter.dart new file mode 100644 index 0000000..e6e2613 --- /dev/null +++ b/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 ChartBehavior, 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.ChartBehavior 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/lib/src/behaviors/initial_selection.dart b/lib/src/behaviors/initial_selection.dart new file mode 100644 index 0000000..cb056b5 --- /dev/null +++ b/lib/src/behaviors/initial_selection.dart @@ -0,0 +1,69 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, InitialSelection, SeriesDatumConfig, SelectionModelType; +import 'package:collection/collection.dart' show ListEquality; +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; + final bool shouldPreserveSelectionOnDraw; + + InitialSelection( + {this.selectionModelType = common.SelectionModelType.info, + this.selectedSeriesConfig, + this.selectedDataConfig, + this.shouldPreserveSelectionOnDraw = false}); + + @override + common.InitialSelection createCommonBehavior() => + new common.InitialSelection( + selectionModelType: selectionModelType, + selectedDataConfig: selectedDataConfig, + selectedSeriesConfig: selectedSeriesConfig, + shouldPreserveSelectionOnDraw: shouldPreserveSelectionOnDraw); + + @override + void updateCommonBehavior(common.ChartBehavior 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/lib/src/behaviors/legend/datum_legend.dart b/lib/src/behaviors/legend/datum_legend.dart new file mode 100644 index 0000000..d0f69c6 --- /dev/null +++ b/lib/src/behaviors/legend/datum_legend.dart @@ -0,0 +1,341 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, + ChartBehavior, + DatumLegend, + InsideJustification, + LegendEntry, + MeasureFormatter, + LegendDefaultMeasure, + OutsideJustification, + SelectionModelType, + TextStyleSpec; +import 'package:flutter/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({ + required this.contentBuilder, + this.selectionModelType, + required this.position, + required this.outsideJustification, + required this.insideJustification, + required this.showMeasures, + this.legendDefaultMeasure, + this.measureFormatter, + this.secondaryMeasureFormatter, + this.entryTextStyle, + }); + + @override + common.DatumLegend createCommonBehavior() => + new _FlutterDatumLegend(this); + + @override + void updateCommonBehavior(common.ChartBehavior 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 Object.hash( + 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/lib/src/behaviors/legend/legend.dart b/lib/src/behaviors/legend/legend.dart new file mode 100644 index 0000000..5cd347b --- /dev/null +++ b/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/lib/src/behaviors/legend/legend_content_builder.dart b/lib/src/behaviors/legend/legend_content_builder.dart new file mode 100644 index 0000000..4463788 --- /dev/null +++ b/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/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 = false}) { + 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 => Object.hash(legendEntryLayout, legendLayout); +} diff --git a/lib/src/behaviors/legend/legend_entry_layout.dart b/lib/src/behaviors/legend/legend_entry_layout.dart new file mode 100644 index 0000000..fa38b84 --- /dev/null +++ b/lib/src/behaviors/legend/legend_entry_layout.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 'package:charts_common/common.dart' as common; +import 'package:charts_flutter/src/util/color.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/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; + final color = entryColor == null ? null : ColorUtil.toDartColor(entryColor); + + // Get the SymbolRendererBuilder wrapping a common.SymbolRenderer if needed. + final SymbolRendererBuilder symbolRendererBuilder = + legendEntry.symbolRenderer! is SymbolRendererBuilder + ? legendEntry.symbolRenderer! as SymbolRendererBuilder + : new SymbolRendererCanvas( + legendEntry.symbolRenderer!, legendEntry.dashPattern); + + 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 = false}) { + 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.bodyMedium!.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/lib/src/behaviors/legend/legend_layout.dart b/lib/src/behaviors/legend/legend_layout.dart new file mode 100644 index 0000000..b8e86ec --- /dev/null +++ b/lib/src/behaviors/legend/legend_layout.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. + +import 'dart:math' show min; +import 'package:flutter/rendering.dart'; +import 'package:flutter/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; + + /// Default EdgeInsets for padding rows to the max column count + static const defaultCellPadding = const EdgeInsets.all(8.0); + + final bool isHorizontalFirst; + final int desiredMaxRows; + final int desiredMaxColumns; + final EdgeInsets? cellPadding; + + TabularLegendLayout._internal( + {required this.isHorizontalFirst, + required this.desiredMaxRows, + required 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 => Object.hash( + 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 = Padding(padding: cellPadding ?? defaultCellPadding); + + // 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/lib/src/behaviors/legend/series_legend.dart b/lib/src/behaviors/legend/series_legend.dart new file mode 100644 index 0000000..652d45d --- /dev/null +++ b/lib/src/behaviors/legend/series_legend.dart @@ -0,0 +1,383 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, + ChartBehavior, + InsideJustification, + LegendEntry, + LegendTapHandling, + MeasureFormatter, + LegendDefaultMeasure, + OutsideJustification, + SeriesLegend, + SelectionModelType, + TextStyleSpec; +import 'package:collection/collection.dart' show ListEquality; +import 'package:flutter/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({ + required this.contentBuilder, + this.selectionModelType, + required this.position, + required this.outsideJustification, + required this.insideJustification, + this.defaultHiddenSeries, + required this.showMeasures, + this.legendDefaultMeasure, + this.measureFormatter, + this.secondaryMeasureFormatter, + this.entryTextStyle, + }); + + @override + common.SeriesLegend createCommonBehavior() => + new _FlutterSeriesLegend(this); + + @override + void updateCommonBehavior(common.ChartBehavior 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 Object.hash( + 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 != null && + 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/lib/src/behaviors/line_point_highlighter.dart b/lib/src/behaviors/line_point_highlighter.dart new file mode 100644 index 0000000..957a8bd --- /dev/null +++ b/lib/src/behaviors/line_point_highlighter.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:collection/collection.dart' show ListEquality; +import 'package:charts_common/common.dart' as common + show + ChartBehavior, + LinePointHighlighter, + LinePointHighlighterFollowLineType, + SelectionModelType, + SymbolRenderer; +import 'package:flutter/widgets.dart' ; +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.ChartBehavior 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 Object.hash( + selectionModelType, + defaultRadiusPx, + radiusPaddingPx, + showHorizontalFollowLine, + showVerticalFollowLine, + dashPattern, + drawFollowLinesAcrossChart, + ); + } +} diff --git a/lib/src/behaviors/range_annotation.dart b/lib/src/behaviors/range_annotation.dart new file mode 100644 index 0000000..8d1e07f --- /dev/null +++ b/lib/src/behaviors/range_annotation.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:charts_common/common.dart' as common + show + AnnotationLabelAnchor, + AnnotationLabelDirection, + AnnotationLabelPosition, + AnnotationSegment, + ChartBehavior, + Color, + MaterialPalette, + RangeAnnotation, + TextStyleSpec; +import 'package:collection/collection.dart' show ListEquality; +import 'package:flutter/widgets.dart' ; +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; + + /// Configures the order in which the behavior should be painted. + /// This value should be relative to LayoutPaintViewOrder.rangeAnnotation. + /// (e.g. LayoutViewPaintOrder.rangeAnnotation + 1) + final int? layoutPaintOrder; + + RangeAnnotation(this.annotations, + {common.Color? defaultColor, + this.defaultLabelAnchor, + this.defaultLabelDirection, + this.defaultLabelPosition, + this.defaultLabelStyleSpec, + this.extendAxis, + this.labelPadding, + this.layoutPaintOrder}) + : this.defaultColor = + 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, + layoutPaintOrder: layoutPaintOrder); + + @override + void updateCommonBehavior(common.ChartBehavior 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 && + layoutPaintOrder == o.layoutPaintOrder; + } + + @override + int get hashCode => Object.hash( + annotations, + defaultColor, + extendAxis, + defaultLabelAnchor, + defaultLabelDirection, + defaultLabelPosition, + defaultLabelStyleSpec, + labelPadding, + layoutPaintOrder); +} diff --git a/lib/src/behaviors/select_nearest.dart b/lib/src/behaviors/select_nearest.dart new file mode 100644 index 0000000..7658367 --- /dev/null +++ b/lib/src/behaviors/select_nearest.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. + +import 'package:charts_common/common.dart' as common + show + ChartBehavior, + SelectNearest, + SelectionMode, + 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 +/// selectionMode - Optional mode for expanding the selection beyond the +/// nearest datum. Defaults to expandToDomain. +/// 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 common.SelectionMode selectionMode; + final bool selectAcrossAllDrawAreaComponents; + final bool selectClosestSeries; + final int? maximumDomainDistancePx; + + SelectNearest._internal( + {required this.selectionModelType, + this.selectionMode = common.SelectionMode.expandToDomain, + this.selectAcrossAllDrawAreaComponents = false, + this.selectClosestSeries = true, + required this.eventTrigger, + required this.desiredGestures, + this.maximumDomainDistancePx}); + + factory SelectNearest( + {common.SelectionModelType selectionModelType = + common.SelectionModelType.info, + common.SelectionMode selectionMode = common.SelectionMode.expandToDomain, + bool selectAcrossAllDrawAreaComponents = false, + bool selectClosestSeries = true, + common.SelectionTrigger eventTrigger = common.SelectionTrigger.tap, + int? maximumDomainDistancePx}) { + return new SelectNearest._internal( + selectionModelType: selectionModelType, + selectionMode: selectionMode, + 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, + selectionMode: selectionMode, + 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) && + (selectionMode == other.selectionMode) && + (selectClosestSeries == other.selectClosestSeries) && + (maximumDomainDistancePx == other.maximumDomainDistancePx); + } else { + return false; + } + } + + int get hashCode { + int hashcode = selectionModelType.hashCode; + hashcode = hashcode * 37 + eventTrigger.hashCode; + hashcode = hashcode * 37 + selectionMode.hashCode; + hashcode = hashcode * 37 + selectClosestSeries.hashCode; + hashcode = hashcode * 37 + maximumDomainDistancePx.hashCode; + return hashcode; + } +} diff --git a/lib/src/behaviors/slider/slider.dart b/lib/src/behaviors/slider/slider.dart new file mode 100644 index 0000000..6b69b88 --- /dev/null +++ b/lib/src/behaviors/slider/slider.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. + +import 'dart:math' show Rectangle; +import 'package:charts_common/common.dart' as common + show + ChartBehavior, + LayoutViewPaintOrder, + RectSymbolRenderer, + SelectionTrigger, + Slider, + SliderListenerCallback, + SliderStyle, + SymbolRenderer; +import 'package:flutter/widgets.dart' ; +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( + {required this.eventTrigger, + this.onChangeCallback, + this.initialDomainValue, + this.roleId, + required this.snapToDatum, + this.style, + this.handleRenderer, + required 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}"'); + } + 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.ChartBehavior 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 Object.hash(eventTrigger, handleRenderer, initialDomainValue, roleId, + snapToDatum, style, layoutPaintOrder); + } +} diff --git a/lib/src/behaviors/sliding_viewport.dart b/lib/src/behaviors/sliding_viewport.dart new file mode 100644 index 0000000..f839d31 --- /dev/null +++ b/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 ChartBehavior, 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.ChartBehavior 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/lib/src/behaviors/zoom/initial_hint_behavior.dart b/lib/src/behaviors/zoom/initial_hint_behavior.dart new file mode 100644 index 0000000..40f7b58 --- /dev/null +++ b/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/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/lib/src/behaviors/zoom/pan_and_zoom_behavior.dart b/lib/src/behaviors/zoom/pan_and_zoom_behavior.dart new file mode 100644 index 0000000..3fbfe54 --- /dev/null +++ b/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/lib/src/behaviors/zoom/pan_behavior.dart b/lib/src/behaviors/zoom/pan_behavior.dart new file mode 100644 index 0000000..3668e65 --- /dev/null +++ b/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 'dart:ui' show lerpDouble; + +import 'package:flutter/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 { + late BaseChartState _chartState; + + set chartState(BaseChartState chartState) { + assert(chartState != null); + + _chartState = chartState; + _flingAnimator = chartState.getAnimationController(this); + _flingAnimator?.addListener(_onFlingTick); + } + + AnimationController? _flingAnimator; + + double _flingAnimationInitialTranslatePx = 0; + double _flingAnimationTargetTranslatePx = 0; + + 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/lib/src/canvas/circle_sector_painter.dart b/lib/src/canvas/circle_sector_painter.dart new file mode 100644 index 0000000..197ef4c --- /dev/null +++ b/lib/src/canvas/circle_sector_painter.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 'dart:math' show cos, pi, sin, Point; +import 'package:flutter/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. + static void draw({ + required Canvas canvas, + required Paint paint, + required Point center, + required double radius, + required double innerRadius, + required double startAngle, + required double endAngle, + common.Color? fill, + }) { + 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.toDouble(), center.y.toDouble()); + + 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/lib/src/canvas/line_painter.dart b/lib/src/canvas/line_painter.dart new file mode 100644 index 0000000..1867ac3 --- /dev/null +++ b/lib/src/canvas/line_painter.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. + +import 'dart:ui' as ui show Shader; +import 'dart:math' show Point, Rectangle; +import 'package:flutter/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." + static void draw( + {required Canvas canvas, + required Paint paint, + required 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.toDouble(), point.y.toDouble()), + strokeWidthPx ?? 0, 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. + static 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. + static 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]. + static 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. + static 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/lib/src/canvas/pie_painter.dart b/lib/src/canvas/pie_painter.dart new file mode 100644 index 0000000..9963261 --- /dev/null +++ b/lib/src/canvas/pie_painter.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 cos, sin, Point; +import 'package:flutter/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 { + /// Draws a pie chart, with an optional hole in the center. + static void draw(Canvas canvas, Paint paint, common.CanvasPie canvasPie) { + 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/lib/src/canvas/point_painter.dart b/lib/src/canvas/point_painter.dart new file mode 100644 index 0000000..fe3eab0 --- /dev/null +++ b/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/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 { + static void draw( + {required Canvas canvas, + required Paint paint, + required Point point, + required 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/lib/src/canvas/polygon_painter.dart b/lib/src/canvas/polygon_painter.dart new file mode 100644 index 0000000..5d1b12f --- /dev/null +++ b/lib/src/canvas/polygon_painter.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 'dart:math' show Point, Rectangle; +import 'package:flutter/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." + static void draw( + {required Canvas canvas, + required Paint paint, + required 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; + if (fillColor != null) { + paint.color = fillColor; + } + paint.style = PaintingStyle.fill; + canvas.drawCircle(new Offset(point.x.toDouble(), point.y.toDouble()), + 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/lib/src/cartesian_chart.dart b/lib/src/cartesian_chart.dart new file mode 100644 index 0000000..6e789fb --- /dev/null +++ b/lib/src/cartesian_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 '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.NumericAxisSpec? primaryMeasureAxis; + final common.NumericAxisSpec? 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 != prev?.primaryMeasureAxis) { + chart.primaryMeasureAxisSpec = primaryMeasureAxis; + chartState.markChartDirty(); + } + + if (secondaryMeasureAxis != prev?.secondaryMeasureAxis) { + chart.secondaryMeasureAxisSpec = secondaryMeasureAxis; + chartState.markChartDirty(); + } + + if (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/lib/src/chart_canvas.dart b/lib/src/chart_canvas.dart new file mode 100644 index 0000000..77b7d77 --- /dev/null +++ b/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 'dart:ui' as ui show Gradient, Shader; +import 'dart:math' show Point, Rectangle, max; +import 'package:charts_common/common.dart' as common + show + BlendMode, + ChartCanvas, + CanvasBarStack, + CanvasPie, + Color, + FillPatternType, + GraphicsFactory, + Link, + LinkOrientation, + StyleFactory, + TextElement, + TextDirection; +import 'package:flutter/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(); + + 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.draw( + canvas: canvas, + paint: _paint, + center: center, + radius: radius, + innerRadius: innerRadius, + startAngle: startAngle, + endAngle: endAngle, + fill: fill, + ); + } + + @override + void drawLink( + common.Link link, common.LinkOrientation orientation, common.Color fill) { + // TODO: Implement drawLink for flutter. + throw ("Flutter drawLink() has not been implemented."); + } + + @override + void drawLine( + {required List points, + Rectangle? clipBounds, + common.Color? fill, + common.Color? stroke, + bool? roundEndCaps, + double? strokeWidthPx, + List? dashPattern}) { + 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.draw(canvas, _paint, canvasPie); + } + + @override + void drawPoint( + {required Point point, + required double radius, + common.Color? fill, + common.Color? stroke, + double? strokeWidthPx, + common.BlendMode? blendMode}) { + PointPainter.draw( + canvas: canvas, + paint: _paint, + point: point, + radius: radius, + fill: fill, + stroke: stroke, + strokeWidthPx: strokeWidthPx); + } + + @override + void drawPolygon( + {required List points, + Rectangle? clipBounds, + common.Color? fill, + common.Color? stroke, + double? strokeWidthPx}) { + 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}) { + // TODO: remove this explicit `bool` type when no longer needed + // to work around https://github.com/dart-lang/language/issues/1785 + final bool 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, + common.Color? patternColor, + common.FillPatternType? fillPattern, + double? patternStrokeWidthPx, + double? strokeWidthPx, + num? radius, + bool roundTopLeft = false, + bool roundTopRight = false, + bool roundBottomLeft = false, + bool roundBottomRight = false}) { + // 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?.toDouble() ?? 0.0, + 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 = barStack.radius != null && 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.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.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 = 0, + 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; + + // 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/lib/src/chart_container.dart b/lib/src/chart_container.dart new file mode 100644 index 0000000..b10f533 --- /dev/null +++ b/lib/src/chart_container.dart @@ -0,0 +1,379 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; + +import 'base_chart.dart' show BaseChart; +import 'base_chart_state.dart' show BaseChartState; +import 'chart_canvas.dart' show ChartCanvas; +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 BaseChartState chartState; + final double animationValue; + final bool rtl; + final common.RTLSpec? rtlSpec; + final UserManagedState? userManagedState; + + ChartContainer( + {this.oldChartWidget, + required this.chartWidget, + required this.chartState, + required this.animationValue, + required this.rtl, + 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; + late BaseChartState _chartState; + bool _chartContainerIsRtl = false; + common.RTLSpec? _rtlSpec; + common.DateTimeFactory? _dateTimeFactory; + bool _exploreMode = false; + List? _a11yNodes; + + 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; + _chartContainerIsRtl = config.rtl; + + common.Performance.timeEnd('chartsConfig'); + + 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 == null || + _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 as ChartContainerCustomPaint?, + 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, + required common.BaseChart chart, + bool exploreMode = false, + List a11yNodes = const [], + TextDirection textDirection = TextDirection.ltr}) { + if (oldPainter != null && + oldPainter.exploreMode == exploreMode && + oldPainter.a11yNodes == a11yNodes && + oldPainter.textDirection == textDirection) { + return oldPainter; + } else { + return new ChartContainerCustomPaint._internal( + chart: chart, + exploreMode: exploreMode, + a11yNodes: a11yNodes, + textDirection: textDirection); + } + } + + ChartContainerCustomPaint._internal( + {required this.chart, + required this.exploreMode, + required this.a11yNodes, + required 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/lib/src/chart_gesture_detector.dart b/lib/src/chart_gesture_detector.dart new file mode 100644 index 0000000..c8c8b9d --- /dev/null +++ b/lib/src/chart_gesture_detector.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. + +import 'dart:async' show Timer; +import 'dart:math' show Point; +import 'package:flutter/material.dart' + show + BuildContext, + GestureDetector, + RenderBox, + 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 = false; + + bool _isDragging = false; + + Timer? _longPressTimer; + Point? _lastTapPoint; + double? _lastScale; + + late _ContainerResolver _containerResolver; + + makeWidget(BuildContext context, ChartContainer chartContainer, + Set desiredGestures) { + _containerResolver = () { + final renderObject = context.findRenderObject()!; + + return getChartContainerRenderObject(renderObject as RenderBox); + }; + + 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/lib/src/chart_state.dart b/lib/src/chart_state.dart new file mode 100644 index 0000000..6103968 --- /dev/null +++ b/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/lib/src/combo_chart/combo_chart.dart b/lib/src/combo_chart/combo_chart.dart new file mode 100644 index 0000000..e4d30fc --- /dev/null +++ b/lib/src/combo_chart/combo_chart.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. + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + NumericCartesianChart, + OrdinalCartesianChart, + NumericAxisSpec, + 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.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? 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.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? 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/lib/src/graphics_factory.dart b/lib/src/graphics_factory.dart new file mode 100644 index 0000000..9a76729 --- /dev/null +++ b/lib/src/graphics_factory.dart @@ -0,0 +1,55 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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/widgets.dart' + show BuildContext, DefaultTextStyle, 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; + final DefaultTextStyle defaultTextStyle; + + GraphicsFactory(BuildContext context, + {GraphicsFactoryHelper helper = const GraphicsFactoryHelper()}) + : textScaleFactor = helper.getTextScaleFactorOf(context), + defaultTextStyle = DefaultTextStyle.of(context); + + /// Returns a [TextStyle] object. + @override + common.TextStyle createTextPaint() => + TextStyle()..fontFamily = defaultTextStyle.style.fontFamily; + + /// Returns a text element from [text]. + @override + common.TextElement createTextElement(String text) { + return TextElement(text, textScaleFactor: textScaleFactor) + ..textStyle = createTextPaint(); + } + + @override + common.LineStyle createLinePaint() => LineStyle(); +} + +/// Wraps the MediaQuery function to allow for testing. +class GraphicsFactoryHelper { + const GraphicsFactoryHelper(); + + double getTextScaleFactorOf(BuildContext context) => + MediaQuery.textScaleFactorOf(context); +} diff --git a/lib/src/line_chart.dart b/lib/src/line_chart.dart new file mode 100644 index 0000000..8e71b61 --- /dev/null +++ b/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.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? 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/lib/src/line_style.dart b/lib/src/line_style.dart new file mode 100644 index 0000000..6a7d0e1 --- /dev/null +++ b/lib/src/line_style.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:charts_common/common.dart' as common show Color, LineStyle; + +class LineStyle implements common.LineStyle { + @override + common.Color? color; + + @override + List? dashPattern; + + @override + int strokeWidth; + + LineStyle({this.strokeWidth = 0}); +} diff --git a/lib/src/pie_chart.dart b/lib/src/pie_chart.dart new file mode 100644 index 0000000..48a8e16 --- /dev/null +++ b/lib/src/pie_chart.dart @@ -0,0 +1,49 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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); +} diff --git a/lib/src/scatter_plot_chart.dart b/lib/src/scatter_plot_chart.dart new file mode 100644 index 0000000..c353410 --- /dev/null +++ b/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.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? 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/lib/src/selection_model_config.dart b/lib/src/selection_model_config.dart new file mode 100644 index 0000000..60305a4 --- /dev/null +++ b/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/lib/src/symbol_renderer.dart b/lib/src/symbol_renderer.dart new file mode 100644 index 0000000..dd26d6f --- /dev/null +++ b/lib/src/symbol_renderer.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 'dart:math' show Rectangle; +import 'package:charts_common/common.dart' as common + show ChartCanvas, Color, FillPatternType, SymbolRenderer; +import 'package:flutter/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; + final List? dashPattern; + + SymbolRendererCanvas(this.commonSymbolRenderer, this.dashPattern); + + @override + Widget build(BuildContext context, + {Color? color, required Size size, bool enabled = true}) { + if (color != null && !enabled) { + color = color.withOpacity(0.26); + } + + return new SizedBox.fromSize( + size: size, + child: new CustomPaint( + painter: new _SymbolCustomPaint( + context, commonSymbolRenderer, color, dashPattern))); + } +} + +/// 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 { + CustomSymbolRenderer() : super(isSolid: false); + + /// Must override this method to build the custom Widget with the given color + /// as + @override + Widget build(BuildContext context, + {Color? color, required Size size, bool enabled = true}); + + @override + void paint(common.ChartCanvas canvas, Rectangle bounds, + {List? dashPattern, + common.Color? fillColor, + common.FillPatternType? fillPattern, + 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, required 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; + final List? dashPattern; + + _SymbolCustomPaint( + this.context, this.symbolRenderer, this.color, this.dashPattern); + + @override + void paint(Canvas canvas, Size size) { + final bounds = + new Rectangle(0, 0, size.width.toInt(), size.height.toInt()); + final commonColor = color == null + ? null + : 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, + dashPattern: dashPattern); + } + + @override + bool shouldRepaint(_SymbolCustomPaint oldDelegate) { + return symbolRenderer.shouldRepaint(oldDelegate.symbolRenderer); + } +} diff --git a/lib/src/text_element.dart b/lib/src/text_element.dart new file mode 100644 index 0000000..b2c37f1 --- /dev/null +++ b/lib/src/text_element.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:ui' show TextAlign, TextDirection; +import 'package:charts_common/common.dart' as common + show + MaxWidthStrategy, + TextElement, + TextDirection, + TextMeasurement, + TextStyle; +import 'package:flutter/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; + + late TextPainter _textPainter; + + late 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 = (textStyle == null || textStyle!.color == null) + ? null + : 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, + height: textStyle?.lineHeight))) + ..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/lib/src/text_style.dart b/lib/src/text_style.dart new file mode 100644 index 0000000..ef81d7f --- /dev/null +++ b/lib/src/text_style.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 'dart:ui' ; +import 'package:charts_common/common.dart' as common show Color, TextStyle; + +class TextStyle implements common.TextStyle { + int? fontSize; + String? fontFamily; + common.Color? color; + double? lineHeight; + String? fontWeight; + + @override + bool operator ==(Object other) => + other is TextStyle && + fontSize == other.fontSize && + fontFamily == other.fontFamily && + fontWeight == other.fontWeight && + color == other.color && + lineHeight == other.lineHeight; + + @override + int get hashCode => + Object.hash(fontSize, fontFamily, color, lineHeight, fontWeight); +} diff --git a/lib/src/time_series_chart.dart b/lib/src/time_series_chart.dart new file mode 100644 index 0000000..13f7e4f --- /dev/null +++ b/lib/src/time_series_chart.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 'dart:collection' show LinkedHashMap; + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + DateTimeFactory, + LocalDateTimeFactory, + 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.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? 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(), + dateTimeFactory: + dateTimeFactory ?? const common.LocalDateTimeFactory()); + } + + @override + void addDefaultInteractions(List behaviors) { + super.addDefaultInteractions(behaviors); + + behaviors.add(new LinePointHighlighter()); + } +} diff --git a/lib/src/user_managed_state.dart b/lib/src/user_managed_state.dart new file mode 100644 index 0000000..8b32536 --- /dev/null +++ b/lib/src/user_managed_state.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 '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 ?? 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 ??= common.SelectionModel.fromConfig( + selectedDataConfig, selectedSeriesConfig, seriesList); + + return _model!; + } +} diff --git a/lib/src/util.dart b/lib/src/util.dart new file mode 100644 index 0000000..b391748 --- /dev/null +++ b/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/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/lib/src/util/color.dart b/lib/src/util/color.dart new file mode 100644 index 0000000..e4297a3 --- /dev/null +++ b/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 'dart:ui' 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/lib/src/widget_layout_delegate.dart b/lib/src/widget_layout_delegate.dart new file mode 100644 index 0000000..b9288c5 --- /dev/null +++ b/lib/src/widget_layout_delegate.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:ui' show Offset; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/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, + {required Size behaviorSize, + required Size chartSize, + required bool isRTL}) { + late 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/minimum_os.bzl b/minimum_os.bzl new file mode 100644 index 0000000..964ed7a --- /dev/null +++ b/minimum_os.bzl @@ -0,0 +1,3 @@ +"""Minimum OS version definitions""" + +IOS_MINIMUM_OS = "9.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..425b5dd --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,33 @@ +name: charts_flutter +version: 0.12.0 +description: Material Design charting library for flutter. +homepage: https://github.com/google/charts + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: '>=2.0.0' + # flutter: '>=2.5.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.12.0 + # charts_common: + # path: ../charts_common/ + collection: ^1.14.5 + flutter: + sdk: flutter + intl: ">=0.15.2 < 0.18.0" + logging: ^1.0.2 + meta: ^1.1.1 + + +dev_dependencies: + mockito: ^5.0.0 + build_runner: ^1.11.0 + flutter_test: + sdk: flutter + test: ^1.3.0 diff --git a/test/behaviors/legend/legend_layout_test.dart b/test/behaviors/legend/legend_layout_test.dart new file mode 100644 index 0000000..0165914 --- /dev/null +++ b/test/behaviors/legend/legend_layout_test.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:flutter/material.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'package:charts_flutter/src/behaviors/legend/legend_layout.dart'; + +// Can't use Mockito annotations with BuildContext yet? Fake it. +class FakeBuildContext extends Fake implements BuildContext {} + +void main() { + late BuildContext context; + + setUp(() { + context = FakeBuildContext(); + }); + + 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) as Table; + 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) as Table; + 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) as Table; + 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) as Table; + 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) as Table; + 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) as Table; + 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/test/text_element_test.dart b/test/text_element_test.dart new file mode 100644 index 0000000..6cdd4cd --- /dev/null +++ b/test/text_element_test.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/material.dart' show BuildContext; +import 'package:mockito/mockito.dart'; +import 'package:flutter/widgets.dart' show InheritedWidget; +import 'package:test/test.dart'; +import 'package:charts_flutter/src/graphics_factory.dart'; +import 'package:charts_flutter/src/text_element.dart'; + +// Can't use Mockito annotations with BuildContext yet? Fake it. +class FakeBuildContext extends Fake implements BuildContext { + @override + T? dependOnInheritedWidgetOfExactType( + {Object? aspect}) { + return null; + } +} + +// Gave up trying to figure out how to use mockito for now. +class FakeGraphicsFactoryHelper extends Fake implements GraphicsFactoryHelper { + double textScaleFactor; + + FakeGraphicsFactoryHelper(this.textScaleFactor) {} + + @override + double getTextScaleFactorOf(BuildContext context) => textScaleFactor; +} + +void main() { + test('Text element gets assigned scale factor', () { + final context = FakeBuildContext(); + final helper = FakeGraphicsFactoryHelper(3.0); + final graphicsFactory = new GraphicsFactory(context, helper: helper); + + final textElement = + graphicsFactory.createTextElement('test') as TextElement; + + expect(textElement.text, equals('test')); + expect(textElement.textScaleFactor, equals(3.0)); + }); +} diff --git a/test/user_managed_state_test.dart b/test/user_managed_state_test.dart new file mode 100644 index 0000000..5fb392b --- /dev/null +++ b/test/user_managed_state_test.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/widgets.dart'; +import 'package:flutter_test/flutter_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( + new Directionality( + textDirection: TextDirection.ltr, + child: testChart, + ), + ); + + expect(currentSelectionModel, isNull); + + await tester.tap(find.byType(charts.BarChart)); + + await tester.pump(); + + expect(currentSelectionModel, isNotNull); + 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 Directionality( + textDirection: TextDirection.ltr, + child: 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/test/widget_layout_delegate_test.dart b/test/widget_layout_delegate_test.dart new file mode 100644 index 0000000..0c710f4 --- /dev/null +++ b/test/widget_layout_delegate_test.dart @@ -0,0 +1,561 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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/material.dart'; +import 'package:flutter_test/flutter_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'; + +// I couldn't get mockito to work with Widget return type, so fake it is. +class FakeBuildableBehavior implements BuildableBehavior { + common.BehaviorPosition position; + common.OutsideJustification outsideJustification; + common.InsideJustification insideJustification; + Rectangle? drawAreaBounds; + + FakeBuildableBehavior(this.position, this.outsideJustification, + this.insideJustification, this.drawAreaBounds) {} + + @override + Widget build(BuildContext context) { + throw UnimplementedError(); + } +} + +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, + // Using these defaults, copied from DatumLegend. + {common.OutsideJustification outsideJustification = + common.OutsideJustification.startDrawArea, + common.InsideJustification insideJustification = + common.InsideJustification.topStart, + required Rectangle drawAreaBounds, + bool isRTL = false}) { + // Create a mock buildable behavior that returns information about the + // position and justification desired. + final behavior = new FakeBuildableBehavior( + position, outsideJustification, insideJustification, 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); + }); + }); +}