├── .github
└── workflows
│ ├── check.yml
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── config
└── detekt.yml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── plot
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
├── publish.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── madrapps
│ └── plot
│ ├── Axes.kt
│ ├── Gestures.kt
│ └── line
│ ├── Components.kt
│ └── LineGraph.kt
├── preview
├── line_graph_1.png
├── line_graph_2.png
├── line_graph_3.png
├── line_graph_4.png
├── line_graph_recording.gif
└── line_graph_recording.mp4
├── sample
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── madrapps
│ │ └── sample
│ │ ├── MainActivity.kt
│ │ ├── MainViewModelImpl.kt
│ │ ├── Utils.kt
│ │ ├── linegraph
│ │ ├── DataPoints.kt
│ │ ├── LineGraph1.kt
│ │ ├── LineGraph2.kt
│ │ ├── LineGraph3.kt
│ │ ├── LineGraph4.kt
│ │ └── LineGraph5.kt
│ │ └── ui
│ │ └── theme
│ │ ├── Color.kt
│ │ ├── Shape.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── values-night
│ └── themes.xml
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
└── settings.gradle.kts
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Check
2 |
3 | on:
4 | pull_request:
5 | types: [opened, ready_for_review, synchronize]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | if: github.event.pull_request.draft == false
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v2
15 |
16 | - name: Setup JDK 11
17 | uses: actions/setup-java@v1
18 | with:
19 | java-version: 11
20 |
21 | - name: Detekt
22 | run: ./gradlew detekt --stacktrace
23 |
24 | - name: Check
25 | run: ./gradlew check --stacktrace
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v2
13 |
14 | - name: Setup JDK 11
15 | uses: actions/setup-java@v1
16 | with:
17 | java-version: 11
18 |
19 | - name: Detekt
20 | run: ./gradlew detekt --stacktrace
21 |
22 | - name: Check
23 | run: ./gradlew check --stacktrace
24 |
25 | - name: Build
26 | run: ./gradlew build --stacktrace
27 |
28 | - name: Publish Artifact
29 | run: ./gradlew plot:publishReleasePublicationToSonatypeRepositor --max-workers=1 closeAndReleaseSonatypeStagingRepository --stacktrace
30 | env:
31 | OSS_USERNAME: ${{ secrets.OSS_USERNAME }}
32 | OSS_PASSWORD: ${{ secrets.OSS_PASSWORD }}
33 | OSS_STAGING_PROFILE_ID: ${{ secrets.OSS_STAGING_PROFILE_ID }}
34 | OSS_SIGNING_KEY_ID: ${{ secrets.OSS_SIGNING_KEY_ID }}
35 | OSS_SIGNING_PASSWORD: ${{ secrets.OSS_SIGNING_PASSWORD }}
36 | OSS_SIGNING_KEY: ${{ secrets.OSS_SIGNING_KEY }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | # Android Studio 3 in .gitignore file.
48 | .idea/caches
49 | .idea/modules.xml
50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
51 | .idea/navEditor.xml
52 |
53 | # Keystore files
54 | # Uncomment the following lines if you do not want to check your keystore files in.
55 | #*.jks
56 | #*.keystore
57 |
58 | # External native build folder generated in Android Studio 2.2 and later
59 | .externalNativeBuild
60 | .cxx/
61 |
62 | # Google Services (e.g. APIs or Firebase)
63 | # google-services.json
64 |
65 | # Freeline
66 | freeline.py
67 | freeline/
68 | freeline_project_description.json
69 |
70 | # fastlane
71 | fastlane/report.xml
72 | fastlane/Preview.html
73 | fastlane/screenshots
74 | fastlane/test_output
75 | fastlane/readme.md
76 |
77 | # Version control
78 | vcs.xml
79 |
80 | # lint
81 | lint/intermediates/
82 | lint/generated/
83 | lint/outputs/
84 | lint/tmp/
85 | # lint/reports/
86 |
87 | .idea/
88 | .gitignore
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2021 Madrapps
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # plot
2 | An android compose library with different Graphs and Charts (*currently supports only Line graph, more types will
3 | be added soon*)
4 |
5 |
6 |
7 |
8 |
9 | Download
10 | -----
11 |
12 | ```gradle
13 | repositories {
14 | mavenCentral()
15 | }
16 |
17 | dependencies {
18 | implementation 'com.github.madrapps:plot:0.1.1'
19 | }
20 | ```
21 |
22 | Features
23 | -----
24 | - Full customization of the various parts of the graph (like the point, line between the points, highlight
25 | when selected, the values in x-axis and y-axis, etc...)
26 | - Supports scrolling, zooming and touch drag selection
27 |
28 | Usage
29 | -----
30 | Just add the `LineGraph` composable and pass it a `LinePlot` with all your configuration and customisation.
31 | Please take a look at the [sample](https://github.com/Madrapps/plot/tree/main/sample) app to see the various
32 | customisations available. Almost every aspect of the graph is customisable. You can even override the default
33 | draw implementations and can draw a `Rectangle` instead of a `Circle`, etc. The below code renders the Orange
34 | graph that you see in the above screenshots.
35 |
36 | ```kotlin
37 | @Composable
38 | fun SampleLineGraph(lines: List>) {
39 | LineGraph(
40 | plot = LinePlot(
41 | listOf(
42 | LinePlot.Line(
43 | lines[0],
44 | LinePlot.Connection(color = Red300),
45 | LinePlot.Intersection(color = Red500),
46 | LinePlot.Highlight(color = Yellow700),
47 | )
48 | ),
49 | grid = LinePlot.Grid(Red100, steps = 4),
50 | ),
51 | modifier = Modifier.fillMaxWidth().height(200.dp),
52 | onSelection = { xLine, points ->
53 | // Do whatever you want here
54 | }
55 | )
56 | }
57 | ```
58 |
59 | License
60 | -----
61 |
62 | **plot** by [Madrapps](http://madrapps.github.io/) is licensed under the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).
63 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | val compose_version by extra("1.0.1")
4 | repositories {
5 | google()
6 | mavenCentral()
7 | }
8 | dependencies {
9 | classpath("com.android.tools.build:gradle:7.0.1")
10 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21")
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle.kts files
14 | }
15 | }
16 |
17 | plugins {
18 | id("io.gitlab.arturbosch.detekt").version("1.10.0")
19 | id("io.github.gradle-nexus.publish-plugin").version("1.1.0")
20 | }
21 |
22 | allprojects {
23 | apply(plugin = "io.gitlab.arturbosch.detekt")
24 |
25 | detekt {
26 | toolVersion = "1.10.0"
27 | config = files("${project.rootDir}/config/detekt.yml")
28 | buildUponDefaultConfig = true
29 |
30 | reports {
31 | html.enabled = true
32 | xml.enabled = false
33 | txt.enabled = false
34 | }
35 | }
36 | }
37 |
38 | tasks.register("clean", Delete::class) {
39 | delete(rootProject.buildDir)
40 | }
41 |
42 | // Publish to Maven Central
43 | nexusPublishing {
44 | repositories {
45 | sonatype {
46 | username.set(System.getenv("OSS_USERNAME"))
47 | password.set(System.getenv("OSS_PASSWORD"))
48 | stagingProfileId.set(System.getenv("OSS_STAGING_PROFILE_ID"))
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/config/detekt.yml:
--------------------------------------------------------------------------------
1 | build:
2 | maxIssues: 0
3 | excludeCorrectable: false
4 | weights:
5 | # complexity: 2
6 | # LongParameterList: 1
7 | # style: 1
8 | # comments: 1
9 |
10 | config:
11 | validation: true
12 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
13 | excludes: ''
14 |
15 | processors:
16 | active: true
17 | exclude:
18 | - 'DetektProgressListener'
19 | # - 'FunctionCountProcessor'
20 | # - 'PropertyCountProcessor'
21 | # - 'ClassCountProcessor'
22 | # - 'PackageCountProcessor'
23 | # - 'KtFileCountProcessor'
24 |
25 | console-reports:
26 | active: true
27 | exclude:
28 | - 'ProjectStatisticsReport'
29 | - 'ComplexityReport'
30 | - 'NotificationReport'
31 | #- 'FindingsReport'
32 | - 'FileBasedFindingsReport'
33 |
34 | comments:
35 | active: true
36 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
37 | AbsentOrWrongFileLicense:
38 | active: false
39 | licenseTemplateFile: 'license.template'
40 | CommentOverPrivateFunction:
41 | active: false
42 | CommentOverPrivateProperty:
43 | active: false
44 | EndOfSentenceFormat:
45 | active: false
46 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
47 | UndocumentedPublicClass:
48 | active: false
49 | searchInNestedClass: true
50 | searchInInnerClass: true
51 | searchInInnerObject: true
52 | searchInInnerInterface: true
53 | UndocumentedPublicFunction:
54 | active: false
55 | UndocumentedPublicProperty:
56 | active: false
57 |
58 | complexity:
59 | active: true
60 | ComplexCondition:
61 | active: true
62 | threshold: 4
63 | ComplexInterface:
64 | active: false
65 | threshold: 10
66 | includeStaticDeclarations: false
67 | includePrivateDeclarations: false
68 | ComplexMethod:
69 | active: false
70 | threshold: 15
71 | ignoreSingleWhenExpression: true
72 | ignoreSimpleWhenEntries: false
73 | ignoreNestingFunctions: false
74 | nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull]
75 | LabeledExpression:
76 | active: false
77 | ignoredLabels: []
78 | LargeClass:
79 | active: true
80 | threshold: 600
81 | LongMethod:
82 | active: false
83 | threshold: 60
84 | LongParameterList:
85 | active: true
86 | functionThreshold: 10
87 | constructorThreshold: 10
88 | ignoreDefaultParameters: true
89 | ignoreDataClasses: true
90 | ignoreAnnotated: []
91 | MethodOverloading:
92 | active: false
93 | threshold: 6
94 | NestedBlockDepth:
95 | active: true
96 | threshold: 4
97 | StringLiteralDuplication:
98 | active: false
99 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
100 | threshold: 3
101 | ignoreAnnotation: true
102 | excludeStringsWithLessThan5Characters: true
103 | ignoreStringsRegex: '$^'
104 | TooManyFunctions:
105 | active: false
106 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
107 | thresholdInFiles: 11
108 | thresholdInClasses: 11
109 | thresholdInInterfaces: 11
110 | thresholdInObjects: 11
111 | thresholdInEnums: 11
112 | ignoreDeprecated: false
113 | ignorePrivate: false
114 | ignoreOverridden: false
115 |
116 | coroutines:
117 | active: true
118 | GlobalCoroutineUsage:
119 | active: false
120 | RedundantSuspendModifier:
121 | active: false
122 |
123 | empty-blocks:
124 | active: true
125 | EmptyCatchBlock:
126 | active: true
127 | allowedExceptionNameRegex: '_|(ignore|expected).*'
128 | EmptyClassBlock:
129 | active: true
130 | EmptyDefaultConstructor:
131 | active: true
132 | EmptyDoWhileBlock:
133 | active: true
134 | EmptyElseBlock:
135 | active: true
136 | EmptyFinallyBlock:
137 | active: true
138 | EmptyForBlock:
139 | active: true
140 | EmptyFunctionBlock:
141 | active: true
142 | ignoreOverridden: false
143 | EmptyIfBlock:
144 | active: true
145 | EmptyInitBlock:
146 | active: true
147 | EmptyKtFile:
148 | active: true
149 | EmptySecondaryConstructor:
150 | active: true
151 | EmptyTryBlock:
152 | active: true
153 | EmptyWhenBlock:
154 | active: true
155 | EmptyWhileBlock:
156 | active: true
157 |
158 | exceptions:
159 | active: true
160 | ExceptionRaisedInUnexpectedLocation:
161 | active: false
162 | methodNames: [toString, hashCode, equals, finalize]
163 | InstanceOfCheckForException:
164 | active: false
165 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
166 | NotImplementedDeclaration:
167 | active: false
168 | PrintStackTrace:
169 | active: false
170 | RethrowCaughtException:
171 | active: false
172 | ReturnFromFinally:
173 | active: false
174 | ignoreLabeled: false
175 | SwallowedException:
176 | active: false
177 | ignoredExceptionTypes:
178 | - InterruptedException
179 | - NumberFormatException
180 | - ParseException
181 | - MalformedURLException
182 | allowedExceptionNameRegex: '_|(ignore|expected).*'
183 | ThrowingExceptionFromFinally:
184 | active: false
185 | ThrowingExceptionInMain:
186 | active: false
187 | ThrowingExceptionsWithoutMessageOrCause:
188 | active: false
189 | exceptions:
190 | - IllegalArgumentException
191 | - IllegalStateException
192 | - IOException
193 | ThrowingNewInstanceOfSameException:
194 | active: false
195 | TooGenericExceptionCaught:
196 | active: true
197 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
198 | exceptionNames:
199 | - ArrayIndexOutOfBoundsException
200 | - Error
201 | - Exception
202 | - IllegalMonitorStateException
203 | - NullPointerException
204 | - IndexOutOfBoundsException
205 | - RuntimeException
206 | - Throwable
207 | allowedExceptionNameRegex: '_|(ignore|expected).*'
208 | TooGenericExceptionThrown:
209 | active: true
210 | exceptionNames:
211 | - Error
212 | - Exception
213 | - Throwable
214 | - RuntimeException
215 |
216 | formatting:
217 | active: true
218 | android: false
219 | autoCorrect: true
220 | AnnotationOnSeparateLine:
221 | active: false
222 | autoCorrect: true
223 | ChainWrapping:
224 | active: true
225 | autoCorrect: true
226 | CommentSpacing:
227 | active: true
228 | autoCorrect: true
229 | EnumEntryNameCase:
230 | active: false
231 | autoCorrect: true
232 | Filename:
233 | active: true
234 | FinalNewline:
235 | active: true
236 | autoCorrect: true
237 | insertFinalNewLine: true
238 | ImportOrdering:
239 | active: false
240 | autoCorrect: true
241 | layout: 'idea'
242 | Indentation:
243 | active: false
244 | autoCorrect: true
245 | indentSize: 4
246 | continuationIndentSize: 4
247 | MaximumLineLength:
248 | active: false
249 | maxLineLength: 120
250 | ModifierOrdering:
251 | active: true
252 | autoCorrect: true
253 | MultiLineIfElse:
254 | active: true
255 | autoCorrect: true
256 | NoBlankLineBeforeRbrace:
257 | active: true
258 | autoCorrect: true
259 | NoConsecutiveBlankLines:
260 | active: true
261 | autoCorrect: true
262 | NoEmptyClassBody:
263 | active: true
264 | autoCorrect: true
265 | NoEmptyFirstLineInMethodBlock:
266 | active: false
267 | autoCorrect: true
268 | NoLineBreakAfterElse:
269 | active: true
270 | autoCorrect: true
271 | NoLineBreakBeforeAssignment:
272 | active: true
273 | autoCorrect: true
274 | NoMultipleSpaces:
275 | active: true
276 | autoCorrect: true
277 | NoSemicolons:
278 | active: true
279 | autoCorrect: true
280 | NoTrailingSpaces:
281 | active: true
282 | autoCorrect: true
283 | NoUnitReturn:
284 | active: true
285 | autoCorrect: true
286 | NoUnusedImports:
287 | active: true
288 | autoCorrect: true
289 | NoWildcardImports:
290 | active: false
291 | PackageName:
292 | active: true
293 | autoCorrect: true
294 | ParameterListWrapping:
295 | active: true
296 | autoCorrect: true
297 | indentSize: 4
298 | SpacingAroundColon:
299 | active: true
300 | autoCorrect: true
301 | SpacingAroundComma:
302 | active: true
303 | autoCorrect: true
304 | SpacingAroundCurly:
305 | active: true
306 | autoCorrect: true
307 | SpacingAroundDot:
308 | active: true
309 | autoCorrect: true
310 | SpacingAroundDoubleColon:
311 | active: false
312 | autoCorrect: true
313 | SpacingAroundKeyword:
314 | active: true
315 | autoCorrect: true
316 | SpacingAroundOperators:
317 | active: true
318 | autoCorrect: true
319 | SpacingAroundParens:
320 | active: true
321 | autoCorrect: true
322 | SpacingAroundRangeOperator:
323 | active: true
324 | autoCorrect: true
325 | SpacingBetweenDeclarationsWithAnnotations:
326 | active: false
327 | autoCorrect: true
328 | SpacingBetweenDeclarationsWithComments:
329 | active: false
330 | autoCorrect: true
331 | StringTemplate:
332 | active: true
333 | autoCorrect: true
334 |
335 | naming:
336 | active: true
337 | ClassNaming:
338 | active: true
339 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
340 | classPattern: '[A-Z][a-zA-Z0-9]*'
341 | ConstructorParameterNaming:
342 | active: true
343 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
344 | parameterPattern: '[a-z][A-Za-z0-9]*'
345 | privateParameterPattern: '[a-z][A-Za-z0-9]*'
346 | excludeClassPattern: '$^'
347 | ignoreOverridden: true
348 | EnumNaming:
349 | active: true
350 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
351 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
352 | ForbiddenClassName:
353 | active: false
354 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
355 | forbiddenName: []
356 | FunctionMaxLength:
357 | active: false
358 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
359 | maximumFunctionNameLength: 30
360 | FunctionMinLength:
361 | active: false
362 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
363 | minimumFunctionNameLength: 3
364 | FunctionNaming:
365 | active: true
366 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
367 | functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
368 | excludeClassPattern: '$^'
369 | ignoreOverridden: true
370 | ignoreAnnotated: ['Composable']
371 | FunctionParameterNaming:
372 | active: true
373 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
374 | parameterPattern: '[a-z][A-Za-z0-9]*'
375 | excludeClassPattern: '$^'
376 | ignoreOverridden: true
377 | InvalidPackageDeclaration:
378 | active: false
379 | rootPackage: ''
380 | MatchingDeclarationName:
381 | active: true
382 | mustBeFirst: true
383 | MemberNameEqualsClassName:
384 | active: true
385 | ignoreOverridden: true
386 | ObjectPropertyNaming:
387 | active: true
388 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
389 | constantPattern: '[A-Za-z][_A-Za-z0-9]*'
390 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
391 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
392 | PackageNaming:
393 | active: true
394 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
395 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
396 | TopLevelPropertyNaming:
397 | active: true
398 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
399 | constantPattern: '[A-Z][_A-Z0-9]*'
400 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
401 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
402 | VariableMaxLength:
403 | active: false
404 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
405 | maximumVariableNameLength: 64
406 | VariableMinLength:
407 | active: false
408 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
409 | minimumVariableNameLength: 1
410 | VariableNaming:
411 | active: true
412 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
413 | variablePattern: '[a-z][A-Za-z0-9]*'
414 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
415 | excludeClassPattern: '$^'
416 | ignoreOverridden: true
417 |
418 | performance:
419 | active: true
420 | ArrayPrimitive:
421 | active: true
422 | ForEachOnRange:
423 | active: true
424 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
425 | SpreadOperator:
426 | active: true
427 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
428 | UnnecessaryTemporaryInstantiation:
429 | active: true
430 |
431 | potential-bugs:
432 | active: true
433 | Deprecation:
434 | active: false
435 | DuplicateCaseInWhenExpression:
436 | active: true
437 | EqualsAlwaysReturnsTrueOrFalse:
438 | active: true
439 | EqualsWithHashCodeExist:
440 | active: true
441 | ExplicitGarbageCollectionCall:
442 | active: true
443 | HasPlatformType:
444 | active: false
445 | IgnoredReturnValue:
446 | active: false
447 | restrictToAnnotatedMethods: true
448 | returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult']
449 | ImplicitDefaultLocale:
450 | active: false
451 | ImplicitUnitReturnType:
452 | active: false
453 | allowExplicitReturnType: true
454 | InvalidRange:
455 | active: true
456 | IteratorHasNextCallsNextMethod:
457 | active: true
458 | IteratorNotThrowingNoSuchElementException:
459 | active: true
460 | LateinitUsage:
461 | active: false
462 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
463 | excludeAnnotatedProperties: []
464 | ignoreOnClassesPattern: ''
465 | MapGetWithNotNullAssertionOperator:
466 | active: false
467 | MissingWhenCase:
468 | active: true
469 | RedundantElseInWhen:
470 | active: true
471 | UnconditionalJumpStatementInLoop:
472 | active: false
473 | UnnecessaryNotNullOperator:
474 | active: false
475 | UnnecessarySafeCall:
476 | active: false
477 | UnreachableCode:
478 | active: true
479 | UnsafeCallOnNullableType:
480 | active: true
481 | UnsafeCast:
482 | active: false
483 | UselessPostfixExpression:
484 | active: false
485 | WrongEqualsTypeParameter:
486 | active: true
487 |
488 | style:
489 | active: true
490 | CollapsibleIfStatements:
491 | active: false
492 | DataClassContainsFunctions:
493 | active: false
494 | conversionFunctionPrefix: 'to'
495 | DataClassShouldBeImmutable:
496 | active: false
497 | EqualsNullCall:
498 | active: true
499 | EqualsOnSignatureLine:
500 | active: false
501 | ExplicitCollectionElementAccessMethod:
502 | active: false
503 | ExplicitItLambdaParameter:
504 | active: false
505 | ExpressionBodySyntax:
506 | active: false
507 | includeLineWrapping: false
508 | ForbiddenComment:
509 | active: true
510 | values: ['TODO:', 'FIXME:', 'STOPSHIP:']
511 | allowedPatterns: ''
512 | ForbiddenImport:
513 | active: false
514 | imports: []
515 | forbiddenPatterns: ''
516 | ForbiddenMethodCall:
517 | active: false
518 | methods: ['kotlin.io.println', 'kotlin.io.print']
519 | ForbiddenPublicDataClass:
520 | active: false
521 | ignorePackages: ['*.internal', '*.internal.*']
522 | ForbiddenVoid:
523 | active: false
524 | ignoreOverridden: false
525 | ignoreUsageInGenerics: false
526 | FunctionOnlyReturningConstant:
527 | active: true
528 | ignoreOverridableFunction: true
529 | excludedFunctions: 'describeContents'
530 | excludeAnnotatedFunction: ['dagger.Provides']
531 | LibraryCodeMustSpecifyReturnType:
532 | active: true
533 | LoopWithTooManyJumpStatements:
534 | active: true
535 | maxJumpCount: 1
536 | MagicNumber:
537 | active: false
538 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
539 | ignoreNumbers: ['-1', '0', '1', '2']
540 | ignoreHashCodeFunction: true
541 | ignorePropertyDeclaration: false
542 | ignoreLocalVariableDeclaration: false
543 | ignoreConstantDeclaration: true
544 | ignoreCompanionObjectPropertyDeclaration: true
545 | ignoreAnnotation: false
546 | ignoreNamedArgument: true
547 | ignoreEnums: true
548 | ignoreRanges: false
549 | MandatoryBracesIfStatements:
550 | active: false
551 | MandatoryBracesLoops:
552 | active: false
553 | MaxLineLength:
554 | active: true
555 | maxLineLength: 120
556 | excludePackageStatements: true
557 | excludeImportStatements: true
558 | excludeCommentStatements: true
559 | MayBeConst:
560 | active: true
561 | ModifierOrder:
562 | active: true
563 | NestedClassesVisibility:
564 | active: false
565 | NewLineAtEndOfFile:
566 | active: true
567 | NoTabs:
568 | active: false
569 | OptionalAbstractKeyword:
570 | active: true
571 | OptionalUnit:
572 | active: false
573 | OptionalWhenBraces:
574 | active: false
575 | PreferToOverPairSyntax:
576 | active: false
577 | ProtectedMemberInFinalClass:
578 | active: true
579 | RedundantExplicitType:
580 | active: false
581 | RedundantVisibilityModifierRule:
582 | active: false
583 | ReturnCount:
584 | active: true
585 | max: 2
586 | excludedFunctions: 'equals'
587 | excludeLabeled: false
588 | excludeReturnFromLambda: true
589 | excludeGuardClauses: false
590 | SafeCast:
591 | active: true
592 | SerialVersionUIDInSerializableClass:
593 | active: false
594 | SpacingBetweenPackageAndImports:
595 | active: false
596 | ThrowsCount:
597 | active: true
598 | max: 2
599 | TrailingWhitespace:
600 | active: false
601 | UnderscoresInNumericLiterals:
602 | active: false
603 | acceptableDecimalLength: 5
604 | UnnecessaryAbstractClass:
605 | active: true
606 | excludeAnnotatedClasses: ['dagger.Module']
607 | UnnecessaryAnnotationUseSiteTarget:
608 | active: false
609 | UnnecessaryApply:
610 | active: false
611 | UnnecessaryInheritance:
612 | active: true
613 | UnnecessaryLet:
614 | active: false
615 | UnnecessaryParentheses:
616 | active: false
617 | UntilInsteadOfRangeTo:
618 | active: false
619 | UnusedImports:
620 | active: true
621 | UnusedPrivateClass:
622 | active: true
623 | UnusedPrivateMember:
624 | active: false
625 | allowedNames: '(_|ignored|expected|serialVersionUID)'
626 | UseArrayLiteralsInAnnotations:
627 | active: false
628 | UseCheckOrError:
629 | active: false
630 | UseDataClass:
631 | active: false
632 | excludeAnnotatedClasses: []
633 | allowVars: false
634 | UseIfInsteadOfWhen:
635 | active: false
636 | UseRequire:
637 | active: false
638 | UselessCallOnNotNull:
639 | active: true
640 | UtilityClassWithPublicConstructor:
641 | active: true
642 | VarCouldBeVal:
643 | active: false
644 | WildcardImport:
645 | active: false
646 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
647 | excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*']
648 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jul 22 19:40:39 IST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/plot/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("kotlin-android")
4 | }
5 |
6 | val groupId = "com.github.madrapps"
7 | val artifactId = "plot"
8 | val libVersion = "0.1.1"
9 |
10 | ext {
11 | set("GROUP_ID", groupId)
12 | set("ARTIFACT_ID", artifactId)
13 | set("VERSION", libVersion)
14 | }
15 |
16 | apply(from = "publish.gradle")
17 |
18 | android {
19 | compileSdk = 30
20 |
21 | defaultConfig {
22 | minSdk = 21
23 | targetSdk = 30
24 | version = libVersion
25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
26 | }
27 |
28 | buildTypes {
29 | release {
30 | isMinifyEnabled = false
31 | proguardFiles(
32 | getDefaultProguardFile("proguard-android-optimize.txt"),
33 | "proguard-rules.pro"
34 | )
35 | }
36 | }
37 | compileOptions {
38 | sourceCompatibility = JavaVersion.VERSION_1_8
39 | targetCompatibility = JavaVersion.VERSION_1_8
40 | }
41 | kotlinOptions {
42 | jvmTarget = "1.8"
43 | }
44 | buildFeatures {
45 | compose = true
46 | }
47 | composeOptions {
48 | kotlinCompilerExtensionVersion = rootProject.extra["compose_version"] as String
49 | }
50 | lint {
51 | isAbortOnError = true
52 | isWarningsAsErrors = true
53 | }
54 | }
55 |
56 | dependencies {
57 | implementation("androidx.core:core-ktx:1.6.0")
58 | implementation("androidx.appcompat:appcompat:1.3.1")
59 | implementation("com.google.android.material:material:1.4.0")
60 | implementation("androidx.compose.ui:ui:${rootProject.extra["compose_version"]}")
61 | implementation("androidx.compose.material:material:${rootProject.extra["compose_version"]}")
62 | implementation("androidx.compose.ui:ui-tooling:${rootProject.extra["compose_version"]}")
63 |
64 | testImplementation("junit:junit:4.13.2")
65 | androidTestImplementation("androidx.test.ext:junit:1.1.3")
66 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
67 | androidTestImplementation("androidx.compose.ui:ui-test-junit4:${rootProject.extra["compose_version"]}")
68 | }
69 |
--------------------------------------------------------------------------------
/plot/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/plot/consumer-rules.pro
--------------------------------------------------------------------------------
/plot/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/plot/publish.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 | apply plugin: 'signing'
3 |
4 | task androidSourcesJar(type: Jar) {
5 | archiveClassifier.set('sources')
6 | from android.sourceSets.main.java.srcDirs
7 | from android.sourceSets.main.kotlin.srcDirs
8 | }
9 |
10 | artifacts {
11 | archives androidSourcesJar
12 | }
13 |
14 | def siteUrl = 'https://github.com/Madrapps/plot'
15 | def gitUrl = 'https://github.com/Madrapps/plot.git'
16 |
17 | group = GROUP_ID
18 | version = VERSION
19 |
20 | afterEvaluate {
21 | publishing {
22 | publications {
23 | release(MavenPublication) {
24 |
25 | groupId GROUP_ID
26 | artifactId ARTIFACT_ID
27 | version VERSION
28 |
29 | from components.release
30 | artifact androidSourcesJar
31 |
32 | pom {
33 | name = ARTIFACT_ID
34 | description = 'A compose library to render different Graphs and Charts'
35 | url = siteUrl
36 | licenses {
37 | license {
38 | name = 'The Apache Software License, Version 2.0'
39 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
40 | }
41 | }
42 | developers {
43 | developer {
44 | id = 'instrap'
45 | name = 'Madrapps'
46 | email = 'madrasappfactory@gmail.com'
47 | }
48 | developer {
49 | id = 'thsaravana'
50 | name = 'Saravana Thiyagaraj'
51 | email = 'th.saravana@gmail.com'
52 | }
53 | }
54 |
55 | scm {
56 | connection = gitUrl
57 | developerConnection = gitUrl
58 | url = siteUrl
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
66 | signing {
67 | useInMemoryPgpKeys(
68 | System.getenv("OSS_SIGNING_KEY_ID"),
69 | System.getenv("OSS_SIGNING_KEY"),
70 | System.getenv("OSS_SIGNING_PASSWORD"),
71 | )
72 | sign publishing.publications
73 | }
--------------------------------------------------------------------------------
/plot/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/plot/src/main/java/com/madrapps/plot/Axes.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.plot
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.layout.Layout
6 | import androidx.compose.ui.unit.Dp
7 |
8 | /**
9 | * Composable that Layouts the child composables in the Y Axis. This does the same thing as a Column
10 | * composable, but with customisation that takes care of the scale.
11 | *
12 | * @param modifier Modifier
13 | * @param paddingTop the top padding
14 | * @param paddingBottom the bottom padding
15 | * @param scale the scale in y axis
16 | * @param content the composable that draws the item in the Y axis
17 | */
18 | @Composable
19 | internal fun GraphYAxis(
20 | modifier: Modifier,
21 | paddingTop: Float,
22 | paddingBottom: Float,
23 | scale: Float,
24 | content: @Composable () -> Unit
25 | ) {
26 | Layout(content, modifier) { measurables, constraints ->
27 | val steps = if (measurables.size <= 1) 1 else measurables.size - 1
28 | val placeables = measurables.map { measurable ->
29 | measurable.measure(constraints.copy(minHeight = 0))
30 | }
31 | val width = placeables.maxOf { it.width }
32 | layout(width, constraints.maxHeight) {
33 | val yBottom = (constraints.maxHeight - paddingBottom)
34 | val availableHeight = yBottom - paddingTop
35 | var yPos = yBottom.toInt()
36 |
37 | placeables.forEach { placeable ->
38 | yPos -= (placeable.height / 2f).toInt()
39 | placeable.place(x = 0, y = yPos)
40 | yPos -= (availableHeight / steps * scale).toInt() - (placeable.height / 2f).toInt()
41 | }
42 | }
43 | }
44 | }
45 |
46 | /**
47 | * Composable that Layouts the child composables in the X Axis. This does the same thing as a Row
48 | * composable, but with customisation that takes care of the scale.
49 | *
50 | * @param modifier Modifier
51 | * @param xStart the left position where the first child is laid out
52 | * @param scrollOffset the offset value that varies based on the scroll
53 | * @param scale the scale in x axis
54 | * @param stepSize the distance between two adjacent data points
55 | * @param content the composable that draws the item in the X axis
56 | */
57 | @Composable
58 | internal fun GraphXAxis(
59 | modifier: Modifier,
60 | xStart: Float,
61 | scrollOffset: Float,
62 | scale: Float,
63 | stepSize: Dp,
64 | content: @Composable () -> Unit
65 | ) {
66 | Layout(content, modifier) { measurables, constraints ->
67 | val placeables = measurables.map { measurable ->
68 | measurable.measure(constraints.copy(minWidth = 0))
69 | }
70 | val height = placeables.maxOf { it.height }
71 | layout(constraints.maxWidth, height) {
72 | var xPos = (xStart - scrollOffset).toInt()
73 | val step = stepSize.toPx()
74 | placeables.forEach { placeable ->
75 | xPos -= (placeable.width / 2f).toInt()
76 | placeable.place(x = xPos, y = 0)
77 | xPos += ((step * scale) + (placeable.width / 2f)).toInt()
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/plot/src/main/java/com/madrapps/plot/Gestures.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.plot
2 |
3 | import androidx.compose.foundation.gestures.awaitFirstDown
4 | import androidx.compose.foundation.gestures.calculateCentroidSize
5 | import androidx.compose.foundation.gestures.calculateZoom
6 | import androidx.compose.foundation.gestures.drag
7 | import androidx.compose.foundation.gestures.forEachGesture
8 | import androidx.compose.ui.geometry.Offset
9 | import androidx.compose.ui.input.pointer.PointerEvent
10 | import androidx.compose.ui.input.pointer.PointerEventPass
11 | import androidx.compose.ui.input.pointer.PointerId
12 | import androidx.compose.ui.input.pointer.PointerInputChange
13 | import androidx.compose.ui.input.pointer.PointerInputScope
14 | import androidx.compose.ui.input.pointer.changedToUp
15 | import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
16 | import androidx.compose.ui.input.pointer.consumeAllChanges
17 | import androidx.compose.ui.input.pointer.consumeDownChange
18 | import androidx.compose.ui.input.pointer.consumePositionChange
19 | import androidx.compose.ui.input.pointer.isOutOfBounds
20 | import androidx.compose.ui.input.pointer.positionChange
21 | import androidx.compose.ui.input.pointer.positionChangeConsumed
22 | import androidx.compose.ui.input.pointer.positionChanged
23 | import kotlinx.coroutines.TimeoutCancellationException
24 | import kotlinx.coroutines.withTimeout
25 | import kotlin.coroutines.cancellation.CancellationException
26 | import kotlin.math.abs
27 |
28 | @SuppressWarnings("LoopWithTooManyJumpStatements")
29 | internal suspend fun PointerInputScope.detectDragZoomGesture(
30 | isZoomAllowed: Boolean = false,
31 | isDragAllowed: Boolean = true,
32 | detectDragTimeOut: Long,
33 | onDragStart: (Offset) -> Unit = { },
34 | onDragEnd: () -> Unit = { },
35 | onZoom: (zoom: Float) -> Unit,
36 | onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
37 | ) {
38 | if (isDragAllowed || isZoomAllowed) {
39 | forEachGesture {
40 | val down = awaitPointerEventScope {
41 | awaitFirstDown(requireUnconsumed = false)
42 | }
43 | awaitPointerEventScope {
44 | var zoom = 1f
45 | var pastTouchSlop = false
46 | val touchSlop = viewConfiguration.touchSlop
47 |
48 | do {
49 | val event = awaitPointerEvent()
50 | val canceled = event.changes.any { it.positionChangeConsumed() }
51 | if (event.changes.size == 1) {
52 | break
53 | } else if (event.changes.size == 2) {
54 | if (isZoomAllowed) {
55 | if (!canceled) {
56 | val zoomChange = event.calculateZoom()
57 | if (!pastTouchSlop) {
58 | zoom *= zoomChange
59 |
60 | val centroidSize =
61 | event.calculateCentroidSize(useCurrent = false)
62 | val zoomMotion = abs(1 - zoom) * centroidSize
63 |
64 | if (zoomMotion > touchSlop) {
65 | pastTouchSlop = true
66 | }
67 | }
68 |
69 | if (pastTouchSlop) {
70 | if (zoomChange != 1f) {
71 | onZoom(zoomChange)
72 | }
73 | event.changes.forEach {
74 | if (it.positionChanged()) {
75 | it.consumeAllChanges()
76 | }
77 | }
78 | }
79 | }
80 | }
81 | } else {
82 | break
83 | }
84 | } while (!canceled && event.changes.any { it.pressed })
85 | }
86 |
87 | if (isDragAllowed) {
88 | try {
89 | val drag = awaitLongPressOrCancellation(down, detectDragTimeOut)
90 | if (drag != null) {
91 | onDragStart.invoke(drag.position)
92 | awaitPointerEventScope {
93 | if (
94 | drag(drag.id) {
95 | onDrag(it, it.positionChange())
96 | it.consumePositionChange()
97 | }
98 | ) {
99 | // consume up if we quit drag gracefully with the up
100 | currentEvent.changes.forEach {
101 | if (it.changedToUp()) {
102 | it.consumeDownChange()
103 | }
104 | }
105 | onDragEnd()
106 | } else {
107 | onDragEnd()
108 | }
109 | }
110 | }
111 | } catch (c: CancellationException) {
112 | onDragEnd()
113 | throw c
114 | }
115 | }
116 | }
117 | }
118 | }
119 |
120 | private suspend fun PointerInputScope.awaitLongPressOrCancellation(
121 | initialDown: PointerInputChange,
122 | longPressTimeout: Long
123 | ): PointerInputChange? {
124 | var longPress: PointerInputChange? = null
125 | var currentDown = initialDown
126 | return try {
127 | // wait for first tap up or long press
128 | withTimeout(longPressTimeout) {
129 | awaitPointerEventScope {
130 | var finished = false
131 | while (!finished) {
132 | val event = awaitPointerEvent(PointerEventPass.Main)
133 | if (event.changes.all { it.changedToUpIgnoreConsumed() }) {
134 | // All pointers are up
135 | finished = true
136 | }
137 |
138 | if (
139 | event.changes.any { it.consumed.downChange || it.isOutOfBounds(size) }
140 | ) {
141 | finished = true // Canceled
142 | }
143 |
144 | // Check for cancel by position consumption. We can look on the Final pass of
145 | // the existing pointer event because it comes after the Main pass we checked
146 | // above.
147 | val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
148 | if (consumeCheck.changes.any { it.positionChangeConsumed() }) {
149 | finished = true
150 | }
151 | if (!event.isPointerUp(currentDown.id)) {
152 | longPress = event.changes.firstOrNull { it.id == currentDown.id }
153 | } else {
154 | val newPressed = event.changes.firstOrNull { it.pressed }
155 | if (newPressed != null) {
156 | currentDown = newPressed
157 | longPress = currentDown
158 | } else {
159 | // should technically never happen as we checked it above
160 | finished = true
161 | }
162 | }
163 | }
164 | }
165 | }
166 | null
167 | } catch (_: TimeoutCancellationException) {
168 | longPress ?: initialDown
169 | }
170 | }
171 |
172 | private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean =
173 | changes.firstOrNull { it.id == pointerId }?.pressed != true
174 |
--------------------------------------------------------------------------------
/plot/src/main/java/com/madrapps/plot/line/Components.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.plot.line
2 |
3 | import androidx.compose.material.MaterialTheme
4 | import androidx.compose.material.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.geometry.Offset
7 | import androidx.compose.ui.geometry.Rect
8 | import androidx.compose.ui.graphics.BlendMode
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.ColorFilter
11 | import androidx.compose.ui.graphics.Path
12 | import androidx.compose.ui.graphics.PathEffect
13 | import androidx.compose.ui.graphics.StrokeCap
14 | import androidx.compose.ui.graphics.drawscope.DrawScope
15 | import androidx.compose.ui.graphics.drawscope.DrawStyle
16 | import androidx.compose.ui.graphics.drawscope.Fill
17 | import androidx.compose.ui.graphics.drawscope.Stroke
18 | import androidx.compose.ui.text.style.TextOverflow
19 | import androidx.compose.ui.unit.Dp
20 | import androidx.compose.ui.unit.dp
21 | import com.madrapps.plot.line.LinePlot.Grid
22 | import com.madrapps.plot.line.LinePlot.Highlight
23 | import com.madrapps.plot.line.LinePlot.Intersection
24 | import com.madrapps.plot.line.LinePlot.Selection
25 | import com.madrapps.plot.line.LinePlot.XAxis
26 | import com.madrapps.plot.line.LinePlot.YAxis
27 | import java.text.DecimalFormat
28 |
29 | /**
30 | * Denotes a point in the graph.
31 | *
32 | * @param x the x coordinate or the number in the x axis
33 | * @param y the y coordinate or the number in the y axis
34 | */
35 | data class DataPoint(val x: Float, val y: Float)
36 |
37 | /**
38 | * The configuration for the [LineGraph]
39 | *
40 | * @param lines list of lines to be represented
41 | * @param grid rendering logic on how the [Grid] should be drawn. If null, no grid is drawn.
42 | * @param selection controls the touch and drag selection behaviour using [Selection]
43 | * @param xAxis controls the behaviour, scale and drawing logic of the X Axis
44 | * @param yAxis controls the behaviour, scale and drawing logic of the Y Axis
45 | * @param isZoomAllowed if true, the graph will zoom on pinch zoom. If false, no zoom action.
46 | * @param paddingTop adjusts the top padding of the graph. If you want to adjust the bottom padding, adjust
47 | * the [XAxis.paddingBottom]
48 | * @param paddingRight adjust the right padding of the graph. If you want to adjust the left padding, adjust
49 | * the [YAxis.paddingStart]
50 | * @param horizontalExtraSpace gives extra space to draw [Intersection] or [Highlight] at the left and right
51 | * extremes of the graph. Adjust this if your graph looks like cropped at the left edge or the right edge.
52 | */
53 | data class LinePlot(
54 | val lines: List,
55 | val grid: Grid? = null,
56 | val selection: Selection = Selection(),
57 | val xAxis: XAxis = XAxis(),
58 | val yAxis: YAxis = YAxis(),
59 | val isZoomAllowed: Boolean = true,
60 | val paddingTop: Dp = 16.dp,
61 | val paddingRight: Dp = 0.dp,
62 | val horizontalExtraSpace: Dp = 6.dp,
63 | ) {
64 | /**
65 | * Represent a Line in the [LineGraph]
66 | *
67 | * @param dataPoints list of points in the line. Note that this list should be sorted by x coordinate
68 | * from decreasing to increasing value, so that the graph can be drawn properly.
69 | * @param connection drawing logic for the line between two adjacent points. If null, no line is drawn.
70 | * @param intersection drawing logic to draw the point itself. If null, the point is not drawn.
71 | * @param highlight drawing logic to draw the highlight at the point when it is selected. If null, the point
72 | * won't be highlighted on selection
73 | * @param areaUnderLine drawing logic for the area under the line. This is the region that is formed by the
74 | * intersection of the line, x-axis and y-axis.
75 | */
76 | data class Line(
77 | val dataPoints: List,
78 | val connection: Connection?,
79 | val intersection: Intersection?,
80 | val highlight: Highlight? = null,
81 | val areaUnderLine: AreaUnderLine? = null
82 | )
83 |
84 | /**
85 | * Represents a line between two data points
86 | *
87 | * @param color the color to be applied to the line
88 | * @param strokeWidth The stroke width to apply to the line
89 | * @param cap treatment applied to the ends of the line segment
90 | * @param pathEffect optional effect or pattern to apply to the line
91 | * @param alpha opacity to be applied to the [color] from 0.0f to 1.0f representing
92 | * fully transparent to fully opaque respectively
93 | * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
94 | * @param blendMode the blending algorithm to apply to the [color]
95 | * @param draw override this to change the default drawLine implementation. You are provided with
96 | * the 'start' [Offset] and 'end' [Offset]
97 | */
98 | data class Connection(
99 | val color: Color = Color.Blue,
100 | val strokeWidth: Dp = 3.dp,
101 | val cap: StrokeCap = Stroke.DefaultCap,
102 | val pathEffect: PathEffect? = null,
103 | val alpha: Float = 1.0f,
104 | val colorFilter: ColorFilter? = null,
105 | val blendMode: BlendMode = DrawScope.DefaultBlendMode,
106 | val draw: DrawScope.(Offset, Offset) -> Unit = { start, end ->
107 | drawLine(
108 | color,
109 | start,
110 | end,
111 | strokeWidth.toPx(),
112 | cap,
113 | pathEffect,
114 | alpha,
115 | colorFilter,
116 | blendMode
117 | )
118 | }
119 | )
120 |
121 | /**
122 | * Represents a data point on the graph
123 | *
124 | * @param color The color or fill to be applied to the circle
125 | * @param radius The radius of the circle
126 | * @param alpha Opacity to be applied to the circle from 0.0f to 1.0f representing
127 | * fully transparent to fully opaque respectively
128 | * @param style Whether or not the circle is stroked or filled in
129 | * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
130 | * @param blendMode Blending algorithm to be applied to the brush
131 | * @param draw override this to change the default drawCircle implementation. You are provided
132 | * with the 'center' [Offset] and the actual [DataPoint] that represents the intersection.
133 | */
134 | data class Intersection(
135 | val color: Color = Color.Blue,
136 | val radius: Dp = 6.dp,
137 | val alpha: Float = 1.0f,
138 | val style: DrawStyle = Fill,
139 | val colorFilter: ColorFilter? = null,
140 | val blendMode: BlendMode = DrawScope.DefaultBlendMode,
141 | val draw: DrawScope.(Offset, DataPoint) -> Unit = { center, _ ->
142 | drawCircle(
143 | color,
144 | radius.toPx(),
145 | center,
146 | alpha,
147 | style,
148 | colorFilter,
149 | blendMode
150 | )
151 | }
152 | )
153 |
154 | /**
155 | * Represents the data point when it is selected
156 | *
157 | * @param color The color or fill to be applied to the circle
158 | * @param radius The radius of the circle
159 | * @param alpha Opacity to be applied to the circle from 0.0f to 1.0f representing
160 | * fully transparent to fully opaque respectively
161 | * @param style Whether or not the circle is stroked or filled in
162 | * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
163 | * @param blendMode Blending algorithm to be applied to the brush
164 | * @param draw override this to change the default drawCircle implementation. You are provided
165 | * with the 'center' [Offset].
166 | */
167 | data class Highlight(
168 | val color: Color = Color.Black,
169 | val radius: Dp = 6.dp,
170 | val alpha: Float = 1.0f,
171 | val style: DrawStyle = Fill,
172 | val colorFilter: ColorFilter? = null,
173 | val blendMode: BlendMode = DrawScope.DefaultBlendMode,
174 | val draw: DrawScope.(Offset) -> Unit = { center ->
175 | drawCircle(
176 | color,
177 | radius.toPx(),
178 | center,
179 | alpha,
180 | style,
181 | colorFilter,
182 | blendMode
183 | )
184 | }
185 | )
186 |
187 | /**
188 | * Configuration for the selection operation
189 | *
190 | * @param enabled if true, you can touch and drag to select the points. The point currently selected
191 | * is exposed via the [onSelection] param in the [LineGraph]. If false, the drag gesture is disabled.
192 | * @param highlight controls how the selection is represented in the graph. The default implementation
193 | * is a vertical dashed line. You can override this by supplying your own [Connection]
194 | * @param detectionTime the time taken for the touch to be recognised as a drag gesture
195 | */
196 | data class Selection(
197 | val enabled: Boolean = true,
198 | val highlight: Connection? = Connection(
199 | Color.Red,
200 | strokeWidth = 2.dp,
201 | pathEffect = PathEffect.dashPathEffect(floatArrayOf(40f, 20f))
202 | ),
203 | val detectionTime: Long = 100L,
204 | )
205 |
206 | /**
207 | * Controls the drawing behaviour of the area under the line. This is the region formed by intersection
208 | * of the Line, x-axis and y-axis.
209 | *
210 | * @param color Color to be applied to the path
211 | * @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing
212 | * fully transparent to fully opaque respectively
213 | * @param style Whether or not the path is stroked or filled in
214 | * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
215 | * @param blendMode Blending algorithm to be applied to the path when it is drawn
216 | * @param draw override this to change the default drawPath implementation. You are provided with
217 | * the [Path] of the line
218 | */
219 | data class AreaUnderLine(
220 | val color: Color = Color.Blue,
221 | val alpha: Float = 0.1f,
222 | val style: DrawStyle = Fill,
223 | val colorFilter: ColorFilter? = null,
224 | val blendMode: BlendMode = DrawScope.DefaultBlendMode,
225 | val draw: DrawScope.(Path) -> Unit = { path ->
226 | drawPath(path, color, alpha, style, colorFilter, blendMode)
227 | }
228 | )
229 |
230 | /**
231 | * Controls how the grid is drawn on the Graph
232 | *
233 | * @param color the color to be applied
234 | * @param steps the number of lines drawn in the grid. The default implementation considers this
235 | * as the horizontal lines
236 | * @param lineWidth the width of the lines
237 | * @param draw override this to change the default drawLine implementation (which is to draw multiple
238 | * horizontal lines based on the number of [steps]. You are provided with the [Rect] region available
239 | * to draw the grid, xOffset (the gap between two points in the x-axis) and the yOffset.
240 | */
241 | data class Grid(
242 | val color: Color,
243 | val steps: Int = 5,
244 | val lineWidth: Dp = 1.dp,
245 | val draw: DrawScope.(Rect, Float, Float) -> Unit = { region, _, _ ->
246 | val (left, top, right, bottom) = region
247 | val availableHeight = bottom - top
248 | val offset = availableHeight / if (steps > 1) steps - 1 else 1
249 | (0 until steps).forEach {
250 | val y = bottom - (it * offset)
251 | drawLine(
252 | color,
253 | Offset(left, y),
254 | Offset(right, y),
255 | lineWidth.toPx()
256 | )
257 | }
258 | }
259 | )
260 |
261 | /**
262 | * Configuration of the X Axis
263 | *
264 | * @param stepSize the distance between two adjacent data points
265 | * @param steps the number of values to be drawn in the axis
266 | * @param unit Represent the range of values in the x axis. For example if this is 1, then the values in
267 | * x axis would be (0, 1, 2, 3, ..., steps-1). If this is 0.1, then the values in x axis would be (0, 0.1,
268 | * 0.2, 0.3, ...)
269 | * @param paddingTop the top padding of the X axis
270 | * @param paddingBottom the bottom padding of the X axis
271 | * @param roundToInt if true, the values is X axis are represented by Integers. If false, the values could
272 | * be decimal values, with 1 decimal precision in the default implementation
273 | * @param content A composable where you could provide how the values should be rendered. The default
274 | * implementation is to show a [Text] composable. You are provided with the min value in x axis, the offset
275 | * between two x coordinates and the max value in x axis
276 | */
277 | data class XAxis(
278 | val stepSize: Dp = 20.dp,
279 | val steps: Int = 10,
280 | val unit: Float = 1f,
281 | val paddingTop: Dp = 8.dp,
282 | val paddingBottom: Dp = 8.dp,
283 | val roundToInt: Boolean = true,
284 | val content: @Composable (Float, Float, Float) -> Unit = { min, offset, max ->
285 | for (it in 0 until steps) {
286 | val value = it * offset + min
287 | Text(
288 | text = value.string(),
289 | maxLines = 1,
290 | overflow = TextOverflow.Ellipsis,
291 | style = MaterialTheme.typography.caption,
292 | color = MaterialTheme.colors.onSurface
293 | )
294 | if (value > max) {
295 | break
296 | }
297 | }
298 | }
299 | )
300 |
301 | /**
302 | * Configuration of the Y Axis
303 | *
304 | * @param steps the number of values to be drawn in the axis
305 | * @param roundToInt if true, the values is Y axis are represented by Integers. If false, the values could
306 | * be decimal values, with 1 decimal precision in the default implementation
307 | * @param paddingStart the start padding of the Y axis
308 | * @param paddingEnd the end padding of the Y axis
309 | * @param content A composable where you could provide how the values should be rendered. The default
310 | * implementation is to show a [Text] composable. You are provided with the min value in y axis, the offset
311 | * between two y coordinates and the max value in y axis
312 | */
313 | data class YAxis(
314 | val steps: Int = 5,
315 | val roundToInt: Boolean = true,
316 | val paddingStart: Dp = 16.dp,
317 | val paddingEnd: Dp = 8.dp,
318 | val content: @Composable (Float, Float, Float) -> Unit = { min, offset, _ ->
319 | for (it in 0 until steps) {
320 | val value = it * offset + min
321 | Text(
322 | text = value.string(),
323 | maxLines = 1,
324 | overflow = TextOverflow.Ellipsis,
325 | style = MaterialTheme.typography.caption,
326 | color = MaterialTheme.colors.onSurface
327 | )
328 | }
329 | }
330 | )
331 | }
332 |
333 | private fun Float.string() = DecimalFormat("#.#").format(this)
334 |
--------------------------------------------------------------------------------
/plot/src/main/java/com/madrapps/plot/line/LineGraph.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.plot.line
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.gestures.Orientation
6 | import androidx.compose.foundation.gestures.rememberScrollableState
7 | import androidx.compose.foundation.gestures.scrollable
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.fillMaxHeight
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.wrapContentHeight
13 | import androidx.compose.foundation.layout.wrapContentWidth
14 | import androidx.compose.material.MaterialTheme
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.CompositionLocalProvider
17 | import androidx.compose.runtime.mutableStateOf
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.clip
22 | import androidx.compose.ui.draw.clipToBounds
23 | import androidx.compose.ui.geometry.Offset
24 | import androidx.compose.ui.geometry.Rect
25 | import androidx.compose.ui.geometry.Size
26 | import androidx.compose.ui.graphics.Outline
27 | import androidx.compose.ui.graphics.Path
28 | import androidx.compose.ui.graphics.Shape
29 | import androidx.compose.ui.input.pointer.pointerInput
30 | import androidx.compose.ui.layout.onGloballyPositioned
31 | import androidx.compose.ui.platform.LocalDensity
32 | import androidx.compose.ui.platform.LocalLayoutDirection
33 | import androidx.compose.ui.unit.Density
34 | import androidx.compose.ui.unit.Dp
35 | import androidx.compose.ui.unit.LayoutDirection
36 | import androidx.compose.ui.unit.dp
37 | import com.madrapps.plot.GraphXAxis
38 | import com.madrapps.plot.GraphYAxis
39 | import com.madrapps.plot.detectDragZoomGesture
40 | import kotlin.math.ceil
41 |
42 | /**
43 | * A composable that draws a Line graph with the configurations provided by the [LinePlot]. The graph
44 | * can be scrolled, zoomed and touch dragged for selection. Every part of the line graph can be customized,
45 | * by changing the configuration in the [LinePlot].
46 | *
47 | * @param plot the configuration to render the full graph
48 | * @param modifier Modifier
49 | * @param onSelectionStart invoked when the selection has started
50 | * @param onSelectionEnd invoked when the selection has ended
51 | * @param onSelection invoked when selection changes from one point to the next. You are provided
52 | * with the xOffset where the selection occurred in the graph and the [DataPoint]s that are selected. If there
53 | * are multiple lines, you will get multiple data points.
54 | */
55 | @Composable
56 | fun LineGraph(
57 | plot: LinePlot, modifier: Modifier = Modifier,
58 | onSelectionStart: () -> Unit = {},
59 | onSelectionEnd: () -> Unit = {},
60 | onSelection: ((Float, List) -> Unit)? = null
61 | ) {
62 | val paddingTop = plot.paddingTop
63 | val paddingRight = plot.paddingRight
64 | val horizontalGap = plot.horizontalExtraSpace
65 | val isZoomAllowed = plot.isZoomAllowed
66 |
67 | val globalXScale = 1f
68 | val globalYScale = 1f
69 |
70 | val offset = remember { mutableStateOf(0f) }
71 | val maxScrollOffset = remember { mutableStateOf(0f) }
72 | val dragOffset = remember { mutableStateOf(0f) }
73 | val isDragging = remember { mutableStateOf(false) }
74 | val xZoom = remember { mutableStateOf(globalXScale) }
75 | val rowHeight = remember { mutableStateOf(0f) }
76 | val columnWidth = remember { mutableStateOf(0f) }
77 | val bgColor = MaterialTheme.colors.surface
78 |
79 | val lines = plot.lines
80 | val xUnit = plot.xAxis.unit
81 |
82 | CompositionLocalProvider(
83 | LocalLayoutDirection provides LayoutDirection.Ltr,
84 | ) {
85 | Box(
86 | modifier = modifier.clipToBounds(),
87 | ) {
88 | val points = lines.flatMap { it.dataPoints }
89 | val (xMin, xMax, xAxisScale) = getXAxisScale(points, plot)
90 | val (yMin, yMax, yAxisScale) = getYAxisScale(points, plot)
91 |
92 | Canvas(modifier = Modifier
93 | .align(Alignment.Center)
94 | .fillMaxHeight()
95 | .fillMaxWidth()
96 | .background(bgColor)
97 | .scrollable(
98 | state = rememberScrollableState { delta ->
99 | offset.value -= delta
100 | if (offset.value < 0f) offset.value = 0f
101 | if (offset.value > maxScrollOffset.value) {
102 | offset.value = maxScrollOffset.value
103 | }
104 | delta
105 | }, Orientation.Horizontal, enabled = true
106 | )
107 | .pointerInput(Unit, Unit) {
108 | detectDragZoomGesture(
109 | isZoomAllowed = isZoomAllowed,
110 | isDragAllowed = plot.selection.enabled,
111 | detectDragTimeOut = plot.selection.detectionTime,
112 | onDragStart = {
113 | dragOffset.value = it.x
114 | onSelectionStart()
115 | isDragging.value = true
116 | }, onDragEnd = {
117 | isDragging.value = false
118 | onSelectionEnd()
119 | }, onZoom = { zoom ->
120 | xZoom.value *= zoom
121 | }
122 | ) { change, _ ->
123 | dragOffset.value = change.position.x
124 | }
125 | },
126 | onDraw = {
127 | val xLeft = columnWidth.value + horizontalGap.toPx()
128 | val yBottom = size.height - rowHeight.value
129 | val xOffset = 20.dp.toPx() * xZoom.value
130 | val maxElementInYAxis =
131 | getMaxElementInYAxis(yAxisScale, plot.yAxis.steps)
132 | val yOffset = ((yBottom - paddingTop.toPx()) / maxElementInYAxis) * globalYScale
133 |
134 | val xLastPoint =
135 | (xMax - xMin) * xOffset * (1 / xUnit) + xLeft + paddingRight.toPx() + horizontalGap.toPx()
136 | maxScrollOffset.value = if (xLastPoint > size.width) {
137 | xLastPoint - size.width
138 | } else 0f
139 |
140 | val dragLocks = mutableMapOf>()
141 |
142 | // Draw Grid lines
143 | val top = yBottom - ((yMax - yMin) * yOffset)
144 | val region =
145 | Rect(xLeft, top, size.width - paddingRight.toPx(), yBottom)
146 | plot.grid?.draw?.invoke(this, region, xOffset * (1 / xUnit), yOffset)
147 |
148 | // Draw Lines and Points and AreaUnderLine
149 | lines.forEach { line ->
150 | val intersection = line.intersection
151 | val connection = line.connection
152 | val areaUnderLine = line.areaUnderLine
153 |
154 | // Draw area under curve
155 | if (areaUnderLine != null) {
156 | val pts = line.dataPoints.map { (x, y) ->
157 | val x1 = ((x - xMin) * xOffset * (1 / xUnit)) + xLeft - offset.value
158 | val y1 = yBottom - ((y - yMin) * yOffset)
159 | Offset(x1, y1)
160 | }
161 | val p = Path()
162 | pts.forEachIndexed { index, offset ->
163 | if (index == 0) {
164 | p.moveTo(offset.x, yBottom)
165 | }
166 | p.lineTo(offset.x, offset.y)
167 | }
168 | val last = pts.last()
169 | val first = pts.first()
170 | p.lineTo(last.x, yBottom)
171 | p.lineTo(first.x, yBottom)
172 | areaUnderLine.draw(this, p)
173 | }
174 |
175 | // Draw Lines and Points
176 | var curOffset: Offset? = null
177 | var nextOffset: Offset? = null
178 | line.dataPoints.forEachIndexed { i, _ ->
179 | if (i == 0) {
180 | val (x, y) = line.dataPoints[i]
181 | val x1 = ((x - xMin) * xOffset * (1 / xUnit)) + xLeft - offset.value
182 | val y1 = yBottom - ((y - yMin) * yOffset)
183 | curOffset = Offset(x1, y1)
184 | }
185 | if (line.dataPoints.indices.contains(i + 1)) {
186 | val (x, y) = line.dataPoints[i + 1]
187 | val x2 = ((x - xMin) * xOffset * (1 / xUnit)) + xLeft - offset.value
188 | val y2 = yBottom - ((y - yMin) * yOffset)
189 | nextOffset = Offset(x2, y2)
190 | }
191 | if (nextOffset != null && curOffset != null) {
192 | connection?.draw?.invoke(
193 | this,
194 | curOffset!!,
195 | nextOffset!!
196 | )
197 | }
198 | curOffset?.let {
199 | if (isDragging.value && isDragLocked(
200 | dragOffset.value,
201 | it,
202 | xOffset
203 | )
204 | ) {
205 | dragLocks[line] = line.dataPoints[i] to it
206 | } else {
207 | intersection?.draw?.invoke(this, it, line.dataPoints[i])
208 | }
209 | }
210 | curOffset = nextOffset
211 | }
212 | }
213 |
214 | // Draw column
215 | drawRect(
216 | bgColor,
217 | Offset(0f, 0f),
218 | Size(columnWidth.value, size.height)
219 | )
220 |
221 | // Draw right padding
222 | drawRect(
223 | bgColor,
224 | Offset(size.width - paddingRight.toPx(), 0f),
225 | Size(paddingRight.toPx(), size.height)
226 | )
227 |
228 | // Draw drag selection Highlight
229 | if (isDragging.value) {
230 | // Draw Drag Line highlight
231 | dragLocks.values.firstOrNull()?.let { (_, location) ->
232 | val (x, _) = location
233 | if (x >= columnWidth.value && x <= size.width - paddingRight.toPx()) {
234 | plot.selection.highlight?.draw?.invoke(
235 | this,
236 | Offset(x, yBottom),
237 | Offset(x, 0f)
238 | )
239 | }
240 | }
241 | // Draw Point Highlight
242 | dragLocks.entries.forEach { (line, lock) ->
243 | val highlight = line.highlight
244 | val location = lock.second
245 | val x = location.x
246 | if (x >= columnWidth.value && x <= size.width - paddingRight.toPx()) {
247 | highlight?.draw?.invoke(this, location)
248 | }
249 | }
250 | }
251 |
252 | // OnSelection
253 | if (isDragging.value) {
254 | val x = dragLocks.values.firstOrNull()?.second?.x
255 | if (x != null) {
256 | onSelection?.invoke(x, dragLocks.values.map { it.first })
257 | }
258 | }
259 | })
260 |
261 | GraphXAxis(
262 | Modifier
263 | .align(Alignment.BottomStart)
264 | .fillMaxWidth()
265 | .wrapContentHeight()
266 | .clip(
267 | RowClip(
268 | columnWidth.value,
269 | paddingRight
270 | )
271 | )
272 | .onGloballyPositioned {
273 | rowHeight.value = it.size.height.toFloat()
274 | }
275 | .padding(bottom = plot.xAxis.paddingBottom, top = plot.xAxis.paddingTop),
276 | columnWidth.value + horizontalGap.value * LocalDensity.current.density,
277 | offset.value,
278 | xZoom.value * xAxisScale * (1 / xUnit),
279 | stepSize = plot.xAxis.stepSize,
280 | ) {
281 | plot.xAxis.content(xMin, xAxisScale, xMax)
282 | }
283 |
284 | GraphYAxis(
285 | Modifier
286 | .align(Alignment.TopStart)
287 | .fillMaxHeight()
288 | .wrapContentWidth()
289 | .onGloballyPositioned {
290 | columnWidth.value = it.size.width.toFloat()
291 | }
292 | .padding(start = plot.yAxis.paddingStart, end = plot.yAxis.paddingEnd),
293 | paddingTop = paddingTop.value * LocalDensity.current.density,
294 | paddingBottom = rowHeight.value,
295 | scale = globalYScale,
296 | ) {
297 | plot.yAxis.content(yMin, yAxisScale, yMax)
298 | }
299 | }
300 | }
301 | }
302 |
303 | private fun isDragLocked(dragOffset: Float, it: Offset, xOffset: Float) =
304 | ((dragOffset) > it.x - xOffset / 2) && ((dragOffset) < it.x + xOffset / 2)
305 |
306 | private fun getXAxisScale(
307 | points: List,
308 | plot: LinePlot
309 | ): Triple {
310 | val xMin = points.minOf { it.x }
311 | val xMax = points.maxOf { it.x }
312 | val totalSteps =
313 | (xMax - xMin) + 1
314 | val temp = totalSteps / plot.xAxis.steps
315 | val scale = if (plot.xAxis.roundToInt) ceil(temp) else temp
316 | return Triple(xMin, xMax, scale)
317 | }
318 |
319 | private fun getYAxisScale(
320 | points: List,
321 | plot: LinePlot
322 | ): Triple {
323 | val steps = plot.yAxis.steps
324 | val yMin = points.minOf { it.y }
325 | val yMax = points.maxOf { it.y }
326 |
327 | val totalSteps = (yMax - yMin)
328 | val temp = totalSteps / if (steps > 1) (steps - 1) else 1
329 |
330 | val scale = if (plot.yAxis.roundToInt) ceil(temp) else temp
331 | return Triple(yMin, yMax, scale)
332 | }
333 |
334 | private fun getMaxElementInYAxis(offset: Float, steps: Int): Float {
335 | return (if (steps > 1) steps - 1 else 1) * offset
336 | }
337 |
338 | private class RowClip(private val leftPadding: Float, private val rightPadding: Dp) : Shape {
339 | override fun createOutline(
340 | size: Size,
341 | layoutDirection: LayoutDirection,
342 | density: Density
343 | ): Outline {
344 | return Outline.Rectangle(
345 | Rect(
346 | leftPadding,
347 | 0f,
348 | size.width - rightPadding.value * density.density,
349 | size.height
350 | )
351 | )
352 | }
353 | }
354 |
--------------------------------------------------------------------------------
/preview/line_graph_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/preview/line_graph_1.png
--------------------------------------------------------------------------------
/preview/line_graph_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/preview/line_graph_2.png
--------------------------------------------------------------------------------
/preview/line_graph_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/preview/line_graph_3.png
--------------------------------------------------------------------------------
/preview/line_graph_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/preview/line_graph_4.png
--------------------------------------------------------------------------------
/preview/line_graph_recording.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/preview/line_graph_recording.gif
--------------------------------------------------------------------------------
/preview/line_graph_recording.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/preview/line_graph_recording.mp4
--------------------------------------------------------------------------------
/sample/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("kotlin-android")
4 | }
5 |
6 | android {
7 | compileSdk = 30
8 | buildToolsVersion = "30.0.3"
9 |
10 | defaultConfig {
11 | applicationId = "com.madrapps.plot"
12 | minSdk = 21
13 | targetSdk = 30
14 | versionCode = 1
15 | versionName = "0.1.0"
16 |
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary = true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = false
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android-optimize.txt"),
28 | "proguard-rules.pro"
29 | )
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility = JavaVersion.VERSION_1_8
34 | targetCompatibility = JavaVersion.VERSION_1_8
35 | }
36 | kotlinOptions {
37 | jvmTarget = "1.8"
38 | useIR = true
39 | }
40 | buildFeatures {
41 | compose = true
42 | }
43 | composeOptions {
44 | kotlinCompilerExtensionVersion = rootProject.extra["compose_version"] as String
45 | }
46 | lint {
47 | isAbortOnError = true
48 | isWarningsAsErrors = true
49 | }
50 | }
51 |
52 | dependencies {
53 | // implementation(project(mapOf("path" to ":plot")))
54 | implementation("com.github.madrapps:plot:0.1.1")
55 |
56 | implementation("androidx.core:core-ktx:1.6.0")
57 | implementation("androidx.appcompat:appcompat:1.3.1")
58 | implementation("com.google.android.material:material:1.4.0")
59 | implementation("androidx.compose.ui:ui:${rootProject.extra["compose_version"]}")
60 | implementation("androidx.compose.material:material:${rootProject.extra["compose_version"]}")
61 | implementation("androidx.compose.ui:ui-tooling:${rootProject.extra["compose_version"]}")
62 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
63 | implementation("androidx.activity:activity-compose:1.3.1")
64 | implementation("androidx.activity:activity-ktx:1.3.1")
65 | implementation("androidx.compose.runtime:runtime-livedata:${rootProject.extra["compose_version"]}")
66 |
67 | testImplementation("junit:junit:4.13.2")
68 | androidTestImplementation("androidx.test.ext:junit:1.1.3")
69 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
70 | androidTestImplementation("androidx.compose.ui:ui-test-junit4:${rootProject.extra["compose_version"]}")
71 | }
--------------------------------------------------------------------------------
/sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.viewModels
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.foundation.lazy.itemsIndexed
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.unit.dp
15 | import com.madrapps.sample.linegraph.LineGraph1
16 | import com.madrapps.sample.linegraph.LineGraph2
17 | import com.madrapps.sample.linegraph.LineGraph3
18 | import com.madrapps.sample.linegraph.LineGraph4
19 | import com.madrapps.sample.linegraph.LineGraph5
20 | import com.madrapps.sample.ui.theme.PlotTheme
21 |
22 | class MainActivity : ComponentActivity() {
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | val model: MainViewModelImpl by viewModels()
26 | setContent {
27 | PlotTheme {
28 | LazyColumn(Modifier.fillMaxWidth()) {
29 | itemsIndexed(model.lines.value) { i, item ->
30 | when (i) {
31 | 0 -> LineGraph2(item)
32 | 1 -> Column(
33 | Modifier
34 | .fillMaxWidth()
35 | .padding(bottom = 16.dp)
36 | ) {
37 | LineGraph4(
38 | item,
39 | modifier = Modifier.align(Alignment.CenterHorizontally)
40 | )
41 | }
42 | 2 -> LineGraph1(item)
43 | 3 -> LineGraph3(item)
44 | 4 -> LineGraph5(item)
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/MainViewModelImpl.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.lifecycle.ViewModel
7 | import com.madrapps.plot.line.DataPoint
8 | import com.madrapps.sample.linegraph.DataPoints.dataPoints1
9 | import com.madrapps.sample.linegraph.DataPoints.dataPoints2
10 | import com.madrapps.sample.linegraph.DataPoints.dataPoints3
11 | import com.madrapps.sample.linegraph.DataPoints.dataPoints4
12 | import com.madrapps.sample.linegraph.DataPoints.dataPoints6
13 | import kotlin.random.Random
14 |
15 | class MainViewModelImpl : MainViewModel, ViewModel() {
16 |
17 | override val line1: MutableState> = mutableStateOf(dataPoints1)
18 |
19 | override fun change() {
20 | val nextInt = 1 + Random.Default.nextInt(5)
21 | val nextInt1 = 1 + Random.Default.nextInt(5)
22 | line1.value = dataPoints1.map { DataPoint(it.x * nextInt, it.y * nextInt1) }
23 | }
24 |
25 | override val lines: State>>> = mutableStateOf(
26 | listOf(
27 | listOf(dataPoints1, dataPoints2),
28 | listOf(dataPoints3, dataPoints2),
29 | listOf(dataPoints1, dataPoints2),
30 | listOf(dataPoints1, dataPoints2),
31 | listOf(dataPoints6, dataPoints4),
32 | )
33 | )
34 | }
35 |
36 | interface MainViewModel {
37 |
38 | val line1: State>
39 | val lines: State>>>
40 |
41 | fun change()
42 | }
43 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample
2 |
3 | import androidx.compose.ui.geometry.CornerRadius
4 | import androidx.compose.ui.geometry.RoundRect
5 | import androidx.compose.ui.geometry.Size
6 | import androidx.compose.ui.geometry.toRect
7 | import androidx.compose.ui.graphics.Outline
8 | import androidx.compose.ui.graphics.Shape
9 | import androidx.compose.ui.unit.Density
10 | import androidx.compose.ui.unit.Dp
11 | import androidx.compose.ui.unit.LayoutDirection
12 | import androidx.compose.ui.unit.dp
13 |
14 | val RoundRectangle: Shape = object : Shape {
15 | override fun createOutline(
16 | size: Size,
17 | layoutDirection: LayoutDirection,
18 | density: Density
19 | ): Outline.Rounded {
20 | val radius = 8.dp.value * density.density
21 | return Outline.Rounded(RoundRect(size.toRect(), CornerRadius(radius, radius)))
22 | }
23 |
24 | override fun toString(): String = "RoundRectangleShape"
25 | }
26 |
27 | internal fun Dp.toPx(density: Density) = value * density.density
28 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/linegraph/DataPoints.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.linegraph
2 |
3 | import com.madrapps.plot.line.DataPoint
4 |
5 | object DataPoints {
6 | val dataPoints1 = listOf(
7 | DataPoint(0f, 0f),
8 | DataPoint(1f, 20f),
9 | DataPoint(2f, 50f),
10 | DataPoint(3f, 10f),
11 | DataPoint(4f, 0f),
12 | DataPoint(5f, -25f),
13 | DataPoint(6f, -75f),
14 | DataPoint(7f, -100f),
15 | DataPoint(8f, -80f),
16 | DataPoint(9f, -75f),
17 | DataPoint(10f, -55f),
18 | DataPoint(11f, -45f),
19 | DataPoint(12f, 50f),
20 | DataPoint(13f, 80f),
21 | DataPoint(14f, 70f),
22 | DataPoint(15f, 125f),
23 | DataPoint(16f, 200f),
24 | DataPoint(17f, 170f),
25 | DataPoint(18f, 135f),
26 | DataPoint(19f, 60f),
27 | DataPoint(20f, 20f),
28 | DataPoint(21f, 40f),
29 | DataPoint(22f, 75f),
30 | DataPoint(23f, 50f),
31 | )
32 |
33 | val dataPoints2 = listOf(
34 | DataPoint(0f, 0f),
35 | DataPoint(1f, 0f),
36 | DataPoint(2f, 25f),
37 | DataPoint(3f, 75f),
38 | DataPoint(4f, 100f),
39 | DataPoint(5f, 80f),
40 | DataPoint(6f, 75f),
41 | DataPoint(7f, 50f),
42 | DataPoint(8f, 80f),
43 | DataPoint(9f, 70f),
44 | DataPoint(10f, 0f),
45 | DataPoint(11f, 0f),
46 | DataPoint(12f, 45f),
47 | DataPoint(13f, 20f),
48 | DataPoint(14f, 40f),
49 | DataPoint(15f, 75f),
50 | DataPoint(16f, 50f),
51 | DataPoint(17f, 75f),
52 | DataPoint(18f, 40f),
53 | DataPoint(19f, 20f),
54 | DataPoint(20f, 0f),
55 | DataPoint(21f, 0f),
56 | DataPoint(22f, 50f),
57 | DataPoint(23f, 25f),
58 | )
59 |
60 | val dataPoints3 = listOf(
61 | DataPoint(0f, 0f),
62 | DataPoint(1f, 0f),
63 | DataPoint(2f, 0f),
64 | DataPoint(3f, 0f),
65 | DataPoint(4f, 0f),
66 | DataPoint(5f, 25f),
67 | DataPoint(6f, 75f),
68 | DataPoint(7f, 100f),
69 | DataPoint(8f, 80f),
70 | DataPoint(9f, 75f),
71 | DataPoint(10f, 55f),
72 | DataPoint(11f, 45f),
73 | DataPoint(12f, 50f),
74 | DataPoint(13f, 80f),
75 | DataPoint(14f, 70f),
76 | DataPoint(15f, 25f),
77 | DataPoint(16f, 0f),
78 | DataPoint(17f, 0f),
79 | DataPoint(18f, 35f),
80 | DataPoint(19f, 60f),
81 | DataPoint(20f, 20f),
82 | DataPoint(21f, 40f),
83 | DataPoint(22f, 75f),
84 | DataPoint(23f, 50f),
85 | )
86 |
87 | val dataPoints4 = listOf(
88 | DataPoint(13f, 20f),
89 | DataPoint(14f, 40f),
90 | DataPoint(15f, 75f),
91 | DataPoint(16f, 50f),
92 | DataPoint(17f, 75f),
93 | DataPoint(18f, 40f),
94 | DataPoint(19f, 20f),
95 | DataPoint(20f, 0f),
96 | DataPoint(21f, 0f),
97 | DataPoint(22f, 50f),
98 | DataPoint(23f, 25f),
99 | )
100 |
101 | val dataPoints6 = listOf(
102 | DataPoint(-0.6f, -1f),
103 | DataPoint(-0.5f, 0f),
104 | DataPoint(-0.4f, 0.5f),
105 | DataPoint(-0.3f, 1f),
106 | DataPoint(-0.2f, 2f),
107 | DataPoint(-0.1f, 1.2f),
108 | DataPoint(-0f, 0.0f),
109 | DataPoint(0.1f, 0.3f),
110 | DataPoint(0.3f, 0.25f),
111 | DataPoint(0.4f, 0.75f),
112 | DataPoint(0.5f, 0.80f),
113 | DataPoint(0.8f, 1f),
114 | DataPoint(0.9f, 1.45f),
115 | DataPoint(1.2f, 0.50f),
116 | DataPoint(1.3f, 1.50f),
117 | DataPoint(1.4f, 0.80f),
118 | DataPoint(1.5f, 1.70f),
119 | DataPoint(1.6f, 2f),
120 | DataPoint(1.7f, 0f),
121 | )
122 | }
123 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/linegraph/LineGraph1.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.linegraph
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.geometry.Offset
8 | import androidx.compose.ui.geometry.Size
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.PathEffect
11 | import androidx.compose.ui.tooling.preview.Preview
12 | import androidx.compose.ui.unit.dp
13 | import com.madrapps.plot.line.DataPoint
14 | import com.madrapps.plot.line.LineGraph
15 | import com.madrapps.plot.line.LinePlot
16 | import com.madrapps.sample.ui.theme.Green900
17 | import com.madrapps.sample.ui.theme.LightGreen600
18 | import com.madrapps.sample.ui.theme.PlotTheme
19 |
20 | @Composable
21 | internal fun LineGraph1(lines: List>) {
22 | LineGraph(
23 | plot = LinePlot(
24 | listOf(
25 | LinePlot.Line(
26 | lines[0],
27 | LinePlot.Connection(LightGreen600, 2.dp),
28 | LinePlot.Intersection(LightGreen600, 5.dp),
29 | LinePlot.Highlight(Green900, 5.dp),
30 | LinePlot.AreaUnderLine(LightGreen600, 0.3f)
31 | ),
32 | LinePlot.Line(
33 | lines[1],
34 | LinePlot.Connection(Color.Gray, 2.dp),
35 | LinePlot.Intersection { center, _ ->
36 | val px = 2.dp.toPx()
37 | val topLeft = Offset(center.x - px, center.y - px)
38 | drawRect(Color.Gray, topLeft, Size(px * 2, px * 2))
39 | },
40 | ),
41 | ),
42 | selection = LinePlot.Selection(
43 | highlight = LinePlot.Connection(
44 | Green900,
45 | strokeWidth = 2.dp,
46 | pathEffect = PathEffect.dashPathEffect(floatArrayOf(40f, 20f))
47 | )
48 | ),
49 | ),
50 | modifier = Modifier
51 | .fillMaxWidth()
52 | .height(200.dp)
53 | )
54 | }
55 |
56 | @Preview(showBackground = true)
57 | @Composable
58 | fun LineGraph1Preview() {
59 | PlotTheme {
60 | LineGraph1(listOf(DataPoints.dataPoints1, DataPoints.dataPoints2))
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/linegraph/LineGraph2.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.linegraph
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.tooling.preview.Preview
9 | import androidx.compose.ui.unit.dp
10 | import com.madrapps.plot.line.DataPoint
11 | import com.madrapps.plot.line.LineGraph
12 | import com.madrapps.plot.line.LinePlot
13 | import com.madrapps.sample.ui.theme.PlotTheme
14 |
15 | @Composable
16 | internal fun LineGraph2(lines: List>) {
17 | LineGraph(
18 | plot = LinePlot(
19 | listOf(
20 | LinePlot.Line(
21 | lines[1],
22 | LinePlot.Connection(Color.Gray, 2.dp),
23 | null,
24 | LinePlot.Highlight { center ->
25 | val color = Color.Gray
26 | drawCircle(color, 9.dp.toPx(), center, alpha = 0.3f)
27 | drawCircle(color, 6.dp.toPx(), center)
28 | drawCircle(Color.White, 3.dp.toPx(), center)
29 | },
30 | ),
31 | LinePlot.Line(
32 | lines[0],
33 | LinePlot.Connection(Color.Blue, 3.dp),
34 | LinePlot.Intersection(Color.Blue, 6.dp) { center, point ->
35 | val x = point.x
36 | val rad = if (x % 4f == 0f) 6.dp else 3.dp
37 | drawCircle(
38 | Color.Blue,
39 | rad.toPx(),
40 | center,
41 | )
42 | },
43 | LinePlot.Highlight { center ->
44 | val color = Color.Blue
45 | drawCircle(color, 9.dp.toPx(), center, alpha = 0.3f)
46 | drawCircle(color, 6.dp.toPx(), center)
47 | drawCircle(Color.White, 3.dp.toPx(), center)
48 | },
49 | LinePlot.AreaUnderLine(Color.Blue, 0.1f)
50 | ),
51 | ), LinePlot.Grid(Color.Gray), paddingRight = 16.dp
52 | ),
53 | modifier = Modifier
54 | .fillMaxWidth()
55 | .height(200.dp)
56 | )
57 | }
58 |
59 |
60 | @Preview(showBackground = true)
61 | @Composable
62 | fun LineGraph2Preview() {
63 | PlotTheme {
64 | LineGraph2(listOf(DataPoints.dataPoints1, DataPoints.dataPoints2))
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/linegraph/LineGraph3.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.linegraph
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.height
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.geometry.Offset
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.text.style.TextOverflow
14 | import androidx.compose.ui.tooling.preview.Preview
15 | import androidx.compose.ui.unit.dp
16 | import com.madrapps.plot.line.DataPoint
17 | import com.madrapps.plot.line.LineGraph
18 | import com.madrapps.plot.line.LinePlot
19 | import com.madrapps.sample.ui.theme.PlotTheme
20 | import java.text.DecimalFormat
21 |
22 | @Composable
23 | internal fun LineGraph3(lines: List>) {
24 | LineGraph(
25 | plot = LinePlot(
26 | listOf(
27 | LinePlot.Line(
28 | lines[0],
29 | LinePlot.Connection(Color.Blue, 2.dp),
30 | LinePlot.Intersection(Color.Blue, 4.dp),
31 | LinePlot.Highlight(Color.Red, 6.dp),
32 | LinePlot.AreaUnderLine(Color.Blue, 0.1f)
33 | )
34 | ), LinePlot.Grid(Color.LightGray.copy(0.5f)),
35 | xAxis = LinePlot.XAxis(steps = 24) { min, offset, max ->
36 | for (it in 0 until 24) {
37 | val value = it * offset + min
38 | androidx.compose.foundation.layout.Column {
39 | val isMajor = value % 4 == 0f
40 | val radius = if (isMajor) 6f else 3f
41 | val color = MaterialTheme.colors.onSurface
42 | Canvas(
43 | modifier = Modifier
44 | .align(Alignment.CenterHorizontally)
45 | .height(20.dp),
46 | onDraw = {
47 | drawCircle(
48 | color = color,
49 | radius * density,
50 | Offset(0f, 10f * density)
51 | )
52 | })
53 | if (isMajor) {
54 | Text(
55 | text = DecimalFormat("#.#").format(value),
56 | maxLines = 1,
57 | overflow = TextOverflow.Ellipsis,
58 | style = MaterialTheme.typography.caption,
59 | color = color
60 | )
61 | }
62 | }
63 | if (value > max) {
64 | break
65 | }
66 | }
67 | }, paddingRight = 8.dp
68 | ),
69 | modifier = Modifier
70 | .fillMaxWidth()
71 | .height(200.dp)
72 | )
73 | }
74 |
75 | @Preview(showBackground = true)
76 | @Composable
77 | fun LineGraph3Preview() {
78 | PlotTheme {
79 | LineGraph3(listOf(DataPoints.dataPoints1, DataPoints.dataPoints2))
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/linegraph/LineGraph4.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.linegraph
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.material.MaterialTheme
13 | import androidx.compose.material.Surface
14 | import androidx.compose.material.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.mutableStateOf
17 | import androidx.compose.runtime.remember
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.draw.clip
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.graphics.graphicsLayer
23 | import androidx.compose.ui.graphics.painter.ColorPainter
24 | import androidx.compose.ui.layout.onGloballyPositioned
25 | import androidx.compose.ui.platform.LocalDensity
26 | import androidx.compose.ui.tooling.preview.Preview
27 | import androidx.compose.ui.unit.dp
28 | import com.madrapps.plot.line.DataPoint
29 | import com.madrapps.plot.line.LineGraph
30 | import com.madrapps.plot.line.LinePlot
31 | import com.madrapps.sample.RoundRectangle
32 | import com.madrapps.sample.toPx
33 | import com.madrapps.sample.ui.theme.GreyCustom
34 | import com.madrapps.sample.ui.theme.PlotTheme
35 | import java.text.DecimalFormat
36 |
37 | @Composable
38 | internal fun LineGraph4(lines: List>, modifier: Modifier) {
39 | val totalWidth = remember { mutableStateOf(0) }
40 | Column(Modifier.onGloballyPositioned {
41 | totalWidth.value = it.size.width
42 | }) {
43 | val xOffset = remember { mutableStateOf(0f) }
44 | val cardWidth = remember { mutableStateOf(0) }
45 | val visibility = remember { mutableStateOf(false) }
46 | val points = remember { mutableStateOf(listOf()) }
47 | val density = LocalDensity.current
48 |
49 | Box(Modifier.height(150.dp)) {
50 | if (visibility.value) {
51 | Surface(
52 | modifier = Modifier
53 | .width(200.dp)
54 | .align(Alignment.BottomCenter)
55 | .onGloballyPositioned {
56 | cardWidth.value = it.size.width
57 | }
58 | .graphicsLayer(translationX = xOffset.value),
59 | shape = RoundRectangle,
60 | color = GreyCustom
61 | ) {
62 | Column(
63 | Modifier
64 | .padding(horizontal = 8.dp)
65 | ) {
66 | val value = points.value
67 | if (value.isNotEmpty()) {
68 | val x = DecimalFormat("#.#").format(value[0].x)
69 | Text(
70 | modifier = Modifier.padding(vertical = 8.dp),
71 | text = "Score at $x:00 hrs",
72 | style = MaterialTheme.typography.subtitle1,
73 | color = Color.Gray
74 | )
75 | ScoreRow("Today", value[1].y, Color.Blue)
76 | ScoreRow("Yesterday", value[0].y, Color.Gray)
77 | }
78 | }
79 | }
80 |
81 | }
82 | }
83 | val padding = 16.dp
84 | MaterialTheme(colors = MaterialTheme.colors.copy(surface = Color.White)) {
85 | LineGraph(
86 | plot = LinePlot(
87 | listOf(
88 | LinePlot.Line(
89 | lines[1],
90 | LinePlot.Connection(Color.Gray, 2.dp),
91 | null,
92 | LinePlot.Highlight { center ->
93 | val color = Color.Gray
94 | drawCircle(color, 9.dp.toPx(), center, alpha = 0.3f)
95 | drawCircle(color, 6.dp.toPx(), center)
96 | drawCircle(Color.White, 3.dp.toPx(), center)
97 | },
98 | ),
99 | LinePlot.Line(
100 | lines[0],
101 | LinePlot.Connection(),
102 | LinePlot.Intersection(),
103 | LinePlot.Highlight { center ->
104 | val color = Color.Blue
105 | drawCircle(color, 9.dp.toPx(), center, alpha = 0.3f)
106 | drawCircle(color, 6.dp.toPx(), center)
107 | drawCircle(Color.White, 3.dp.toPx(), center)
108 | },
109 | LinePlot.AreaUnderLine()
110 | ),
111 | ),
112 | ),
113 | modifier = modifier
114 | .fillMaxWidth()
115 | .height(200.dp)
116 | .padding(horizontal = padding),
117 | onSelectionStart = { visibility.value = true },
118 | onSelectionEnd = { visibility.value = false }
119 | ) { x, pts ->
120 | val cWidth = cardWidth.value.toFloat()
121 | var xCenter = x + padding.toPx(density)
122 | xCenter = when {
123 | xCenter + cWidth / 2f > totalWidth.value -> totalWidth.value - cWidth
124 | xCenter - cWidth / 2f < 0f -> 0f
125 | else -> xCenter - cWidth / 2f
126 | }
127 | xOffset.value = xCenter
128 | points.value = pts
129 | }
130 | }
131 | }
132 | }
133 |
134 | @Composable
135 | private fun ScoreRow(title: String, value: Float, color: Color) {
136 | val formatted = DecimalFormat("#.#").format(value)
137 | Box(
138 | Modifier
139 | .fillMaxWidth()
140 | .padding(bottom = 8.dp)
141 | ) {
142 | Row(modifier = Modifier.align(Alignment.CenterStart)) {
143 | Image(
144 | painter = ColorPainter(color), contentDescription = "Line color",
145 | modifier = Modifier
146 | .align(Alignment.CenterVertically)
147 | .padding(end = 4.dp)
148 | .size(10.dp)
149 | .clip(RoundRectangle)
150 | )
151 | Text(
152 | text = title,
153 | style = MaterialTheme.typography.subtitle1,
154 | color = Color.DarkGray
155 | )
156 | }
157 | Text(
158 | modifier = Modifier
159 | .padding(end = 8.dp)
160 | .align(Alignment.CenterEnd),
161 | text = formatted,
162 | style = MaterialTheme.typography.subtitle2,
163 | color = Color.DarkGray
164 | )
165 | }
166 | }
167 |
168 | @Preview(showBackground = true)
169 | @Composable
170 | fun LineGraph4Preview() {
171 | PlotTheme {
172 | LineGraph4(listOf(DataPoints.dataPoints1, DataPoints.dataPoints2), Modifier)
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/linegraph/LineGraph5.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.linegraph
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.tooling.preview.Preview
8 | import androidx.compose.ui.unit.dp
9 | import com.madrapps.plot.line.DataPoint
10 | import com.madrapps.plot.line.LineGraph
11 | import com.madrapps.plot.line.LinePlot
12 | import com.madrapps.sample.ui.theme.PlotTheme
13 | import com.madrapps.sample.ui.theme.Red100
14 | import com.madrapps.sample.ui.theme.Red300
15 | import com.madrapps.sample.ui.theme.Red500
16 | import com.madrapps.sample.ui.theme.Yellow700
17 |
18 | @Composable
19 | internal fun LineGraph5(lines: List>) {
20 | LineGraph(
21 | plot = LinePlot(
22 | listOf(
23 | LinePlot.Line(
24 | lines[0],
25 | LinePlot.Connection(color = Red300),
26 | LinePlot.Intersection(color = Red500),
27 | LinePlot.Highlight(color = Yellow700),
28 | )
29 | ),
30 | horizontalExtraSpace = 12.dp,
31 | xAxis = LinePlot.XAxis(unit = 0.1f, roundToInt = false),
32 | yAxis = LinePlot.YAxis(steps = 4, roundToInt = false),
33 | grid = LinePlot.Grid(Red100, steps = 4),
34 | ),
35 | modifier = Modifier
36 | .fillMaxWidth()
37 | .height(200.dp)
38 | )
39 | }
40 |
41 | @Preview(showBackground = true)
42 | @Composable
43 | fun LineGraph5Preview() {
44 | PlotTheme {
45 | LineGraph5(listOf(DataPoints.dataPoints1, DataPoints.dataPoints2))
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
9 | val GreyCustom = Color(0xFFEBEFF3)
10 | val Grey50 = Color(0xFFFAFAFA)
11 | val LightGreen600 = Color(0xFF7cb342)
12 | val Green900 = Color(0xFF1b5e20)
13 | val Yellow700 = Color(0xFFfbc02d)
14 | val Red500 = Color(0xFFf44336)
15 | val Red300 = Color(0xFFe57373)
16 | val Red100 = Color(0xFFffcdd2)
17 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
12 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.graphics.Color
9 |
10 | private val DarkColorPalette = darkColors(
11 | primary = Purple200,
12 | primaryVariant = Purple700,
13 | secondary = Teal200,
14 | background = Color.White,
15 | onSurface = Color.Black,
16 | surface = Grey50
17 | )
18 |
19 | private val LightColorPalette = lightColors(
20 | primary = Purple500,
21 | primaryVariant = Purple700,
22 | secondary = Teal200,
23 | background = Color.White,
24 | onSurface = Color.Black,
25 | surface = Grey50
26 |
27 | /* Other default colors to override
28 | background = Color.White,
29 | surface = Color.White,
30 | onPrimary = Color.White,
31 | onSecondary = Color.Black,
32 | onBackground = Color.Black,
33 | onSurface = Color.Black,
34 | */
35 | )
36 |
37 | @Composable
38 | fun PlotTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
39 | val colors = if (darkTheme) {
40 | DarkColorPalette
41 | } else {
42 | LightColorPalette
43 | }
44 |
45 | MaterialTheme(
46 | colors = colors,
47 | typography = Typography,
48 | shapes = Shapes,
49 | content = content
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/madrapps/sample/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.madrapps.sample.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | ),
16 | caption = TextStyle(
17 | fontFamily = FontFamily.Default,
18 | fontWeight = FontWeight.Normal,
19 | fontSize = 14.sp,
20 | lineHeight = 18.sp
21 | ),
22 | subtitle1 = TextStyle(
23 | fontFamily = FontFamily.SansSerif,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 16.sp,
26 | lineHeight = 18.sp
27 | ),
28 | subtitle2 = TextStyle(
29 | fontFamily = FontFamily.SansSerif,
30 | fontWeight = FontWeight.Medium,
31 | fontSize = 16.sp,
32 | lineHeight = 18.sp
33 | )
34 |
35 | )
36 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Madrapps/plot/4fde7d442002e7e57597cb5a904d978f57a1b2c7/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | plot
3 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | jcenter() // Warning: this repository is going to shut down soon
7 | }
8 | }
9 | rootProject.name = "plot"
10 | include(":sample")
11 | include(":plot")
12 |
--------------------------------------------------------------------------------