的
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
## :loudspeaker: Type of change
|
||||
<!--- Put an `x` in the boxes that apply, remove checkboxes from remaining -->
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] New feature
|
||||
- [ ] Enhancement
|
||||
- [ ] Refactoring
|
||||
|
||||
## :scroll: Description
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
## :bulb: Motivation and Context
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
<!--- If it fixes an open issue, please link to the issue here. -->
|
||||
|
||||
## :green_heart: How did you test it?
|
||||
<!--- Describe how the changes were tested -->
|
||||
|
||||
## :pencil: Checklist
|
||||
<!--- Put an `x` in the boxes that apply, remove checkboxes from remaining -->
|
||||
|
||||
- [ ] I reviewed submitted code
|
||||
- [ ] I added tests to verify changes
|
||||
- [ ] All tests passing
|
||||
- [ ] All linters passing
|
||||
- [ ] There are no code climate issues
|
||||
|
||||
## :crystal_ball: Next steps
|
||||
<!--- Describe any changes that need to be made after merging this PR -->
|
||||
<!--- Describe any changes that you would like to make after merging this PR -->
|
||||
|
||||
## :camera_flash: Screenshots / GIFs
|
||||
<!--- Mandatory for UI changes -->
|
||||
<!-- Uncomment the next line and replace it with links to your screenshots. -->
|
||||
<!--
|
||||
<img src="https://placekitten.com/260/260" width="260"> 
|
||||
<img src="https://placekitten.com/300/300" width="260"> 
|
||||
→
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
name: Android CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Clean and Build with Gradle
|
||||
run: ./gradlew clean build
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
name: Maven Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java for publishing to Maven Central Repository
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew :YChartsLib:clean build publish
|
||||
|
||||
env:
|
||||
MAVEN_USERNAME: ${{ secrets.MAVENCENTRALUSERNAME }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVENCENTRALPASSWORD}}
|
||||
MEMORY_KEY: ${{ secrets.SIGNINGINMEMORYKEY}}
|
||||
SIGNINGKEY: ${{ secrets.SIGNINGKEYID}}
|
||||
SIGNINGPASSWORD: ${{ secrets.SIGNINGPASSWORD}}
|
||||
@@ -0,0 +1,18 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
.idea
|
||||
/buildSrc/build
|
||||
/YChartsLib/build
|
||||
@@ -0,0 +1,3 @@
|
||||
# Default code owner for this repo
|
||||
* @preetham1316 @dkk009 @kikoso
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,661 @@
|
||||
# YCharts
|
||||
|
||||
YCharts is a Jetpack-compose based charts library which enables developers to easily integrate various types of charts/graphs into their existing ui to visually represent statistical data. YCharts supports both cartesian(XY-charts) and polar charts(Radial charts), which include:
|
||||
|
||||
A. Cartesian charts:
|
||||
1. Line chart
|
||||
<div>
|
||||
<figure>
|
||||
<img width=100 src="https://github.com/AkYML/YCharts/assets/102035914/58352fc4-8039-46e6-9248-1f338efc705d"/>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
2. Bar chart
|
||||
<div>
|
||||
<figure>
|
||||
<img width=100 src="https://github.com/AkYML/YCharts/assets/102035914/5af22ba1-232f-4d1a-8067-c08f56a61b13"/>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
3. Wave chart
|
||||
<div>
|
||||
<figure>
|
||||
<img width=100 src="https://github.com/AkYML/YCharts/assets/102035914/be694054-d5cc-49ec-b7ee-be80c1562df7"/>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
4. Bubble chart
|
||||
<div>
|
||||
<figure>
|
||||
<img width=100 src="https://github.com/AkYML/YCharts/assets/102035914/cb177d4d-3fa8-4171-b3bd-7064458b1964"/>
|
||||
</figure>
|
||||
</div>
|
||||
5. Combined chart
|
||||
<div>
|
||||
<figure>
|
||||
<img width=100 src="https://github.com/AkYML/YCharts/assets/102035914/809bff58-a857-4ed1-8415-cd1d97be300f"/>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
B.Radial charts:
|
||||
1. Pie chart
|
||||
<div>
|
||||
<figure>
|
||||
<img width=100 src="https://github.com/AkYML/YCharts/assets/102035914/ac6eb6c7-033d-439b-b416-d6083ba0ee83"/>
|
||||
</figure>
|
||||
</div>
|
||||
2. Donut chart
|
||||
<div>
|
||||
<figure>
|
||||
<img width=100 src="https://github.com/AkYML/YCharts/assets/102035914/3296d983-4a9a-4b91-8b46-49f84a80041c"/>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
It comprises two main modules:
|
||||
|
||||
- `YChartslib` (Chart components for Jetpack Compose)
|
||||
- `app` (sample app to showcase chart components)
|
||||
|
||||
## Adding YCharts to your project:
|
||||
|
||||
You can add the library via Maven:
|
||||
|
||||
Gradle:
|
||||
|
||||
```groovy
|
||||
implementation 'co.yml:ycharts:2.1.0'
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
The following table outlines the modules included in this library:
|
||||
|
||||
| Artifact | Description |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `axis` | Includes the horizontal and vertical axis components along with the math engines. |
|
||||
| `charts` | Includes the all the chart components i.e: Line, Bar, Combined, Pie & Donut etc. also the math engines |
|
||||
| `chartcontainer` | Provides the base container to draw any chart inside it with scroll tap feature etc out of the box. |
|
||||
| `piechart` | Includes all the 360' chart components i.e Pie, Donut charts etc. |
|
||||
|
||||
## Sample app
|
||||
|
||||
Included in this repository is a [sample app](https://github.com/yml-org/YCharts/tree/main/app) with multiple charts with different styling in each section. Studying the source code of the app will give you a deep understanding of how to use YCharts, including customizing and styling the charts. All of the charts i.e line, bar, groupedBar, pie & donut are implemented in the sample app.
|
||||
|
||||
## Examples
|
||||
|
||||
Let's see how we can use the chart components and style them with available customization options.
|
||||
|
||||
### 1. Line Chart:
|
||||
|
||||
- Create list of points with x & y co-ordinates using `Point` data class.
|
||||
|
||||
```kotlin
|
||||
val pointsData: List<Point> =
|
||||
listOf(Point(0f, 40f), Point(1f, 90f), Point(2f, 0f), Point(3f, 60f), Point(4f, 10f))
|
||||
```
|
||||
|
||||
- Initialize X and Y Axis builders using the `AxisData` data class.
|
||||
|
||||
```kotlin
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(100.dp)
|
||||
.backgroundColor(Color.Blue)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> i.toString() }
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.build()
|
||||
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.backgroundColor(Color.Red)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.labelData { i ->
|
||||
val yScale = 100 / steps
|
||||
(i * yScale).formatToSinglePrecision()
|
||||
}.build()
|
||||
```
|
||||
|
||||
- Initialize the Line chart data with axis and other line related styling using `LineChartData` data class.
|
||||
|
||||
```kotlin
|
||||
val lineChartData = LineChartData(
|
||||
linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
dataPoints = pointsData,
|
||||
LineStyle(),
|
||||
IntersectionPoint(),
|
||||
SelectionHighlightPoint(),
|
||||
ShadowUnderLine(),
|
||||
SelectionHighlightPopUp()
|
||||
)
|
||||
),
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines(),
|
||||
backgroundColor = Color.White
|
||||
)
|
||||
```
|
||||
|
||||
- Finally use the _**`LineChart`**_ Component to render the line chart with the above input params.
|
||||
|
||||
```kotlin
|
||||
LineChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
lineChartData = lineChartData
|
||||
)
|
||||
```
|
||||
|
||||
<figure>
|
||||
<div align = "center">
|
||||
<img width=317 src="https://github.com/AkYML/YCharts/assets/102035914/58352fc4-8039-46e6-9248-1f338efc705d"/>
|
||||
<p> Line chart looks like this! </p>
|
||||
|
||||
<img width=317 src="https://github.com/AkYML/YCharts/assets/102035914/a5d5c153-c2df-43d7-8d05-41548ed64e5c"/>
|
||||
<p> Line chart with dots looks like this! </p>
|
||||
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
|
||||
### 2. Bar Chart:
|
||||
|
||||
- Create list of barChartData using the random generator extension and `BarData` data class.
|
||||
|
||||
```kotlin
|
||||
val barChartData = DataUtils.getBarChartData(barChartListSize, maxRange)
|
||||
```
|
||||
|
||||
- Initialize X and Y Axis builders using the `AxisData` data class.
|
||||
|
||||
```kotlin
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(barChartData.size - 1)
|
||||
.bottomPadding(40.dp)
|
||||
.axisLabelAngle(20f)
|
||||
.labelData { index -> barData[index].label }
|
||||
.build()
|
||||
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
```
|
||||
|
||||
- Initialize the Bar chart data with axis and other line related styling using `BarChartData` data class.
|
||||
|
||||
```kotlin
|
||||
val barChartData = BarChartData(
|
||||
chartData = barChartData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
paddingBetweenBars = 20.dp,
|
||||
barWidth = 25.dp
|
||||
)
|
||||
```
|
||||
|
||||
- Last, use the _**`BarChart`**_ Component to render the bar chart with the above input params.
|
||||
|
||||
```kotlin
|
||||
BarChart(modifier = Modifier.height(350.dp), barChartData = barChartData)
|
||||
```
|
||||
|
||||
<figure>
|
||||
<div align = "center">
|
||||
<img width=347 src="https://github.com/AkYML/YCharts/assets/102035914/0d2a6cfd-98c4-46ee-92a7-b88814f8d186" />
|
||||
<p> Bar chart looks like this!</p>
|
||||
<img width=347 src="https://github.com/AkYML/YCharts/assets/102035914/6dec92d7-95ed-4892-89b7-44a03d0711b9" />
|
||||
<p> Horizontal Bar chart looks like this!</p>
|
||||
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
|
||||
### 3. Grouped Bar Chart:
|
||||
|
||||
- Create list of grouped combinations of bar chart data using the random generator extension and `BarPlotData` data class.
|
||||
|
||||
```kotlin
|
||||
val groupBarPlotData = BarPlotData(
|
||||
groupBarList = DataUtils.getGroupBarChartData(
|
||||
barChartListSize,
|
||||
maxRange,
|
||||
eachGroupBarSize
|
||||
),
|
||||
barColorPaletteList = getColorPaletteList(barSize)
|
||||
)
|
||||
```
|
||||
|
||||
- Initialize X and Y Axis builders using the `AxisData` data class.
|
||||
|
||||
```kotlin
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(groupBarData.size - 1)
|
||||
.bottomPadding(40.dp)
|
||||
.labelData { index -> groupBarData[index].label }
|
||||
.build()
|
||||
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
```
|
||||
|
||||
- Initialize the group bar chart data with axis and other line related styling using `GroupBarChartData` data class.
|
||||
|
||||
```kotlin
|
||||
val groupBarChartData = GroupBarChartData(
|
||||
barPlotData = groupBarPlotData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
```
|
||||
|
||||
- Use the _**`GroupBarChart`**_ Component to render the bar chart with the above input params.
|
||||
|
||||
```kotlin
|
||||
GroupBarChart(modifier = Modifier.height(300.dp), groupBarChartData = groupBarChartData)
|
||||
```
|
||||
|
||||
<figure>
|
||||
<div align = "center">
|
||||
<img width=338 src="https://github.com/AkYML/YCharts/assets/102035914/55df945d-bcd9-4b08-b47e-5d439a7abf39" />
|
||||
<p> Grouped Bar chart looks like this!</p>
|
||||
<img width=338 src="https://github.com/AkYML/YCharts/assets/102035914/00af9045-4054-4544-b4c5-93b568df2b85" />
|
||||
<p> Stacked Bar chart looks like this!</p>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
### 4. Pie Chart:
|
||||
|
||||
- Create list of slices using the `PieChartData` data class.
|
||||
|
||||
```kotlin
|
||||
val pieChartData = PieChartData(
|
||||
slices = listOf(
|
||||
PieChartData.Slice("SciFi", 65f, Color(0xFF333333)),
|
||||
PieChartData.Slice("Comedy", 35f, Color(0xFF666a86)),
|
||||
PieChartData.Slice("Drama", 10f, Color(0xFF95B8D1)),
|
||||
PieChartData.Slice("Romance", 40f, Color(0xFFF53844))
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
- Initialize the pie chart config with `PieChartConfig` data class in order to achieve styling and configurations related to pie chart.
|
||||
|
||||
```kotlin
|
||||
val pieChartConfig = PieChartConfig(
|
||||
percentVisible = true,
|
||||
isAnimationEnable = true,
|
||||
showSliceLabels = false,
|
||||
animationDuration = 1500
|
||||
)
|
||||
```
|
||||
|
||||
- Finally, use the _**`PieChart`**_ component to render the chart.
|
||||
|
||||
```kotlin
|
||||
PieChart(
|
||||
modifier = Modifier
|
||||
.width(400.dp)
|
||||
.height(400.dp),
|
||||
pieChartData,
|
||||
pieChartConfig
|
||||
)
|
||||
```
|
||||
|
||||
<figure>
|
||||
<div align = "center">
|
||||
<img width=238 src="https://github.com/AkYML/YCharts/assets/102035914/457b6034-eef0-4f70-bd40-0fc0e950298a" />
|
||||
<p> Pie chart looks like this!</p>
|
||||
|
||||
<img width=238 src="https://github.com/AkYML/YCharts/assets/102035914/ac6eb6c7-033d-439b-b416-d6083ba0ee83" />
|
||||
<p> Pie chart with lables like this!</p>
|
||||
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 5. Donut Chart:
|
||||
|
||||
- Similar to pie chart here we need create list of slices using the `PieChartData` data class.
|
||||
|
||||
```kotlin
|
||||
val donutChartData = PieChartData(
|
||||
slices = listOf(
|
||||
PieChartData.Slice("HP", 15f, Color(0xFF5F0A87)),
|
||||
PieChartData.Slice("Dell", 30f, Color(0xFF20BF55)),
|
||||
PieChartData.Slice("Lenovo", 40f, Color(0xFFEC9F05)),
|
||||
PieChartData.Slice("Asus", 10f, Color(0xFFF53844))
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
- Initialize the pie chart config with `PieChartConfig` data class in order to achieve styling and configurations related to pie chart.
|
||||
|
||||
```kotlin
|
||||
val donutChartConfig = PieChartConfig(
|
||||
percentVisible = true,
|
||||
percentageFontSize = 42.sp,
|
||||
strokeWidth = 120f,
|
||||
percentColor = Color.Black,
|
||||
activeSliceAlpha = .9f,
|
||||
isAnimationEnable = true
|
||||
)
|
||||
```
|
||||
|
||||
- Finally, use the _**`DonutPieChart`**_ component to render the chart.
|
||||
|
||||
```kotlin
|
||||
DonutPieChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(500.dp),
|
||||
donutChartData,
|
||||
donutChartConfig
|
||||
)
|
||||
```
|
||||
|
||||
<figure>
|
||||
<div align = "center">
|
||||
<img width=233 src="https://github.com/AkYML/YCharts/assets/102035914/3296d983-4a9a-4b91-8b46-49f84a80041c" />
|
||||
<p> Donut chart looks like this!</p>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
|
||||
### 6. Combined Chart:
|
||||
|
||||
- Similar to line and bar chart we can combine both entities in one chart, just need to initialize the line and bar plot data using the random generator extension and add styling related to individual component.
|
||||
|
||||
```kotlin
|
||||
val linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
DataUtils.getLineChartData(listSize, maxRange = 100),
|
||||
lineStyle = LineStyle(color = Color.Blue),
|
||||
intersectionPoint = IntersectionPoint(),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp()
|
||||
),
|
||||
Line(
|
||||
DataUtils.getLineChartData(listSize, maxRange),
|
||||
lineStyle = LineStyle(color = Color.Black),
|
||||
intersectionPoint = IntersectionPoint(),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val barPlotData = BarPlotData(
|
||||
groupBarList = DataUtils.getGroupBarChartData(listSize, maxValueRange, barSize),
|
||||
barStyle = BarStyle(barWidth = 35.dp),
|
||||
barColorPaletteList = colorPaletteList
|
||||
)
|
||||
```
|
||||
|
||||
- Initialize X and Y Axis builders using the `AxisData` data class.
|
||||
|
||||
```kotlin
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(maxOf(barChartData.size - 1, lineChartData.size - 1))
|
||||
.bottomPadding(40.dp)
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
```
|
||||
|
||||
- Initialize the combined chart config data with `CombinedChartData` data class in order to achieve styling and configurations related to same.
|
||||
|
||||
```kotlin
|
||||
val combinedChartData = CombinedChartData(
|
||||
combinedPlotDataList = listOf(barPlotData, linePlotData),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
```
|
||||
|
||||
- Finally, use the _**`CombinedChart`**_ component to render the chart.
|
||||
```kotlin
|
||||
CombinedChart(
|
||||
modifier = Modifier.height(400.dp),
|
||||
combinedCharthData = combinedChartData
|
||||
)
|
||||
```
|
||||
_Note_ : To show legends infomartion related to bar, `Legends` component can be used.
|
||||
<figure>
|
||||
<div align = "center">
|
||||
<img width=292 src="https://github.com/AkYML/YCharts/assets/102035914/809bff58-a857-4ed1-8415-cd1d97be300f" />
|
||||
<p> Combined bar with line chart looks like this!</p>
|
||||
</div>
|
||||
</figure>
|
||||
<br>
|
||||
|
||||
|
||||
|
||||
### 7. Wave Chart:
|
||||
|
||||
- Wave charts, also referred to as waveform charts or waveform displays, are specialized visualizations used primarily in the field of signal processing and electronics. These charts illustrate the amplitude (vertical axis) and time (horizontal axis) of a waveform.
|
||||
|
||||
```kotlin
|
||||
val waveChartData = WaveChartData(
|
||||
wavePlotData = WavePlotData(
|
||||
lines = listOf(
|
||||
Wave(
|
||||
dataPoints = pointsData,
|
||||
waveStyle = LineStyle(color = Color.Black),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(),
|
||||
shadowUnderLine = ShadowUnderLine(),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(),
|
||||
waveFillColor = WaveFillColor(topColor = Color.Green, bottomColor = Color.Red),
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines()
|
||||
)
|
||||
```
|
||||
|
||||
- Initialize X and Y Axis builders using the `AxisData` data class.
|
||||
|
||||
```kotlin
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(maxOf(barChartData.size - 1, lineChartData.size - 1))
|
||||
.bottomPadding(40.dp)
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
```
|
||||
|
||||
- Initialize the Wave chart config data with `WaveChartData` data class in order to achieve styling and configurations related to same.
|
||||
|
||||
```kotlin
|
||||
val waveChartData = WaveChartData(
|
||||
wavePlotData = WavePlotData(
|
||||
lines = listOf(
|
||||
Wave(
|
||||
dataPoints = pointsData,
|
||||
waveStyle = LineStyle(color = Color.Black),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(),
|
||||
shadowUnderLine = ShadowUnderLine(),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(),
|
||||
waveFillColor = WaveFillColor(topColor = Color.Green, bottomColor = Color.Red),
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines()
|
||||
)
|
||||
```
|
||||
|
||||
- Finally, use the _**`WaveChart`**_ component to render the chart.
|
||||
```kotlin
|
||||
WaveChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
waveChartData = data
|
||||
)
|
||||
```
|
||||
_Note_ : To show legends infomartion related to bar, `Legends` component can be used.
|
||||
<figure>
|
||||
<div align = "center">
|
||||
<img width=292 src="https://github.com/AkYML/YCharts/assets/102035914/be694054-d5cc-49ec-b7ee-be80c1562df7" />
|
||||
<p> Wavechart looks like this!</p>
|
||||
<img width=292 src="https://github.com/AkYML/YCharts/assets/102035914/2d8b02af-1252-44fb-83e1-06d1f6230733" />
|
||||
<p> Sinewavechart looks like this!</p>
|
||||
</div>
|
||||
</figure>
|
||||
<br>
|
||||
|
||||
### 8. Bubble Chart:
|
||||
|
||||
- Bubble charts are a variation of the scatter plot, where the data points are represented as bubbles or circles instead of simple dots. The bubbles' size represents the value of a third variable, in addition to the x-axis and y-axis variables. Bubble charts are effective in visualizing and comparing three different dimensions of data simultaneously.
|
||||
|
||||
```kotlin
|
||||
val bubbleChartData = BubbleChartData(
|
||||
DataUtils.getBubbleChartDataWithGradientStyle(pointsData),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines()
|
||||
)
|
||||
```
|
||||
|
||||
- Initialize X and Y Axis builders using the `AxisData` data class.
|
||||
|
||||
```kotlin
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(maxOf(barChartData.size - 1, lineChartData.size - 1))
|
||||
.bottomPadding(40.dp)
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
```
|
||||
|
||||
- Initialize the Bubble chart config data with `BubbleChartData` data class in order to achieve styling and configurations related to same.
|
||||
|
||||
```kotlin
|
||||
val bubbleChartData = BubbleChartData(
|
||||
DataUtils.getBubbleChartDataWithGradientStyle(pointsData),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines()
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
- Finally, use the _**`BubbleChart`**_ component to render the chart.
|
||||
```kotlin
|
||||
BubbleChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(500.dp),
|
||||
bubbleChartData = data
|
||||
)
|
||||
```
|
||||
_Note_ : To show legends infomartion related to bar, `Legends` component can be used.
|
||||
<figure>
|
||||
<div align = "center">
|
||||
<img width=292 src="https://github.com/AkYML/YCharts/assets/102035914/be103f93-8562-4939-add8-1693d76441ef" />
|
||||
<p> Gradient-Bubble chart looks like this!</p>
|
||||
<img width=292 src="https://github.com/AkYML/YCharts/assets/102035914/cb177d4d-3fa8-4171-b3bd-7064458b1964" />
|
||||
<p> Bubble chart looks like this!</p>
|
||||
</div>
|
||||
</figure>
|
||||
<br>
|
||||
|
||||
|
||||
## Accessibility Support
|
||||
|
||||
To interact with your device with touch and spoken feedback, you can turn on the TalkBack screen reader. TalkBack describes the graphs/charts when tapped on the graph container.
|
||||
Compose views by deafult supports accessibility services, but for views drawn using canvas has no straight forward approach as of now, hence all our graph components supports an accessibility popup out of the box that will be shown over the graph with tapped on the container, with all the values described in an order to support accessibility services. User will be able to scroll the popup and find all the points, bars, slices or combined values in a descriptive manner, where user can tap and talkback would read it out loud.
|
||||
|
||||
<figure>
|
||||
<div align = "center">
|
||||
<span align = "center" style = "display:inline-block">
|
||||
<img width=250 src="https://user-images.githubusercontent.com/107846675/195833801-24e17e52-d3cd-45e1-9d3e-1d9bab4b0e22.png" />
|
||||
<p> fig (a)</p>
|
||||
</span>
|
||||
<span align = "center" style = "display:inline-block; padding-left:30px">
|
||||
<img width= 250 src="https://user-images.githubusercontent.com/107846675/195833551-28f21bdb-a035-4858-832c-a9a0c4dc9927.png" />
|
||||
<p> fig (b)</p>
|
||||
</span>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
Here fig(a) represents the line graph with the container being highlighted & fig(b) represents the accessibility sheet with all values laid out in detail so that talkback can describe the graph values.<br><br>
|
||||
_Note_: All the descriptions that are visible in the accessibility popup can be customized to the required string.
|
||||
|
||||
## KMM Support.
|
||||
|
||||
- For the ongoing effort to provide multiplatform solution, Ycharts now being written in Compose Multiplatform, adopting KMM has been a game-changer, we hope to achieve good results in this effort with community contribution.
|
||||
Currently KMM compatible code can be found at _**`kmm-main`**_ branch
|
||||
|
||||
## Changes in version 2.0
|
||||
|
||||
- Fix for multiple line graph/chart has been added, now it can be used to plot multiple line graphs/line charts on a single canvas or a single line with multiple colors.
|
||||
- Background color for Pie chart/Donut chart now can be set via _**`backgroundColor`**_ parameter through PieChartConfig model, this is done to keep the behaviour consistent with current syntax pattern.
|
||||
- Kotlin-library updates: _**`Kotlin version`**_ has been upgraded to '1.8.10' along with agp, compose-compiler, core-ktx, target sdk, and other compatible library updates.
|
||||
- Sample App has been updated to showcase more use-cases for each chart type.
|
||||
|
||||
## License
|
||||
|
||||
```
|
||||
Copyright 2022 YCharts
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
@Suppress("DSL_SCOPE_VIOLATION") // scope violation issue: work around suggested from: https://github.com/gradle/gradle/issues/22797
|
||||
plugins {
|
||||
id("ycharts.android.library")
|
||||
id("ycharts.android.library.compose")
|
||||
id("ycharts.android.test")
|
||||
id("maven-publish")
|
||||
id("signing")
|
||||
alias(versionCatalogLibs.plugins.dokka)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
namespace = "co.yml.charts.components"
|
||||
defaultConfig {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val dokkaOutputDir = "$buildDir/dokka"
|
||||
|
||||
tasks.dokkaHtml {
|
||||
outputDirectory.set(file(dokkaOutputDir))
|
||||
}
|
||||
|
||||
val deleteDokkaOutputDir by tasks.register<Delete>("deleteDokkaOutputDirectory") {
|
||||
delete(dokkaOutputDir)
|
||||
}
|
||||
val javadocJar = tasks.register<Jar>("javadocJar") {
|
||||
dependsOn(deleteDokkaOutputDir, tasks.dokkaHtml)
|
||||
archiveClassifier.set("javadoc")
|
||||
from(dokkaOutputDir)
|
||||
}
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "YCharts"
|
||||
setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2")
|
||||
credentials {
|
||||
username = project.findProperty("mavenCentralUsername")?.toString() ?: System.getenv("MAVEN_USERNAME")
|
||||
password = project.findProperty("mavenCentralPassword")?.toString() ?: System.getenv("MAVEN_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
publications {
|
||||
register<MavenPublication>("release") {
|
||||
groupId = "co.yml"
|
||||
artifactId = "ycharts"
|
||||
version = "2.1.0"
|
||||
afterEvaluate {
|
||||
from(components["release"])
|
||||
}
|
||||
artifact(javadocJar)
|
||||
pom {
|
||||
name.set("YCharts")
|
||||
description.set("YCharts is a light and extensible chart library for Jetpack Compose system.")
|
||||
url.set("https://github.com/yml-org/YCharts")
|
||||
licenses {
|
||||
license {
|
||||
name.set("The Apache License, Version 2.0")
|
||||
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
|
||||
distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id.set("dkk009")
|
||||
name.set("Deepak KK")
|
||||
url.set("https://github.com/dkk009")
|
||||
}
|
||||
developer {
|
||||
id.set("preetham1316")
|
||||
name.set("Preetham Ivan Dsouza")
|
||||
url.set("https://github.com/preetham1316")
|
||||
}
|
||||
developer {
|
||||
id.set("kikoso")
|
||||
name.set("Enrique López Mañas")
|
||||
url.set("https://github.com/kikoso")
|
||||
}
|
||||
}
|
||||
scm {
|
||||
url.set("https://github.com/yml-org/YCharts")
|
||||
connection.set("scm:git:git://github.com/yml-org/YCharts.git")
|
||||
developerConnection.set("scm:git:ssh://git@github.com:yml-org/YCharts.git")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
signing {
|
||||
useInMemoryPgpKeys(
|
||||
project.findProperty("signing.keyId")?.toString() ?: System.getenv("SIGNINGKEY"),
|
||||
project.findProperty("signing.InMemoryKey")?.toString() ?: System.getenv("MEMORY_KEY"),
|
||||
project.findProperty("signing.password")?.toString()?:System.getenv("SIGNINGPASSWORD")
|
||||
)
|
||||
sign(publishing.publications)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,71 @@
|
||||
package co.yml.charts
|
||||
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.axis.Gravity
|
||||
import co.yml.charts.axis.YAxis
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
|
||||
class YChartsTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Ignore("Need to fix as nodes are not visible for components drawn inside canvas")
|
||||
fun whenGraphAxisIsRenderedYaxisLabelShouldBeDisplayed(): Unit =
|
||||
with(composeTestRule) {
|
||||
// Given
|
||||
val axisData = AxisData.Builder()
|
||||
.steps(DEFAULT_AXIS_STEP_SIZE)
|
||||
.axisPosition(Gravity.LEFT)
|
||||
.axisLabelFontSize(AXIS_LABEL_FONT_SIZE.sp)
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
|
||||
// When
|
||||
setContent {
|
||||
YAxis(modifier = Modifier.height(AXIS_HEIGHT.dp), yAxisData = axisData)
|
||||
}
|
||||
|
||||
// Then
|
||||
onNode(hasText("8")).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Ignore("Need to fix as nodes are not visible for components drawn inside canvas")
|
||||
fun whenGivenYaxisLineNotRequiredIsConfiguredShouldNotBeVisible(): Unit =
|
||||
with(composeTestRule) {
|
||||
// Given
|
||||
val axisData = co.yml.charts.axis.AxisData.Builder()
|
||||
.axisPosition(Gravity.LEFT)
|
||||
.axisLabelFontSize(AXIS_LABEL_FONT_SIZE.sp)
|
||||
.axisConfig(co.yml.charts.axis.AxisConfig(isAxisLineRequired = false))
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
|
||||
// When
|
||||
setContent {
|
||||
YAxis(
|
||||
modifier = Modifier.height(AXIS_HEIGHT.dp),
|
||||
yAxisData = axisData
|
||||
)
|
||||
}
|
||||
|
||||
// Then
|
||||
composeTestRule.onNodeWithText("8").assertIsNotDisplayed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val AXIS_HEIGHT = 250
|
||||
private const val AXIS_LABEL_FONT_SIZE = 14
|
||||
private const val DEFAULT_AXIS_STEP_SIZE = 10
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package co.yml.charts.barchart
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
import androidx.compose.ui.test.onChildren
|
||||
import androidx.compose.ui.test.onFirst
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onSiblings
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.extensions.getMaxElementInYAxis
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.barchart.StackedBarChart
|
||||
import co.yml.charts.ui.barchart.models.BarData
|
||||
import co.yml.charts.ui.barchart.models.BarPlotData
|
||||
import co.yml.charts.ui.barchart.models.BarStyle
|
||||
import co.yml.charts.ui.barchart.models.GroupBar
|
||||
import co.yml.charts.ui.barchart.models.GroupBarChartData
|
||||
import co.yml.charts.ui.barchart.models.SelectionHighlightData
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class StackedBarChartTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val barSize = 3
|
||||
private val listSize = 5
|
||||
private val groupBarData = getGroupBarChartTestData(listSize, barSize)
|
||||
private val yStepSize = 10
|
||||
|
||||
private val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(listSize - 1)
|
||||
.startDrawPadding(48.dp)
|
||||
.labelData { index -> "C $index" }
|
||||
.build()
|
||||
|
||||
private val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index ->
|
||||
val valueList = mutableListOf<Float>()
|
||||
groupBarData.map { groupBar ->
|
||||
var yMax = 0f
|
||||
groupBar.barList.forEach {
|
||||
yMax += it.point.y
|
||||
}
|
||||
valueList.add(yMax)
|
||||
}
|
||||
val maxElementInYAxis = getMaxElementInYAxis(valueList.maxOrNull() ?: 0f, yStepSize)
|
||||
|
||||
(index * (maxElementInYAxis / yStepSize)).toString()
|
||||
}
|
||||
.topPadding(36.dp)
|
||||
.build()
|
||||
|
||||
private val colorPaletteList = listOf(Color.Black, Color.Yellow, Color.Blue)
|
||||
private val groupBarPlotData = BarPlotData(
|
||||
groupBarList = groupBarData,
|
||||
barStyle = BarStyle(
|
||||
barWidth = 35.dp,
|
||||
selectionHighlightData = SelectionHighlightData(
|
||||
isHighlightFullBar = true,
|
||||
groupBarPopUpLabel = { name, value ->
|
||||
"Name"
|
||||
}
|
||||
)
|
||||
),
|
||||
barColorPaletteList = colorPaletteList
|
||||
)
|
||||
private val groupBarChartData = GroupBarChartData(
|
||||
barPlotData = groupBarPlotData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
paddingBetweenStackedBars = 4.dp
|
||||
)
|
||||
|
||||
private fun startStackedBarChart(isTalkBackEnabled: Boolean = false) {
|
||||
composeTestRule.setContent {
|
||||
StackedBarChart(
|
||||
modifier = Modifier,
|
||||
groupBarChartData = groupBarChartData,
|
||||
isTalkBackEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assertIfStackedBarChartIsDisplayed() {
|
||||
|
||||
startStackedBarChart()
|
||||
composeTestRule.onNodeWithTag("stacked_bar_chart", useUnmergedTree = true)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assertIfCanvasIsDisplayed() {
|
||||
startStackedBarChart()
|
||||
|
||||
composeTestRule.onNodeWithTag("chart_canvas", useUnmergedTree = true)
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assertIfXAxisIsDisplayed() {
|
||||
startStackedBarChart()
|
||||
|
||||
composeTestRule.onNodeWithTag("x_axis", useUnmergedTree = true)
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assertIfYAxisIsDisplayed() {
|
||||
startStackedBarChart()
|
||||
|
||||
composeTestRule.onNodeWithTag("y_axis", useUnmergedTree = true)
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun assertIfStackedBarClicked() {
|
||||
startStackedBarChart()
|
||||
composeTestRule.onAllNodesWithTag("chart_canvas", useUnmergedTree = true)
|
||||
.onFirst()
|
||||
.performClick()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assertIfStackedBarChartHasCorrectBarData() {
|
||||
startStackedBarChart(isTalkBackEnabled = true)
|
||||
|
||||
composeTestRule.onNodeWithText("Bar at 0 with label B0 has value 20.00")
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun assertIfStackedBarHasChartHasCorrectXAxisLabel() {
|
||||
startStackedBarChart(isTalkBackEnabled = true)
|
||||
|
||||
composeTestRule.onNodeWithText("X Axis label C 0")
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assertIfStackedBarHasChartHasCorrectPrimaryCategoryData() {
|
||||
startStackedBarChart(isTalkBackEnabled = true)
|
||||
|
||||
composeTestRule.onNodeWithTag("AccessibilityBottomSheet List")
|
||||
.onChildren()
|
||||
.assertCountEquals(5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assertIfPrimaryCategoryHasCorrectSecondaryVariableData() {
|
||||
startStackedBarChart(isTalkBackEnabled = true)
|
||||
|
||||
composeTestRule.onNodeWithText("X Axis label C 0")
|
||||
.onSiblings()
|
||||
.assertCountEquals(4)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getGroupBarChartTestData(listSize: Int, barSize: Int): List<GroupBar> {
|
||||
val list = mutableListOf<GroupBar>()
|
||||
for (index in 0 until listSize) {
|
||||
val barList = mutableListOf<BarData>()
|
||||
for (i in 0 until barSize) {
|
||||
val barValue = (((i + 1) * 10) + ((index + 1) * 10)).toFloat()
|
||||
barList.add(
|
||||
BarData(
|
||||
Point(
|
||||
index.toFloat(),
|
||||
barValue
|
||||
),
|
||||
label = "B$i",
|
||||
description = "Bar at $index with label B$i has value ${
|
||||
String.format(
|
||||
"%.2f", barValue
|
||||
)
|
||||
}"
|
||||
)
|
||||
)
|
||||
}
|
||||
list.add(GroupBar(index.toString(), barList))
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package co.yml.charts.piechart
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import co.yml.charts.common.components.Legends
|
||||
import co.yml.charts.common.model.PlotType
|
||||
import co.yml.charts.common.utils.DataUtils
|
||||
import co.yml.charts.ui.piechart.charts.DonutPieChart
|
||||
import co.yml.charts.ui.piechart.models.PieChartConfig
|
||||
import co.yml.charts.ui.piechart.models.PieChartData
|
||||
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
|
||||
class DonutPieChartTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val pieChartConfig = PieChartConfig(
|
||||
labelVisible = false,
|
||||
strokeWidth = 120f,
|
||||
labelColor = Color.Black
|
||||
)
|
||||
|
||||
private val pieChartData = PieChartData(
|
||||
slices = listOf(
|
||||
PieChartData.Slice("A", 15f, Color(0xFF58BDFF)),
|
||||
PieChartData.Slice("B", 35f, Color(0xFF125B7F)),
|
||||
PieChartData.Slice("C", 40f, Color(0xFF092D40)),
|
||||
),
|
||||
plotType = PlotType.Donut
|
||||
)
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Test
|
||||
fun whenIsLegendVisibleIsTrueLegendLabelsAreVisible() {
|
||||
composeTestRule.setContent {
|
||||
Column(modifier = Modifier) {
|
||||
Legends(legendsConfig = DataUtils.getLegendsConfigFromPieChartData(pieChartData, 3))
|
||||
DonutPieChart(
|
||||
modifier = Modifier,
|
||||
pieChartData = pieChartData,
|
||||
pieChartConfig = pieChartConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
composeTestRule.onNodeWithText("A").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("B").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("C").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Test
|
||||
fun whenIsLegendVisibleIsFalseNoLegendLabelsAreShown() {
|
||||
composeTestRule.setContent {
|
||||
DonutPieChart(
|
||||
modifier = Modifier, pieChartData = pieChartData,
|
||||
pieChartConfig = pieChartConfig
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("A").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("B").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("C").assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package co.yml.charts.piechart
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import co.yml.charts.common.components.Legends
|
||||
import co.yml.charts.common.model.PlotType
|
||||
import co.yml.charts.common.utils.DataUtils
|
||||
import co.yml.charts.ui.piechart.charts.PieChart
|
||||
import co.yml.charts.ui.piechart.models.PieChartConfig
|
||||
import co.yml.charts.ui.piechart.models.PieChartData
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
|
||||
class PieChartTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val pieChartConfig = PieChartConfig(
|
||||
labelVisible = false,
|
||||
strokeWidth = 120f,
|
||||
labelColor = Color.Black
|
||||
)
|
||||
private val pieChartData = PieChartData(
|
||||
slices = listOf(
|
||||
PieChartData.Slice("A", 15f, Color(0xFF58BDFF)),
|
||||
PieChartData.Slice("B", 35f, Color(0xFF125B7F)),
|
||||
PieChartData.Slice("C", 40f, Color(0xFF092D40)),
|
||||
), plotType = PlotType.Pie
|
||||
)
|
||||
|
||||
|
||||
@Test
|
||||
fun whenIsLegendVisibleIsTrueLegendLabelsAreVisible() {
|
||||
composeTestRule.setContent {
|
||||
Column(modifier = Modifier) {
|
||||
Legends(legendsConfig = DataUtils.getLegendsConfigFromPieChartData(pieChartData, 3))
|
||||
PieChart(
|
||||
modifier = Modifier,
|
||||
pieChartData = pieChartData,
|
||||
pieChartConfig = pieChartConfig
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
composeTestRule.onNodeWithText("A").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("B").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("C").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenIsLegendVisibleIsFalseNoLegendLabelsAreShown() {
|
||||
composeTestRule.setContent {
|
||||
PieChart(
|
||||
modifier = Modifier, pieChartData = pieChartData,
|
||||
pieChartConfig = pieChartConfig
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("A").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("B").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("C").assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package co.yml.charts.wavechart
|
||||
|
||||
import android.graphics.Typeface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
import androidx.compose.ui.test.onFirst
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.extensions.formatToSinglePrecision
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.linechart.getMappingPointsToGraph
|
||||
import co.yml.charts.ui.linechart.model.LineStyle
|
||||
import co.yml.charts.ui.linechart.model.LineType
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPoint
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPopUp
|
||||
import co.yml.charts.ui.linechart.model.ShadowUnderLine
|
||||
import co.yml.charts.ui.wavechart.WaveChart
|
||||
import co.yml.charts.ui.wavechart.model.Wave
|
||||
import co.yml.charts.ui.wavechart.model.WaveChartData
|
||||
import co.yml.charts.ui.wavechart.model.WavePlotData
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class WaveChartTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val pointsData = listOf(
|
||||
Point(0f, 0f),
|
||||
Point(1f, -3f),
|
||||
Point(2f, -5f),
|
||||
Point(3f, -2f),
|
||||
Point(4f, 2f),
|
||||
Point(5f, 5f),
|
||||
Point(6f, 4f),
|
||||
Point(7f, 0f),
|
||||
Point(8f, -1f),
|
||||
Point(9f, -3f),
|
||||
Point(10f, 0f),
|
||||
)
|
||||
private val steps = 10
|
||||
private val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(40.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> i.toString() }
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.axisLineColor(Color.Red)
|
||||
.build()
|
||||
private val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.labelData { i ->
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}
|
||||
.axisLineColor(Color.Red)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.bottomPadding(15.dp)
|
||||
.build()
|
||||
private val data = WaveChartData(
|
||||
wavePlotData = WavePlotData(
|
||||
lines = listOf(
|
||||
Wave(
|
||||
dataPoints = pointsData,
|
||||
waveStyle = LineStyle(
|
||||
lineType = LineType.SmoothCurve(isDotted = true),
|
||||
color = Color.Green
|
||||
),
|
||||
shadowUnderLine = ShadowUnderLine(),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(
|
||||
color = Color.Green
|
||||
),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(
|
||||
backgroundColor = Color.Black,
|
||||
backgroundStyle = Stroke(2f),
|
||||
labelColor = Color.Red,
|
||||
labelTypeface = Typeface.DEFAULT_BOLD
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
|
||||
@Test
|
||||
fun assertIfWaveChartIsDisplayed() {
|
||||
composeTestRule.setContent {
|
||||
WaveChart(modifier = Modifier, waveChartData = data)
|
||||
}
|
||||
composeTestRule.onNodeWithTag("wave_chart").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assertIfScrollableContainerIsDisplayed() {
|
||||
composeTestRule.setContent {
|
||||
WaveChart(modifier = Modifier, waveChartData = data)
|
||||
}
|
||||
composeTestRule.onAllNodesWithTag(testTag = "scrollable_container", useUnmergedTree = true)
|
||||
.onFirst()
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getMappingPointsToGraphTest() {
|
||||
val mappedPoints = getMappingPointsToGraph(
|
||||
pointsData, 0f, 1f, 0f, 0f, 0f, 10f, 1f
|
||||
)
|
||||
val expectedResult = mutableListOf<Offset>()
|
||||
pointsData.forEach {
|
||||
expectedResult.add(Offset(it.x, (10f - it.y)))
|
||||
}
|
||||
Assert.assertEquals(expectedResult, mappedPoints)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest/>
|
||||
@@ -0,0 +1,192 @@
|
||||
package co.yml.charts.axis
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextUtils
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
*
|
||||
* YAxis data class params used in drawing yAxis in any graph.
|
||||
* @param steps: No of step for label segmentation
|
||||
* @param axisBottomPadding: Axis Label offset bottom padding
|
||||
* @param labelData(Int)-> String: lambda method for providing labels, @param Int will be the index
|
||||
* given for each level in Axis
|
||||
* @param axisLabelFontSize: Font size of axis label data
|
||||
* @param axisPos :Gravity of axis
|
||||
* @param labelAndAxisLinePadding: Text label padding from Axis
|
||||
* @param axisOffset: Drawing offset for axis.
|
||||
* @param axisLineThickness: Thickness of yAxis line
|
||||
* @param axisTopPadding: Axis top padding
|
||||
* @param axisStartPadding: Axis start padding.
|
||||
* @param indicatorLineWidth: Indicator width on Y axis line for showing points
|
||||
* @param axisConfig: All config related param to toggle the elements while drawing graph
|
||||
* @param axisStepSize: Size of each Axis step in Dp
|
||||
* @param axisLineColor Color of the Y or X axis
|
||||
* @param axisLabelColor Color of the Y or X axis labels
|
||||
* @param backgroundColor Background color of the Y or X components
|
||||
* @param typeface The type of font style
|
||||
* @param axisBottomPadding: Axis bottom padding,
|
||||
* @param axisLabelAngle: Angle for the axis labels
|
||||
* @param startDrawPadding: Padding between Axis and first point on the Axis
|
||||
* @param shouldDrawAxisLineTillEnd : Boolean to draw axis line till end.
|
||||
* @param axisLabelDescription: Description to describe axis value for accessibility service
|
||||
*/
|
||||
data class AxisData(
|
||||
val steps: Int,
|
||||
val labelData: (Int) -> String,
|
||||
val axisPos: Gravity,
|
||||
val labelAndAxisLinePadding: Dp,
|
||||
val axisOffset: Dp,
|
||||
val axisTopPadding: Dp,
|
||||
val axisBottomPadding: Dp,
|
||||
val axisStartPadding: Dp,
|
||||
val axisEndPadding: Dp,
|
||||
val axisStepSize: Dp,
|
||||
val axisLabelAngle: Float,
|
||||
val axisLineColor: Color,
|
||||
val axisLabelColor: Color,
|
||||
val axisLabelFontSize: TextUnit,
|
||||
val axisLineThickness: Dp,
|
||||
val indicatorLineWidth: Dp,
|
||||
val backgroundColor: Color,
|
||||
val typeface: Typeface,
|
||||
val axisConfig: AxisConfig,
|
||||
val startDrawPadding: Dp,
|
||||
val shouldDrawAxisLineTillEnd: Boolean,
|
||||
val axisLabelDescription: (String) -> String,
|
||||
val dataCategoryOptions: DataCategoryOptions
|
||||
) {
|
||||
class Builder {
|
||||
private var steps: Int = 1
|
||||
private var labelData: (Int) -> String = { _ -> "" }
|
||||
private var axisPos: Gravity = Gravity.LEFT
|
||||
private var labelAndAxisLinePadding: Dp = 20.dp
|
||||
private var axisStartPadding: Dp = 10.dp
|
||||
private var axisOffset: Dp = 10.dp
|
||||
private var axisTopPadding: Dp = 20.dp
|
||||
private var axisEndPadding: Dp = 20.dp
|
||||
private var axisBottomPadding: Dp = 10.dp
|
||||
private var axisStepSize: Dp = 30.dp
|
||||
private var axisConfig = AxisConfig()
|
||||
private var indicatorLineWidth: Dp = 5.dp
|
||||
private var backgroundColor: Color = Color.Transparent
|
||||
private var typeface: Typeface = Typeface.DEFAULT
|
||||
private var axisLineColor: Color = Color.Black
|
||||
private var axisLabelFontSize: TextUnit = 14.sp
|
||||
private var axisLineThickness: Dp = 2.dp
|
||||
private var axisLabelColor: Color = Color.Black
|
||||
private var startDrawPadding: Dp = 0.dp
|
||||
private var axisLabelAngle: Float = 0f
|
||||
private var shouldDrawAxisLineTillEnd: Boolean = false
|
||||
private var dataCategoryOptions: DataCategoryOptions = DataCategoryOptions()
|
||||
private var axisLabelDescription: (String) -> String =
|
||||
{ label -> if (dataCategoryOptions.isDataCategoryInYAxis) "Y Axis label $label" else "X Axis label $label" }
|
||||
|
||||
fun steps(count: Int) = apply { this.steps = count }
|
||||
|
||||
fun axisOffset(offset: Dp) = apply { this.axisOffset = offset }
|
||||
|
||||
fun labelAndAxisLinePadding(padding: Dp) = apply { this.labelAndAxisLinePadding = padding }
|
||||
|
||||
fun axisStepSize(size: Dp) = apply { this.axisStepSize = size }
|
||||
|
||||
fun labelData(labelData: (Int) -> String) = apply { this.labelData = labelData }
|
||||
|
||||
fun axisLineColor(lineColor: Color) = apply { this.axisLineColor = lineColor }
|
||||
|
||||
fun axisLabelFontSize(fontSize: TextUnit) = apply { this.axisLabelFontSize = fontSize }
|
||||
|
||||
fun axisPosition(pos: Gravity) = apply { this.axisPos = pos }
|
||||
|
||||
fun axisLineThickness(thickness: Dp) = apply { this.axisLineThickness = thickness }
|
||||
|
||||
fun topPadding(padding: Dp) = apply { this.axisTopPadding = padding }
|
||||
|
||||
fun startPadding(padding: Dp) = apply { this.axisStartPadding = padding }
|
||||
|
||||
fun endPadding(padding: Dp) = apply { this.axisEndPadding = padding }
|
||||
|
||||
fun bottomPadding(padding: Dp) = apply { this.axisBottomPadding = padding }
|
||||
|
||||
fun indicatorLineWidth(lineWidth: Dp) = apply { this.indicatorLineWidth = lineWidth }
|
||||
|
||||
fun backgroundColor(color: Color) = apply { this.backgroundColor = color }
|
||||
|
||||
fun typeFace(typeface: Typeface) = apply { this.typeface = typeface }
|
||||
|
||||
fun axisConfig(config: AxisConfig) = apply { this.axisConfig = config }
|
||||
|
||||
fun axisLabelColor(color: Color) = apply { this.axisLabelColor = color }
|
||||
|
||||
fun startDrawPadding(padding: Dp) =
|
||||
apply { this.startDrawPadding = padding }
|
||||
|
||||
fun axisLabelAngle(angle: Float) = apply { this.axisLabelAngle = angle }
|
||||
|
||||
fun shouldDrawAxisLineTillEnd(flag: Boolean) =
|
||||
apply { this.shouldDrawAxisLineTillEnd = flag }
|
||||
|
||||
fun axisLabelDescription(description: (String) -> String) =
|
||||
apply { this.axisLabelDescription = description }
|
||||
|
||||
fun setDataCategoryOptions(dataCategoryOptions: DataCategoryOptions) = apply {
|
||||
this.dataCategoryOptions = dataCategoryOptions
|
||||
}
|
||||
|
||||
|
||||
fun build() = AxisData(
|
||||
steps,
|
||||
labelData,
|
||||
axisPos,
|
||||
labelAndAxisLinePadding,
|
||||
axisOffset,
|
||||
axisTopPadding,
|
||||
axisBottomPadding,
|
||||
axisStartPadding,
|
||||
axisEndPadding,
|
||||
axisStepSize,
|
||||
axisLabelAngle,
|
||||
axisLineColor,
|
||||
axisLabelColor,
|
||||
axisLabelFontSize,
|
||||
axisLineThickness,
|
||||
indicatorLineWidth,
|
||||
backgroundColor,
|
||||
typeface,
|
||||
axisConfig,
|
||||
startDrawPadding,
|
||||
shouldDrawAxisLineTillEnd,
|
||||
axisLabelDescription,
|
||||
dataCategoryOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* AxisConfig data class used to mention all config related param required to draw graph.
|
||||
* @param isAxisLineRequired : true if should show the axis and points on the line else false
|
||||
* @param shouldEllipsizeAxisLabel : true if should ellipsize the axis label at end else false
|
||||
* @param minTextWidthToEllipsize : minimum width of the axis label post which label will be ellipsized
|
||||
* @param ellipsizeAt : position at which the label will be truncated or ellipsized
|
||||
*/
|
||||
data class AxisConfig(
|
||||
val isAxisLineRequired: Boolean = true,
|
||||
val shouldEllipsizeAxisLabel: Boolean = false,
|
||||
val minTextWidthToEllipsize: Dp = 40.dp,
|
||||
val ellipsizeAt: TextUtils.TruncateAt = TextUtils.TruncateAt.END
|
||||
)
|
||||
|
||||
/**
|
||||
* DataCategoryOptions used to hold information about data category like where should draw the data category.
|
||||
* @param isDataCategoryInYAxis: true if data category draws in y axis, false if it draws is in x axis.
|
||||
* @param isDataCategoryStartFromBottom true when data category start from bottom of y axis, false if it start from top of y axis
|
||||
*/
|
||||
data class DataCategoryOptions(
|
||||
val isDataCategoryInYAxis: Boolean = false,
|
||||
val isDataCategoryStartFromBottom: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package co.yml.charts.axis
|
||||
|
||||
/**
|
||||
* To specify gravity positions
|
||||
*/
|
||||
enum class Gravity {
|
||||
TOP, LEFT, RIGHT, BOTTOM
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package co.yml.charts.axis
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.text.TextPaint
|
||||
import android.text.TextUtils
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.withRotation
|
||||
import co.yml.charts.common.extensions.getTextHeight
|
||||
import co.yml.charts.common.extensions.getTextWidth
|
||||
import co.yml.charts.common.model.Point
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
*
|
||||
* XAxis compose method used for drawing xAxis in any given graph.
|
||||
* @param xAxisData : All data needed to draw Yaxis
|
||||
* @see co.yml.charts.axis.AxisData Data class to save all params related to axis
|
||||
* @param modifier : All modifier related property.
|
||||
* @param xStart : Start position of xAxis Points.
|
||||
* @param scrollOffset : Offset of delta scrolled position.
|
||||
* @param zoomScale : Scale at which zoom transformation being applied.
|
||||
* @param chartData : List of data points used in the graph.
|
||||
*/
|
||||
@Composable
|
||||
fun XAxis(
|
||||
xAxisData: AxisData,
|
||||
modifier: Modifier,
|
||||
xStart: Float,
|
||||
scrollOffset: Float,
|
||||
zoomScale: Float,
|
||||
chartData: List<Point>,
|
||||
axisStart: Float
|
||||
) {
|
||||
with(xAxisData) {
|
||||
var xAxisHeight by remember { mutableStateOf(0.dp) }
|
||||
Row(modifier = modifier.clipToBounds()) {
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(xAxisHeight)
|
||||
.semantics {
|
||||
this.testTag = "x_axis"
|
||||
}
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
val (_, _, xAxisScale) = getXAxisScale(chartData, steps)
|
||||
//this is used when data category draws in Y axis and value in X axis
|
||||
val xAxisSegmentWidth = (size.width - xStart - axisEndPadding.toPx()) / steps
|
||||
|
||||
var xPos = xStart + (startDrawPadding.toPx() * zoomScale) - scrollOffset
|
||||
|
||||
// used in the case of barchart
|
||||
if (startDrawPadding != 0.dp) {
|
||||
drawLine(
|
||||
axisLineColor,
|
||||
Offset(axisStart, 0f),
|
||||
Offset(xStart + (startDrawPadding.toPx() * zoomScale), 0f),
|
||||
strokeWidth = axisLineThickness.toPx()
|
||||
)
|
||||
}
|
||||
|
||||
for (index in 0..steps) {
|
||||
xAxisHeight = drawXAxisLabel(
|
||||
xAxisData,
|
||||
index,
|
||||
xAxisScale,
|
||||
xPos,
|
||||
xStart,
|
||||
xAxisSegmentWidth
|
||||
)
|
||||
drawAxisLineWithPointers(
|
||||
xPos,
|
||||
xAxisData,
|
||||
zoomScale,
|
||||
xAxisScale,
|
||||
index != steps,
|
||||
xStart,
|
||||
index,
|
||||
xAxisSegmentWidth,
|
||||
startDrawPadding.toPx()
|
||||
)
|
||||
xPos += ((axisStepSize.toPx() * (zoomScale * xAxisScale)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawAxisLineWithPointers(
|
||||
xPos: Float,
|
||||
axisData: AxisData,
|
||||
zoomScale: Float,
|
||||
xAxisScale: Float,
|
||||
canDrawEndLine: Boolean, // Added check to avoid drawing an extra line post the last point
|
||||
xStart: Float,
|
||||
index: Int,
|
||||
dataValueWidth: Float,
|
||||
startDrawPadding: Float
|
||||
) {
|
||||
with(axisData) {
|
||||
if (axisConfig.isAxisLineRequired) {
|
||||
if (canDrawEndLine) {
|
||||
val axisStepWidth = (axisStepSize.toPx() * (zoomScale * xAxisScale))
|
||||
drawLine(
|
||||
axisLineColor,
|
||||
if (axisData.dataCategoryOptions.isDataCategoryInYAxis) Offset(
|
||||
xStart,
|
||||
0f
|
||||
) else Offset(xStart + (startDrawPadding * zoomScale), 0f),
|
||||
if (shouldDrawAxisLineTillEnd) {
|
||||
Offset(
|
||||
(xPos + (axisStepWidth / 2) + axisStepWidth) + (startDrawPadding * zoomScale),
|
||||
0f
|
||||
)
|
||||
} else {
|
||||
if (axisData.dataCategoryOptions.isDataCategoryInYAxis) Offset(
|
||||
xStart + (dataValueWidth * (index + 1)),
|
||||
0f
|
||||
) else Offset(xPos + axisStepWidth, 0f)
|
||||
}, strokeWidth = axisLineThickness.toPx()
|
||||
)
|
||||
}
|
||||
drawLine(
|
||||
axisLineColor,
|
||||
if (axisData.dataCategoryOptions.isDataCategoryInYAxis) Offset(
|
||||
xStart + (dataValueWidth * index),
|
||||
0f
|
||||
) else Offset(xPos, 0f),
|
||||
if (axisData.dataCategoryOptions.isDataCategoryInYAxis) Offset(
|
||||
xStart + (dataValueWidth * index),
|
||||
indicatorLineWidth.toPx()
|
||||
) else Offset(xPos, indicatorLineWidth.toPx()),
|
||||
strokeWidth = axisLineThickness.toPx()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawXAxisLabel(
|
||||
axisData: AxisData,
|
||||
index: Int,
|
||||
xAxisScale: Float,
|
||||
xPos: Float,
|
||||
xStart: Float,
|
||||
dataValueWidth: Float
|
||||
): Dp = with(axisData) {
|
||||
val calculatedXAxisHeight: Dp
|
||||
val xAxisTextPaint = TextPaint().apply {
|
||||
textSize = axisLabelFontSize.toPx()
|
||||
color = axisLabelColor.toArgb()
|
||||
textAlign = Paint.Align.LEFT
|
||||
typeface = axisData.typeface
|
||||
}
|
||||
val xLabel =
|
||||
if (axisData.dataCategoryOptions.isDataCategoryInYAxis) labelData(index) else labelData((index * xAxisScale).toInt())
|
||||
val labelHeight = xLabel.getTextHeight(xAxisTextPaint)
|
||||
val labelWidth = xLabel.getTextWidth(xAxisTextPaint)
|
||||
calculatedXAxisHeight =
|
||||
if (axisConfig.isAxisLineRequired) {
|
||||
labelHeight.toDp() + axisLineThickness +
|
||||
indicatorLineWidth + labelAndAxisLinePadding + axisBottomPadding
|
||||
} else labelHeight.toDp() + labelAndAxisLinePadding
|
||||
val ellipsizedText = TextUtils.ellipsize(
|
||||
xLabel,
|
||||
xAxisTextPaint,
|
||||
axisStepSize.toPx(),
|
||||
axisConfig.ellipsizeAt
|
||||
)
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
val x =
|
||||
if (axisData.dataCategoryOptions.isDataCategoryInYAxis) xStart + (dataValueWidth * index) - (labelWidth / 2) else xPos - (labelWidth / 2)
|
||||
val y = labelHeight / 2 + indicatorLineWidth.toPx() + labelAndAxisLinePadding.toPx()
|
||||
withRotation(axisLabelAngle, x, y) {
|
||||
drawText(
|
||||
if (axisConfig.shouldEllipsizeAxisLabel) ellipsizedText.toString() else xLabel,
|
||||
x,
|
||||
y,
|
||||
xAxisTextPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
calculatedXAxisHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns triple of Xmax, Xmin & scale for given list of points and steps
|
||||
* @param points: List of points in axis
|
||||
* @param steps: Total steps in axis
|
||||
*/
|
||||
fun getXAxisScale(
|
||||
points: List<Point>,
|
||||
steps: Int,
|
||||
): Triple<Float, Float, Float> {
|
||||
val xMin = points.takeIf { it.isNotEmpty() }?.minOf { it.x } ?: 0f
|
||||
val xMax = points.takeIf { it.isNotEmpty() }?.maxOf { it.x } ?: 0f
|
||||
val totalSteps = (xMax - xMin)
|
||||
val temp = totalSteps / steps
|
||||
val scale = ceil(temp)
|
||||
return Triple(xMin, xMax, scale)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun XAxisPreview() {
|
||||
val axisData = AxisData.Builder()
|
||||
.labelAndAxisLinePadding(10.dp)
|
||||
.startDrawPadding(12.dp)
|
||||
.axisPosition(Gravity.BOTTOM)
|
||||
.axisLabelFontSize(14.sp)
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
XAxis(
|
||||
modifier = Modifier.height(40.dp),
|
||||
xAxisData = axisData,
|
||||
xStart = 0f,
|
||||
scrollOffset = 0f,
|
||||
zoomScale = 1f,
|
||||
chartData = listOf(),
|
||||
axisStart = 0f
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
package co.yml.charts.axis
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.text.TextPaint
|
||||
import android.text.TextUtils
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.common.extensions.getTextHeight
|
||||
import co.yml.charts.common.extensions.getTextWidth
|
||||
import co.yml.charts.common.model.Point
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
*
|
||||
* YAxis compose method used for drawing yAxis in any given graph.
|
||||
* @param modifier : All modifier related property.
|
||||
* @param yAxisData : All data needed to draw Yaxis.
|
||||
* @param scrollOffset : Offset of delta scrolled position.
|
||||
* @param zoomScale : Scale at which zoom transformation being applied.
|
||||
* @param chartData : List of data points used in the graph.
|
||||
* @param dataCategoryWidth length of horizontal bar
|
||||
* @param yStart start position of Y axis pointer
|
||||
* @see co.yml.charts.axis Data class to save all params related to Yaxis
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun YAxis(
|
||||
modifier: Modifier,
|
||||
yAxisData: AxisData,
|
||||
scrollOffset: Float = 0f,
|
||||
zoomScale: Float = 0f,
|
||||
chartData: List<Point> = emptyList(),
|
||||
dataCategoryWidth: Float = 0f,
|
||||
yStart: Float = 0f,
|
||||
barWidth: Float = 0f
|
||||
) {
|
||||
with(yAxisData) {
|
||||
var yAxisWidth by remember { mutableStateOf(0.dp) }
|
||||
val isRightAligned = axisPos == Gravity.RIGHT
|
||||
Column(modifier = modifier.clipToBounds()) {
|
||||
val steps = steps + 1
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.clipToBounds()
|
||||
.width(yAxisWidth)
|
||||
.semantics {
|
||||
this.testTag = "y_axis"
|
||||
}
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
val (yAxisHeight, segmentHeight) = getAxisInitValues(
|
||||
yAxisData,
|
||||
size.height,
|
||||
axisBottomPadding.toPx(),
|
||||
axisTopPadding.toPx(),
|
||||
dataCategoryOptions.isDataCategoryInYAxis,
|
||||
dataCategoryWidth
|
||||
)
|
||||
val (_, _, yAxisScale) = getYAxisScale(chartData, yAxisData.steps)
|
||||
val yPositionFromBottom = yAxisHeight - yStart + scrollOffset
|
||||
var yPos =
|
||||
if (dataCategoryOptions.isDataCategoryStartFromBottom) yPositionFromBottom else {
|
||||
if (zoomScale < 1) yPositionFromBottom else yStart - scrollOffset
|
||||
}
|
||||
|
||||
for (index in 0 until steps) {
|
||||
// Drawing the axis labels
|
||||
yAxisWidth = drawAxisLabel(
|
||||
yPos,
|
||||
index,
|
||||
yAxisData,
|
||||
yAxisWidth,
|
||||
isRightAligned,
|
||||
yAxisHeight,
|
||||
segmentHeight,
|
||||
zoomScale,
|
||||
steps - 1
|
||||
)
|
||||
drawAxisLineWithPointers(
|
||||
yPos,
|
||||
yAxisData,
|
||||
index,
|
||||
steps,
|
||||
isRightAligned,
|
||||
yAxisWidth,
|
||||
yAxisHeight,
|
||||
segmentHeight,
|
||||
zoomScale,
|
||||
yAxisScale,
|
||||
yStart,
|
||||
barWidth
|
||||
)
|
||||
val yPosChangeFromBottom = (axisStepSize.toPx() * (zoomScale * yAxisScale))
|
||||
if (dataCategoryOptions.isDataCategoryStartFromBottom) {
|
||||
yPos -= yPosChangeFromBottom
|
||||
} else {
|
||||
if (zoomScale < 1) {
|
||||
yPos -= yPosChangeFromBottom
|
||||
} else {
|
||||
yPos += ((axisStepSize.toPx() * (zoomScale * yAxisScale)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAxisInitValues(
|
||||
axisData: AxisData,
|
||||
canvasHeight: Float,
|
||||
bottomPadding: Float,
|
||||
topPadding: Float,
|
||||
isDataCategoryInYAxis: Boolean = false,
|
||||
dataCategoryWidth: Float = 0f
|
||||
): Pair<Float, Float> = with(axisData) {
|
||||
val yAxisHeight = canvasHeight - bottomPadding
|
||||
|
||||
// Minus the top padding to avoid cropping at the top
|
||||
val segmentHeight = if (isDataCategoryInYAxis) {
|
||||
dataCategoryWidth
|
||||
} else {
|
||||
(yAxisHeight - topPadding) / axisData.steps
|
||||
}
|
||||
Pair(yAxisHeight, segmentHeight)
|
||||
}
|
||||
|
||||
|
||||
private fun DrawScope.drawAxisLineWithPointers(
|
||||
yPos: Float,
|
||||
axisData: AxisData,
|
||||
index: Int,
|
||||
totalSteps: Int,
|
||||
isRightAligned: Boolean,
|
||||
yAxisWidth: Dp,
|
||||
yAxisHeight: Float,
|
||||
segmentHeight: Float,
|
||||
zoomScale: Float,
|
||||
yAxisScale: Float,
|
||||
yStart: Float = 0f,
|
||||
barWidth: Float
|
||||
) {
|
||||
with(axisData) {
|
||||
if (axisConfig.isAxisLineRequired) {
|
||||
// Draw line only until reqXLabelsQuo -1 else will be a extra line at top with no label
|
||||
val axisStepWidth = (axisStepSize.toPx() * (zoomScale * yAxisScale))
|
||||
|
||||
if (startDrawPadding != 0.dp && dataCategoryOptions.isDataCategoryInYAxis) {
|
||||
drawLine(
|
||||
start = Offset(
|
||||
x = if (isRightAligned) 0.dp.toPx() else yAxisWidth.toPx(),
|
||||
y = yAxisHeight - startDrawPadding.toPx() * zoomScale
|
||||
),
|
||||
end = Offset(
|
||||
x = if (isRightAligned) 0.dp.toPx() else yAxisWidth.toPx(),
|
||||
y = yAxisHeight
|
||||
),
|
||||
color = axisLineColor, strokeWidth = axisLineThickness.toPx()
|
||||
)
|
||||
}
|
||||
|
||||
if (index != (totalSteps - 1)) {
|
||||
//Draw Yaxis line
|
||||
drawLine(
|
||||
start = Offset(
|
||||
x = if (isRightAligned) 0.dp.toPx() else yAxisWidth.toPx(),
|
||||
y = if (dataCategoryOptions.isDataCategoryInYAxis) {
|
||||
if (shouldDrawAxisLineTillEnd) {
|
||||
if (dataCategoryOptions.isDataCategoryStartFromBottom) yPos else {
|
||||
if (zoomScale < 1) yPos else yPos - axisStepWidth / 2
|
||||
}
|
||||
} else
|
||||
yPos
|
||||
} else yAxisHeight - (segmentHeight * index)
|
||||
),
|
||||
end = Offset(
|
||||
x = if (isRightAligned) 0.dp.toPx() else yAxisWidth.toPx(),
|
||||
y = if (dataCategoryOptions.isDataCategoryInYAxis) {
|
||||
if (dataCategoryOptions.isDataCategoryStartFromBottom) {
|
||||
if (shouldDrawAxisLineTillEnd) {
|
||||
yPos - axisStepWidth - barWidth / 2
|
||||
} else {
|
||||
yPos - axisStepWidth
|
||||
}
|
||||
} else {
|
||||
if (zoomScale < 1) {
|
||||
if (shouldDrawAxisLineTillEnd) {
|
||||
yPos - axisStepWidth - barWidth / 2
|
||||
} else {
|
||||
yPos - axisStepWidth
|
||||
}
|
||||
|
||||
} else {
|
||||
if (shouldDrawAxisLineTillEnd) {
|
||||
yPos + axisStepWidth + barWidth / 2 + (startDrawPadding.toPx() * zoomScale)
|
||||
} else {
|
||||
yPos + axisStepWidth + (startDrawPadding.toPx() * zoomScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else yAxisHeight - (segmentHeight * (index + 1))
|
||||
),
|
||||
color = axisLineColor, strokeWidth = axisLineThickness.toPx()
|
||||
)
|
||||
}
|
||||
|
||||
//Draw pointer lines on Yaxis
|
||||
drawLine(
|
||||
start = Offset(
|
||||
x = if (isRightAligned) 0.dp.toPx() else {
|
||||
yAxisWidth.toPx() - indicatorLineWidth.toPx()
|
||||
},
|
||||
y = if (dataCategoryOptions.isDataCategoryInYAxis) {
|
||||
yPos
|
||||
} else yAxisHeight - (segmentHeight * index)
|
||||
),
|
||||
end = Offset(
|
||||
x = if (isRightAligned) indicatorLineWidth.toPx() else yAxisWidth.toPx(),
|
||||
y = if (dataCategoryOptions.isDataCategoryInYAxis) {
|
||||
yPos
|
||||
} else yAxisHeight - (segmentHeight * index)
|
||||
),
|
||||
color = axisLineColor, strokeWidth = axisLineThickness.toPx()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun DrawScope.drawAxisLabel(
|
||||
yPos: Float,
|
||||
index: Int,
|
||||
axisData: AxisData,
|
||||
yAxisWidth: Dp,
|
||||
isRightAligned: Boolean,
|
||||
yAxisHeight: Float,
|
||||
segmentHeight: Float,
|
||||
zoomScale: Float,
|
||||
lastIndex: Int
|
||||
): Dp = with(axisData) {
|
||||
var calculatedYAxisWidth = yAxisWidth
|
||||
val yAxisTextPaint = TextPaint().apply {
|
||||
textSize = axisLabelFontSize.toPx()
|
||||
color = axisLabelColor.toArgb()
|
||||
textAlign = if (isRightAligned) Paint.Align.RIGHT else Paint.Align.LEFT
|
||||
typeface = axisData.typeface
|
||||
}
|
||||
val yAxisLabel = if (dataCategoryOptions.isDataCategoryInYAxis) {
|
||||
if (dataCategoryOptions.isDataCategoryStartFromBottom) labelData(index) else {
|
||||
if (zoomScale < 1) labelData(lastIndex - index) else labelData(index)
|
||||
}
|
||||
} else labelData(index)
|
||||
val measuredWidth = yAxisLabel.getTextWidth(yAxisTextPaint)
|
||||
val height: Int = yAxisLabel.getTextHeight(yAxisTextPaint)
|
||||
if (measuredWidth > calculatedYAxisWidth.toPx()) {
|
||||
val width =
|
||||
if (axisConfig.shouldEllipsizeAxisLabel) {
|
||||
axisConfig.minTextWidthToEllipsize
|
||||
} else measuredWidth.toDp()
|
||||
calculatedYAxisWidth =
|
||||
width + labelAndAxisLinePadding + axisOffset
|
||||
}
|
||||
val ellipsizedText = TextUtils.ellipsize(
|
||||
yAxisLabel,
|
||||
yAxisTextPaint,
|
||||
axisConfig.minTextWidthToEllipsize.toPx(),
|
||||
axisConfig.ellipsizeAt
|
||||
)
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
drawText(
|
||||
if (axisConfig.shouldEllipsizeAxisLabel) ellipsizedText.toString() else yAxisLabel,
|
||||
if (isRightAligned) calculatedYAxisWidth.toPx() - labelAndAxisLinePadding.toPx() else {
|
||||
axisStartPadding.toPx()
|
||||
},
|
||||
if (dataCategoryOptions.isDataCategoryInYAxis)
|
||||
yPos + height / 2
|
||||
else yAxisHeight + height / 2 - ((segmentHeight * index)),
|
||||
yAxisTextPaint
|
||||
)
|
||||
}
|
||||
return calculatedYAxisWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns triple of Ymax, Ymin & scale for given list of points and steps
|
||||
* @param points: List of points in axis
|
||||
* @param steps: Total steps in axis
|
||||
*/
|
||||
fun getYAxisScale(
|
||||
points: List<Point>,
|
||||
steps: Int,
|
||||
): Triple<Float, Float, Float> {
|
||||
val yMin = points.takeIf { it.isNotEmpty() }?.minOf { it.y } ?: 0f
|
||||
val yMax = points.takeIf { it.isNotEmpty() }?.maxOf { it.y } ?: 0f
|
||||
val totalSteps = (yMax - yMin)
|
||||
val temp = totalSteps / steps
|
||||
val scale = ceil(temp)
|
||||
return Triple(yMin, yMax, scale)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun YAxisPreview() {
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(5)
|
||||
.bottomPadding(10.dp)
|
||||
.axisPosition(Gravity.LEFT)
|
||||
.axisLabelFontSize(14.sp)
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
YAxis(
|
||||
modifier = Modifier.height(300.dp), yAxisData = yAxisData,
|
||||
scrollOffset = 0f,
|
||||
zoomScale = 1f,
|
||||
chartData = listOf(),
|
||||
150f
|
||||
)
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package co.yml.charts.chartcontainer.container
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.rememberScrollableState
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import co.yml.charts.chartcontainer.gestures.detectZoomGesture
|
||||
|
||||
/**
|
||||
*
|
||||
* ScrollableCanvasContainer is a container used to draw any graph which supports scroll,
|
||||
* zoom & tap or drag gestures.
|
||||
* @param modifier : All modifier related property.
|
||||
* @param calculateMaxDistance: callback to calculate the maximum scrolling distance.
|
||||
* @param onDraw: Draw any canvas inside the onDraw scope using the input params in the lambda fxn
|
||||
* @param drawXAndYAxis: Draw the X and Y axis along with the drawing area.
|
||||
* @param containerBackgroundColor: Background color of the whole container.
|
||||
* @param isPinchZoomEnabled: True if user can zoom in and out else false
|
||||
* @param layoutDirection: Used to define the direction of scroll.
|
||||
* @param onPointClicked: Callback for tap detected along with offset for tap.
|
||||
* @param onScroll: Callback when user starts scrolling the graph.
|
||||
* @param onZoomInAndOut: Callback when user starts zoomIn and Out w.r.t to the graph
|
||||
* @param scrollOrientation: Used to define the scroll orientation
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun ScrollableCanvasContainer(
|
||||
modifier: Modifier,
|
||||
calculateMaxDistance: DrawScope.(Float) -> Float,
|
||||
onDraw: DrawScope.(Float, Float) -> Unit,
|
||||
drawXAndYAxis: @Composable BoxScope.(Float, Float) -> Unit,
|
||||
containerBackgroundColor: Color = Color.White,
|
||||
layoutDirection: LayoutDirection = LayoutDirection.Ltr,
|
||||
onPointClicked: (Offset, Float) -> Unit = { _, _ -> },
|
||||
isPinchZoomEnabled: Boolean = true,
|
||||
onScroll: () -> Unit = {},
|
||||
onZoomInAndOut: () -> Unit = {},
|
||||
scrollOrientation: Orientation = Orientation.Horizontal
|
||||
) {
|
||||
val scrollOffset = remember { mutableStateOf(0f) }
|
||||
val maxScrollOffset = remember { mutableStateOf(0f) }
|
||||
val xZoom = remember { mutableStateOf(1f) }
|
||||
val scrollState = rememberScrollableState { delta ->
|
||||
scrollOffset.value -= delta
|
||||
scrollOffset.value = checkAndGetMaxScrollOffset(
|
||||
scrollOffset.value,
|
||||
maxScrollOffset.value
|
||||
)
|
||||
delta
|
||||
}
|
||||
|
||||
if (scrollState.isScrollInProgress){
|
||||
onScroll()
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides layoutDirection
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.clipToBounds()
|
||||
) {
|
||||
Canvas(modifier = modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth()
|
||||
.semantics {
|
||||
this.testTag = "chart_canvas"
|
||||
}
|
||||
.background(containerBackgroundColor)
|
||||
.scrollable(
|
||||
state = scrollState, scrollOrientation, enabled = true
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(onTap = {
|
||||
onPointClicked(it, scrollOffset.value)
|
||||
})
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectZoomGesture(
|
||||
isZoomAllowed = isPinchZoomEnabled,
|
||||
onZoom = { zoom ->
|
||||
xZoom.value *= zoom
|
||||
onZoomInAndOut()
|
||||
}
|
||||
)
|
||||
},
|
||||
onDraw = {
|
||||
maxScrollOffset.value = calculateMaxDistance(xZoom.value)
|
||||
onDraw(scrollOffset.value, xZoom.value)
|
||||
})
|
||||
drawXAndYAxis(scrollOffset.value, xZoom.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scroll state within the start and computed max scrollOffset & filters invalid scroll states.
|
||||
* @param currentScrollOffset: Current scroll offset when user trying to scroll the canvas.
|
||||
* @param computedMaxScrollOffset: Maximum calculated scroll offset for given data set.
|
||||
*/
|
||||
fun checkAndGetMaxScrollOffset(currentScrollOffset: Float, computedMaxScrollOffset: Float): Float {
|
||||
return when {
|
||||
currentScrollOffset < 0f -> 0f
|
||||
currentScrollOffset > computedMaxScrollOffset -> computedMaxScrollOffset
|
||||
else -> currentScrollOffset
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package co.yml.charts.chartcontainer.gestures
|
||||
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.calculateCentroidSize
|
||||
import androidx.compose.foundation.gestures.calculateZoom
|
||||
import androidx.compose.foundation.gestures.forEachGesture
|
||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||
import androidx.compose.ui.input.pointer.positionChanged
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Gesture support to detect and filter pointer scopes to give a zoom start callback
|
||||
* @param isZoomAllowed: True if user is allowed to zoom.
|
||||
* @param onZoom: Callback when zoom gesture is detected.
|
||||
*/
|
||||
internal suspend fun PointerInputScope.detectZoomGesture(
|
||||
isZoomAllowed: Boolean = true,
|
||||
onZoom: (zoom: Float) -> Unit
|
||||
) {
|
||||
if (isZoomAllowed) {
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
awaitFirstDown(requireUnconsumed = false)
|
||||
}
|
||||
awaitPointerEventScope {
|
||||
var zoom = 1f
|
||||
var pastTouchSlop = false
|
||||
val touchSlop = viewConfiguration.touchSlop
|
||||
|
||||
do {
|
||||
val event = awaitPointerEvent()
|
||||
val canceled = event.changes.any { it.isConsumed }
|
||||
if (event.changes.size == 1) break
|
||||
else if (event.changes.size == 2) {
|
||||
if (isZoomAllowed) {
|
||||
if (!canceled) {
|
||||
val zoomChange = event.calculateZoom()
|
||||
if (!pastTouchSlop) {
|
||||
zoom *= zoomChange
|
||||
val centroidSize =
|
||||
event.calculateCentroidSize(useCurrent = false)
|
||||
val zoomMotion = abs(1 - zoom) * centroidSize
|
||||
if (zoomMotion > touchSlop) pastTouchSlop = true
|
||||
}
|
||||
if (pastTouchSlop) {
|
||||
if (zoomChange != 1f) onZoom(zoomChange)
|
||||
event.changes.forEach { if (it.positionChanged()) it.consume() }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else break
|
||||
} while (!canceled && event.changes.any { it.pressed })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package co.yml.charts.common.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
/**
|
||||
* Draws a horizontal line as a divider for given thickness and color
|
||||
* @param thickness: Defines the thickness of the divider.
|
||||
* @param dividerColor: Defines the color of the divider.
|
||||
*/
|
||||
@Composable
|
||||
fun ItemDivider(thickness: Dp, dividerColor: Color = Color.Black) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(thickness)
|
||||
.background(dividerColor)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package co.yml.charts.common.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.common.model.LegendLabel
|
||||
import co.yml.charts.common.model.LegendsConfig
|
||||
|
||||
/**
|
||||
* Renders the list of legends in a grid format for given given grid column count
|
||||
* @param modifier: Defines the arrangements of ui compositions.
|
||||
* @param legendsConfig: Defines the configurations required for rendering legends in [LegendsConfig]
|
||||
*/
|
||||
@Composable
|
||||
fun Legends(modifier: Modifier = Modifier, legendsConfig: LegendsConfig) {
|
||||
with(legendsConfig) {
|
||||
if (legendLabelList.size > 1) {
|
||||
LazyVerticalGrid(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = gridPaddingHorizontal, vertical = gridPaddingVertical
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
columns = GridCells.Fixed(gridColumnCount)
|
||||
) {
|
||||
items(legendLabelList) {
|
||||
Legend(legendsConfig, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Legend(config: LegendsConfig, legendLabel: LegendLabel) {
|
||||
Row(
|
||||
horizontalArrangement = config.legendsArrangement,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val boxModifier = Modifier.size(config.colorBoxSize)
|
||||
if (legendLabel.brush != null) {
|
||||
Box(modifier = boxModifier.background(legendLabel.brush))
|
||||
} else {
|
||||
Box(modifier = boxModifier.background(legendLabel.color))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.padding(config.spaceBWLabelAndColorBox))
|
||||
Text(
|
||||
text = legendLabel.name, style = config.textStyle, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package co.yml.charts.common.components.accessibility
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetLayout
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Bottom sheet to show items in vertical list view with a close button at top
|
||||
*/
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun AccessibilityBottomSheetDialog(
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color,
|
||||
popUpTopRightButtonTitle: String,
|
||||
popUpTopRightButtonDescription: String,
|
||||
sheetState: ModalBottomSheetState
|
||||
) {
|
||||
val composeScope = rememberCoroutineScope()
|
||||
ModalBottomSheetLayout(
|
||||
sheetContent = {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Button(onClick = {
|
||||
composeScope.launch { sheetState.hide() }
|
||||
}) {
|
||||
Text(
|
||||
text = popUpTopRightButtonTitle,
|
||||
modifier = Modifier.semantics {
|
||||
contentDescription = popUpTopRightButtonDescription
|
||||
})
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
},
|
||||
sheetState = sheetState,
|
||||
sheetBackgroundColor = Color.White,
|
||||
sheetElevation = 0.dp
|
||||
) {}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package co.yml.charts.common.components.accessibility
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Composable to display each bar item for given bar chart
|
||||
* @param axisLabelDescription: Axis label description
|
||||
* @param barDescription: Bar description
|
||||
* @param titleTextSize: TextUnit title font size
|
||||
* @param descriptionTextSize: TextUnit description font size
|
||||
*/
|
||||
@Composable
|
||||
fun BarInfo(
|
||||
axisLabelDescription: String,
|
||||
barDescription: String, barColor: Color,
|
||||
titleTextSize:TextUnit,
|
||||
descriptionTextSize:TextUnit
|
||||
) {
|
||||
// Merge elements below for accessibility purposes
|
||||
Row(modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp)
|
||||
.clickable { }
|
||||
.semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(axisLabelDescription, fontSize = titleTextSize)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(10.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.background(barColor)
|
||||
.size(20.dp)
|
||||
)
|
||||
Text(barDescription, fontSize = descriptionTextSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package co.yml.charts.common.components.accessibility
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Composable to display each Bubble point data for given bubble chart.
|
||||
* @param axisLabelDescription: Axis label description.
|
||||
* @param pointDescription: Details of each point on the line.
|
||||
* @param bubbleColor: Color of each bubble.
|
||||
* @param titleTextSize: TextUnit title font size
|
||||
* @param descriptionTextSize: TextUnit description font size
|
||||
*/
|
||||
@Composable
|
||||
fun BubblePointInfo(
|
||||
axisLabelDescription: String,
|
||||
pointDescription: String,
|
||||
bubbleColor: Color,
|
||||
titleTextSize: TextUnit,
|
||||
descriptionTextSize: TextUnit
|
||||
) {
|
||||
// Merge elements below for accessibility purposes
|
||||
Row(modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp)
|
||||
.clickable { }
|
||||
.semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(axisLabelDescription, fontSize = titleTextSize)
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(5.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.background(bubbleColor)
|
||||
.size(20.dp)
|
||||
)
|
||||
Text(pointDescription, fontSize = descriptionTextSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package co.yml.charts.common.components.accessibility
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.ui.barchart.models.GroupBar
|
||||
import co.yml.charts.common.components.ItemDivider
|
||||
import co.yml.charts.common.model.Point
|
||||
|
||||
/**
|
||||
* Composable to display each combined chart item for given combined chart.
|
||||
* @param pointsList: List of points in each item.
|
||||
* @param lineColor: List of colors of lines as per line chart.
|
||||
* @param groupBar: Details of each group bar.
|
||||
* @param axisLabelDescription: Axis label description.
|
||||
* @param barColorPaletteList: List of each bar colors for a given group bar.
|
||||
* @param dividerColor: Divider color between each point items.
|
||||
* @param titleTextSize: TextUnit title font size
|
||||
* @param descriptionTextSize: TextUnit description font size
|
||||
*/
|
||||
@Composable
|
||||
fun CombinedChartInfo(
|
||||
pointsList: List<Point>,
|
||||
lineColor: List<Color>,
|
||||
groupBar: GroupBar?,
|
||||
axisLabelDescription: String,
|
||||
barColorPaletteList: List<Color>,
|
||||
dividerColor: Color,
|
||||
titleTextSize: TextUnit,
|
||||
descriptionTextSize: TextUnit
|
||||
) {
|
||||
// Merge elements below for accessibility purposes
|
||||
Row(modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp)
|
||||
.clickable { }
|
||||
.semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(axisLabelDescription, fontSize = titleTextSize)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(5.dp)
|
||||
) {
|
||||
pointsList.forEachIndexed { index, point ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.background(
|
||||
color = lineColor[index],
|
||||
shape = RoundedCornerShape(corner = CornerSize(10.dp))
|
||||
)
|
||||
.size(10.dp)
|
||||
)
|
||||
Text(point.description, fontSize = descriptionTextSize)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
}
|
||||
ItemDivider(thickness = 1.dp, dividerColor = dividerColor)
|
||||
groupBar?.barList?.forEachIndexed { index, value ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.background(barColorPaletteList[index])
|
||||
.size(20.dp)
|
||||
)
|
||||
Text(value.description, fontSize = descriptionTextSize)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package co.yml.charts.common.components.accessibility
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.ui.barchart.models.GroupBar
|
||||
|
||||
|
||||
/**
|
||||
* Composable to display each group bar item for given group bar chart.
|
||||
* @param axisLabelDescription: Axis label description.
|
||||
* @param groupBar: Details of each group bar.
|
||||
* @param barColorPaletteList: List of each bar colors for a given group bar.
|
||||
* @param titleTextSize: TextUnit title font size
|
||||
* @param descriptionTextSize: TextUnit description font size
|
||||
*/
|
||||
@Composable
|
||||
fun GroupBarInfo(
|
||||
groupBar: GroupBar,
|
||||
axisLabelDescription: String,
|
||||
barColorPaletteList: List<Color>,
|
||||
titleTextSize: TextUnit,
|
||||
descriptionTextSize: TextUnit
|
||||
) {
|
||||
// Merge elements below for accessibility purposes
|
||||
Row(modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp)
|
||||
.clickable { }
|
||||
.semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(axisLabelDescription, fontSize =titleTextSize)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(5.dp)
|
||||
) {
|
||||
groupBar.barList.forEachIndexed { index, value ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.background(barColorPaletteList[index])
|
||||
.size(20.dp)
|
||||
)
|
||||
Text(value.description, fontSize = descriptionTextSize)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package co.yml.charts.common.components.accessibility
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Composable to display each line point data for given line chart.
|
||||
* @param axisLabelDescription: Axis label description.
|
||||
* @param pointDescription: Details of each point on the line.
|
||||
* @param lineColor: Color of each line.
|
||||
* @param titleTextSize: TextUnit title font size
|
||||
* @param descriptionTextSize: TextUnit description font size
|
||||
*
|
||||
*/
|
||||
@Composable
|
||||
fun LinePointInfo(
|
||||
axisLabelDescription: String,
|
||||
pointDescription: String,
|
||||
lineColor: Color,
|
||||
titleTextSize: TextUnit,
|
||||
descriptionTextSize: TextUnit
|
||||
) {
|
||||
// Merge elements below for accessibility purposes
|
||||
Row(modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp)
|
||||
.clickable { }
|
||||
.semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(axisLabelDescription, fontSize = titleTextSize)
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(5.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.background(lineColor)
|
||||
.size(20.dp)
|
||||
)
|
||||
Text(pointDescription, fontSize = descriptionTextSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package co.yml.charts.common.components.accessibility
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.ui.piechart.models.PieChartData
|
||||
|
||||
/**
|
||||
* Composable to display each slice for a given pie chart.
|
||||
* @param slice: Details of each slice in pie/ donut chart.
|
||||
* @param slicePercentage: Percentage of each slice.
|
||||
* @param textSize: TextUnit text font size
|
||||
*/
|
||||
@Composable
|
||||
fun SliceInfo(slice: PieChartData.Slice, slicePercentage: Int, textSize: TextUnit) {
|
||||
// Merge elements below for accessibility purposes
|
||||
Row(modifier = Modifier
|
||||
.clickable { }
|
||||
.semantics(mergeDescendants = true) {},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.background(slice.color)
|
||||
.size(30.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(top = 10.dp, bottom = 10.dp)
|
||||
) {
|
||||
Text(slice.sliceDescription(slicePercentage), fontSize = textSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package co.yml.charts.common.extensions
|
||||
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
import android.content.Context
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.text.TextPaint
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Outline
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.linechart.model.GridLines
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
/**
|
||||
return the width of text in canvas drawn text
|
||||
*/
|
||||
fun String.getTextWidth(paint: Paint): Float {
|
||||
return paint.measureText(this)
|
||||
}
|
||||
|
||||
/**
|
||||
return the height of text in canvas drawn text
|
||||
*/
|
||||
fun String.getTextHeight(paint: Paint): Int {
|
||||
val bounds = Rect()
|
||||
paint.getTextBounds(
|
||||
this,
|
||||
0,
|
||||
this.length,
|
||||
bounds
|
||||
)
|
||||
return bounds.height()
|
||||
}
|
||||
|
||||
/**
|
||||
return the shape that is used to mask a particular area for given leftPadding & rightPadding
|
||||
*/
|
||||
internal class RowClip(
|
||||
private val leftPadding: Float,
|
||||
private val rightPadding: Dp,
|
||||
private val topPadding: Float = 0f
|
||||
) : Shape {
|
||||
override fun createOutline(
|
||||
size: Size,
|
||||
layoutDirection: LayoutDirection,
|
||||
density: Density
|
||||
): Outline {
|
||||
return Outline.Rectangle(
|
||||
androidx.compose.ui.geometry.Rect(
|
||||
leftPadding,
|
||||
topPadding,
|
||||
size.width - rightPadding.value * density.density,
|
||||
size.height
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true or false if the object is null
|
||||
*/
|
||||
fun Any?.isNotNull() = this != null
|
||||
|
||||
|
||||
/**
|
||||
* Returns the background rect for the highlighted text.
|
||||
* @param x : X point.
|
||||
* @param y: Y point.
|
||||
* @param text: Text to be drawn inside the background.
|
||||
* @param paint: Background paint.
|
||||
*/
|
||||
fun getTextBackgroundRect(
|
||||
x: Float,
|
||||
y: Float,
|
||||
text: String,
|
||||
paint: TextPaint
|
||||
): Rect {
|
||||
val fontMetrics = paint.fontMetrics
|
||||
val textLength = paint.measureText(text)
|
||||
return Rect(
|
||||
(x - (textLength / 2)).toInt(),
|
||||
(y + fontMetrics.top).toInt(),
|
||||
(x + (textLength / 2)).toInt(),
|
||||
(y + fontMetrics.bottom).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum and minimum points of X axis.
|
||||
* @param points: List of the points to be drawn.
|
||||
*/
|
||||
fun getXMaxAndMinPoints(
|
||||
points: List<Point>,
|
||||
): Pair<Float, Float> {
|
||||
val xMin = points.minOf { it.x }
|
||||
val xMax = points.maxOf { it.x }
|
||||
return Pair(xMin, xMax)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the maximum and minimum points of Y axis
|
||||
* @param points List of points
|
||||
*/
|
||||
fun getYMaxAndMinPoints(
|
||||
points: List<Point>,
|
||||
): Pair<Float, Float> {
|
||||
if (points.isEmpty())
|
||||
return Pair(0f, 0f)
|
||||
val yMin = points.minOf { it.y }
|
||||
val yMax = points.maxOf { it.y }
|
||||
return Pair(yMin, yMax)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum value of Y axis
|
||||
* @param yMax Maximum value in the Y axis
|
||||
* @param yStepSize size of one step in the Y axis
|
||||
*/
|
||||
fun getMaxElementInYAxis(yMax: Float, yStepSize: Int): Int {
|
||||
var reqYLabelsQuo =
|
||||
(yMax / yStepSize)
|
||||
val reqYLabelsRem = yMax.rem(yStepSize)
|
||||
if (reqYLabelsRem > 0f) {
|
||||
reqYLabelsQuo += 1
|
||||
}
|
||||
return reqYLabelsQuo.toInt() * yStepSize
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return true if the point is locked
|
||||
* @param dragOffset Tapped offset
|
||||
* @param xOffset in the X axis
|
||||
*/
|
||||
fun Offset.isDragLocked(dragOffset: Float, xOffset: Float) =
|
||||
((dragOffset) > x - xOffset / 2) && ((dragOffset) < x + xOffset / 2)
|
||||
|
||||
|
||||
/**
|
||||
* Return true if the point is selected
|
||||
* @param tapOffset Tapped offset
|
||||
* @param xOffset in the X axis
|
||||
* @param bottom bottom Value
|
||||
*/
|
||||
fun Offset.isTapped(tapOffset: Offset, xOffset: Float, bottom: Float, tapPadding: Float) =
|
||||
((tapOffset.x) > x - (xOffset + tapPadding) / 2) && ((tapOffset.x) < x + (xOffset + tapPadding) / 2) &&
|
||||
((tapOffset.plus(Offset(0f, tapPadding))).y > y) && ((tapOffset.y) < bottom)
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the tapped point is withing the given boundries else false
|
||||
* @param tapOffset Tapped offset
|
||||
* @param tapPadding plus or minus padding from the point or clickable padding
|
||||
*/
|
||||
fun Offset.isPointTapped(tapOffset: Offset, tapPadding: Float) =
|
||||
((tapOffset.x) > x - tapPadding) && ((tapOffset.x) < x + tapPadding) &&
|
||||
((tapOffset.plus(Offset(0f, tapPadding))).y > y) &&
|
||||
((tapOffset.minus(Offset(0f, tapPadding))).y < y)
|
||||
|
||||
|
||||
/***
|
||||
* Returns converted single precision string from float value
|
||||
*/
|
||||
fun Float.formatToSinglePrecision(): String = DecimalFormat("#.#").format(this)
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawGridLines is the extension method used to draw the grid lines on any graph
|
||||
* @param yBottom : Bottom value for Y-Axis
|
||||
* @param top: Top value for Y axis
|
||||
* @param xLeft: Total left padding in X-Axis.
|
||||
* @param paddingRight : Total right padding.
|
||||
* @param scrollOffset : Total scroll offset.
|
||||
* @param verticalPointsSize : Total points in the X-Axis.
|
||||
* @param xZoom : Total zoom offset.
|
||||
* @param xAxisScale: Scale of each point in X-Axis.
|
||||
* @param ySteps : Number of steps in y-Axis.
|
||||
* @param xAxisStepSize: The size of each X-Axis step.
|
||||
* @param gridLines: Data class to handle styling related to grid lines.
|
||||
*/
|
||||
fun DrawScope.drawGridLines(
|
||||
yBottom: Float,
|
||||
top: Float,
|
||||
xLeft: Float,
|
||||
paddingRight: Dp,
|
||||
scrollOffset: Float,
|
||||
verticalPointsSize: Int,
|
||||
xZoom: Float,
|
||||
xAxisScale: Float,
|
||||
ySteps: Int,
|
||||
xAxisStepSize: Dp,
|
||||
gridLines: GridLines
|
||||
) {
|
||||
val availableHeight = yBottom - top
|
||||
val steps = ySteps + 1 // Considering 0 as step
|
||||
val gridOffset = availableHeight / if (steps > 1) steps - 1 else 1
|
||||
// Should start from 1 as we don't consider the XAxis
|
||||
if (gridLines.enableHorizontalLines) {
|
||||
(1 until steps).forEach {
|
||||
val y = yBottom - (it * gridOffset)
|
||||
gridLines.drawHorizontalLines(this, xLeft, y, size.width - paddingRight.toPx())
|
||||
}
|
||||
}
|
||||
if (gridLines.enableVerticalLines) {
|
||||
var xPos = xLeft - scrollOffset
|
||||
(0 until verticalPointsSize).forEach { _ ->
|
||||
gridLines.drawVerticalLines(this, xPos, yBottom, top)
|
||||
xPos += ((xAxisStepSize.toPx() * (xZoom * xAxisScale)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if accessibility services are enabled else false
|
||||
*/
|
||||
@Composable
|
||||
internal fun Context.collectIsTalkbackEnabledAsState(): State<Boolean> {
|
||||
val accessibilityManager =
|
||||
this.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager?
|
||||
fun isTalkbackEnabled(): Boolean {
|
||||
val accessibilityServiceInfoList =
|
||||
accessibilityManager?.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN)
|
||||
return accessibilityServiceInfoList?.any {
|
||||
it.resolveInfo.serviceInfo.processName.equals(TALKBACK_PACKAGE_NAME) || it.resolveInfo.serviceInfo.processName.equals(
|
||||
TALKBACK_PACKAGE_NAME_SAMSUNG
|
||||
)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
val talkbackEnabled = remember { mutableStateOf(isTalkbackEnabled()) }
|
||||
val accessibilityManagerEnabled = accessibilityManager?.isEnabled ?: false
|
||||
var accessibilityEnabled by remember { mutableStateOf(accessibilityManagerEnabled) }
|
||||
|
||||
accessibilityManager?.addAccessibilityStateChangeListener { accessibilityEnabled = it }
|
||||
|
||||
LaunchedEffect(accessibilityEnabled) {
|
||||
talkbackEnabled.value = if (accessibilityEnabled) isTalkbackEnabled() else false
|
||||
}
|
||||
return talkbackEnabled
|
||||
}
|
||||
|
||||
private const val TALKBACK_PACKAGE_NAME = "com.google.android.marvin.talkback"
|
||||
private const val TALKBACK_PACKAGE_NAME_SAMSUNG = "com.samsung.android.accessibility.talkback"
|
||||
|
||||
|
||||
/**
|
||||
return the shape that is used to mask a particular area for given top & bottom
|
||||
*/
|
||||
internal class ColumnClip(
|
||||
private val leftPadding: Float = 0f,
|
||||
private val topPadding: Float = 0f,
|
||||
private val rightPadding: Float,
|
||||
private val bottomPadding: Float
|
||||
) : Shape {
|
||||
override fun createOutline(
|
||||
size: Size,
|
||||
layoutDirection: LayoutDirection,
|
||||
density: Density
|
||||
): Outline {
|
||||
return Outline.Rectangle(
|
||||
androidx.compose.ui.geometry.Rect(
|
||||
leftPadding,
|
||||
topPadding,
|
||||
rightPadding,
|
||||
size.height - bottomPadding
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum value of X axis
|
||||
* @param xMax Maximum value in the X axis
|
||||
* @param xStepSize size of one step in the X axis
|
||||
*/
|
||||
fun getMaxElementInXAxis(xMax: Float, xStepSize: Int): Int {
|
||||
var reqYLabelsQuo =
|
||||
(xMax / xStepSize)
|
||||
val reqYLabelsRem = xMax.rem(xStepSize)
|
||||
if (reqYLabelsRem > 0f) {
|
||||
reqYLabelsQuo += 1
|
||||
}
|
||||
return reqYLabelsQuo.toInt() * xStepSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the point is selected
|
||||
* @param tapOffset Tapped offset
|
||||
* @param yOffset in the Y axis
|
||||
* @param left left Value
|
||||
* @param tapPadding plus or minus padding from the point or clickable padding
|
||||
* @param xAxisWidth width of horizontal bar
|
||||
*/
|
||||
fun Offset.isYAxisTapped(
|
||||
tapOffset: Offset,
|
||||
yOffset: Float,
|
||||
left: Float,
|
||||
tapPadding: Float,
|
||||
xAxisWidth: Float
|
||||
) =
|
||||
((tapOffset.y) < y + (yOffset + tapPadding) / 2) && ((tapOffset.y) > y - (yOffset + tapPadding) / 2) &&
|
||||
((tapOffset.plus(Offset(tapPadding, 0f))).x < xAxisWidth) && ((tapOffset.x) > left)
|
||||
|
||||
/**
|
||||
* Round double value to 2 decimal point
|
||||
*/
|
||||
fun Double.roundTwoDecimal(): Double {
|
||||
return (this * 100.0).roundToInt() / 100.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the point is selected
|
||||
* @param tapOffset Tapped offset
|
||||
* @param barWidth width of the bar
|
||||
* @param barHeight height of the bar
|
||||
* @param tapPadding plus or minus padding from the point or clickable padding
|
||||
*/
|
||||
fun Offset.isStackedBarTapped(
|
||||
tapOffset: Offset,
|
||||
barWidth: Float,
|
||||
barHeight: Float,
|
||||
tapPadding: Float
|
||||
) =
|
||||
((tapOffset.x) > x - (barWidth + tapPadding) / 2) && ((tapOffset.x) < x + (barWidth + tapPadding) / 2) &&
|
||||
(tapOffset.y > y) && ((tapOffset.y) < barHeight)
|
||||
@@ -0,0 +1,31 @@
|
||||
package co.yml.charts.common.model
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.common.utils.ChartConstants
|
||||
|
||||
/**
|
||||
* AccessibilityConfig is a data class to configure all params needed for accessibility service.
|
||||
* @param shouldHandleBackWhenTalkBackPopUpShown: True by default to dismiss the accessibility dialog when back pressed else false.
|
||||
* @param chartDescription: Description used by accessibility service when tapped on the chart.
|
||||
* @param popUpTopRightButtonTitle: Title of the button visible on the accessibility popUp at the top right.
|
||||
* @param popUpTopRightButtonDescription: Content description of the button visible on the accessibility popUp at the top right.
|
||||
* @param dividerColor: Defines the color for the divider used in the accessibility popUp
|
||||
* @param dividerThickness: Defines the thickness for the divider in Dp used in the accessibility popUp
|
||||
* @param titleTextSize: Defines the font-size of titles used in the accessibility popUp
|
||||
* @param descriptionTextSize: Defines the font-size of descriptions used in the accessibility popUp
|
||||
*/
|
||||
data class AccessibilityConfig(
|
||||
val chartDescription: String = ChartConstants.CHART_DESCRIPTION,
|
||||
val shouldHandleBackWhenTalkBackPopUpShown: Boolean = true,
|
||||
val popUpTopRightButtonTitle: String = ChartConstants.POPUP_TOP_RIGHT_BUTTON_TITLE,
|
||||
val popUpTopRightButtonDescription: String = ChartConstants.POPUP_TOP_RIGHT_BUTTON_DESCRIPTION,
|
||||
val dividerColor: Color = Color.Gray,
|
||||
val dividerThickness: Dp = 2.dp,
|
||||
val titleTextSize: TextUnit = 14.sp,
|
||||
val descriptionTextSize: TextUnit = 14.sp,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package co.yml.charts.common.model
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* LegendsConfig data class params used in config label in graph.
|
||||
* @param legendLabelList: stackLabelList is used to show labels with colors
|
||||
* @param gridColumnCount : Column Count for stackLabel grid
|
||||
* @param gridPaddingHorizontal : Horizontal padding for stackLabel grid
|
||||
* @param gridPaddingVertical : Vertical padding for stackLabel grid
|
||||
* @param spaceBWLabelAndColorBox: Space between Label and ColorBox for stackLabel grid item
|
||||
* @param colorBoxSize: Blend mode for the groupSeparator
|
||||
* @param textStyle: TextStyle for label
|
||||
* */
|
||||
data class LegendsConfig(
|
||||
val legendLabelList: List<LegendLabel>,
|
||||
val textSize: TextUnit = 14.sp,
|
||||
val gridColumnCount: Int = 1,
|
||||
val gridPaddingHorizontal: Dp = 8.dp,
|
||||
val gridPaddingVertical: Dp = 8.dp,
|
||||
val colorBoxSize: Dp = 25.dp,
|
||||
val textStyle: TextStyle = TextStyle(),
|
||||
val spaceBWLabelAndColorBox: Dp = 8.dp,
|
||||
val legendsArrangement: Arrangement.Horizontal = Arrangement.Center
|
||||
)
|
||||
|
||||
/**
|
||||
* LegendLabel data class params used in drawing label in graph.
|
||||
* @param color : Color of label.
|
||||
* @param name : Name of label.
|
||||
* @param brush : Used for gradient color
|
||||
* */
|
||||
data class LegendLabel(
|
||||
val color: Color = Color.Black,
|
||||
val name: String,
|
||||
val brush: Brush? = null
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package co.yml.charts.common.model
|
||||
|
||||
/**
|
||||
* PlotData is a base class to all the types of available graphs
|
||||
* @param plotType: Type of the graph given in [PlotType]
|
||||
*/
|
||||
interface PlotData {
|
||||
val plotType: PlotType
|
||||
}
|
||||
|
||||
/**
|
||||
* PlotType is a sealed class to define the types of supported graphs/plots
|
||||
*/
|
||||
sealed interface PlotType {
|
||||
object Line : PlotType
|
||||
object Bar : PlotType
|
||||
object Pie : PlotType
|
||||
object Donut : PlotType
|
||||
object Wave : PlotType
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package co.yml.charts.common.model
|
||||
|
||||
/**
|
||||
*
|
||||
* Point data class is used for holding the point on the graph.
|
||||
* @param x: x co-ordinate value in the graph
|
||||
* @param y: y co-ordinate value in the graph
|
||||
* @param description: Description given to describe the value of the point for accessibility service
|
||||
* Used this class over PointF as its a java class and unable to mock the same
|
||||
*/
|
||||
data class Point(
|
||||
val x: Float,
|
||||
val y: Float,
|
||||
val description: String = "Value of point is ${String.format("%.2f", y)}"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package co.yml.charts.common.utils
|
||||
|
||||
object ChartConstants {
|
||||
const val DEFAULT_YAXIS_BOTTOM_PADDING = 10
|
||||
const val DEFAULT_DETECT_DRAG_TIME_OUT = 100L
|
||||
const val CHART_DESCRIPTION = "Double tap to know the chart in detail"
|
||||
const val POPUP_TOP_RIGHT_BUTTON_TITLE = "Close"
|
||||
const val POPUP_TOP_RIGHT_BUTTON_DESCRIPTION = "Tap to close the dialog"
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
package co.yml.charts.common.utils
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import co.yml.charts.axis.DataCategoryOptions
|
||||
import co.yml.charts.common.model.LegendLabel
|
||||
import co.yml.charts.common.model.LegendsConfig
|
||||
import co.yml.charts.common.model.PlotType
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.barchart.models.BarChartType
|
||||
import co.yml.charts.ui.barchart.models.BarData
|
||||
import co.yml.charts.ui.barchart.models.GroupBar
|
||||
import co.yml.charts.ui.bubblechart.model.Bubble
|
||||
import co.yml.charts.ui.bubblechart.model.BubbleGradientType
|
||||
import co.yml.charts.ui.bubblechart.model.BubbleStyle
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPoint
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPopUp
|
||||
import co.yml.charts.ui.piechart.models.PieChartData
|
||||
import kotlin.math.sin
|
||||
import kotlin.random.Random
|
||||
|
||||
object DataUtils {
|
||||
/**
|
||||
* Returns list of points
|
||||
* @param listSize: Size of total number of points needed.
|
||||
* @param start: X values to start from. ex: 50 to 100
|
||||
* @param maxRange: Max range of Y values
|
||||
*/
|
||||
fun getLineChartData(listSize: Int, start: Int = 0, maxRange: Int): List<Point> {
|
||||
val list = arrayListOf<Point>()
|
||||
for (index in 0 until listSize) {
|
||||
list.add(
|
||||
Point(
|
||||
index.toFloat(),
|
||||
(start until maxRange).random().toFloat()
|
||||
)
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of points
|
||||
* @param listSize: Size of total number of points needed.
|
||||
* @param start: X values to start from. ex: 50 to 100
|
||||
* @param maxRange: Max range of Y values
|
||||
*/
|
||||
fun getRandomPoints(listSize: Int, start: Int = 0, maxRange: Int): List<Point> {
|
||||
val list = arrayListOf<Point>()
|
||||
for (index in 0 until listSize) {
|
||||
list.add(
|
||||
Point(
|
||||
index.toFloat(),
|
||||
(start until maxRange).random().toFloat()
|
||||
)
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of points
|
||||
* @param listSize: Size of total number of points needed.
|
||||
* @param start: X values to start from. ex: 50 to 100
|
||||
* @param maxRange: Max range of Y values
|
||||
*/
|
||||
fun getBubbleChartDataWithGradientStyle(
|
||||
points: List<Point>,
|
||||
minDensity: Float = 10F,
|
||||
maxDensity: Float = 100F
|
||||
): List<Bubble> {
|
||||
val list = arrayListOf<Bubble>()
|
||||
points.forEachIndexed { index, point ->
|
||||
val bubbleColor1 = Color(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
|
||||
val bubbleColor2 = Color(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
|
||||
val bubbleColor3 = Color(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
|
||||
val bubbleColor4 = Color(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
|
||||
|
||||
when(Random.nextInt(0,5)){
|
||||
0->{
|
||||
list.add(
|
||||
Bubble(
|
||||
center = point,
|
||||
density = (minDensity.toInt() until maxDensity.toInt()).random().toFloat(),
|
||||
bubbleStyle = BubbleStyle(gradientColors = listOf(bubbleColor1, bubbleColor2,bubbleColor3,bubbleColor4), useGradience = true, gradientType = BubbleGradientType.RadialGradient),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(Color.Black),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(Color.White)
|
||||
)
|
||||
)
|
||||
}
|
||||
1->{
|
||||
list.add(
|
||||
Bubble(
|
||||
center = point,
|
||||
density = (minDensity.toInt() until maxDensity.toInt()).random().toFloat(),
|
||||
bubbleStyle = BubbleStyle(gradientColors = listOf(bubbleColor1, bubbleColor2), useGradience = true, gradientType = BubbleGradientType.LinearGradient),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(Color.Black),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(Color.White)
|
||||
)
|
||||
)
|
||||
}
|
||||
2->{
|
||||
list.add(
|
||||
Bubble(
|
||||
center = point,
|
||||
density = (minDensity.toInt() until maxDensity.toInt()).random().toFloat(),
|
||||
bubbleStyle = BubbleStyle(gradientColors = listOf(bubbleColor1, bubbleColor2), useGradience = true, gradientType = BubbleGradientType.VerticalGradient),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(Color.Black),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(Color.White)
|
||||
)
|
||||
)
|
||||
}
|
||||
3->{
|
||||
list.add(
|
||||
Bubble(
|
||||
center = point,
|
||||
density = (minDensity.toInt() until maxDensity.toInt()).random().toFloat(),
|
||||
bubbleStyle = BubbleStyle(gradientColors = listOf(bubbleColor1, bubbleColor2), useGradience = true, gradientType = BubbleGradientType.HorizontalGradient),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(Color.Black),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(Color.White)
|
||||
)
|
||||
)
|
||||
}
|
||||
4->{
|
||||
list.add(
|
||||
Bubble(
|
||||
center = point,
|
||||
density = (minDensity.toInt() until maxDensity.toInt()).random().toFloat(),
|
||||
bubbleStyle = BubbleStyle(gradientColors = listOf(bubbleColor1, bubbleColor2,bubbleColor3,bubbleColor4), useGradience = true, gradientType = BubbleGradientType.HorizontalGradient,tileMode = TileMode.Repeated),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(Color.Black),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(Color.White)
|
||||
)
|
||||
)
|
||||
}
|
||||
5->{
|
||||
list.add(
|
||||
Bubble(
|
||||
center = point,
|
||||
density = (minDensity.toInt() until maxDensity.toInt()).random().toFloat(),
|
||||
bubbleStyle = BubbleStyle(gradientColors = listOf(bubbleColor1, bubbleColor2,bubbleColor3,bubbleColor4), useGradience = true, gradientType = BubbleGradientType.HorizontalGradient,tileMode = TileMode.Mirror),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(Color.Black),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(Color.White)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of points
|
||||
* @param listSize: Size of total number of points needed.
|
||||
* @param start: X values to start from. ex: 50 to 100
|
||||
* @param maxRange: Max range of Y values
|
||||
*/
|
||||
fun getBubbleChartDataWithSolidStyle(
|
||||
points: List<Point>,
|
||||
minDensity: Float = 10F,
|
||||
maxDensity: Float = 100F
|
||||
): List<Bubble> {
|
||||
val list = arrayListOf<Bubble>()
|
||||
points.forEachIndexed { index, point ->
|
||||
val bubbleColor =Color(red = Random.nextInt(256),green= Random.nextInt(256),blue= Random.nextInt(256), alpha =Random.nextInt(from = 150, until = 256) )
|
||||
list.add(
|
||||
Bubble(
|
||||
center = point,
|
||||
density = (minDensity.toInt() until maxDensity.toInt()).random().toFloat(),
|
||||
bubbleStyle = BubbleStyle(solidColor = bubbleColor, useGradience = false),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(Color.Black),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(Color.White)
|
||||
)
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sample bar chart data
|
||||
* @param duration : Duration of the wave in seconds
|
||||
* @param sampleRate : Number of samples per second
|
||||
* @param frequency : Frequency of the wave in Hz
|
||||
*/
|
||||
fun getWaveChartData(duration: Double, sampleRate: Double, frequency: Double): List<Point> {
|
||||
val list = mutableListOf<Point>()
|
||||
|
||||
val amplitude = 1.0 // Amplitude of the wave
|
||||
val numSamples = (duration * sampleRate).toInt() // Total number of samples
|
||||
|
||||
for (i in 0 until numSamples) {
|
||||
val time = i.toDouble() / sampleRate // Time at the current sample
|
||||
val sample =
|
||||
amplitude * sin(2.0 * Math.PI * frequency * time) // Calculate the sample value
|
||||
list.add(Point(time.toFloat(), sample.toFloat()))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun getBarChartData(
|
||||
listSize: Int,
|
||||
maxRange: Int,
|
||||
barChartType: BarChartType,
|
||||
dataCategoryOptions: DataCategoryOptions
|
||||
): List<BarData> {
|
||||
val list = arrayListOf<BarData>()
|
||||
for (index in 0 until listSize) {
|
||||
val point = when (barChartType) {
|
||||
BarChartType.VERTICAL -> {
|
||||
Point(
|
||||
index.toFloat(),
|
||||
"%.2f".format(Random.nextDouble(1.0, maxRange.toDouble())).toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
BarChartType.HORIZONTAL -> {
|
||||
Point(
|
||||
"%.2f".format(Random.nextDouble(1.0, maxRange.toDouble())).toFloat(),
|
||||
index.toFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
list.add(
|
||||
BarData(
|
||||
point = point,
|
||||
color = Color(
|
||||
Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)
|
||||
),
|
||||
dataCategoryOptions = dataCategoryOptions,
|
||||
label = "Bar$index",
|
||||
)
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sample gradient bar chart data
|
||||
* @param listSize Size of the list
|
||||
* @param maxRange Maximum range for the values
|
||||
*/
|
||||
fun getGradientBarChartData(listSize: Int, maxRange: Int): List<BarData> {
|
||||
val list = arrayListOf<BarData>()
|
||||
for (index in 0 until listSize) {
|
||||
list.add(
|
||||
BarData(
|
||||
point = Point(
|
||||
index.toFloat(),
|
||||
"%.2f".format(Random.nextDouble(1.0, maxRange.toDouble())).toFloat()
|
||||
),
|
||||
gradientColorList = listOf(
|
||||
Color(
|
||||
Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)
|
||||
),
|
||||
Color(
|
||||
Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)
|
||||
),
|
||||
Color(
|
||||
Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)
|
||||
),
|
||||
Color(
|
||||
Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)
|
||||
)
|
||||
),
|
||||
label = "Bar$index"
|
||||
)
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sample pie chart data
|
||||
*/
|
||||
fun getPieChartData(): PieChartData {
|
||||
return PieChartData(
|
||||
slices = listOf(
|
||||
PieChartData.Slice("SciFi", 15f, Color(0xFF333333)),
|
||||
PieChartData.Slice("Comedy", 15f, Color(0xFF666a86)),
|
||||
PieChartData.Slice("Drama", 10f, Color(0xFF95B8D1)),
|
||||
PieChartData.Slice("Romance", 10f, Color(0xFFE8DDB5)),
|
||||
PieChartData.Slice("Action", 20f, Color(0xFFEDAFB8)),
|
||||
PieChartData.Slice("Thriller", 100f, Color(0xFFF94892)),
|
||||
PieChartData.Slice("Western", 10f, Color(0xFFA675A1)),
|
||||
PieChartData.Slice("Fantasy", 10f, Color(0xFF8F3985)),
|
||||
),
|
||||
plotType = PlotType.Pie
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sample pie chart data
|
||||
*/
|
||||
fun getPieChartData2(): PieChartData {
|
||||
return PieChartData(
|
||||
slices = listOf(
|
||||
PieChartData.Slice("Android", 30f, Color(0xFF002B5B)),
|
||||
PieChartData.Slice("iOS", 30f, Color(0xFF2B4865)),
|
||||
PieChartData.Slice("Windows", 15f, Color(0xFF256D85)),
|
||||
PieChartData.Slice("Other", 25f, Color(0xFF806D85)),
|
||||
),
|
||||
plotType = PlotType.Pie
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sample donut chart data
|
||||
*/
|
||||
fun getDonutChartData(): PieChartData {
|
||||
return PieChartData(
|
||||
slices = listOf(
|
||||
PieChartData.Slice("HP", 15f, Color(0xFF5F0A87)),
|
||||
PieChartData.Slice("Dell", 30f, Color(0xFF20BF55)),
|
||||
PieChartData.Slice("Lenovo", 10f, Color(0xFFA40606)),
|
||||
PieChartData.Slice("Asus", 15f, Color(0xFFF53844)),
|
||||
PieChartData.Slice("Acer", 10f, Color(0xFFEC9F05)),
|
||||
PieChartData.Slice("Apple", 30f, Color(0xFF009FFD)),
|
||||
),
|
||||
plotType = PlotType.Donut
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample gradient bar chart data.
|
||||
* @param listSize Size of the list
|
||||
* @param maxRange Maximum range for the values
|
||||
* @param barSize size of bars in one group
|
||||
*/
|
||||
fun getGroupBarChartData(listSize: Int, maxRange: Int, barSize: Int): List<GroupBar> {
|
||||
val list = mutableListOf<GroupBar>()
|
||||
for (index in 0 until listSize) {
|
||||
val barList = mutableListOf<BarData>()
|
||||
for (i in 0 until barSize) {
|
||||
val barValue = "%.2f".format(Random.nextDouble(1.0, maxRange.toDouble())).toFloat()
|
||||
barList.add(
|
||||
BarData(
|
||||
Point(
|
||||
index.toFloat(),
|
||||
barValue
|
||||
),
|
||||
label = "B$i",
|
||||
description = "Bar at $index with label B$i has value ${
|
||||
String.format(
|
||||
"%.2f", barValue
|
||||
)
|
||||
}"
|
||||
)
|
||||
)
|
||||
}
|
||||
list.add(GroupBar(index.toString(), barList))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample stackLabelList data
|
||||
* @param colorPaletteList color list for each legend
|
||||
*/
|
||||
fun getLegendsLabelData(colorPaletteList: List<Color>): List<LegendLabel> {
|
||||
val legendLabelList = mutableListOf<LegendLabel>()
|
||||
for (index in colorPaletteList.indices) {
|
||||
legendLabelList.add(
|
||||
LegendLabel(
|
||||
colorPaletteList[index],
|
||||
"B$index"
|
||||
)
|
||||
)
|
||||
}
|
||||
return legendLabelList
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample colors list for given size
|
||||
* @param listSize: Size of the colors list.
|
||||
*/
|
||||
fun getColorPaletteList(listSize: Int): List<Color> {
|
||||
val colorList = mutableListOf<Color>()
|
||||
|
||||
for (index in 0 until listSize) {
|
||||
colorList.add(
|
||||
Color(
|
||||
(0 until 256).random(), (0 until 256).random(), (0 until 256).random()
|
||||
)
|
||||
)
|
||||
}
|
||||
return colorList
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the legends config for given pie chart data
|
||||
* @param pieChartData: Pie chart details.
|
||||
* @param gridSize: Legends grid size.
|
||||
*/
|
||||
fun getLegendsConfigFromPieChartData(pieChartData: PieChartData, gridSize: Int): LegendsConfig {
|
||||
val legendsList = mutableListOf<LegendLabel>()
|
||||
pieChartData.slices.forEach { slice ->
|
||||
legendsList.add(LegendLabel(slice.color, slice.label))
|
||||
}
|
||||
return LegendsConfig(
|
||||
legendLabelList = legendsList,
|
||||
gridColumnCount = gridSize,
|
||||
legendsArrangement = Arrangement.Start,
|
||||
textStyle = TextStyle()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package co.yml.charts.ui.barchart
|
||||
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import co.yml.charts.common.components.ItemDivider
|
||||
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
|
||||
import co.yml.charts.common.components.accessibility.BarInfo
|
||||
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
|
||||
import co.yml.charts.ui.barchart.models.BarChartData
|
||||
import co.yml.charts.ui.barchart.models.BarChartType
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* [BarChart] compose method for drawing bar chart.
|
||||
* @param modifier: All modifier related properties
|
||||
* @param barChartData : All data needed to Bar Chart
|
||||
* @see [BarChartData] Data class to save all params related to Bar Chart
|
||||
*/
|
||||
@Composable
|
||||
fun BarChart(modifier: Modifier, barChartData: BarChartData) {
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val isTalkBackEnabled by LocalContext.current.collectIsTalkbackEnabledAsState()
|
||||
|
||||
if (accessibilitySheetState.isVisible && isTalkBackEnabled
|
||||
&& barChartData.accessibilityConfig.shouldHandleBackWhenTalkBackPopUpShown
|
||||
) {
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
accessibilitySheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(modifier) {
|
||||
when (barChartData.barChartType) {
|
||||
BarChartType.HORIZONTAL -> HorizontalBarChart(
|
||||
barChartData = barChartData,
|
||||
modifier = modifier,
|
||||
scope = scope,
|
||||
accessibilitySheetState = accessibilitySheetState,
|
||||
isTalkBackEnabled = isTalkBackEnabled
|
||||
)
|
||||
|
||||
BarChartType.VERTICAL -> VerticalBarChart(
|
||||
barChartData = barChartData,
|
||||
modifier = modifier,
|
||||
scope = scope,
|
||||
accessibilitySheetState = accessibilitySheetState,
|
||||
isTalkBackEnabled = isTalkBackEnabled
|
||||
)
|
||||
}
|
||||
|
||||
if (isTalkBackEnabled) {
|
||||
with(barChartData) {
|
||||
AccessibilityBottomSheetDialog(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = Color.White,
|
||||
content = {
|
||||
LazyColumn {
|
||||
items(chartData.size) { index ->
|
||||
Column {
|
||||
BarInfo(
|
||||
if (barChartType == BarChartType.VERTICAL) {
|
||||
xAxisData.axisLabelDescription(
|
||||
xAxisData.labelData(
|
||||
index
|
||||
)
|
||||
)
|
||||
} else {
|
||||
yAxisData.axisLabelDescription(
|
||||
yAxisData.labelData(
|
||||
index
|
||||
)
|
||||
)
|
||||
},
|
||||
chartData[index].description,
|
||||
chartData[index].color,
|
||||
accessibilityConfig.titleTextSize,
|
||||
accessibilityConfig.descriptionTextSize
|
||||
)
|
||||
ItemDivider(
|
||||
thickness = accessibilityConfig.dividerThickness,
|
||||
dividerColor = accessibilityConfig.dividerColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
popUpTopRightButtonTitle = accessibilityConfig.popUpTopRightButtonTitle,
|
||||
popUpTopRightButtonDescription = accessibilityConfig.popUpTopRightButtonDescription,
|
||||
sheetState = accessibilitySheetState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package co.yml.charts.ui.barchart
|
||||
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.XAxis
|
||||
import co.yml.charts.axis.YAxis
|
||||
import co.yml.charts.chartcontainer.container.ScrollableCanvasContainer
|
||||
import co.yml.charts.common.components.ItemDivider
|
||||
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
|
||||
import co.yml.charts.common.components.accessibility.GroupBarInfo
|
||||
import co.yml.charts.common.extensions.RowClip
|
||||
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
|
||||
import co.yml.charts.common.extensions.getMaxElementInYAxis
|
||||
import co.yml.charts.common.extensions.isTapped
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.common.utils.ChartConstants.DEFAULT_YAXIS_BOTTOM_PADDING
|
||||
import co.yml.charts.ui.barchart.models.BarData
|
||||
import co.yml.charts.ui.barchart.models.GroupBarChartData
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* [GroupBarChart] compose method for drawing group bar chart.
|
||||
* @param modifier: All modifier related properties
|
||||
* @param groupBarChartData : All data needed to group bar chart
|
||||
* @see [GroupBarChartData] Data class to save all params related to Bar Chart
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun GroupBarChart(modifier: Modifier, groupBarChartData: GroupBarChartData) {
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val isTalkBackEnabled by LocalContext.current.collectIsTalkbackEnabledAsState()
|
||||
if (accessibilitySheetState.isVisible && isTalkBackEnabled
|
||||
&& groupBarChartData.accessibilityConfig.shouldHandleBackWhenTalkBackPopUpShown
|
||||
) {
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
accessibilitySheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(modifier.fillMaxSize()) {
|
||||
with(groupBarChartData.barPlotData) {
|
||||
var visibility by remember { mutableStateOf(false) }
|
||||
var identifiedPoint by remember { mutableStateOf(BarData(Point(0f, 0f))) }
|
||||
var xOffset by remember { mutableStateOf(0f) }
|
||||
var tapOffset by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
var isTapped by remember { mutableStateOf(false) }
|
||||
var columnWidth by remember { mutableStateOf(0f) }
|
||||
var horizontalGap by remember { mutableStateOf(0f) }
|
||||
var rowHeight by remember { mutableStateOf(0f) }
|
||||
val paddingRight = groupBarChartData.paddingEnd
|
||||
val valueList = groupBarList.map { it.yMax }
|
||||
val bgColor = MaterialTheme.colorScheme.surface
|
||||
|
||||
val xMax = groupBarList.size
|
||||
val yMax = valueList.maxOrNull() ?: 0f
|
||||
val xAxisData =
|
||||
groupBarChartData.xAxisData.copy(
|
||||
axisStepSize = ((barStyle.barWidth * groupingSize) + barStyle.paddingBetweenBars),
|
||||
shouldDrawAxisLineTillEnd = true,
|
||||
steps = groupBarList.size - 1
|
||||
)
|
||||
val yAxisData =
|
||||
groupBarChartData.yAxisData.copy(axisBottomPadding = LocalDensity.current.run { rowHeight.toDp() })
|
||||
|
||||
val maxElementInYAxis = getMaxElementInYAxis(yMax, yAxisData.steps)
|
||||
|
||||
if (!groupBarChartData.showXAxis) {
|
||||
rowHeight = LocalDensity.current.run { DEFAULT_YAXIS_BOTTOM_PADDING.dp.toPx() }
|
||||
}
|
||||
ScrollableCanvasContainer(modifier = modifier
|
||||
.semantics {
|
||||
contentDescription = groupBarChartData.accessibilityConfig.chartDescription
|
||||
}
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
containerBackgroundColor = groupBarChartData.backgroundColor,
|
||||
calculateMaxDistance = { xZoom ->
|
||||
horizontalGap = groupBarChartData.horizontalExtraSpace.toPx()
|
||||
val xLeft = columnWidth + horizontalGap
|
||||
xOffset =
|
||||
((barStyle.barWidth.toPx() * groupingSize) + barStyle.paddingBetweenBars.toPx()) * xZoom
|
||||
getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax.toFloat(),
|
||||
0f,
|
||||
xOffset,
|
||||
xLeft,
|
||||
paddingRight.toPx(),
|
||||
size.width
|
||||
)
|
||||
},
|
||||
onDraw = { scrollOffset, xZoom ->
|
||||
val yBottom = size.height - rowHeight
|
||||
val yOffset = ((yBottom - yAxisData.axisTopPadding.toPx()) / maxElementInYAxis)
|
||||
xOffset =
|
||||
((barStyle.barWidth.toPx() * groupingSize) + barStyle.paddingBetweenBars.toPx()) * xZoom
|
||||
val xLeft = columnWidth + horizontalGap
|
||||
val dragLocks = mutableMapOf<Int, Pair<BarData, Offset>>()
|
||||
|
||||
// Draw bar lines
|
||||
groupBarList.forEachIndexed { index, groupBarData ->
|
||||
var insideOffset = 0f
|
||||
|
||||
groupBarData.barList.forEachIndexed { subIndex, individualBar ->
|
||||
val drawOffset = getGroupBarDrawOffset(
|
||||
x = index,
|
||||
y = individualBar.point.y,
|
||||
xOffset = xOffset,
|
||||
xLeft = xLeft,
|
||||
scrollOffset = scrollOffset,
|
||||
yBottom = yBottom,
|
||||
yOffset = yOffset,
|
||||
yMin = 0f,
|
||||
xMin = 0f,
|
||||
startDrawPadding = xAxisData.startDrawPadding.toPx(),
|
||||
zoomScale = xZoom,
|
||||
barWidth = barStyle.barWidth.toPx()
|
||||
)
|
||||
val height = yBottom - drawOffset.y
|
||||
|
||||
val individualOffset = Offset(drawOffset.x + insideOffset, drawOffset.y)
|
||||
|
||||
// drawing each individual bars
|
||||
groupBarChartData.drawBar(
|
||||
this,
|
||||
groupBarChartData,
|
||||
barStyle,
|
||||
individualOffset,
|
||||
height,
|
||||
subIndex
|
||||
)
|
||||
|
||||
insideOffset += barStyle.barWidth.toPx()
|
||||
|
||||
val middleOffset = Offset(
|
||||
individualOffset.x + barStyle.barWidth.toPx() / 2, drawOffset.y
|
||||
)
|
||||
// store the tap points for selection
|
||||
if (isTapped && middleOffset.isTapped(
|
||||
tapOffset,
|
||||
barStyle.barWidth.toPx(),
|
||||
yBottom,
|
||||
groupBarChartData.tapPadding.toPx()
|
||||
)
|
||||
) {
|
||||
dragLocks[0] = individualBar to individualOffset
|
||||
}
|
||||
}
|
||||
|
||||
if (groupBarChartData.groupSeparatorConfig.showSeparator && index != groupBarList.size - 1) {
|
||||
// drawing each Group Separator bars
|
||||
val yOffset2 = (yBottom - yAxisData.axisTopPadding.toPx())
|
||||
val height = yBottom - yAxisData.axisTopPadding.toPx()
|
||||
val drawOffset2 = getGroupBarDrawOffset(
|
||||
x = index,
|
||||
y = rowHeight,
|
||||
xOffset = xOffset,
|
||||
xLeft = xLeft,
|
||||
scrollOffset = scrollOffset,
|
||||
yBottom = yBottom,
|
||||
yOffset = yOffset2,
|
||||
yMin = 0f,
|
||||
xMin = 0f,
|
||||
startDrawPadding = xAxisData.startDrawPadding.toPx(),
|
||||
zoomScale = xZoom,
|
||||
barStyle.barWidth.toPx()
|
||||
)
|
||||
val xOffset2 =
|
||||
(drawOffset2.x + insideOffset + (barStyle.paddingBetweenBars.toPx() / 2) - groupBarChartData.groupSeparatorConfig.separatorWidth.toPx() / 2)
|
||||
val individualOffset = Offset(xOffset2, yAxisData.axisTopPadding.toPx())
|
||||
|
||||
drawGroupSeparator(
|
||||
individualOffset,
|
||||
height,
|
||||
groupBarChartData.groupSeparatorConfig.separatorWidth.toPx(),
|
||||
groupBarChartData.groupSeparatorConfig.separatorColor,
|
||||
groupBarChartData
|
||||
)
|
||||
}
|
||||
}
|
||||
drawUnderScrollMask(columnWidth, paddingRight, bgColor)
|
||||
|
||||
if (barStyle.selectionHighlightData != null) {
|
||||
// highlighting the selected bar and showing the data points
|
||||
identifiedPoint = highlightGroupBar(
|
||||
dragLocks,
|
||||
visibility,
|
||||
identifiedPoint,
|
||||
barStyle.selectionHighlightData,
|
||||
isTapped,
|
||||
columnWidth,
|
||||
yBottom,
|
||||
paddingRight,
|
||||
yOffset,
|
||||
barStyle.barWidth
|
||||
)
|
||||
}
|
||||
},
|
||||
drawXAndYAxis = { scrollOffset, xZoom ->
|
||||
val points = mutableListOf<Point>()
|
||||
for (index in groupBarList.indices) {
|
||||
points.add(Point(index.toFloat(), 0f))
|
||||
}
|
||||
|
||||
if (groupBarChartData.showXAxis) {
|
||||
XAxis(
|
||||
xAxisData = xAxisData,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.clip(
|
||||
RowClip(
|
||||
columnWidth, paddingRight
|
||||
)
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
rowHeight = it.size.height.toFloat()
|
||||
},
|
||||
xStart = columnWidth + horizontalGap + LocalDensity.current.run {
|
||||
(barStyle.barWidth.toPx() * groupingSize) / 2
|
||||
},
|
||||
scrollOffset = scrollOffset,
|
||||
zoomScale = xZoom,
|
||||
chartData = points,
|
||||
axisStart = columnWidth
|
||||
)
|
||||
}
|
||||
|
||||
if (groupBarChartData.showYAxis) {
|
||||
YAxis(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.fillMaxHeight()
|
||||
.wrapContentWidth()
|
||||
.onGloballyPositioned {
|
||||
columnWidth = it.size.width.toFloat()
|
||||
},
|
||||
yAxisData = yAxisData,
|
||||
)
|
||||
}
|
||||
},
|
||||
onPointClicked = { offset: Offset, _: Float ->
|
||||
isTapped = true
|
||||
visibility = true
|
||||
tapOffset = offset
|
||||
},
|
||||
onScroll = {
|
||||
isTapped = false
|
||||
visibility = false
|
||||
})
|
||||
}
|
||||
if (isTalkBackEnabled) {
|
||||
with(groupBarChartData) {
|
||||
AccessibilityBottomSheetDialog(
|
||||
modifier = Modifier.fillMaxSize(), backgroundColor = Color.White, content = {
|
||||
LazyColumn {
|
||||
items(barPlotData.groupBarList.size) { index ->
|
||||
Column {
|
||||
GroupBarInfo(
|
||||
barPlotData.groupBarList[index],
|
||||
xAxisData.axisLabelDescription(
|
||||
xAxisData.labelData(index)
|
||||
),
|
||||
barPlotData.barColorPaletteList,
|
||||
accessibilityConfig.titleTextSize,
|
||||
accessibilityConfig.descriptionTextSize
|
||||
)
|
||||
ItemDivider(
|
||||
thickness = accessibilityConfig.dividerThickness,
|
||||
dividerColor = accessibilityConfig.dividerColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
popUpTopRightButtonTitle = accessibilityConfig.popUpTopRightButtonTitle,
|
||||
popUpTopRightButtonDescription = accessibilityConfig.popUpTopRightButtonDescription,
|
||||
sheetState = accessibilitySheetState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package co.yml.charts.ui.barchart
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.DataCategoryOptions
|
||||
import co.yml.charts.axis.XAxis
|
||||
import co.yml.charts.axis.YAxis
|
||||
import co.yml.charts.chartcontainer.container.ScrollableCanvasContainer
|
||||
import co.yml.charts.common.extensions.ColumnClip
|
||||
import co.yml.charts.common.extensions.getMaxElementInXAxis
|
||||
import co.yml.charts.common.extensions.getXMaxAndMinPoints
|
||||
import co.yml.charts.common.extensions.getYMaxAndMinPoints
|
||||
import co.yml.charts.common.extensions.isYAxisTapped
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.common.utils.ChartConstants
|
||||
import co.yml.charts.ui.barchart.models.BarChartData
|
||||
import co.yml.charts.ui.barchart.models.BarData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
/**
|
||||
* compose method for drawing Horizontal bar chart
|
||||
*
|
||||
* @param barChartData
|
||||
* @param modifier
|
||||
* @param scope
|
||||
* @param accessibilitySheetState
|
||||
* @param isTalkBackEnabled
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun HorizontalBarChart(
|
||||
barChartData: BarChartData,
|
||||
modifier: Modifier,
|
||||
scope: CoroutineScope,
|
||||
accessibilitySheetState: ModalBottomSheetState,
|
||||
isTalkBackEnabled: Boolean
|
||||
) {
|
||||
with(barChartData) {
|
||||
var barHighlightVisibility by remember { mutableStateOf(false) }
|
||||
var identifiedPoint by remember { mutableStateOf(BarData(Point(0f, 0f))) }
|
||||
var xOffset by remember { mutableStateOf(0f) }
|
||||
var yOffset by remember { mutableStateOf(0f) }
|
||||
var tapOffset by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
var isTapped by remember { mutableStateOf(false) }
|
||||
var columnWidth by remember { mutableStateOf(0f) }
|
||||
var horizontalGap by remember { mutableStateOf(0f) }
|
||||
var rowHeight by remember { mutableStateOf(0f) }
|
||||
val points = chartData.map { it.point }
|
||||
val bgColor = MaterialTheme.colorScheme.surface
|
||||
|
||||
val (_, xMax) = getXMaxAndMinPoints(points)
|
||||
val (yMin, yMax) = getYMaxAndMinPoints(points)
|
||||
|
||||
val xAxisData = xAxisData.copy(
|
||||
dataCategoryOptions = DataCategoryOptions(isDataCategoryInYAxis = true)
|
||||
)
|
||||
val yAxisData = yAxisData.copy(
|
||||
axisStepSize = barStyle.barWidth + barStyle.paddingBetweenBars,
|
||||
steps = chartData.size - 1,
|
||||
axisBottomPadding = LocalDensity.current.run { rowHeight.toDp() })
|
||||
val maxElementInXAxis = getMaxElementInXAxis(xMax, xAxisData.steps)
|
||||
|
||||
if (!showXAxis) {
|
||||
rowHeight =
|
||||
LocalDensity.current.run { ChartConstants.DEFAULT_YAXIS_BOTTOM_PADDING.dp.toPx() }
|
||||
}
|
||||
|
||||
ScrollableCanvasContainer(modifier = modifier
|
||||
.semantics {
|
||||
contentDescription = barChartData.accessibilityConfig.chartDescription
|
||||
}
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollOrientation = Orientation.Vertical,
|
||||
containerBackgroundColor = backgroundColor,
|
||||
calculateMaxDistance = { yZoom ->
|
||||
horizontalGap = horizontalExtraSpace.toPx()
|
||||
val yStart = horizontalGap + (yAxisData.startDrawPadding.toPx() * yZoom)
|
||||
yOffset =
|
||||
(barStyle.barWidth.toPx() + barStyle.paddingBetweenBars.toPx()) * yZoom
|
||||
|
||||
getMaxScrollDistance(
|
||||
rowHeight,
|
||||
yMax,
|
||||
yMin,
|
||||
yOffset,
|
||||
yStart,
|
||||
yAxisData.axisTopPadding.toPx(),
|
||||
size.height
|
||||
)
|
||||
},
|
||||
onDraw = { scrollOffset, yZoom ->
|
||||
val xLeft = columnWidth
|
||||
val startPaddingFromBottom =
|
||||
(yAxisData.startDrawPadding.toPx() * yZoom) + barStyle.barWidth.toPx() / 2
|
||||
val yStart =
|
||||
if (yAxisData.dataCategoryOptions.isDataCategoryStartFromBottom) startPaddingFromBottom else {
|
||||
if (yZoom < 1) startPaddingFromBottom else yAxisData.axisTopPadding.toPx()
|
||||
}
|
||||
val yBottom = size.height - rowHeight
|
||||
yOffset = (barStyle.barWidth.toPx() + barStyle.paddingBetweenBars.toPx()) * yZoom
|
||||
|
||||
xOffset = (size.width - xLeft - xAxisData.axisEndPadding.toPx()) / maxElementInXAxis
|
||||
val dragLocks = mutableMapOf<Int, Pair<BarData, Offset>>()
|
||||
|
||||
// Draw bar lines
|
||||
chartData.forEachIndexed { _, barData ->
|
||||
val drawOffset = getDrawHorizontalOffset(
|
||||
barData.point,
|
||||
xLeft,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yOffset,
|
||||
yMin,
|
||||
yMax,
|
||||
yStart,
|
||||
yAxisData.dataCategoryOptions,
|
||||
yZoom
|
||||
)
|
||||
val width = (barData.point.x) * xOffset
|
||||
|
||||
// drawing each individual bars
|
||||
barChartData.drawBar(this, barData, drawOffset, width, barChartType, barStyle)
|
||||
|
||||
val middleOffset =
|
||||
Offset(drawOffset.x, drawOffset.y + barStyle.barWidth.toPx() / 2)
|
||||
|
||||
val xAxisWidth =
|
||||
xLeft + ((barData.point.x - 0) * xOffset) + barStyle.barWidth.toPx()
|
||||
// store the tap points for selection
|
||||
if (isTapped && middleOffset.isYAxisTapped(
|
||||
tapOffset,
|
||||
barStyle.barWidth.toPx(),
|
||||
xLeft,
|
||||
tapPadding.toPx(),
|
||||
xAxisWidth
|
||||
)
|
||||
) {
|
||||
dragLocks[0] = barData to drawOffset
|
||||
}
|
||||
}
|
||||
|
||||
drawUnderXAxisScrollMask(rowHeight, yAxisData.axisTopPadding, bgColor)
|
||||
|
||||
if (barStyle.selectionHighlightData != null) {
|
||||
// highlighting the selected bar and showing the data points
|
||||
identifiedPoint = highlightHorizontalBar(
|
||||
dragLocks,
|
||||
barHighlightVisibility,
|
||||
identifiedPoint,
|
||||
barChartData.barStyle,
|
||||
isTapped,
|
||||
yBottom,
|
||||
yAxisData.axisTopPadding,
|
||||
xLeft,
|
||||
xOffset
|
||||
)
|
||||
}
|
||||
},
|
||||
drawXAndYAxis = { scrollOffset, xZoom ->
|
||||
if (showXAxis) {
|
||||
XAxis(
|
||||
xAxisData = xAxisData,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.onGloballyPositioned {
|
||||
rowHeight = it.size.height.toFloat()
|
||||
},
|
||||
xStart = columnWidth,
|
||||
scrollOffset = 0f,
|
||||
zoomScale = xZoom,
|
||||
chartData = points,
|
||||
axisStart = columnWidth
|
||||
)
|
||||
}
|
||||
|
||||
if (showYAxis) {
|
||||
val startPaddingFromBottom = LocalDensity.current.run {
|
||||
(yAxisData.startDrawPadding.toPx() * xZoom)
|
||||
}
|
||||
YAxis(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.fillMaxHeight()
|
||||
.wrapContentWidth()
|
||||
.clip(
|
||||
ColumnClip(
|
||||
0f,
|
||||
LocalDensity.current.run { yAxisData.axisTopPadding.toPx() },
|
||||
columnWidth,
|
||||
rowHeight
|
||||
)
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
columnWidth = it.size.width.toFloat()
|
||||
},
|
||||
yAxisData = yAxisData,
|
||||
scrollOffset = scrollOffset,
|
||||
zoomScale = xZoom,
|
||||
chartData = points,
|
||||
dataCategoryWidth = LocalDensity.current.run { yAxisData.axisStepSize.toPx() },
|
||||
yStart =
|
||||
if (yAxisData.dataCategoryOptions.isDataCategoryStartFromBottom) {
|
||||
startPaddingFromBottom
|
||||
} else {
|
||||
if (xZoom < 1) {
|
||||
startPaddingFromBottom
|
||||
} else {
|
||||
LocalDensity.current.run {
|
||||
(yAxisData.axisTopPadding.toPx() + barStyle.barWidth.toPx() / 2)
|
||||
}
|
||||
}
|
||||
}, LocalDensity.current.run { barStyle.barWidth.toPx() }
|
||||
)
|
||||
}
|
||||
}, onPointClicked = { offset: Offset, _: Float ->
|
||||
isTapped = true
|
||||
barHighlightVisibility = true
|
||||
tapOffset = offset
|
||||
}, onScroll = {
|
||||
isTapped = false
|
||||
barHighlightVisibility = false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package co.yml.charts.ui.barchart
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.XAxis
|
||||
import co.yml.charts.axis.YAxis
|
||||
import co.yml.charts.chartcontainer.container.ScrollableCanvasContainer
|
||||
import co.yml.charts.common.components.ItemDivider
|
||||
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
|
||||
import co.yml.charts.common.components.accessibility.GroupBarInfo
|
||||
import co.yml.charts.common.extensions.RowClip
|
||||
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
|
||||
import co.yml.charts.common.extensions.getMaxElementInYAxis
|
||||
import co.yml.charts.common.extensions.isStackedBarTapped
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.common.utils.ChartConstants
|
||||
import co.yml.charts.ui.barchart.models.BarData
|
||||
import co.yml.charts.ui.barchart.models.GroupBarChartData
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
*
|
||||
* [StackedBarChart] compose method for drawing stacked bar chart.
|
||||
* @param modifier: All modifier related properties
|
||||
* @param groupBarChartData : All data needed to stacked bar chart
|
||||
* @see [GroupBarChartData] Data class to save all params related to stacked bar chart
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun StackedBarChart(
|
||||
modifier: Modifier,
|
||||
groupBarChartData: GroupBarChartData,
|
||||
isTalkBackEnabled: Boolean = LocalContext.current.collectIsTalkbackEnabledAsState().value
|
||||
) {
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
if (accessibilitySheetState.isVisible && isTalkBackEnabled
|
||||
&& groupBarChartData.accessibilityConfig.shouldHandleBackWhenTalkBackPopUpShown
|
||||
) {
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
accessibilitySheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.testTag("stacked_bar_chart")) {
|
||||
with(groupBarChartData.barPlotData) {
|
||||
var visibility by remember { mutableStateOf(false) }
|
||||
var identifiedPoint by remember { mutableStateOf(BarData(Point(0f, 0f))) }
|
||||
var xOffset by remember { mutableStateOf(0f) }
|
||||
var tapOffset by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
var isTapped by remember { mutableStateOf(false) }
|
||||
var columnWidth by remember { mutableStateOf(0f) }
|
||||
var horizontalGap by remember { mutableStateOf(0f) }
|
||||
var rowHeight by remember { mutableStateOf(0f) }
|
||||
val paddingRight = groupBarChartData.paddingEnd
|
||||
val valueList = mutableListOf<Float>()
|
||||
groupBarList.map { groupBar ->
|
||||
var yMax = 0f
|
||||
groupBar.barList.forEach {
|
||||
yMax += it.point.y
|
||||
}
|
||||
valueList.add(yMax)
|
||||
}
|
||||
val bgColor = MaterialTheme.colorScheme.surface
|
||||
|
||||
val xMax = groupBarList.size
|
||||
val yMax = valueList.maxOrNull() ?: 0f
|
||||
val xAxisData =
|
||||
groupBarChartData.xAxisData.copy(axisStepSize = barStyle.barWidth + barStyle.paddingBetweenBars)
|
||||
val yAxisData =
|
||||
groupBarChartData.yAxisData.copy(axisBottomPadding = LocalDensity.current.run { rowHeight.toDp() })
|
||||
|
||||
val maxElementInYAxis = getMaxElementInYAxis(yMax, yAxisData.steps)
|
||||
val paddingBetweenBars =
|
||||
LocalDensity.current.run { groupBarChartData.paddingBetweenStackedBars.toPx() }
|
||||
|
||||
if (!groupBarChartData.showXAxis) {
|
||||
rowHeight =
|
||||
LocalDensity.current.run { ChartConstants.DEFAULT_YAXIS_BOTTOM_PADDING.dp.toPx() }
|
||||
}
|
||||
ScrollableCanvasContainer(
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription = groupBarChartData.accessibilityConfig.chartDescription
|
||||
}
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
containerBackgroundColor = groupBarChartData.backgroundColor,
|
||||
calculateMaxDistance = { xZoom ->
|
||||
horizontalGap = groupBarChartData.horizontalExtraSpace.toPx()
|
||||
val xLeft = (xAxisData.startDrawPadding.toPx() * xZoom) + horizontalGap
|
||||
xOffset =
|
||||
(barStyle.barWidth.toPx() + barStyle.paddingBetweenBars.toPx()) * xZoom
|
||||
getMaxScrollDistance(
|
||||
columnWidth = columnWidth,
|
||||
xMax = xMax.toFloat(),
|
||||
xMin = 0f,
|
||||
xOffset = xOffset,
|
||||
xLeft = xLeft,
|
||||
paddingRight = paddingRight.toPx(),
|
||||
canvasWidth = size.width
|
||||
)
|
||||
},
|
||||
onDraw = { scrollOffset, xZoom ->
|
||||
val isHighlightFullBar =
|
||||
barStyle.selectionHighlightData?.isHighlightFullBar ?: false
|
||||
val yBottom = size.height - rowHeight
|
||||
|
||||
val totalPaddingBtwBars =
|
||||
(groupBarList.first().barList.size - 1) * paddingBetweenBars
|
||||
val yOffset =
|
||||
(yBottom - yAxisData.axisTopPadding.toPx() - totalPaddingBtwBars) / maxElementInYAxis
|
||||
xOffset =
|
||||
(barStyle.barWidth.toPx() + barStyle.paddingBetweenBars.toPx()) * xZoom
|
||||
val xLeft = columnWidth
|
||||
val dragLocks = mutableMapOf<Int, Pair<BarData, Offset>>()
|
||||
|
||||
// Draw bar lines
|
||||
groupBarList.forEachIndexed { index, groupBarData ->
|
||||
var insideOffset = 0f
|
||||
val xPointOffset =
|
||||
groupBarData.barList.first().point.x * xOffset + xLeft + (xAxisData.startDrawPadding.toPx() * xZoom) - barStyle.barWidth.toPx() / 2 - scrollOffset
|
||||
val fullBarDetails = getFullBarDetails(
|
||||
groupBarData.barList,
|
||||
totalPaddingBtwBars,
|
||||
yOffset,
|
||||
yBottom,
|
||||
xPointOffset
|
||||
)
|
||||
|
||||
groupBarData.barList.forEachIndexed { subIndex, individualBar ->
|
||||
val drawOffset = getGroupBarDrawOffset(
|
||||
x = index,
|
||||
y = individualBar.point.y,
|
||||
xOffset = xOffset,
|
||||
xLeft = xLeft,
|
||||
scrollOffset = scrollOffset,
|
||||
yBottom = yBottom,
|
||||
yOffset = yOffset,
|
||||
yMin = 0f,
|
||||
xMin = 0f,
|
||||
startDrawPadding = xAxisData.startDrawPadding.toPx(),
|
||||
zoomScale = xZoom,
|
||||
barWidth = barStyle.barWidth.toPx()
|
||||
)
|
||||
|
||||
val height = yBottom - drawOffset.y
|
||||
|
||||
val individualOffset = Offset(drawOffset.x, drawOffset.y - insideOffset)
|
||||
|
||||
// drawing each individual bars
|
||||
groupBarChartData.drawBar(
|
||||
this,
|
||||
groupBarChartData,
|
||||
barStyle,
|
||||
individualOffset,
|
||||
height,
|
||||
subIndex
|
||||
)
|
||||
|
||||
insideOffset += height + paddingBetweenBars
|
||||
|
||||
val middleOffset =
|
||||
Offset(
|
||||
drawOffset.x + barStyle.barWidth.toPx() / 2,
|
||||
individualOffset.y
|
||||
)
|
||||
|
||||
if (isTapped && middleOffset.isStackedBarTapped(
|
||||
tapOffset = tapOffset,
|
||||
barWidth = barStyle.barWidth.toPx(),
|
||||
barHeight = if (isHighlightFullBar) yBottom else individualOffset.y + height,
|
||||
tapPadding = groupBarChartData.tapPadding.toPx()
|
||||
)
|
||||
) {
|
||||
if (isHighlightFullBar) {
|
||||
dragLocks[0] = fullBarDetails.first to fullBarDetails.second
|
||||
} else {
|
||||
dragLocks[0] = individualBar to individualOffset
|
||||
}
|
||||
}
|
||||
|
||||
drawUnderScrollMask(columnWidth, paddingRight, bgColor)
|
||||
|
||||
if (barStyle.selectionHighlightData != null) {
|
||||
// highlighting the selected bar and showing the data points
|
||||
identifiedPoint = highlightGroupBar(
|
||||
dragLocks = dragLocks,
|
||||
visibility = visibility,
|
||||
identifiedPoint = identifiedPoint,
|
||||
selectionHighlightData = barStyle.selectionHighlightData,
|
||||
isDragging = isTapped,
|
||||
columnWidth = columnWidth,
|
||||
yBottom = yBottom,
|
||||
paddingRight = paddingRight,
|
||||
yOffset = yOffset,
|
||||
barWidth = barStyle.barWidth,
|
||||
totalPaddingBtwBars,
|
||||
isHighlightFullBar
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
drawXAndYAxis = { scrollOffset, xZoom ->
|
||||
val points = mutableListOf<Point>()
|
||||
for (index in groupBarList.indices) {
|
||||
points.add(Point(index.toFloat(), 0f))
|
||||
}
|
||||
|
||||
if (groupBarChartData.showXAxis) {
|
||||
XAxis(
|
||||
xAxisData = xAxisData,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.clip(
|
||||
RowClip(
|
||||
columnWidth, paddingRight
|
||||
)
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
rowHeight = it.size.height.toFloat()
|
||||
},
|
||||
xStart = columnWidth,
|
||||
scrollOffset = scrollOffset,
|
||||
zoomScale = xZoom,
|
||||
chartData = points,
|
||||
axisStart = columnWidth
|
||||
)
|
||||
}
|
||||
if (groupBarChartData.showYAxis) {
|
||||
YAxis(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.fillMaxHeight()
|
||||
.wrapContentWidth()
|
||||
.onGloballyPositioned {
|
||||
columnWidth = it.size.width.toFloat()
|
||||
},
|
||||
yAxisData = yAxisData,
|
||||
)
|
||||
}
|
||||
},
|
||||
onPointClicked = { offset: Offset, _: Float ->
|
||||
isTapped = true
|
||||
visibility = true
|
||||
tapOffset = offset
|
||||
},
|
||||
onScroll = {
|
||||
isTapped = false
|
||||
visibility = false
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isTalkBackEnabled) {
|
||||
with(groupBarChartData) {
|
||||
AccessibilityBottomSheetDialog(
|
||||
modifier = Modifier.fillMaxSize(), backgroundColor = Color.White, content = {
|
||||
LazyColumn(modifier = Modifier.semantics {
|
||||
this.testTag = "AccessibilityBottomSheet List"
|
||||
}) {
|
||||
items(barPlotData.groupBarList.size) { index ->
|
||||
Column {
|
||||
GroupBarInfo(
|
||||
barPlotData.groupBarList[index],
|
||||
xAxisData.axisLabelDescription(
|
||||
xAxisData.labelData(index)
|
||||
),
|
||||
barPlotData.barColorPaletteList,
|
||||
accessibilityConfig.titleTextSize,
|
||||
accessibilityConfig.descriptionTextSize
|
||||
)
|
||||
ItemDivider(
|
||||
thickness = accessibilityConfig.dividerThickness,
|
||||
dividerColor = accessibilityConfig.dividerColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
popUpTopRightButtonTitle = accessibilityConfig.popUpTopRightButtonTitle,
|
||||
popUpTopRightButtonDescription = accessibilityConfig.popUpTopRightButtonDescription,
|
||||
sheetState = accessibilitySheetState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
package co.yml.charts.ui.barchart
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.XAxis
|
||||
import co.yml.charts.axis.YAxis
|
||||
import co.yml.charts.chartcontainer.container.ScrollableCanvasContainer
|
||||
import co.yml.charts.common.extensions.RowClip
|
||||
import co.yml.charts.common.extensions.getMaxElementInYAxis
|
||||
import co.yml.charts.common.extensions.getXMaxAndMinPoints
|
||||
import co.yml.charts.common.extensions.getYMaxAndMinPoints
|
||||
import co.yml.charts.common.extensions.isTapped
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.common.utils.ChartConstants
|
||||
import co.yml.charts.ui.barchart.models.BarChartData
|
||||
import co.yml.charts.ui.barchart.models.BarData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
/**
|
||||
* composable for drawing Vertical bar chart
|
||||
*
|
||||
* @param barChartData
|
||||
* @param modifier
|
||||
* @param scope
|
||||
* @param accessibilitySheetState
|
||||
* @param isTalkBackEnabled
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun VerticalBarChart(
|
||||
barChartData: BarChartData,
|
||||
modifier: Modifier,
|
||||
scope: CoroutineScope,
|
||||
accessibilitySheetState: ModalBottomSheetState,
|
||||
isTalkBackEnabled: Boolean
|
||||
) {
|
||||
with(barChartData) {
|
||||
var barHighlightVisibility by remember { mutableStateOf(false) }
|
||||
var identifiedPoint by remember { mutableStateOf(BarData(Point(0f, 0f))) }
|
||||
var xOffset by remember { mutableStateOf(0f) }
|
||||
var tapOffset by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
var isTapped by remember { mutableStateOf(false) }
|
||||
var columnWidth by remember { mutableStateOf(0f) }
|
||||
var horizontalGap by remember { mutableStateOf(0f) }
|
||||
var rowHeight by remember { mutableStateOf(0f) }
|
||||
val paddingRight = paddingEnd
|
||||
val points = chartData.map { it.point }
|
||||
val bgColor = MaterialTheme.colorScheme.surface
|
||||
|
||||
val (xMin, xMax) = getXMaxAndMinPoints(points)
|
||||
val (_, yMax) = getYMaxAndMinPoints(points)
|
||||
|
||||
val xAxisData =
|
||||
xAxisData.copy(
|
||||
axisStepSize = barStyle.barWidth + barStyle.paddingBetweenBars,
|
||||
steps = chartData.size - 1
|
||||
)
|
||||
val yAxisData =
|
||||
yAxisData.copy(
|
||||
axisBottomPadding = LocalDensity.current.run { rowHeight.toDp() }
|
||||
)
|
||||
val maxElementInYAxis = getMaxElementInYAxis(yMax, yAxisData.steps)
|
||||
|
||||
if (!showXAxis) {
|
||||
rowHeight = LocalDensity.current.run { ChartConstants.DEFAULT_YAXIS_BOTTOM_PADDING.dp.toPx() }
|
||||
}
|
||||
|
||||
ScrollableCanvasContainer(modifier = modifier
|
||||
.semantics {
|
||||
contentDescription = barChartData.accessibilityConfig.chartDescription
|
||||
}
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
containerBackgroundColor = backgroundColor,
|
||||
calculateMaxDistance = { xZoom ->
|
||||
horizontalGap = horizontalExtraSpace.toPx()
|
||||
val xLeft = (xAxisData.startDrawPadding.toPx() * xZoom) + horizontalGap
|
||||
xOffset =
|
||||
(barStyle.barWidth.toPx() + barStyle.paddingBetweenBars.toPx()) * xZoom
|
||||
getMaxScrollDistance(
|
||||
columnWidth, xMax, xMin, xOffset, xLeft, paddingRight.toPx(), size.width
|
||||
)
|
||||
},
|
||||
onDraw = { scrollOffset, xZoom ->
|
||||
val yBottom = size.height - rowHeight
|
||||
val yOffset = ((yBottom - yAxisData.axisTopPadding.toPx()) / maxElementInYAxis)
|
||||
xOffset =
|
||||
((barStyle.barWidth).toPx() + barStyle.paddingBetweenBars.toPx()) * xZoom
|
||||
val xLeft = columnWidth
|
||||
val dragLocks = mutableMapOf<Int, Pair<BarData, Offset>>()
|
||||
|
||||
// Draw bar lines
|
||||
chartData.forEachIndexed { _, barData ->
|
||||
val drawOffset = getDrawOffset(
|
||||
barData.point,
|
||||
xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yOffset,
|
||||
0f,
|
||||
xAxisData.startDrawPadding.toPx(),
|
||||
xZoom,
|
||||
barStyle.barWidth.toPx()
|
||||
)
|
||||
val height = yBottom - drawOffset.y
|
||||
|
||||
// drawing each individual bars
|
||||
barChartData.drawBar(this, barData, drawOffset, height, barChartType, barStyle)
|
||||
|
||||
val middleOffset =
|
||||
Offset(drawOffset.x + barStyle.barWidth.toPx() / 2, drawOffset.y)
|
||||
// store the tap points for selection
|
||||
if (isTapped && middleOffset.isTapped(
|
||||
tapOffset, barStyle.barWidth.toPx(), yBottom, tapPadding.toPx()
|
||||
)
|
||||
) {
|
||||
dragLocks[0] = barData to drawOffset
|
||||
}
|
||||
}
|
||||
|
||||
drawUnderScrollMask(columnWidth, paddingRight, bgColor)
|
||||
|
||||
if (barStyle.selectionHighlightData != null) {
|
||||
// highlighting the selected bar and showing the data points
|
||||
identifiedPoint = highlightVerticalBar(
|
||||
dragLocks,
|
||||
barHighlightVisibility,
|
||||
identifiedPoint,
|
||||
barChartData.barStyle,
|
||||
isTapped,
|
||||
columnWidth,
|
||||
yBottom,
|
||||
paddingRight,
|
||||
yOffset,
|
||||
true,
|
||||
0f,
|
||||
barChartType
|
||||
)
|
||||
}
|
||||
},
|
||||
drawXAndYAxis = { scrollOffset, xZoom ->
|
||||
if (showXAxis) {
|
||||
XAxis(
|
||||
xAxisData = xAxisData,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.clip(
|
||||
RowClip(
|
||||
columnWidth, paddingRight
|
||||
)
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
rowHeight = it.size.height.toFloat()
|
||||
},
|
||||
xStart = columnWidth,
|
||||
scrollOffset = scrollOffset,
|
||||
zoomScale = xZoom,
|
||||
chartData = points,
|
||||
axisStart = columnWidth
|
||||
)
|
||||
}
|
||||
|
||||
if (showYAxis) {
|
||||
YAxis(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.fillMaxHeight()
|
||||
.wrapContentWidth()
|
||||
.onGloballyPositioned {
|
||||
columnWidth = it.size.width.toFloat()
|
||||
},
|
||||
yAxisData = yAxisData,
|
||||
scrollOffset = scrollOffset,
|
||||
zoomScale = xZoom,
|
||||
chartData = points
|
||||
)
|
||||
}
|
||||
}, onPointClicked = { offset: Offset, _: Float ->
|
||||
isTapped = true
|
||||
barHighlightVisibility = true
|
||||
tapOffset = offset
|
||||
}, onScroll = {
|
||||
isTapped = false
|
||||
barHighlightVisibility = false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package co.yml.charts.ui.barchart.models
|
||||
|
||||
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.model.AccessibilityConfig
|
||||
|
||||
|
||||
/**
|
||||
* BarGraph data class params used in drawing bar graph.
|
||||
* @param chartData : List of BarData
|
||||
* @param xAxisData: All the configurations related to X-Axis to be defined here in [AxisData]
|
||||
* @param yAxisData: All the configurations related to Y-Axis to be defined here in [AxisData]
|
||||
* @param horizontalExtraSpace: Extra space added in the horizontal axis
|
||||
* @param paddingEnd: End Padding
|
||||
* @param paddingTop: Top Padding
|
||||
* @param tapPadding: Extra padding area for tapping
|
||||
* @param showXAxis: Boolean Flag to enable/disable X axis
|
||||
* @param showYAxis: Boolean Flag to enable/disable Y axis
|
||||
* @param accessibilityConfig: Configs related to accessibility service defined here in [AccessibilityConfig]
|
||||
*/
|
||||
data class BarChartData(
|
||||
val chartData: List<BarData>,
|
||||
val xAxisData: AxisData = AxisData.Builder().build(),
|
||||
val yAxisData: AxisData = AxisData.Builder().build(),
|
||||
val backgroundColor: Color = Color.White,
|
||||
val horizontalExtraSpace: Dp = 0.dp,
|
||||
val barStyle: BarStyle = BarStyle(),
|
||||
val paddingEnd: Dp = 10.dp,
|
||||
val paddingTop: Dp = 0.dp,
|
||||
val tapPadding: Dp = 10.dp,
|
||||
val showYAxis: Boolean = true,
|
||||
val showXAxis: Boolean = true,
|
||||
val accessibilityConfig: AccessibilityConfig = AccessibilityConfig(),
|
||||
val barChartType: BarChartType = BarChartType.VERTICAL,
|
||||
val drawBar: (DrawScope, BarData, Offset, Float, BarChartType, BarStyle) -> Unit = { drawScope, barChartData, drawOffset, height, barChartType, barStyle ->
|
||||
//default implementation
|
||||
drawBarGraph(drawScope, barChartData, drawOffset, height, barChartType, barStyle)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* BarStyle data class adds styling related to each bar in the bar graph
|
||||
* @param barWidth: Width of a bar
|
||||
* @param cornerRadius: Corner radius for the bars
|
||||
* @param paddingBetweenBars: Space between adjacent bars
|
||||
* @param isGradientEnabled: Boolean Flag to enable/disable gradient bars
|
||||
* @param barBlendMode: Blend mode for the bars
|
||||
* @param barDrawStyle: Draw style for the bars
|
||||
* @param drawBar : Draw an individual bar
|
||||
*/
|
||||
data class BarStyle(
|
||||
val barWidth: Dp = 30.dp,
|
||||
val cornerRadius: Dp = 4.dp,
|
||||
val paddingBetweenBars: Dp = 15.dp,
|
||||
val isGradientEnabled: Boolean = false,
|
||||
val barBlendMode: BlendMode = DrawScope.DefaultBlendMode,
|
||||
val barDrawStyle: DrawStyle = Fill,
|
||||
val selectionHighlightData: SelectionHighlightData? = SelectionHighlightData()
|
||||
)
|
||||
|
||||
/**
|
||||
*
|
||||
* Used to draw the individual bars
|
||||
* @param drawScope : Creates a scoped drawing environment
|
||||
* @param barStyle : all meta data related to the bar styling
|
||||
* @param barData : data related to a single bar
|
||||
* @param drawOffset: top left offset for the drawing the bar
|
||||
* @param height : height of the bar graph
|
||||
* @param barChartType : type of bar chart
|
||||
*/
|
||||
fun drawBarGraph(
|
||||
drawScope: DrawScope,
|
||||
barData: BarData,
|
||||
drawOffset: Offset,
|
||||
height: Float,
|
||||
barChartType: BarChartType,
|
||||
barStyle: BarStyle
|
||||
) {
|
||||
with(drawScope) {
|
||||
with(barStyle) {
|
||||
// Draw bar lines
|
||||
if (isGradientEnabled) {
|
||||
val brush = Brush.verticalGradient(
|
||||
colors = barData.gradientColorList
|
||||
)
|
||||
drawRoundRect(
|
||||
brush = brush,
|
||||
topLeft = drawOffset,
|
||||
size = if (barChartType == BarChartType.VERTICAL) Size(
|
||||
barWidth.toPx(),
|
||||
height
|
||||
) else Size(height, barWidth.toPx()),
|
||||
cornerRadius = CornerRadius(
|
||||
cornerRadius.toPx(), cornerRadius.toPx()
|
||||
),
|
||||
style = barDrawStyle,
|
||||
blendMode = barBlendMode
|
||||
)
|
||||
} else {
|
||||
drawRoundRect(
|
||||
color = barData.color,
|
||||
topLeft = drawOffset,
|
||||
size = if (barChartType == BarChartType.VERTICAL) Size(
|
||||
barWidth.toPx(),
|
||||
height
|
||||
) else Size(height, barWidth.toPx()),
|
||||
cornerRadius = CornerRadius(
|
||||
cornerRadius.toPx(), cornerRadius.toPx()
|
||||
),
|
||||
style = barDrawStyle,
|
||||
blendMode = barBlendMode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package co.yml.charts.ui.barchart.models
|
||||
|
||||
/**
|
||||
* To specify barchart type
|
||||
*/
|
||||
enum class BarChartType {
|
||||
VERTICAL, HORIZONTAL
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package co.yml.charts.ui.barchart.models
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import co.yml.charts.axis.DataCategoryOptions
|
||||
import co.yml.charts.common.model.Point
|
||||
|
||||
|
||||
/**
|
||||
* Data class for individual bar.
|
||||
* @param point : Axis point
|
||||
* @param color: Color of a bar
|
||||
* @param label: label of a bar
|
||||
* @param gradientColorList: Color list for the gradient bar
|
||||
* @param description: Description to describe bar value for accessibility service.
|
||||
*/
|
||||
data class BarData(
|
||||
val point: Point,
|
||||
val color: Color = Color.Red,
|
||||
val label: String = "",
|
||||
val gradientColorList: List<Color> = listOf(Color.Red, Color.Blue),
|
||||
val dataCategoryOptions: DataCategoryOptions = DataCategoryOptions(),
|
||||
val description: String = if (dataCategoryOptions.isDataCategoryInYAxis) "Value of bar $label is value ${
|
||||
String.format(
|
||||
"%.2f",
|
||||
point.x
|
||||
)
|
||||
}" else "Value of bar $label is value ${String.format("%.2f", point.y)}"
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package co.yml.charts.ui.barchart.models
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import co.yml.charts.common.model.PlotData
|
||||
import co.yml.charts.common.model.PlotType
|
||||
|
||||
/**
|
||||
* BarPlotData is a data class that holds bar graph related data and styling components
|
||||
* @param plotType : Defines the type of plot/graph
|
||||
* @param groupBarList : Data related to the bar point.
|
||||
* @param barStyle : Styling related to the bars.
|
||||
*/
|
||||
data class BarPlotData(
|
||||
override val plotType: PlotType = PlotType.Bar,
|
||||
val groupBarList: List<GroupBar>,
|
||||
val groupingSize: Int = groupBarList.firstOrNull()?.barList?.size ?: 1,
|
||||
val barColorPaletteList: List<Color> = listOf(),
|
||||
val barStyle: BarStyle = BarStyle(),
|
||||
) : PlotData {
|
||||
companion object {
|
||||
fun default() = BarPlotData(groupBarList = listOf())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package co.yml.charts.ui.barchart.models
|
||||
|
||||
/**
|
||||
* Data class for individual group bar.
|
||||
* @param label: Label of a bar
|
||||
* @param barList: List of individual bars inside a group bar with each bar data of type [BarData]
|
||||
*/
|
||||
data class GroupBar(val label: String, val barList: List<BarData>) {
|
||||
val yMax: Float
|
||||
get() = this.barList.map { it.point }.maxOf { it.y }
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package co.yml.charts.ui.barchart.models
|
||||
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.model.AccessibilityConfig
|
||||
|
||||
/**
|
||||
* GroupBarGraph data class params used in drawing bar graph.
|
||||
* @param barPlotData : Group bar plot data.
|
||||
* @param xAxisData: All the configurations related to X-Axis to be defined here in [AxisData]
|
||||
* @param yAxisData: All the configurations related to Y-Axis to be defined here in [AxisData]
|
||||
* @param horizontalExtraSpace: Extra space added in the horizontal axis.
|
||||
* @param backgroundColor: Background color of the graph.
|
||||
* @param paddingEnd: End Padding.
|
||||
* @param paddingTop: Top Padding.
|
||||
* @param showXAxis: Boolean Flag to enable/disable X axis.
|
||||
* @param showYAxis: Boolean Flag to enable/disable Y axis.
|
||||
* @param tapPadding: Tap padding offset.
|
||||
* @param groupSeparatorConfig : All config related to the GroupSeparator.
|
||||
* @param accessibilityConfig: Configs related to accessibility service defined here in [AccessibilityConfig]
|
||||
*/
|
||||
|
||||
data class GroupBarChartData(
|
||||
val barPlotData: BarPlotData,
|
||||
val xAxisData: AxisData = AxisData.Builder().build(),
|
||||
val yAxisData: AxisData = AxisData.Builder().build(),
|
||||
val backgroundColor: Color = Color.White,
|
||||
val horizontalExtraSpace: Dp = 10.dp,
|
||||
val paddingEnd: Dp = 10.dp,
|
||||
val paddingTop: Dp = 0.dp,
|
||||
val showYAxis: Boolean = true,
|
||||
val showXAxis: Boolean = true,
|
||||
val tapPadding: Dp = 10.dp,
|
||||
val groupSeparatorConfig: GroupSeparatorConfig = GroupSeparatorConfig(),
|
||||
val accessibilityConfig: AccessibilityConfig = AccessibilityConfig(),
|
||||
val paddingBetweenStackedBars: Dp = 0.dp,
|
||||
val drawBar: (DrawScope, GroupBarChartData, BarStyle, Offset, Float, Int) -> Unit = { drawScope, barChartData, barStyle, drawOffset, height, barIndex ->
|
||||
//default implementation
|
||||
drawGroupBarGraph(
|
||||
drawScope,
|
||||
barChartData,
|
||||
barStyle,
|
||||
drawOffset,
|
||||
height,
|
||||
barPlotData.barColorPaletteList[barIndex],
|
||||
barIndex
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
*
|
||||
* Used to draw the individual bar[Used round rec as a default shape]
|
||||
* @param drawScope : Creates a scoped drawing environment
|
||||
* @param barGraphData : all meta data related to the bar graph
|
||||
* @param barStyle : all meta data related to the bar styling
|
||||
* @param drawOffset: topLeft offset for the drawing the bar
|
||||
* @param height : height of the bar graph
|
||||
* @param barIndex : Index of the bar
|
||||
*/
|
||||
fun drawGroupBarGraph(
|
||||
drawScope: DrawScope,
|
||||
barGraphData: GroupBarChartData,
|
||||
barStyle: BarStyle,
|
||||
drawOffset: Offset,
|
||||
height: Float,
|
||||
barColor: Color,
|
||||
barIndex: Int
|
||||
) {
|
||||
with(drawScope) {
|
||||
drawRoundRect(
|
||||
color = barColor,
|
||||
topLeft = drawOffset,
|
||||
size = Size(barStyle.barWidth.toPx(), height),
|
||||
cornerRadius = CornerRadius(
|
||||
barStyle.cornerRadius.toPx(), barStyle.cornerRadius.toPx()
|
||||
),
|
||||
style = barStyle.barDrawStyle,
|
||||
blendMode = barStyle.barBlendMode
|
||||
)
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package co.yml.charts.ui.barchart.models
|
||||
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* GroupSeparatorConfig data class params used in config groupSeparator.
|
||||
* @param separatorWidth : width of the groupSeparator
|
||||
* @param separatorColor : color of the groupSeparator
|
||||
* @param showSeparator : Boolean Flag to show/hide groupSeparator
|
||||
* @param separatorBlendMode: Blend mode for the groupSeparator
|
||||
* */
|
||||
data class GroupSeparatorConfig(
|
||||
val separatorWidth: Dp = 1.dp,
|
||||
val separatorColor: Color = Color.Gray,
|
||||
val showSeparator: Boolean = true,
|
||||
val separatorBlendMode: BlendMode = DrawScope.DefaultBlendMode,
|
||||
)
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package co.yml.charts.ui.barchart.models
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextPaint
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.common.extensions.getTextBackgroundRect
|
||||
|
||||
/**
|
||||
* Used to customise the highlighted text and the bar
|
||||
*
|
||||
* @param highlightTextOffset: Padding between the highlighted bar and the text
|
||||
* @param highlightTextSize: Text size of the highlighted bar text
|
||||
* @param highlightTextColor: Text color of the highlighted bar text
|
||||
* @param highlightTextBackgroundColor: Background color of the highlight background
|
||||
* @param highlightTextBackgroundAlpha: Alpha for the highlighted text background
|
||||
* @param highlightTextTypeface: Typeface of the highlighted bar text
|
||||
* @param highlightBarCornerRadius: Corner radius of the highlighted bar
|
||||
* @param highlightBarColor: Color of the highlighted bar
|
||||
* @param highlightBarStrokeWidth: Stroke width of the highlighted bar
|
||||
* @param highlightPopUpCornerRadius: Corner radius of the highlighted background
|
||||
* @param backgroundColorFilter: ColorFilter to apply to the color when drawn into the destination.
|
||||
* @param backgroundBlendMode:Blending algorithm to be applied to the path when it is drawn.
|
||||
* @param backgroundStyle: Whether or not the path is stroked or filled in.
|
||||
* @param isHighlightBarRequired: Boolean flag to enable disable highlight
|
||||
* @param popUpLabel : The text that can be shown on the popup given 2 input params x and y values
|
||||
* @param drawPopUp: Override this to change the default background implementation. You are provided
|
||||
* with the selected offset, x, y values, center point of bar.
|
||||
* @param drawHighlightBar: draw override this to change the default drawRoundRect implementation. You are provided
|
||||
* with the actual point x, y, height, width.
|
||||
* @param groupBarPopUpLabel: Popup label for selected bar in group bar chart.
|
||||
* @param drawGroupBarPopUp: draw override this to change the default popup implementation. You are provided
|
||||
* with the actual point x, y, height, width.
|
||||
*/
|
||||
data class SelectionHighlightData(
|
||||
//highlight text
|
||||
val highlightTextOffset: Dp = 15.dp,
|
||||
val highlightTextSize: TextUnit = 12.sp,
|
||||
val highlightTextColor: Color = Color.Black,
|
||||
val highlightTextBackgroundColor: Color = Color.Yellow,
|
||||
val highlightTextBackgroundAlpha: Float = 0.7f,
|
||||
val highlightTextTypeface: Typeface = Typeface.DEFAULT,
|
||||
// highlight bar
|
||||
val highlightBarCornerRadius: Dp = 2.dp,
|
||||
val highlightBarColor: Color = Color.Black,
|
||||
val highlightBarStrokeWidth: Dp = 2.dp,
|
||||
|
||||
val highlightPopUpCornerRadius: CornerRadius = CornerRadius(5f),
|
||||
val backgroundColorFilter: ColorFilter? = null,
|
||||
val backgroundBlendMode: BlendMode = DrawScope.DefaultBlendMode,
|
||||
val backgroundStyle: DrawStyle = Fill,
|
||||
val highlightLabelAlignment: Paint.Align = Paint.Align.CENTER,
|
||||
val isHighlightBarRequired: Boolean = true,
|
||||
val isHighlightFullBar: Boolean = false,
|
||||
val popUpLabel: (Float, Float) -> (String) = { x, y ->
|
||||
val xLabel = "x : ${x.toInt()} "
|
||||
val yLabel = "y : ${String.format("%.2f", y)}"
|
||||
"$xLabel $yLabel"
|
||||
},
|
||||
|
||||
val drawPopUp: DrawScope.(Offset, BarData, Float, Float, BarChartType) -> Unit = { selectedOffset, identifiedPoint, centerPointOfBar, selectedXAxisWidth, barChartType ->
|
||||
val highlightTextPaint = TextPaint().apply {
|
||||
textSize = highlightTextSize.toPx()
|
||||
color = highlightTextColor.toArgb()
|
||||
textAlign = highlightLabelAlignment
|
||||
typeface = highlightTextTypeface
|
||||
}
|
||||
val label = popUpLabel(identifiedPoint.point.x, identifiedPoint.point.y)
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
val x =
|
||||
if (barChartType == BarChartType.VERTICAL) centerPointOfBar else selectedXAxisWidth + highlightTextOffset.toPx()
|
||||
val y =
|
||||
if (barChartType == BarChartType.VERTICAL) selectedOffset.y else centerPointOfBar
|
||||
val background = getTextBackgroundRect(
|
||||
x,
|
||||
y,
|
||||
label,
|
||||
highlightTextPaint
|
||||
)
|
||||
drawRoundRect(
|
||||
color = highlightTextBackgroundColor,
|
||||
topLeft = Offset(
|
||||
background.left.toFloat(),
|
||||
if (barChartType == BarChartType.VERTICAL) background.top.toFloat() - highlightTextOffset.toPx() else (y - background.height() / 2)
|
||||
),
|
||||
size = Size(background.width().toFloat(), background.height().toFloat()),
|
||||
alpha = highlightTextBackgroundAlpha,
|
||||
cornerRadius = highlightPopUpCornerRadius,
|
||||
colorFilter = backgroundColorFilter,
|
||||
blendMode = backgroundBlendMode,
|
||||
style = backgroundStyle
|
||||
)
|
||||
val drawTextX =
|
||||
if (barChartType == BarChartType.VERTICAL) centerPointOfBar else selectedXAxisWidth + highlightTextOffset.toPx()
|
||||
val drawTextY =
|
||||
if (barChartType == BarChartType.VERTICAL) selectedOffset.y - highlightTextOffset.toPx() else (y + background.height() / 2) - (highlightTextPaint.fontMetrics.descent)
|
||||
drawText(
|
||||
label,
|
||||
drawTextX,
|
||||
drawTextY,
|
||||
highlightTextPaint
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
val drawHighlightBar: DrawScope.(Float, Float, Float, Float, BarChartType) -> Unit = { x, y, width, height, barChartType ->
|
||||
val rectWidth = if (barChartType == BarChartType.VERTICAL) width else height
|
||||
val rectHeight = if (barChartType == BarChartType.VERTICAL) height else width
|
||||
drawRoundRect(
|
||||
color = highlightBarColor,
|
||||
topLeft = Offset(x, y),
|
||||
size = Size(rectWidth, rectHeight),
|
||||
cornerRadius = CornerRadius(
|
||||
highlightBarCornerRadius.toPx(),
|
||||
highlightBarCornerRadius.toPx()
|
||||
),
|
||||
style = Stroke(width = highlightBarStrokeWidth.toPx())
|
||||
)
|
||||
},
|
||||
|
||||
val groupBarPopUpLabel: (String, Float) -> (String) = { name, value ->
|
||||
val xLabel = "Name : $name "
|
||||
val yLabel = "Value : ${String.format("%.2f", value)}"
|
||||
"$xLabel $yLabel"
|
||||
},
|
||||
|
||||
|
||||
val drawGroupBarPopUp: DrawScope.(Offset, BarData, Float) -> Unit = { selectedOffset, identifiedPoint, centerPointOfBar ->
|
||||
val highlightTextPaint = TextPaint().apply {
|
||||
textSize = highlightTextSize.toPx()
|
||||
color = highlightTextColor.toArgb()
|
||||
textAlign = highlightLabelAlignment
|
||||
typeface = highlightTextTypeface
|
||||
}
|
||||
val xLabel = "${identifiedPoint.point.x.toInt()}"
|
||||
val label = groupBarPopUpLabel(xLabel, identifiedPoint.point.y)
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
val background = getTextBackgroundRect(
|
||||
centerPointOfBar,
|
||||
selectedOffset.y,
|
||||
label,
|
||||
highlightTextPaint
|
||||
)
|
||||
drawRoundRect(
|
||||
color = highlightTextBackgroundColor,
|
||||
topLeft = Offset(
|
||||
background.left.toFloat(),
|
||||
background.top.toFloat() - highlightTextOffset.toPx()
|
||||
),
|
||||
size = Size(background.width().toFloat(), background.height().toFloat()),
|
||||
alpha = highlightTextBackgroundAlpha,
|
||||
cornerRadius = highlightPopUpCornerRadius,
|
||||
colorFilter = backgroundColorFilter,
|
||||
blendMode = backgroundBlendMode,
|
||||
style = backgroundStyle
|
||||
)
|
||||
drawText(
|
||||
label,
|
||||
centerPointOfBar,
|
||||
selectedOffset.y - highlightTextOffset.toPx(),
|
||||
highlightTextPaint
|
||||
)
|
||||
}
|
||||
},
|
||||
val barChartType: BarChartType = BarChartType.VERTICAL
|
||||
)
|
||||
@@ -0,0 +1,497 @@
|
||||
package co.yml.charts.ui.barchart
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import co.yml.charts.axis.DataCategoryOptions
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.barchart.models.BarChartType
|
||||
import co.yml.charts.ui.barchart.models.BarData
|
||||
import co.yml.charts.ui.barchart.models.BarStyle
|
||||
import co.yml.charts.ui.barchart.models.GroupBarChartData
|
||||
import co.yml.charts.ui.barchart.models.SelectionHighlightData
|
||||
|
||||
/**
|
||||
*
|
||||
* Used to draw the highlighted text
|
||||
* @param identifiedPoint : Selected points
|
||||
* @param selectedOffset: Offset selected
|
||||
* @param barWidth: Width of single bar
|
||||
* @param highlightData: Data for the highlight section
|
||||
* @param selectedXAxisWidth: when data value is present in xAxis [e.g. Horizontal barchart]
|
||||
*/
|
||||
internal fun DrawScope.drawHighlightText(
|
||||
identifiedPoint: BarData,
|
||||
selectedOffset: Offset,
|
||||
barWidth: Dp,
|
||||
highlightData: SelectionHighlightData,
|
||||
selectedXAxisWidth: Float = 0f,
|
||||
barChartType: BarChartType
|
||||
) {
|
||||
|
||||
val centerPointOfBar =
|
||||
if (barChartType == BarChartType.VERTICAL) selectedOffset.x + barWidth.toPx() / 2 else selectedOffset.y + barWidth.toPx() / 2
|
||||
// Drawing the highlighted background and text
|
||||
highlightData.drawPopUp(
|
||||
this,
|
||||
selectedOffset,
|
||||
identifiedPoint,
|
||||
centerPointOfBar,
|
||||
selectedXAxisWidth,
|
||||
barChartType
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* returns identified point and displaying the data points and highlighted bar .
|
||||
* @param dragLocks : Mutable map of BarData and drawOffset.
|
||||
* @param barHighlightVisibility : Flag to control the visibility of highlighted text.
|
||||
* @param identifiedPoint: selected bar data.
|
||||
* @param barStyle: Data related to the bar graph styling.
|
||||
* @param isDragging : Boolean flag for the dragging status.
|
||||
* @param columnWidth : Width of the Y axis.
|
||||
* @param yBottom : Bottom padding.
|
||||
* @param paddingRight : Right padding.
|
||||
* @param yOffset : Distance between two y points.
|
||||
* @param shouldShowHighlightPopUp : Default true but if highlight popup not required make it false
|
||||
*/
|
||||
internal fun DrawScope.highlightVerticalBar(
|
||||
dragLocks: MutableMap<Int, Pair<BarData, Offset>>,
|
||||
barHighlightVisibility: Boolean,
|
||||
identifiedPoint: BarData,
|
||||
barStyle: BarStyle,
|
||||
isDragging: Boolean,
|
||||
columnWidth: Float,
|
||||
yBottom: Float,
|
||||
paddingRight: Dp,
|
||||
yOffset: Float,
|
||||
shouldShowHighlightPopUp: Boolean = true,
|
||||
selectedXAxisWidth: Float = 0f,
|
||||
barChartType: BarChartType
|
||||
): BarData {
|
||||
var mutableIdentifiedPoint: BarData = identifiedPoint
|
||||
// Handle the show the selected bar
|
||||
if (isDragging) {
|
||||
val x = dragLocks.values.firstOrNull()?.second?.x
|
||||
if (x != null) {
|
||||
mutableIdentifiedPoint = dragLocks.values.map { it.first }.first()
|
||||
}
|
||||
|
||||
// Draw highlight bar on selection
|
||||
if (barStyle.selectionHighlightData?.isHighlightBarRequired == true) {
|
||||
dragLocks.values.firstOrNull()?.let { (barData, location) ->
|
||||
val (xPoint, yPoint) = location
|
||||
if (xPoint >= columnWidth && xPoint <= size.width - paddingRight.toPx()) {
|
||||
val y1 = yBottom - ((barData.point.y - 0) * yOffset)
|
||||
barStyle.selectionHighlightData.drawHighlightBar(
|
||||
this,
|
||||
xPoint,
|
||||
yPoint,
|
||||
barStyle.barWidth.toPx(),
|
||||
(yBottom - y1),
|
||||
barStyle.selectionHighlightData.barChartType
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldShowHighlightPopUp) {
|
||||
val selectedOffset = dragLocks.values.firstOrNull()?.second
|
||||
if (barHighlightVisibility && selectedOffset != null && barStyle.selectionHighlightData != null) {
|
||||
drawHighlightText(
|
||||
mutableIdentifiedPoint,
|
||||
selectedOffset,
|
||||
barStyle.barWidth,
|
||||
barStyle.selectionHighlightData,
|
||||
selectedXAxisWidth,
|
||||
barChartType
|
||||
)
|
||||
}
|
||||
}
|
||||
return mutableIdentifiedPoint
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* returns identified point and displaying the data points and highlighted bar .
|
||||
* @param dragLocks : Mutable map of BarData and drawOffset.
|
||||
* @param barHighlightVisibility : Flag to control the visibility of highlighted text.
|
||||
* @param identifiedPoint: selected bar data.
|
||||
* @param barStyle: Data related to the bar graph styling.
|
||||
* @param isDragging : Boolean flag for the dragging status.
|
||||
* @param columnWidth : Width of the Y axis.
|
||||
* @param yBottom : Bottom padding.
|
||||
* @param paddingRight : Right padding.
|
||||
* @param yOffset : Distance between two y points.
|
||||
* @param shouldShowHighlightPopUp : Default true but if highlight popup not required make it false
|
||||
*/
|
||||
internal fun DrawScope.highlightHorizontalBar(
|
||||
dragLocks: MutableMap<Int, Pair<BarData, Offset>>,
|
||||
barHighlightVisibility: Boolean,
|
||||
identifiedPoint: BarData,
|
||||
barStyle: BarStyle,
|
||||
isDragging: Boolean,
|
||||
yBottom: Float,
|
||||
paddingTop: Dp,
|
||||
xStart: Float,
|
||||
xOffset: Float,
|
||||
shouldShowHighlightPopUp: Boolean = true
|
||||
): BarData {
|
||||
var mutableIdentifiedPoint: BarData = identifiedPoint
|
||||
// Handle the show the selected bar
|
||||
var selectedXAxisWidth = 0f
|
||||
|
||||
if (isDragging) {
|
||||
val y = dragLocks.values.firstOrNull()?.second?.y
|
||||
if (y != null) {
|
||||
mutableIdentifiedPoint = dragLocks.values.map { it.first }.first()
|
||||
}
|
||||
|
||||
if (barStyle.selectionHighlightData?.isHighlightBarRequired == true) {
|
||||
dragLocks.values.firstOrNull()?.let { (barData, location) ->
|
||||
val (xPoint, yPoint) = location
|
||||
selectedXAxisWidth =
|
||||
xStart + ((barData.point.x - 0) * xOffset) + barStyle.barWidth.toPx()
|
||||
if (yPoint + barStyle.barWidth.toPx() <= yBottom && yPoint >= paddingTop.toPx()) {
|
||||
val x1 = xStart + ((barData.point.x - 0) * xOffset)
|
||||
barStyle.selectionHighlightData?.drawHighlightBar?.invoke(
|
||||
this,
|
||||
xPoint,
|
||||
yPoint,
|
||||
barStyle.barWidth.toPx(),
|
||||
(x1 - xStart),
|
||||
barStyle.selectionHighlightData.barChartType
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldShowHighlightPopUp) {
|
||||
val selectedOffset = dragLocks.values.firstOrNull()?.second
|
||||
if (barHighlightVisibility && selectedOffset != null && barStyle.selectionHighlightData != null) {
|
||||
drawHighlightText(
|
||||
mutableIdentifiedPoint,
|
||||
selectedOffset,
|
||||
barStyle.barWidth,
|
||||
barStyle.selectionHighlightData ?: SelectionHighlightData(),
|
||||
selectedXAxisWidth,
|
||||
barStyle.selectionHighlightData.barChartType
|
||||
)
|
||||
}
|
||||
}
|
||||
return mutableIdentifiedPoint
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawUnderXAxisScrollMask extension method used for drawing a rectangular mask to make graph scrollable under the XAxis.
|
||||
* @param columnWidth : Width of the rectangular mask here width of Y Axis is used.
|
||||
* @param paddingRight : Padding given at the end of the graph.
|
||||
* @param bgColor : Background of the rectangular mask.
|
||||
*/
|
||||
internal fun DrawScope.drawUnderXAxisScrollMask(
|
||||
columnWidth: Float,
|
||||
paddingTop: Dp,
|
||||
bgColor: Color
|
||||
) {
|
||||
// Draw column to make graph look scrollable under Xaxis
|
||||
drawRect(
|
||||
bgColor, Offset(0f, size.height - columnWidth), Size(size.width, columnWidth)
|
||||
)
|
||||
// Draw top padding
|
||||
drawRect(
|
||||
bgColor,
|
||||
Offset(0f, 0f),
|
||||
Size(size.width, paddingTop.toPx())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the draw offset for bar graph.
|
||||
* @param point : bar point
|
||||
* @param xMin: Minimum value on the x axis
|
||||
* @param yMin: Minimum value on the y axis
|
||||
* @param xOffset: Distance between bars
|
||||
* @param yOffset: Distance between y axis points
|
||||
* @param xLeft: X starting point of bar graph
|
||||
* @param scrollOffset: Scroll offset
|
||||
* @param yBottom: Y starting point of bar graph
|
||||
*/
|
||||
internal fun getDrawOffset(
|
||||
point: Point,
|
||||
xMin: Float,
|
||||
xOffset: Float,
|
||||
xLeft: Float,
|
||||
scrollOffset: Float,
|
||||
yBottom: Float,
|
||||
yOffset: Float,
|
||||
yMin: Float,
|
||||
startDrawPadding: Float,
|
||||
zoomScale: Float,
|
||||
barWidth: Float
|
||||
): Offset {
|
||||
val (x, y) = point
|
||||
val x1 =
|
||||
((x - xMin) * xOffset) + xLeft + (startDrawPadding * zoomScale) - barWidth / 2 - scrollOffset
|
||||
val y1 = yBottom - ((y - yMin) * yOffset)
|
||||
return Offset(x1, y1)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get draw horizontal offset
|
||||
*
|
||||
* @param point
|
||||
* @param xLeft
|
||||
* @param scrollOffset
|
||||
* @param yBottom
|
||||
* @param yOffset
|
||||
* @param yMin
|
||||
* @param yMax
|
||||
* @param yStart
|
||||
* @param dataCategoryOptions
|
||||
* @param zoomScale
|
||||
* @return
|
||||
*/
|
||||
internal fun getDrawHorizontalOffset(
|
||||
point: Point,
|
||||
xLeft: Float,
|
||||
scrollOffset: Float,
|
||||
yBottom: Float,
|
||||
yOffset: Float,
|
||||
yMin: Float,
|
||||
yMax: Float,
|
||||
yStart: Float,
|
||||
dataCategoryOptions: DataCategoryOptions,
|
||||
zoomScale: Float
|
||||
): Offset {
|
||||
val (_, y) = point
|
||||
val x1 = xLeft
|
||||
val y1 =
|
||||
if (dataCategoryOptions.isDataCategoryStartFromBottom) yBottom - yStart - ((y - yMin) * yOffset) + scrollOffset else {
|
||||
if (zoomScale < 1) {
|
||||
yBottom - yStart - ((yMax - y) * yOffset) + scrollOffset
|
||||
} else {
|
||||
yStart + ((y - yMin) * yOffset) - scrollOffset
|
||||
}
|
||||
}
|
||||
return Offset(x1, y1)
|
||||
}
|
||||
|
||||
|
||||
/////
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Used to draw the highlighted text
|
||||
* @param identifiedPoint : Selected points
|
||||
* @param selectedOffset: Offset selected
|
||||
* @param barWidth: Width of single bar
|
||||
* @param highlightData: Data for the highlight section
|
||||
*/
|
||||
internal fun DrawScope.drawGroupHighlightText(
|
||||
identifiedPoint: BarData,
|
||||
selectedOffset: Offset,
|
||||
barWidth: Dp,
|
||||
highlightData: SelectionHighlightData
|
||||
) {
|
||||
val centerPointOfBar = selectedOffset.x + barWidth.toPx() / 2
|
||||
// Drawing the highlighted background and text
|
||||
highlightData.drawGroupBarPopUp(this, selectedOffset, identifiedPoint, centerPointOfBar)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* returns identified point and displaying the data points and highlighted bar .
|
||||
* @param dragLocks : Mutable map of BarData and drawOffset.
|
||||
* @param visibility : Flag to control the visibility of highlighted text.
|
||||
* @param identifiedPoint: selected bar data.
|
||||
* @param selectionHighlightData: Data related to the bar graph highlight.
|
||||
* @param isDragging : Boolean flag for the dragging status.
|
||||
* @param columnWidth : Width of the Y axis.
|
||||
* @param yBottom : Bottom padding.
|
||||
* @param paddingRight : Right padding.
|
||||
* @param yOffset : Distance between two y points.
|
||||
* @param barWidth : Width of each bar.
|
||||
* @param totalPaddingBtwBars : total padding between stacked bars. For group chart it will be 0.
|
||||
* @param isHighlightFullBar : User configured value for highlighting the entire bar in case of stacked bar chart
|
||||
*/
|
||||
internal fun DrawScope.highlightGroupBar(
|
||||
dragLocks: MutableMap<Int, Pair<BarData, Offset>>,
|
||||
visibility: Boolean,
|
||||
identifiedPoint: BarData,
|
||||
selectionHighlightData: SelectionHighlightData?,
|
||||
isDragging: Boolean,
|
||||
columnWidth: Float,
|
||||
yBottom: Float,
|
||||
paddingRight: Dp,
|
||||
yOffset: Float,
|
||||
barWidth: Dp,
|
||||
totalPaddingBtwBars: Float = 0f,
|
||||
isHighlightFullBar: Boolean = false
|
||||
): BarData {
|
||||
var mutableIdentifiedPoint: BarData = identifiedPoint
|
||||
// Handle the show the selected bar
|
||||
if (isDragging) {
|
||||
val x = dragLocks.values.firstOrNull()?.second?.x
|
||||
if (x != null) {
|
||||
mutableIdentifiedPoint = dragLocks.values.map { it.first }.first()
|
||||
}
|
||||
|
||||
// Draw highlight bar on selection
|
||||
if (selectionHighlightData?.isHighlightBarRequired == true) {
|
||||
dragLocks.values.firstOrNull()?.let { (barData, location) ->
|
||||
val (xPoint, yPoint) = location
|
||||
if (xPoint >= columnWidth && xPoint <= size.width - paddingRight.toPx()) {
|
||||
val y1 =
|
||||
yBottom - ((barData.point.y - 0) * yOffset) - if (isHighlightFullBar) totalPaddingBtwBars else 0f
|
||||
selectionHighlightData.drawHighlightBar(
|
||||
this, xPoint, yPoint, barWidth.toPx(), yBottom - y1, BarChartType.VERTICAL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selectedOffset = dragLocks.values.firstOrNull()?.second
|
||||
if (visibility && selectedOffset != null && selectionHighlightData != null) {
|
||||
drawGroupHighlightText(
|
||||
mutableIdentifiedPoint, selectedOffset, barWidth, selectionHighlightData
|
||||
)
|
||||
}
|
||||
return mutableIdentifiedPoint
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the draw offset for bar graph.
|
||||
* @param yMin: Minimum value on the y axis
|
||||
* @param xOffset: Distance between bars
|
||||
* @param yOffset: Distance between y axis points
|
||||
* @param xLeft: X starting point of bar graph
|
||||
* @param scrollOffset: Scroll offset
|
||||
* @param yBottom: Y starting point of bar graph
|
||||
*/
|
||||
internal fun getGroupBarDrawOffset(
|
||||
x: Int,
|
||||
y: Float,
|
||||
xOffset: Float,
|
||||
xLeft: Float,
|
||||
scrollOffset: Float,
|
||||
yBottom: Float,
|
||||
yOffset: Float,
|
||||
yMin: Float,
|
||||
xMin: Float,
|
||||
startDrawPadding: Float,
|
||||
zoomScale: Float,
|
||||
barWidth: Float
|
||||
): Offset {
|
||||
val x1 =
|
||||
((x - xMin) * xOffset) + xLeft + (startDrawPadding * zoomScale) - barWidth / 2 - scrollOffset
|
||||
|
||||
val y1 = yBottom - ((y - yMin) * yOffset)
|
||||
return Offset(x1, y1)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Used to draw the group separator
|
||||
* @param barGraphData: [GroupBarChartData]
|
||||
* @param drawOffset: topLeft offset for the drawing the separator
|
||||
* @param height : height of the separator
|
||||
* @param width : width of the separator
|
||||
* @param color : color of the separator
|
||||
*/
|
||||
internal fun DrawScope.drawGroupSeparator(
|
||||
drawOffset: Offset,
|
||||
height: Float,
|
||||
width: Float,
|
||||
color: Color,
|
||||
barGraphData: GroupBarChartData,
|
||||
) {
|
||||
drawRoundRect(
|
||||
color = color,
|
||||
topLeft = drawOffset,
|
||||
size = Size(width, height),
|
||||
blendMode = barGraphData.groupSeparatorConfig.separatorBlendMode
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* returns the max scrollable distance based on the points to be drawn along with padding etc.
|
||||
* @param columnWidth : Width of the Y-Axis.
|
||||
* @param xMax : Max X-Axis value.
|
||||
* @param xMin: Min X-Axis value.
|
||||
* @param xOffset: Total distance between two X-Axis points.
|
||||
* @param xLeft: Total Left padding.
|
||||
* @param paddingRight : Padding at the end of the canvas.
|
||||
* @param canvasWidth : Total available canvas width.
|
||||
*/
|
||||
internal fun getMaxScrollDistance(
|
||||
columnWidth: Float,
|
||||
xMax: Float,
|
||||
xMin: Float,
|
||||
xOffset: Float,
|
||||
xLeft: Float,
|
||||
paddingRight: Float,
|
||||
canvasWidth: Float
|
||||
): Float {
|
||||
val xLastPoint = (xMax - xMin) * xOffset + xLeft + columnWidth + paddingRight
|
||||
return if (xLastPoint > canvasWidth) {
|
||||
xLastPoint - canvasWidth
|
||||
} else 0f
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawUnderScrollMask extension method used for drawing a rectangular mask to make graph scrollable under the YAxis.
|
||||
* @param columnWidth : Width of the rectangular mask here width of Y Axis is used.
|
||||
* @param paddingRight : Padding given at the end of the graph.
|
||||
* @param bgColor : Background of the rectangular mask.
|
||||
*/
|
||||
internal fun DrawScope.drawUnderScrollMask(columnWidth: Float, paddingRight: Dp, bgColor: Color) {
|
||||
// Draw column to make graph look scrollable under Yaxis
|
||||
drawRect(
|
||||
bgColor, Offset(0f, 0f), Size(columnWidth, size.height)
|
||||
)
|
||||
// Draw right padding
|
||||
drawRect(
|
||||
bgColor,
|
||||
Offset(size.width - paddingRight.toPx(), 0f),
|
||||
Size(paddingRight.toPx(), size.height)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param barDataList : List of bar details for selected stacked bar
|
||||
* @param totalPaddingBtwBars : Total padding stacked bars
|
||||
* @param yOffset : Offset of Y axis point
|
||||
* @param yBottom : starting y offset of y Axis
|
||||
* @param xPointOffset : Offset of X axis point
|
||||
*/
|
||||
internal fun getFullBarDetails(
|
||||
barDataList: List<BarData>,
|
||||
totalPaddingBtwBars: Float,
|
||||
yOffset: Float,
|
||||
yBottom: Float,
|
||||
xPointOffset: Float
|
||||
): Pair<BarData, Offset> {
|
||||
var yPoint = 0f
|
||||
|
||||
barDataList.forEach {
|
||||
yPoint += it.point.y
|
||||
}
|
||||
val fullBarYOffset = yBottom - totalPaddingBtwBars - (yPoint * yOffset)
|
||||
val fullBarOffset = Offset(xPointOffset, fullBarYOffset)
|
||||
val fullBarData = BarData(Point(barDataList.first().point.x, yPoint))
|
||||
|
||||
return Pair(fullBarData, fullBarOffset)
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package co.yml.charts.ui.bubblechart
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.XAxis
|
||||
import co.yml.charts.axis.YAxis
|
||||
import co.yml.charts.axis.getXAxisScale
|
||||
import co.yml.charts.chartcontainer.container.ScrollableCanvasContainer
|
||||
import co.yml.charts.common.components.ItemDivider
|
||||
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
|
||||
import co.yml.charts.common.components.accessibility.BubblePointInfo
|
||||
import co.yml.charts.common.extensions.RowClip
|
||||
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
|
||||
import co.yml.charts.common.extensions.drawGridLines
|
||||
import co.yml.charts.common.extensions.isNotNull
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.bubblechart.model.Bubble
|
||||
import co.yml.charts.ui.bubblechart.model.BubbleChartData
|
||||
import co.yml.charts.ui.bubblechart.model.BubbleStyle
|
||||
import co.yml.charts.ui.linechart.drawHighLightOnSelectedPoint
|
||||
import co.yml.charts.ui.linechart.drawHighlightText
|
||||
import co.yml.charts.ui.linechart.getMappingPointsToGraph
|
||||
import co.yml.charts.ui.linechart.getMaxElementInYAxis
|
||||
import co.yml.charts.ui.linechart.getYAxisScale
|
||||
import co.yml.charts.ui.linechart.isTapped
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPoint
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPopUp
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Bubble chart
|
||||
*
|
||||
* @param modifier
|
||||
* @param bubbleChartData
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun BubbleChart(modifier: Modifier, bubbleChartData: BubbleChartData) {
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val isTalkBackEnabled by LocalContext.current.collectIsTalkbackEnabledAsState()
|
||||
if (accessibilitySheetState.isVisible && isTalkBackEnabled
|
||||
&& bubbleChartData.accessibilityConfig.shouldHandleBackWhenTalkBackPopUpShown
|
||||
) {
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
accessibilitySheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(modifier = modifier) {
|
||||
with(bubbleChartData) {
|
||||
var columnWidth by remember { mutableStateOf(0f) }
|
||||
var rowHeight by remember { mutableStateOf(0f) }
|
||||
var xOffset by remember { mutableStateOf(0f) }
|
||||
val bgColor = MaterialTheme.colorScheme.surface
|
||||
var isTapped by remember { mutableStateOf(false) }
|
||||
var tapOffset by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
var selectionTextVisibility by remember { mutableStateOf(false) }
|
||||
var identifiedPoint by remember { mutableStateOf(Point(0f, 0f)) }
|
||||
|
||||
val bubblePoints: List<Point> = bubbles.map { bubble -> bubble.center }
|
||||
|
||||
val (xMin, xMax, xAxisScale) = getXAxisScale(bubblePoints, xAxisData.steps)
|
||||
val (yMin, _, yAxisScale) = getYAxisScale(bubblePoints, yAxisData.steps)
|
||||
val maxElementInYAxis = getMaxElementInYAxis(yAxisScale, yAxisData.steps)
|
||||
val xAxisData = xAxisData.copy(axisBottomPadding = bottomPadding)
|
||||
val yAxisData = yAxisData.copy(
|
||||
axisBottomPadding = LocalDensity.current.run { rowHeight.toDp() },
|
||||
axisTopPadding = paddingTop
|
||||
)
|
||||
|
||||
ScrollableCanvasContainer(modifier = modifier
|
||||
.semantics {
|
||||
contentDescription = bubbleChartData.accessibilityConfig.chartDescription
|
||||
}
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
calculateMaxDistance = { xZoom ->
|
||||
xOffset = xAxisData.axisStepSize.toPx() * xZoom
|
||||
getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
paddingRight.toPx(),
|
||||
size.width,
|
||||
containerPaddingEnd.toPx()
|
||||
)
|
||||
},
|
||||
containerBackgroundColor = backgroundColor,
|
||||
isPinchZoomEnabled = isZoomAllowed,
|
||||
drawXAndYAxis = { scrollOffset, xZoom ->
|
||||
YAxis(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.onGloballyPositioned {
|
||||
columnWidth = it.size.width.toFloat()
|
||||
}, yAxisData = yAxisData
|
||||
)
|
||||
|
||||
XAxis(xAxisData = xAxisData,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.BottomStart)
|
||||
.onGloballyPositioned {
|
||||
rowHeight = it.size.height.toFloat()
|
||||
}
|
||||
.clip(
|
||||
RowClip(
|
||||
columnWidth, paddingRight
|
||||
)
|
||||
),
|
||||
xStart = columnWidth,
|
||||
scrollOffset = scrollOffset,
|
||||
zoomScale = xZoom,
|
||||
chartData = bubblePoints,
|
||||
axisStart = columnWidth)
|
||||
|
||||
},
|
||||
onDraw = { scrollOffset, xZoom ->
|
||||
val yBottom = size.height - rowHeight
|
||||
|
||||
val yOffset = ((yBottom - paddingTop.toPx()) / maxElementInYAxis)
|
||||
xOffset = xAxisData.axisStepSize.toPx() * xZoom
|
||||
val xLeft = columnWidth // To add extra space if needed
|
||||
val pointsData = getMappingPointsToGraph(
|
||||
bubblePoints,
|
||||
xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yMin,
|
||||
yOffset
|
||||
)
|
||||
val tapPointLocks = mutableMapOf<Int, Pair<Point, Offset>>()
|
||||
// Draw guide lines
|
||||
gridLines?.let {
|
||||
drawGridLines(
|
||||
yBottom,
|
||||
yAxisData.axisTopPadding.toPx(),
|
||||
xLeft,
|
||||
paddingRight,
|
||||
scrollOffset,
|
||||
bubbles.size,
|
||||
xZoom,
|
||||
xAxisScale,
|
||||
yAxisData.steps,
|
||||
xAxisData.axisStepSize,
|
||||
it
|
||||
)
|
||||
}
|
||||
drawUnderScrollMask(columnWidth, paddingRight, bgColor)
|
||||
|
||||
pointsData.forEachIndexed { index, offset ->
|
||||
val drawingOffset = Offset(offset.x, offset.y)
|
||||
bubbles[index].draw(
|
||||
this,
|
||||
drawingOffset,
|
||||
bubbleChartData.maximumBubbleRadius
|
||||
)
|
||||
|
||||
if (isTapped && offset.isTapped(tapOffset.x, xOffset)) {
|
||||
tapPointLocks[0] = bubbles[index].center to offset
|
||||
identifiedPoint = tapPointLocks.values.map { it.first }.first()
|
||||
drawHighLightOnSelectedPoint(
|
||||
tapPointLocks,
|
||||
columnWidth,
|
||||
paddingRight,
|
||||
yBottom,
|
||||
bubbles[index].selectionHighlightPoint
|
||||
)
|
||||
|
||||
}
|
||||
val selectedOffset = tapPointLocks.values.firstOrNull()?.second
|
||||
|
||||
if (selectionTextVisibility && selectedOffset.isNotNull()) {
|
||||
drawHighlightText(
|
||||
identifiedPoint,
|
||||
selectedOffset ?: Offset(0f, 0f),
|
||||
bubbles[index].selectionHighlightPopUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
onPointClicked = { offset: Offset, _: Float ->
|
||||
isTapped = true
|
||||
selectionTextVisibility = true
|
||||
tapOffset = offset
|
||||
},
|
||||
onScroll = {
|
||||
isTapped = false
|
||||
selectionTextVisibility = false
|
||||
},
|
||||
onZoomInAndOut = {
|
||||
isTapped = false
|
||||
selectionTextVisibility = false
|
||||
})
|
||||
|
||||
}
|
||||
if (isTalkBackEnabled) {
|
||||
with(bubbleChartData) {
|
||||
AccessibilityBottomSheetDialog(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = Color.White,
|
||||
content = {
|
||||
LazyColumn {
|
||||
bubbles.size?.let {
|
||||
items(it) { index ->
|
||||
Column {
|
||||
BubblePointInfo(
|
||||
xAxisData.axisLabelDescription(
|
||||
xAxisData.labelData(
|
||||
index
|
||||
)
|
||||
),
|
||||
bubbles[index].center.description + "density is" + { bubbles[index].density },
|
||||
bubbles[index].bubbleStyle.solidColor,
|
||||
accessibilityConfig.titleTextSize,
|
||||
accessibilityConfig.descriptionTextSize
|
||||
)
|
||||
ItemDivider(
|
||||
thickness = accessibilityConfig.dividerThickness,
|
||||
dividerColor = accessibilityConfig.dividerColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
popUpTopRightButtonTitle = accessibilityConfig.popUpTopRightButtonTitle,
|
||||
popUpTopRightButtonDescription = accessibilityConfig.popUpTopRightButtonDescription,
|
||||
sheetState = accessibilitySheetState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max scroll distance
|
||||
*
|
||||
* @param columnWidth
|
||||
* @param xMax
|
||||
* @param xMin
|
||||
* @param xOffset
|
||||
* @param paddingRight
|
||||
* @param canvasWidth
|
||||
* @param containerPaddingEnd
|
||||
* @return
|
||||
*/
|
||||
fun getMaxScrollDistance(
|
||||
columnWidth: Float,
|
||||
xMax: Float,
|
||||
xMin: Float,
|
||||
xOffset: Float,
|
||||
paddingRight: Float,
|
||||
canvasWidth: Float,
|
||||
containerPaddingEnd: Float = 0f
|
||||
): Float {
|
||||
val xLastPoint = (xMax - xMin) * xOffset + columnWidth + paddingRight + containerPaddingEnd
|
||||
return if (xLastPoint > canvasWidth) {
|
||||
xLastPoint - canvasWidth
|
||||
} else 0f
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawUnderScrollMask extension method used for drawing a rectangular mask to make graph scrollable under the YAxis.
|
||||
* @param columnWidth : Width of the rectangular mask here width of Y Axis is used.
|
||||
* @param paddingRight : Padding given at the end of the graph.
|
||||
* @param bgColor : Background of the rectangular mask.
|
||||
*/
|
||||
private fun DrawScope.drawUnderScrollMask(columnWidth: Float, paddingRight: Dp, bgColor: Color) {
|
||||
drawRect(
|
||||
bgColor, Offset(0f, 0f), Size(columnWidth, size.height)
|
||||
)
|
||||
drawRect(
|
||||
bgColor,
|
||||
Offset(size.width - paddingRight.toPx(), 0f),
|
||||
Size(paddingRight.toPx(), size.height)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get cubic points
|
||||
*
|
||||
* @param pointsData
|
||||
* @return
|
||||
*/
|
||||
fun getCubicPoints(pointsData: List<Offset>): Pair<MutableList<Offset>, MutableList<Offset>> {
|
||||
val cubicPoints1 = mutableListOf<Offset>()
|
||||
val cubicPoints2 = mutableListOf<Offset>()
|
||||
|
||||
for (i in 1 until pointsData.size) {
|
||||
cubicPoints1.add(
|
||||
Offset(
|
||||
(pointsData[i].x + pointsData[i - 1].x) / 2, pointsData[i - 1].y
|
||||
)
|
||||
)
|
||||
cubicPoints2.add(
|
||||
Offset(
|
||||
(pointsData[i].x + pointsData[i - 1].x) / 2, pointsData[i].y
|
||||
)
|
||||
)
|
||||
}
|
||||
return Pair(cubicPoints1, cubicPoints2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw highlight text
|
||||
*
|
||||
* @param identifiedPoint
|
||||
* @param selectedOffset
|
||||
* @param selectionHighlightPopUp
|
||||
*/
|
||||
fun DrawScope.drawHighlightText(
|
||||
identifiedPoint: Point,
|
||||
selectedOffset: Offset,
|
||||
selectionHighlightPopUp: SelectionHighlightPopUp?
|
||||
) {
|
||||
selectionHighlightPopUp?.run {
|
||||
draw(selectedOffset, identifiedPoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw high light on selected point
|
||||
*
|
||||
* @param dragLocks
|
||||
* @param columnWidth
|
||||
* @param paddingRight
|
||||
* @param yBottom
|
||||
* @param selectionHighlightPoint
|
||||
*/
|
||||
fun DrawScope.drawHighLightOnSelectedPoint(
|
||||
dragLocks: MutableMap<Int, Pair<Point, Offset>>,
|
||||
columnWidth: Float,
|
||||
paddingRight: Dp,
|
||||
yBottom: Float,
|
||||
selectionHighlightPoint: SelectionHighlightPoint?
|
||||
) {
|
||||
if (selectionHighlightPoint.isNotNull()) {
|
||||
dragLocks.values.firstOrNull()?.let { (_, location) ->
|
||||
val (x, y) = location
|
||||
if (x >= columnWidth && x <= size.width - paddingRight.toPx()) {
|
||||
selectionHighlightPoint?.draw?.let { it(this, Offset(x, y)) }
|
||||
if (selectionHighlightPoint?.isHighlightLineRequired == true) {
|
||||
selectionHighlightPoint.drawHighlightLine(
|
||||
this, Offset(x, yBottom), Offset(x, y)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package co.yml.charts.ui.bubblechart.model
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.model.AccessibilityConfig
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.linechart.model.GridLines
|
||||
import co.yml.charts.ui.linechart.model.IntersectionPoint
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPoint
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPopUp
|
||||
|
||||
|
||||
/**
|
||||
* Bubble chart data
|
||||
*
|
||||
* @property bubbles
|
||||
* @property maximumBubbleRadius
|
||||
* @property xAxisData
|
||||
* @property yAxisData
|
||||
* @property isZoomAllowed
|
||||
* @property paddingTop
|
||||
* @property bottomPadding
|
||||
* @property paddingRight
|
||||
* @property containerPaddingEnd
|
||||
* @property backgroundColor
|
||||
* @property gridLines
|
||||
* @property accessibilityConfig
|
||||
* @constructor Create empty Bubble chart data
|
||||
*/
|
||||
data class BubbleChartData(
|
||||
val bubbles: List<Bubble>,
|
||||
val maximumBubbleRadius:Float = 100f,
|
||||
val xAxisData: AxisData = AxisData.Builder().build(),
|
||||
val yAxisData: AxisData = AxisData.Builder().build(),
|
||||
val isZoomAllowed: Boolean = true,
|
||||
val paddingTop: Dp = 30.dp,
|
||||
val bottomPadding: Dp = 10.dp,
|
||||
val paddingRight: Dp = 10.dp,
|
||||
val containerPaddingEnd: Dp = 15.dp,
|
||||
val backgroundColor: Color = Color.White,
|
||||
val gridLines: GridLines? = null,
|
||||
val accessibilityConfig: AccessibilityConfig = AccessibilityConfig()
|
||||
)
|
||||
|
||||
/**
|
||||
* Bubble
|
||||
*
|
||||
* @property center
|
||||
* @property density
|
||||
* @property bubbleStyle
|
||||
* @property intersectionPoint
|
||||
* @property selectionHighlightPoint
|
||||
* @property selectionHighlightPopUp
|
||||
* @property draw
|
||||
* @constructor Create empty Bubble
|
||||
*/
|
||||
data class Bubble(
|
||||
val center: Point,
|
||||
val density: Float,
|
||||
val bubbleStyle: BubbleStyle = BubbleStyle(),
|
||||
val intersectionPoint: IntersectionPoint? = null,
|
||||
val selectionHighlightPoint: SelectionHighlightPoint? = null,
|
||||
val selectionHighlightPopUp: SelectionHighlightPopUp? = null,
|
||||
val draw: DrawScope.(Offset,Float) -> Unit = { center,maximumRadius ->
|
||||
val drawingRadius:Float = (density / maximumRadius) * 100
|
||||
if (bubbleStyle.useGradience) {
|
||||
drawCircle(
|
||||
brush = getBrush(bubbleStyle, center, density),
|
||||
center = center,
|
||||
radius = drawingRadius,
|
||||
alpha = bubbleStyle.alpha,
|
||||
style = bubbleStyle.style,
|
||||
colorFilter = bubbleStyle.colorFilter,
|
||||
blendMode = bubbleStyle.blendMode
|
||||
)
|
||||
} else {
|
||||
drawCircle(
|
||||
bubbleStyle.solidColor,
|
||||
drawingRadius,
|
||||
center,
|
||||
bubbleStyle.alpha,
|
||||
bubbleStyle.style,
|
||||
bubbleStyle.colorFilter,
|
||||
bubbleStyle.blendMode
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private fun getBrush(bubbleStyle: BubbleStyle, center: Offset, density: Float): Brush {
|
||||
when (bubbleStyle.gradientType) {
|
||||
BubbleGradientType.RadialGradient -> {
|
||||
return Brush.radialGradient(
|
||||
colors = bubbleStyle.gradientColors,
|
||||
center = center,
|
||||
radius = density,
|
||||
tileMode = bubbleStyle.tileMode
|
||||
)
|
||||
}
|
||||
|
||||
BubbleGradientType.LinearGradient -> {
|
||||
return Brush.linearGradient(
|
||||
colors = bubbleStyle.gradientColors,
|
||||
tileMode = bubbleStyle.tileMode,
|
||||
start = center,
|
||||
end = center
|
||||
)
|
||||
}
|
||||
|
||||
BubbleGradientType.VerticalGradient -> {
|
||||
return Brush.verticalGradient(
|
||||
colors = bubbleStyle.gradientColors,
|
||||
tileMode = bubbleStyle.tileMode,
|
||||
startY = center.y - density / 2,
|
||||
endY = center.y + density / 2,
|
||||
)
|
||||
}
|
||||
|
||||
BubbleGradientType.HorizontalGradient -> {
|
||||
return Brush.horizontalGradient(
|
||||
colors = bubbleStyle.gradientColors,
|
||||
tileMode = bubbleStyle.tileMode,
|
||||
startX = center.x - density / 2,
|
||||
endX = center.x + density / 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package co.yml.charts.ui.bubblechart.model
|
||||
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.LinearGradient
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultBlendMode
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import co.yml.charts.ui.linechart.model.LineType
|
||||
|
||||
/**
|
||||
* Bubble style
|
||||
*
|
||||
* @property gradientColors
|
||||
* @property gradientType
|
||||
* @property solidColor
|
||||
* @property useGradience
|
||||
* @property width
|
||||
* @property alpha
|
||||
* @property style
|
||||
* @property colorFilter
|
||||
* @property blendMode
|
||||
* @constructor Create empty Bubble style
|
||||
*/
|
||||
data class BubbleStyle(
|
||||
val gradientColors: List<Color> = listOf(Color.Blue, Color.Red),
|
||||
val gradientType: BubbleGradientType = BubbleGradientType.HorizontalGradient,
|
||||
val tileMode: TileMode = TileMode.Clamp,
|
||||
val useGradience: Boolean = false,
|
||||
val solidColor: Color = Color.Blue,
|
||||
val width: Float = 8f,
|
||||
val alpha: Float = 1.0f,
|
||||
val style: DrawStyle = Fill,
|
||||
val colorFilter: ColorFilter? = null,
|
||||
val blendMode: BlendMode = DefaultBlendMode
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Bubble gradient type
|
||||
*
|
||||
* @constructor Create empty Bubble gradient type
|
||||
*/
|
||||
sealed class BubbleGradientType {
|
||||
|
||||
/**
|
||||
* Linear gradient
|
||||
*
|
||||
* @constructor Create empty Linear gradient
|
||||
*/
|
||||
object LinearGradient : BubbleGradientType()
|
||||
|
||||
/**
|
||||
* Radial gradient
|
||||
*
|
||||
* @constructor Create empty Radial gradient
|
||||
*/
|
||||
object RadialGradient : BubbleGradientType()
|
||||
|
||||
/**
|
||||
* Vertical gradient
|
||||
*
|
||||
* @constructor Create empty Vertical gradient
|
||||
*/
|
||||
object VerticalGradient : BubbleGradientType()
|
||||
|
||||
/**
|
||||
* Horizontal gradient
|
||||
*
|
||||
* @constructor Create empty Horizontal gradient
|
||||
*/
|
||||
object HorizontalGradient : BubbleGradientType()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package co.yml.charts.ui.combinedchart
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.XAxis
|
||||
import co.yml.charts.axis.YAxis
|
||||
import co.yml.charts.chartcontainer.container.ScrollableCanvasContainer
|
||||
import co.yml.charts.common.components.ItemDivider
|
||||
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
|
||||
import co.yml.charts.common.components.accessibility.CombinedChartInfo
|
||||
import co.yml.charts.common.extensions.*
|
||||
import co.yml.charts.common.extensions.getMaxElementInYAxis
|
||||
import co.yml.charts.common.model.PlotData
|
||||
import co.yml.charts.common.model.PlotType
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.barchart.drawUnderScrollMask
|
||||
import co.yml.charts.ui.barchart.getGroupBarDrawOffset
|
||||
import co.yml.charts.ui.barchart.getMaxScrollDistance
|
||||
import co.yml.charts.ui.barchart.highlightGroupBar
|
||||
import co.yml.charts.ui.barchart.models.BarData
|
||||
import co.yml.charts.ui.barchart.models.BarPlotData
|
||||
import co.yml.charts.ui.barchart.models.GroupBar
|
||||
import co.yml.charts.ui.combinedchart.model.CombinedChartData
|
||||
import co.yml.charts.ui.linechart.drawHighLightOnSelectedPoint
|
||||
import co.yml.charts.ui.linechart.drawHighlightText
|
||||
import co.yml.charts.ui.linechart.drawShadowUnderLineAndIntersectionPoint
|
||||
import co.yml.charts.ui.linechart.drawStraightOrCubicLine
|
||||
import co.yml.charts.ui.linechart.getCubicPoints
|
||||
import co.yml.charts.ui.linechart.getMappingPointsToGraph
|
||||
import co.yml.charts.ui.linechart.model.LinePlotData
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
*
|
||||
* CombinedChart compose method for drawing combined line and bar charts.
|
||||
* @param modifier: All modifier related properties
|
||||
* @param combinedChartData : All data needed to draw combined chart. [CombinedChartData] Data
|
||||
* class to save all params related to combined line and bar chart.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun CombinedChart(modifier: Modifier, combinedChartData: CombinedChartData) {
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val isTalkBackEnabled by LocalContext.current.collectIsTalkbackEnabledAsState()
|
||||
if (accessibilitySheetState.isVisible && isTalkBackEnabled
|
||||
&& combinedChartData.accessibilityConfig.shouldHandleBackWhenTalkBackPopUpShown
|
||||
) {
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
accessibilitySheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(modifier) {
|
||||
with(combinedChartData) {
|
||||
var xOffset by remember { mutableStateOf(0f) }
|
||||
var isTapped by remember { mutableStateOf(false) }
|
||||
var columnWidth by remember { mutableStateOf(0f) }
|
||||
var rowHeight by remember { mutableStateOf(0f) }
|
||||
val paddingRight = paddingEnd
|
||||
val linePlotData: LinePlotData =
|
||||
getDataFromType(combinedPlotDataList, PlotType.Line) as? LinePlotData
|
||||
?: LinePlotData.default()
|
||||
val barPlotData: BarPlotData =
|
||||
getDataFromType(combinedPlotDataList, PlotType.Bar) as? BarPlotData
|
||||
?: BarPlotData.default()
|
||||
val linePoints: List<Point> =
|
||||
linePlotData.lines.flatMap { line -> line.dataPoints.map { it } }
|
||||
val barPoints = barPlotData.groupBarList.flatMap { bar -> bar.barList.map { it } }
|
||||
val bgColor = MaterialTheme.colorScheme.surface
|
||||
val xMin =
|
||||
minOf(if(linePoints.isEmpty()) 0.0f else linePoints.minOf { it.x }, (barPlotData.groupBarList.size).toFloat())
|
||||
val xMax =
|
||||
maxOf(if(linePoints.isEmpty()) 0.0f else linePoints.maxOf { it.x }, (barPlotData.groupBarList.size).toFloat())
|
||||
val yMin = minOf(if(linePoints.isEmpty()) 0.0f else linePoints.minOf { it.y }, if(barPoints.isEmpty())0.0f else barPoints.minOf { it.point.y })
|
||||
val yMax = maxOf(if(linePoints.isEmpty()) 0.0f else linePoints.maxOf { it.y }, if(barPoints.isEmpty())0.0f else barPoints.maxOf { it.point.y })
|
||||
val requiredSteps =
|
||||
maxOf(
|
||||
if(linePlotData.lines.isEmpty()) 0 else linePlotData.lines.map { it.dataPoints.size - 1 }.maxOf { it },
|
||||
if(barPlotData.groupBarList.isEmpty()) 0 else barPlotData.groupBarList.size
|
||||
)
|
||||
val xAxisData = xAxisData.copy(
|
||||
axisStepSize = if(barPlotData.groupBarList.isEmpty()) 30.dp else((barPlotData.barStyle.barWidth * barPlotData.groupingSize) +
|
||||
barPlotData.barStyle.paddingBetweenBars),
|
||||
steps = requiredSteps,
|
||||
startDrawPadding = LocalDensity.current.run { columnWidth.toDp() },
|
||||
shouldDrawAxisLineTillEnd = true
|
||||
)
|
||||
val yAxisData =
|
||||
yAxisData.copy(
|
||||
axisBottomPadding = LocalDensity.current.run { rowHeight.toDp() },
|
||||
axisTopPadding = paddingTop
|
||||
)
|
||||
val maxElementInYAxis = getMaxElementInYAxis(yMax, yAxisData.steps)
|
||||
var identifiedBarPoint by remember { mutableStateOf(BarData(Point(0f, 0f))) }
|
||||
var identifiedPoint by remember { mutableStateOf(Point(0f, 0f)) }
|
||||
var tapOffset by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
|
||||
ScrollableCanvasContainer(
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription = accessibilityConfig.chartDescription
|
||||
}
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
containerBackgroundColor = backgroundColor,
|
||||
isPinchZoomEnabled = isZoomAllowed,
|
||||
calculateMaxDistance = { xZoom ->
|
||||
xOffset =
|
||||
((barPlotData.barStyle.barWidth.toPx() * barPlotData.groupingSize) +
|
||||
barPlotData.barStyle.paddingBetweenBars.toPx()) * xZoom
|
||||
getMaxScrollDistance(
|
||||
columnWidth + (xAxisData.startDrawPadding.toPx() * xZoom),
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
0f,
|
||||
paddingRight.toPx(),
|
||||
size.width
|
||||
)
|
||||
},
|
||||
drawXAndYAxis = { scrollOffset, xZoom ->
|
||||
val axisPoints = mutableListOf<Point>()
|
||||
for (index in 0 until xMax.toInt()) {
|
||||
axisPoints.add(Point(index.toFloat(), 0f))
|
||||
}
|
||||
XAxis(
|
||||
xAxisData = xAxisData,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.clip(
|
||||
RowClip(
|
||||
columnWidth,
|
||||
paddingRight
|
||||
)
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
rowHeight = it.size.height.toFloat()
|
||||
},
|
||||
xStart = columnWidth + LocalDensity.current.run {
|
||||
(barPlotData.barStyle.barWidth.toPx() * barPlotData.groupingSize) / 2 + horizontalExtraSpace.toPx()
|
||||
},
|
||||
scrollOffset = scrollOffset,
|
||||
zoomScale = xZoom,
|
||||
chartData = if (barPoints.isEmpty()) linePoints else axisPoints,
|
||||
axisStart = columnWidth
|
||||
)
|
||||
YAxis(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.fillMaxHeight()
|
||||
.wrapContentWidth()
|
||||
.onGloballyPositioned {
|
||||
columnWidth = it.size.width.toFloat()
|
||||
},
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
},
|
||||
onDraw = { scrollOffset, xZoom ->
|
||||
val yBottom = size.height - rowHeight
|
||||
val yOffset =
|
||||
((yBottom - yAxisData.axisTopPadding.toPx()) / maxElementInYAxis)
|
||||
xOffset = if(barPoints.isEmpty())xAxisData.axisStepSize.toPx() * xZoom else
|
||||
((barPlotData.barStyle.barWidth.toPx() * barPlotData.groupingSize) +
|
||||
barPlotData.barStyle.paddingBetweenBars.toPx()) * xZoom
|
||||
val xLeft =
|
||||
columnWidth + horizontalExtraSpace.toPx() + (xAxisData.startDrawPadding.toPx() * xZoom)
|
||||
val barTapLocks = mutableMapOf<Int, Pair<BarData, Offset>>()
|
||||
val linePointLocks = mutableMapOf<Int, Pair<Point, Offset>>()
|
||||
|
||||
for (plotData in combinedPlotDataList) {
|
||||
when (plotData) {
|
||||
is LinePlotData -> {
|
||||
// Draw line chart
|
||||
val xStartPosition =
|
||||
columnWidth + horizontalExtraSpace.toPx() +
|
||||
((barPlotData.barStyle.barWidth.toPx() * barPlotData.groupingSize) / 2) + (xAxisData.startDrawPadding.toPx() * xZoom)
|
||||
plotData.lines.forEach { line ->
|
||||
val pointsData = getMappingPointsToGraph(
|
||||
line.dataPoints,
|
||||
xMin,
|
||||
xOffset,
|
||||
xStartPosition,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yMin,
|
||||
yOffset
|
||||
)
|
||||
val (cubicPoints1, cubicPoints2) = getCubicPoints(pointsData)
|
||||
|
||||
// Draw cubic line using the points and form a line graph
|
||||
val cubicPath =
|
||||
drawStraightOrCubicLine(
|
||||
pointsData,
|
||||
cubicPoints1,
|
||||
cubicPoints2,
|
||||
line.lineStyle
|
||||
)
|
||||
|
||||
// Draw area under curve
|
||||
drawShadowUnderLineAndIntersectionPoint(
|
||||
cubicPath,
|
||||
pointsData,
|
||||
yBottom,
|
||||
line
|
||||
)
|
||||
|
||||
pointsData.forEachIndexed { index, point ->
|
||||
if (isTapped && point.isPointTapped(
|
||||
tapOffset,
|
||||
tapPadding.toPx()
|
||||
)
|
||||
) {
|
||||
// Dealing with only one line graph hence tapPointLocks[0]
|
||||
linePointLocks[0] = line.dataPoints[index] to point
|
||||
}
|
||||
}
|
||||
if (isTapped && linePointLocks.isNotEmpty()) {
|
||||
drawHighLightOnSelectedPoint(
|
||||
linePointLocks,
|
||||
columnWidth,
|
||||
paddingRight,
|
||||
yBottom,
|
||||
line.selectionHighlightPoint?.copy(
|
||||
isHighlightLineRequired = false
|
||||
)
|
||||
)
|
||||
if (line.selectionHighlightPopUp != null) {
|
||||
val x =
|
||||
linePointLocks.values.firstOrNull()?.second?.x
|
||||
if (x != null) identifiedPoint =
|
||||
linePointLocks.values.map { it.first }.first()
|
||||
val selectedOffset =
|
||||
linePointLocks.values.firstOrNull()?.second
|
||||
if (selectedOffset.isNotNull()) {
|
||||
drawHighlightText(
|
||||
identifiedPoint,
|
||||
selectedOffset ?: Offset(0f, 0f),
|
||||
line.selectionHighlightPopUp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is BarPlotData -> {
|
||||
// Draw bar graph
|
||||
plotData.groupBarList.forEachIndexed { index, groupBarData ->
|
||||
var insideOffset = 0f
|
||||
groupBarData.barList.forEachIndexed { subIndex, individualBar ->
|
||||
val drawOffset = getGroupBarDrawOffset(
|
||||
index, individualBar.point.y, xOffset, xLeft,
|
||||
scrollOffset, yBottom, yOffset, 0f,
|
||||
0f,
|
||||
xAxisData.startDrawPadding.toPx(),
|
||||
xZoom,
|
||||
barPlotData.barStyle.barWidth.toPx()
|
||||
)
|
||||
val height = yBottom - drawOffset.y
|
||||
|
||||
val individualOffset =
|
||||
Offset(drawOffset.x + insideOffset, drawOffset.y)
|
||||
|
||||
// drawing each individual bars
|
||||
drawGroupBarGraph(
|
||||
plotData,
|
||||
individualOffset,
|
||||
height,
|
||||
subIndex
|
||||
)
|
||||
insideOffset += plotData.barStyle.barWidth.toPx()
|
||||
|
||||
val middleOffset =
|
||||
Offset(
|
||||
individualOffset.x + plotData.barStyle.barWidth.toPx() / 2,
|
||||
drawOffset.y
|
||||
)
|
||||
// store the tap points for selection
|
||||
if (isTapped && middleOffset.isTapped(
|
||||
tapOffset,
|
||||
plotData.barStyle.barWidth.toPx(),
|
||||
yBottom,
|
||||
tapPadding.toPx()
|
||||
)
|
||||
) {
|
||||
barTapLocks[0] = individualBar to individualOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isTapped && linePointLocks.isEmpty() &&
|
||||
barPlotData.barStyle.selectionHighlightData != null
|
||||
) {
|
||||
// highlighting the selected bar and showing the data points
|
||||
identifiedBarPoint = highlightGroupBar(
|
||||
barTapLocks,
|
||||
true,
|
||||
identifiedBarPoint,
|
||||
barPlotData.barStyle.selectionHighlightData,
|
||||
isTapped,
|
||||
columnWidth,
|
||||
yBottom,
|
||||
paddingRight,
|
||||
yOffset,
|
||||
barPlotData.barStyle.barWidth
|
||||
)
|
||||
}
|
||||
drawUnderScrollMask(columnWidth, paddingRight, bgColor)
|
||||
},
|
||||
onPointClicked = { offset: Offset, _: Float ->
|
||||
isTapped = true
|
||||
tapOffset = offset
|
||||
},
|
||||
onScroll = {
|
||||
isTapped = false
|
||||
}
|
||||
)
|
||||
if (isTalkBackEnabled) {
|
||||
AccessibilityBottomSheetDialog(
|
||||
modifier = Modifier.fillMaxSize(), backgroundColor = Color.White, content = {
|
||||
LazyColumn {
|
||||
items(xMax.toInt()) { index ->
|
||||
Column {
|
||||
val lineData: MutableList<Point> = mutableListOf()
|
||||
val lineColors: MutableList<Color> = mutableListOf()
|
||||
linePlotData.lines.forEachIndexed { _, line ->
|
||||
lineColors.add(line.lineStyle.color)
|
||||
line.dataPoints
|
||||
line.dataPoints.forEachIndexed { pointIndex, point ->
|
||||
if (pointIndex == index) {
|
||||
lineData.add(point)
|
||||
}
|
||||
}
|
||||
}
|
||||
val groupBarData: GroupBar? =
|
||||
barPlotData.groupBarList.filterIndexed { barIndex, _ -> barIndex == index }
|
||||
.firstOrNull()
|
||||
CombinedChartInfo(
|
||||
pointsList = lineData,
|
||||
lineColor = lineColors,
|
||||
groupBar = groupBarData,
|
||||
axisLabelDescription = xAxisData.axisLabelDescription(
|
||||
xAxisData.labelData(index)
|
||||
),
|
||||
barColorPaletteList = barPlotData.barColorPaletteList,
|
||||
dividerColor = accessibilityConfig.dividerColor,
|
||||
accessibilityConfig.titleTextSize,
|
||||
accessibilityConfig.descriptionTextSize
|
||||
)
|
||||
ItemDivider(
|
||||
thickness = accessibilityConfig.dividerThickness,
|
||||
dividerColor = accessibilityConfig.dividerColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
popUpTopRightButtonTitle = accessibilityConfig.popUpTopRightButtonTitle,
|
||||
popUpTopRightButtonDescription = accessibilityConfig.popUpTopRightButtonDescription,
|
||||
sheetState = accessibilitySheetState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data for given plot type from the combinedPlotDataList
|
||||
* @param combinedPlotDataList : List of combined plot data
|
||||
* @param type: Type of plot of with data is to be returned.
|
||||
*/
|
||||
private fun getDataFromType(combinedPlotDataList: List<PlotData>, type: PlotType): PlotData? {
|
||||
return when (type) {
|
||||
is PlotType.Line -> combinedPlotDataList.filterIsInstance<LinePlotData?>().firstOrNull()
|
||||
is PlotType.Bar -> combinedPlotDataList.filterIsInstance<BarPlotData?>().firstOrNull()
|
||||
else -> null // Handle if required in future.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Used to draw the individual bars
|
||||
* @param barPlotData : all meta data related to the bar graph
|
||||
* @param drawOffset: topLeft offset for the drawing the bar
|
||||
* @param height : height of the bar graph
|
||||
* @param subIndex : Index of the bar
|
||||
*/
|
||||
private fun DrawScope.drawGroupBarGraph(
|
||||
barPlotData: BarPlotData, drawOffset: Offset,
|
||||
height: Float,
|
||||
subIndex: Int
|
||||
) {
|
||||
val color = if (subIndex < barPlotData.barColorPaletteList.size) {
|
||||
barPlotData.barColorPaletteList[subIndex]
|
||||
} else Color.Transparent
|
||||
with(barPlotData.barStyle) {
|
||||
drawRoundRect(
|
||||
color = color,
|
||||
topLeft = drawOffset,
|
||||
size = Size(barWidth.toPx(), height),
|
||||
cornerRadius = CornerRadius(
|
||||
cornerRadius.toPx(),
|
||||
cornerRadius.toPx()
|
||||
),
|
||||
style = barDrawStyle,
|
||||
blendMode = barBlendMode
|
||||
)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package co.yml.charts.ui.combinedchart.model
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.model.AccessibilityConfig
|
||||
import co.yml.charts.common.model.PlotData
|
||||
|
||||
/**
|
||||
*
|
||||
* CombinedLineAndBarGraphData data class that contains all params user need to define to draw a bar and line graph.
|
||||
* @param combinedPlotDataList: Defines list of plot data's to be drawn and order of graph drawing is maintained as
|
||||
* per the list order. Distinct plot data's are only allowed.
|
||||
* @param xAxisData: All the configurations related to X-Axis to be defined here in [AxisData]
|
||||
* @param yAxisData: All the configurations related to Y-Axis to be defined here in [AxisData]
|
||||
* @param paddingTop: Padding from the top of the canvas to start of the graph container.
|
||||
* @param bottomPadding: Padding from the bottom of the canvas to bottom of the graph container.
|
||||
* @param containerPaddingEnd: Container inside padding end after the last point of the graph.
|
||||
* @param backgroundColor: Background color of the Y & X components.,
|
||||
* @param isZoomAllowed: True if zoom in for all vertical graph components is allowed else false.
|
||||
* @param tapPadding: Tap padding offset for selected point.
|
||||
* @param horizontalExtraSpace: Extra padding at the end of the canvas container.
|
||||
* @param accessibilityConfig: Configs related to accessibility service defined here in [AccessibilityConfig]
|
||||
*/
|
||||
data class CombinedChartData(
|
||||
val combinedPlotDataList: List<PlotData>,
|
||||
val xAxisData: AxisData = AxisData.Builder().build(),
|
||||
val yAxisData: AxisData = AxisData.Builder().build(),
|
||||
val paddingTop: Dp = 30.dp,
|
||||
val bottomPadding: Dp = 10.dp,
|
||||
val paddingEnd: Dp = 10.dp,
|
||||
val horizontalExtraSpace: Dp = 10.dp,
|
||||
val containerPaddingEnd: Dp = 15.dp,
|
||||
val backgroundColor: Color = Color.White,
|
||||
val tapPadding: Dp = 10.dp,
|
||||
val isZoomAllowed: Boolean = true,
|
||||
val accessibilityConfig: AccessibilityConfig = AccessibilityConfig()
|
||||
)
|
||||
@@ -0,0 +1,524 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialApi::class)
|
||||
|
||||
package co.yml.charts.ui.linechart
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import co.yml.charts.axis.XAxis
|
||||
import co.yml.charts.axis.YAxis
|
||||
import co.yml.charts.axis.getXAxisScale
|
||||
import co.yml.charts.chartcontainer.container.ScrollableCanvasContainer
|
||||
import co.yml.charts.common.components.ItemDivider
|
||||
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
|
||||
import co.yml.charts.common.components.accessibility.CombinedChartInfo
|
||||
import co.yml.charts.common.components.accessibility.LinePointInfo
|
||||
import co.yml.charts.common.extensions.RowClip
|
||||
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
|
||||
import co.yml.charts.common.extensions.drawGridLines
|
||||
import co.yml.charts.common.extensions.isNotNull
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.linechart.model.IntersectionPoint
|
||||
import co.yml.charts.ui.linechart.model.Line
|
||||
import co.yml.charts.ui.linechart.model.LineChartData
|
||||
import co.yml.charts.ui.linechart.model.LineStyle
|
||||
import co.yml.charts.ui.linechart.model.LineType
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPoint
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPopUp
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
*
|
||||
* [LineChart] compose method used for drawing a Line Chart.
|
||||
* @param modifier :All modifier related property.
|
||||
* Data class [LineChartData] to save all params needed to draw the line chart.
|
||||
* @param lineChartData : Add data related to line chart.
|
||||
*/
|
||||
@Composable
|
||||
fun LineChart(modifier: Modifier, lineChartData: LineChartData) {
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val isTalkBackEnabled by LocalContext.current.collectIsTalkbackEnabledAsState()
|
||||
if (accessibilitySheetState.isVisible && isTalkBackEnabled
|
||||
&& lineChartData.accessibilityConfig.shouldHandleBackWhenTalkBackPopUpShown
|
||||
) {
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
accessibilitySheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(modifier = modifier) {
|
||||
with(lineChartData) {
|
||||
var columnWidth by remember { mutableStateOf(0f) }
|
||||
var rowHeight by remember { mutableStateOf(0f) }
|
||||
var xOffset by remember { mutableStateOf(0f) }
|
||||
val bgColor = MaterialTheme.colorScheme.surface
|
||||
var isTapped by remember { mutableStateOf(false) }
|
||||
var tapOffset by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
var selectionTextVisibility by remember { mutableStateOf(false) }
|
||||
var identifiedPoint by remember { mutableStateOf(Point(0f, 0f)) }
|
||||
// Update must required values
|
||||
val linePoints: List<Point> = linePlotData.lines.flatMap { line -> line.dataPoints.map { it } }
|
||||
|
||||
val (xMin, xMax, xAxisScale) = getXAxisScale(linePoints, xAxisData.steps)
|
||||
val (yMin, _, yAxisScale) = getYAxisScale(linePoints, yAxisData.steps)
|
||||
val maxElementInYAxis = getMaxElementInYAxis(yAxisScale, yAxisData.steps)
|
||||
val xAxisData = xAxisData.copy(axisBottomPadding = bottomPadding)
|
||||
val yAxisData = yAxisData.copy(
|
||||
axisBottomPadding = LocalDensity.current.run { rowHeight.toDp() },
|
||||
axisTopPadding = paddingTop
|
||||
)
|
||||
|
||||
ScrollableCanvasContainer(modifier = modifier
|
||||
.semantics {
|
||||
contentDescription = lineChartData.accessibilityConfig.chartDescription
|
||||
}
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
calculateMaxDistance = { xZoom ->
|
||||
xOffset = xAxisData.axisStepSize.toPx() * xZoom
|
||||
getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
paddingRight.toPx(),
|
||||
size.width,
|
||||
containerPaddingEnd.toPx()
|
||||
)
|
||||
},
|
||||
containerBackgroundColor = backgroundColor,
|
||||
isPinchZoomEnabled = isZoomAllowed,
|
||||
drawXAndYAxis = { scrollOffset, xZoom ->
|
||||
YAxis(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.onGloballyPositioned {
|
||||
columnWidth = it.size.width.toFloat()
|
||||
}, yAxisData = yAxisData
|
||||
)
|
||||
XAxis(xAxisData = xAxisData,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.BottomStart)
|
||||
.onGloballyPositioned {
|
||||
rowHeight = it.size.height.toFloat()
|
||||
}
|
||||
.clip(
|
||||
RowClip(
|
||||
columnWidth, paddingRight
|
||||
)
|
||||
),
|
||||
xStart = columnWidth,
|
||||
scrollOffset = scrollOffset,
|
||||
zoomScale = xZoom,
|
||||
chartData = linePoints,axisStart = columnWidth)
|
||||
},
|
||||
onDraw = { scrollOffset, xZoom ->
|
||||
linePlotData.lines.forEach { line->
|
||||
val yBottom = size.height - rowHeight
|
||||
val yOffset = ((yBottom - paddingTop.toPx()) / maxElementInYAxis)
|
||||
xOffset = xAxisData.axisStepSize.toPx() * xZoom
|
||||
val xLeft = columnWidth // To add extra space if needed
|
||||
val pointsData = getMappingPointsToGraph(
|
||||
line.dataPoints, xMin, xOffset, xLeft, scrollOffset, yBottom, yMin, yOffset
|
||||
)
|
||||
val (cubicPoints1, cubicPoints2) = getCubicPoints(pointsData)
|
||||
val tapPointLocks = mutableMapOf<Int, Pair<Point, Offset>>()
|
||||
|
||||
// Draw guide lines
|
||||
gridLines?.let {
|
||||
drawGridLines(
|
||||
yBottom,
|
||||
yAxisData.axisTopPadding.toPx(),
|
||||
xLeft,
|
||||
paddingRight,
|
||||
scrollOffset,
|
||||
pointsData.size,
|
||||
xZoom,
|
||||
xAxisScale,
|
||||
yAxisData.steps,
|
||||
xAxisData.axisStepSize,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
// Draw cubic line using the points and form a line graph
|
||||
val cubicPath = drawStraightOrCubicLine(
|
||||
pointsData, cubicPoints1, cubicPoints2, line.lineStyle
|
||||
)
|
||||
|
||||
// Draw Lines and Points and AreaUnderLine
|
||||
// Draw area under curve
|
||||
drawShadowUnderLineAndIntersectionPoint(
|
||||
cubicPath, pointsData, yBottom, line
|
||||
)
|
||||
|
||||
// Draw column to make graph look scrollable under Yaxis
|
||||
drawUnderScrollMask(columnWidth, paddingRight, bgColor)
|
||||
|
||||
pointsData.forEachIndexed { index, point ->
|
||||
if (isTapped && point.isTapped(tapOffset.x, xOffset)) {
|
||||
// Dealing with only one line graph hence tapPointLocks[0]
|
||||
tapPointLocks[0] = line.dataPoints[index] to point
|
||||
}
|
||||
}
|
||||
|
||||
val selectedOffset = tapPointLocks.values.firstOrNull()?.second
|
||||
if (selectionTextVisibility && selectedOffset.isNotNull()) {
|
||||
drawHighlightText(
|
||||
identifiedPoint,
|
||||
selectedOffset ?: Offset(0f, 0f),
|
||||
line.selectionHighlightPopUp
|
||||
)
|
||||
}
|
||||
if (isTapped) {
|
||||
val x = tapPointLocks.values.firstOrNull()?.second?.x
|
||||
if (x != null) identifiedPoint =
|
||||
tapPointLocks.values.map { it.first }.first()
|
||||
drawHighLightOnSelectedPoint(
|
||||
tapPointLocks,
|
||||
columnWidth,
|
||||
paddingRight,
|
||||
yBottom,
|
||||
line.selectionHighlightPoint
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onPointClicked = { offset: Offset, _: Float ->
|
||||
isTapped = true
|
||||
selectionTextVisibility = true
|
||||
tapOffset = offset
|
||||
},
|
||||
onScroll = {
|
||||
isTapped = false
|
||||
selectionTextVisibility = false
|
||||
},
|
||||
onZoomInAndOut = {
|
||||
isTapped = false
|
||||
selectionTextVisibility = false
|
||||
})
|
||||
if (isTalkBackEnabled) {
|
||||
AccessibilityBottomSheetDialog(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = Color.White,
|
||||
content = {
|
||||
LazyColumn {
|
||||
items(count = linePlotData.lines.size) { lineIndex ->
|
||||
linePlotData.lines[lineIndex].dataPoints.forEachIndexed { pointIndex, point ->
|
||||
Column {
|
||||
LinePointInfo(
|
||||
xAxisData.axisLabelDescription(
|
||||
xAxisData.labelData(
|
||||
pointIndex
|
||||
)
|
||||
),
|
||||
point.description,
|
||||
linePlotData.lines[lineIndex].lineStyle.color,
|
||||
accessibilityConfig.titleTextSize,
|
||||
accessibilityConfig.descriptionTextSize
|
||||
)
|
||||
|
||||
ItemDivider(
|
||||
thickness = accessibilityConfig.dividerThickness,
|
||||
dividerColor = accessibilityConfig.dividerColor
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
popUpTopRightButtonTitle = accessibilityConfig.popUpTopRightButtonTitle,
|
||||
popUpTopRightButtonDescription = accessibilityConfig.popUpTopRightButtonDescription,
|
||||
sheetState = accessibilitySheetState
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* returns the list of transformed points supported to be drawn on the container using the input points .
|
||||
* @param lineChartPoints :Input data points
|
||||
* @param xMin: Min X-Axis value.
|
||||
* @param xOffset : Total distance between two X-Axis points.
|
||||
* @param xLeft: Total left padding in X-Axis.
|
||||
* @param scrollOffset : Total scrolled offset.
|
||||
* @param yBottom : Bottom start offset for X-Axis.
|
||||
* @param yMin : Min Y-Axis value.
|
||||
* @param yOffset : Distance between two Y-Axis points.
|
||||
*/
|
||||
fun getMappingPointsToGraph(
|
||||
lineChartPoints: List<Point>,
|
||||
xMin: Float,
|
||||
xOffset: Float,
|
||||
xLeft: Float,
|
||||
scrollOffset: Float,
|
||||
yBottom: Float,
|
||||
yMin: Float,
|
||||
yOffset: Float,
|
||||
): MutableList<Offset> {
|
||||
val pointsData = mutableListOf<Offset>()
|
||||
lineChartPoints.forEachIndexed { _, point ->
|
||||
val (x, y) = point
|
||||
val x1 = ((x - xMin) * xOffset) + xLeft - scrollOffset
|
||||
val y1 = yBottom - ((y - yMin) * yOffset)
|
||||
pointsData.add(Offset(x1, y1))
|
||||
}
|
||||
return pointsData
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* returns the max scrollable distance based on the points to be drawn along with padding etc.
|
||||
* @param columnWidth : Width of the Y-Axis.
|
||||
* @param xMax : Max X-Axis value.
|
||||
* @param xMin: Min X-Axis value.
|
||||
* @param xOffset: Total distance between two X-Axis points.
|
||||
* @param paddingRight : Padding at the end of the canvas.
|
||||
* @param canvasWidth : Total available canvas width.
|
||||
* @param containerPaddingEnd : Container inside padding end after the last point of the graph.
|
||||
*/
|
||||
fun getMaxScrollDistance(
|
||||
columnWidth: Float,
|
||||
xMax: Float,
|
||||
xMin: Float,
|
||||
xOffset: Float,
|
||||
paddingRight: Float,
|
||||
canvasWidth: Float,
|
||||
containerPaddingEnd: Float = 0f
|
||||
): Float {
|
||||
val xLastPoint = (xMax - xMin) * xOffset + columnWidth + paddingRight + containerPaddingEnd
|
||||
return if (xLastPoint > canvasWidth) {
|
||||
xLastPoint - canvasWidth
|
||||
} else 0f
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawStraightOrCubicLine extension method used for drawing a straight/cubic line for a given Point(x,y).
|
||||
* @param pointsData : List of points to be drawn on the canvas
|
||||
* @param cubicPoints1 : List of average left side values for a given Point(x,y).
|
||||
* @param cubicPoints2 : List of average right side values for a given Point(x,y).
|
||||
* @param lineStyle : All styles related to the path are included in [LineStyle].
|
||||
*/
|
||||
fun DrawScope.drawStraightOrCubicLine(
|
||||
pointsData: MutableList<Offset>,
|
||||
cubicPoints1: MutableList<Offset>,
|
||||
cubicPoints2: MutableList<Offset>,
|
||||
lineStyle: LineStyle
|
||||
): Path {
|
||||
val path = Path()
|
||||
path.moveTo(pointsData.first().x, pointsData.first().y)
|
||||
for (i in 1 until pointsData.size) {
|
||||
when (lineStyle.lineType) {
|
||||
is LineType.Straight -> {
|
||||
path.lineTo(pointsData[i].x, pointsData[i].y)
|
||||
}
|
||||
is LineType.SmoothCurve -> {
|
||||
path.cubicTo(
|
||||
cubicPoints1[i - 1].x,
|
||||
cubicPoints1[i - 1].y,
|
||||
cubicPoints2[i - 1].x,
|
||||
cubicPoints2[i - 1].y,
|
||||
pointsData[i].x,
|
||||
pointsData[i].y
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
with(lineStyle) {
|
||||
drawPath(
|
||||
path,
|
||||
color = color,
|
||||
style = getDrawStyleForPath(lineStyle.lineType, lineStyle),
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
blendMode = blendMode
|
||||
)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Returns the Drawstyle for the path.
|
||||
* @param lineType : Type of the line [LineType]
|
||||
* @param lineStyle : The style for the path [lineStyle]
|
||||
*/
|
||||
internal fun getDrawStyleForPath(
|
||||
lineType: LineType, lineStyle: LineStyle
|
||||
): DrawStyle = if (lineType.isDotted) Stroke(
|
||||
width = lineStyle.width, pathEffect = PathEffect.dashPathEffect(lineType.intervals)
|
||||
) else lineStyle.style
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawPointOnLine extension method used for drawing a circle/mark on a line for a given Point(x,y).
|
||||
* @param offset : Point at which circle/mark has to be drawn.
|
||||
*/
|
||||
internal fun DrawScope.drawPointOnLine(offset: Offset, intersectionPoint: IntersectionPoint?) {
|
||||
intersectionPoint?.draw?.let { it(this, offset) }
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawUnderScrollMask extension method used for drawing a rectangular mask to make graph scrollable under the YAxis.
|
||||
* @param columnWidth : Width of the rectangular mask here width of Y Axis is used.
|
||||
* @param paddingRight : Padding given at the end of the graph.
|
||||
* @param bgColor : Background of the rectangular mask.
|
||||
*/
|
||||
private fun DrawScope.drawUnderScrollMask(columnWidth: Float, paddingRight: Dp, bgColor: Color) {
|
||||
drawRect(
|
||||
bgColor, Offset(0f, 0f), Size(columnWidth, size.height)
|
||||
)
|
||||
drawRect(
|
||||
bgColor,
|
||||
Offset(size.width - paddingRight.toPx(), 0f),
|
||||
Size(paddingRight.toPx(), size.height)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawShadowUnderLineAndIntersectionPoint extension method used for drawing a
|
||||
* shadow below the line graph points and also drawing intersection points on the line graph.
|
||||
* @param cubicPath : Path used to draw the shadow
|
||||
* @param pointsData : List of the points on the Line graph.
|
||||
* @param yBottom : Offset of X-Axis starting position i.e shade to be drawn until.
|
||||
* @param line : line on which shadow & intersectionPoints has to be drawn.
|
||||
*/
|
||||
fun DrawScope.drawShadowUnderLineAndIntersectionPoint(
|
||||
cubicPath: Path, pointsData: MutableList<Offset>, yBottom: Float, line: Line
|
||||
) {
|
||||
if (line.shadowUnderLine.isNotNull()) {
|
||||
cubicPath.lineTo(pointsData.last().x, yBottom)
|
||||
cubicPath.lineTo(pointsData.first().x, yBottom)
|
||||
line.shadowUnderLine?.draw?.let { it(this, cubicPath) }
|
||||
}
|
||||
if (line.intersectionPoint.isNotNull()) {
|
||||
pointsData.forEach { offset ->
|
||||
drawPointOnLine(offset, line.intersectionPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* getCubicPoints method provides left and right average value for a given point to get a smooth curve.
|
||||
* @param pointsData : List of the points on the Line graph.
|
||||
*/
|
||||
fun getCubicPoints(pointsData: List<Offset>): Pair<MutableList<Offset>, MutableList<Offset>> {
|
||||
val cubicPoints1 = mutableListOf<Offset>()
|
||||
val cubicPoints2 = mutableListOf<Offset>()
|
||||
|
||||
for (i in 1 until pointsData.size) {
|
||||
cubicPoints1.add(
|
||||
Offset(
|
||||
(pointsData[i].x + pointsData[i - 1].x) / 2, pointsData[i - 1].y
|
||||
)
|
||||
)
|
||||
cubicPoints2.add(
|
||||
Offset(
|
||||
(pointsData[i].x + pointsData[i - 1].x) / 2, pointsData[i].y
|
||||
)
|
||||
)
|
||||
}
|
||||
return Pair(cubicPoints1, cubicPoints2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to draw the highlighted text
|
||||
* @param identifiedPoint : Selected points
|
||||
* @param selectedOffset: Offset selected
|
||||
* @param selectionHighlightPopUp : Data class with all styling related info [SelectionHighlightPopUp]
|
||||
*/
|
||||
fun DrawScope.drawHighlightText(
|
||||
identifiedPoint: Point,
|
||||
selectedOffset: Offset,
|
||||
selectionHighlightPopUp: SelectionHighlightPopUp?
|
||||
) {
|
||||
selectionHighlightPopUp?.run {
|
||||
draw(selectedOffset, identifiedPoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawHighLightOnSelectedPoint extension method used for drawing and highlight the selected
|
||||
* point when dragging.
|
||||
* @param dragLocks : List of points to be drawn on the canvas and that can be selected.
|
||||
* @param columnWidth : Width of the Axis in the left or right.
|
||||
* @param paddingRight : Padding given to the right side of the canvas.
|
||||
* @param yBottom : Start position from below of the canvas.
|
||||
* @param selectionHighlightPoint : Data class to define all the styles to be drawn in [SelectionHighlightPoint]
|
||||
*/
|
||||
fun DrawScope.drawHighLightOnSelectedPoint(
|
||||
dragLocks: MutableMap<Int, Pair<Point, Offset>>,
|
||||
columnWidth: Float,
|
||||
paddingRight: Dp,
|
||||
yBottom: Float,
|
||||
selectionHighlightPoint: SelectionHighlightPoint?
|
||||
) {
|
||||
if (selectionHighlightPoint.isNotNull()) {
|
||||
dragLocks.values.firstOrNull()?.let { (_, location) ->
|
||||
val (x, y) = location
|
||||
if (x >= columnWidth && x <= size.width - paddingRight.toPx()) {
|
||||
selectionHighlightPoint?.draw?.let { it(this, Offset(x, y)) }
|
||||
if (selectionHighlightPoint?.isHighlightLineRequired == true) {
|
||||
selectionHighlightPoint.drawHighlightLine(
|
||||
this, Offset(x, yBottom), Offset(x, y)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package co.yml.charts.ui.linechart
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import co.yml.charts.common.model.Point
|
||||
|
||||
/**
|
||||
* returns total offset for given no of steps and offset .
|
||||
* @param offset: Distance of each step.
|
||||
* @param steps: No of steps in Y-Axis.
|
||||
*/
|
||||
fun getMaxElementInYAxis(offset: Float, steps: Int): Float {
|
||||
return (if (steps > 1) steps - 1 else 1) * offset
|
||||
}
|
||||
|
||||
/**
|
||||
* returns Triple value with minY,maxY and scale of each Y-Axis step.
|
||||
* @param points: List of points to be drawn.
|
||||
* @param steps: Count of steps in the Y-Axis.
|
||||
*/
|
||||
fun getYAxisScale(
|
||||
points: List<Point>,
|
||||
steps: Int
|
||||
): Triple<Float, Float, Float> {
|
||||
val yMin = points.takeIf { it.isNotEmpty() }?.minOf { it.y } ?: 0f
|
||||
val yMax = points.takeIf { it.isNotEmpty() }?.maxOf { it.y } ?: 0f
|
||||
val totalSteps = (yMax - yMin)
|
||||
val scale =
|
||||
totalSteps / if (steps > 1) (steps - 1) else 1 // First step starts from 0 by default
|
||||
return Triple(yMin, yMax, scale)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given tap offset is selected point or not else false
|
||||
* @param tapOffset: Offset of the tapped point.
|
||||
* @param xOffset: Distance between two points in X-Axis.
|
||||
*/
|
||||
fun Offset.isTapped(tapOffset: Float, xOffset: Float) =
|
||||
((tapOffset) > x - xOffset / 2) && ((tapOffset) < x + xOffset / 2)
|
||||
@@ -0,0 +1,57 @@
|
||||
package co.yml.charts.ui.linechart.model
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents the grid lines for any graph
|
||||
* @param color: Defines the color of the grid lines.
|
||||
* @param lineWidth: Defines the width of the lines.
|
||||
* @param pathEffect optional effect or pattern to apply to the line.
|
||||
* @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing.
|
||||
* fully transparent to fully opaque respectively.
|
||||
* @param colorFilter ColorFilter to apply to the [color] when drawn into the destination.
|
||||
* @param blendMode Blending algorithm to be applied to the path when it is drawn.
|
||||
* @param enableHorizontalLines False to disable horizontal line else true.
|
||||
* @param enableVerticalLines False to disable vertical lines else true.
|
||||
* @param drawHorizontalLines Draw param used to draw the horizontal lines with given input params.
|
||||
* @param drawVerticalLines Draw param used to draw the vertical lines with given input params.
|
||||
*/
|
||||
data class GridLines(
|
||||
val color: Color = Color.LightGray,
|
||||
val lineWidth: Dp = 1.dp,
|
||||
val pathEffect: PathEffect? = null,
|
||||
val alpha: Float = 1.0f,
|
||||
val colorFilter: ColorFilter? = null,
|
||||
val blendMode: BlendMode = DrawScope.DefaultBlendMode,
|
||||
val enableHorizontalLines: Boolean = true,
|
||||
val enableVerticalLines: Boolean = true,
|
||||
val drawHorizontalLines: DrawScope.(Float, Float, Float) -> Unit = { xStart, y, xEnd ->
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(xStart, y),
|
||||
end = Offset(xEnd, y),
|
||||
strokeWidth = lineWidth.toPx(),
|
||||
pathEffect = pathEffect,
|
||||
colorFilter = colorFilter,
|
||||
blendMode = blendMode
|
||||
)
|
||||
},
|
||||
val drawVerticalLines: DrawScope.(Float, Float, Float) -> Unit = { x, yStart, yEnd ->
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(x, yStart),
|
||||
end = Offset(x, yEnd),
|
||||
strokeWidth = lineWidth.toPx(),
|
||||
pathEffect = pathEffect,
|
||||
colorFilter = colorFilter,
|
||||
blendMode = blendMode
|
||||
)
|
||||
}
|
||||
)
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package co.yml.charts.ui.linechart.model
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents a point on the line graph
|
||||
*
|
||||
* @param color The color or fill to be applied to the circle
|
||||
* @param radius The radius of the circle
|
||||
* @param alpha Opacity to be applied to the circle from 0.0f to 1.0f representing
|
||||
* fully transparent to fully opaque respectively
|
||||
* @param style Whether or not the circle is stroked or filled in
|
||||
* @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
|
||||
* @param blendMode Blending algorithm to be applied to the brush
|
||||
* @param draw override this to change the default drawCircle implementation. You are provided
|
||||
* with the actual [Point] that represents the intersection.
|
||||
*/
|
||||
data class IntersectionPoint(
|
||||
val color: Color = Color.Black,
|
||||
val radius: Dp = 6.dp,
|
||||
val alpha: Float = 1.0f,
|
||||
val style: DrawStyle = Fill,
|
||||
val colorFilter: ColorFilter? = null,
|
||||
val blendMode: BlendMode = DrawScope.DefaultBlendMode,
|
||||
val draw: DrawScope.(Offset) -> Unit = { center ->
|
||||
drawCircle(
|
||||
color,
|
||||
radius.toPx(),
|
||||
center,
|
||||
alpha,
|
||||
style,
|
||||
colorFilter,
|
||||
blendMode
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
package co.yml.charts.ui.linechart.model
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.model.AccessibilityConfig
|
||||
import co.yml.charts.common.model.Point
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* LineGraphData data class that contains all params user need to define to draw a line graph.
|
||||
* @param linePlotData: The path to be drawn on the graph represented by a line.
|
||||
* @param xAxisData: All the configurations related to X-Axis to be defined here in [AxisData]
|
||||
* @param yAxisData: All the configurations related to Y-Axis to be defined here in [AxisData]
|
||||
* @param isZoomAllowed: True if zoom in for all vertical graph components is allowed else false.
|
||||
* @param paddingTop: Padding from the top of the canvas to start of the graph container.
|
||||
* @param paddingRight: Padding from the end of the canvas to end of the graph container.
|
||||
* @param bottomPadding: Padding from the bottom of the canvas to bottom of the graph container.
|
||||
* @param containerPaddingEnd: Container inside padding end after the last point of the graph.
|
||||
* @param backgroundColor: Background color of the Y & X components,
|
||||
* @param gridLines: This enables graph to draw horizontal and vertical grid lines
|
||||
* @param accessibilityConfig: Configs related to accessibility service defined here in [AccessibilityConfig]
|
||||
*/
|
||||
data class LineChartData(
|
||||
val linePlotData: LinePlotData,
|
||||
val xAxisData: AxisData = AxisData.Builder().build(),
|
||||
val yAxisData: AxisData = AxisData.Builder().build(),
|
||||
val isZoomAllowed: Boolean = true,
|
||||
val paddingTop: Dp = 30.dp,
|
||||
val bottomPadding: Dp = 10.dp,
|
||||
val paddingRight: Dp = 10.dp,
|
||||
val containerPaddingEnd: Dp = 15.dp,
|
||||
val backgroundColor: Color = Color.White,
|
||||
val gridLines: GridLines? = null,
|
||||
val accessibilityConfig: AccessibilityConfig = AccessibilityConfig()
|
||||
)
|
||||
|
||||
/**
|
||||
* Represent a Line in the [co.yml.charts.ui.linechart]
|
||||
*
|
||||
* @param dataPoints list of points [Point] in the line
|
||||
* @param lineStyle Adds styling options in [LineStyle] to the line path drawn.
|
||||
* @param intersectionPoint drawing logic to draw the point itself in [IntersectionPoint].
|
||||
* If null, the point is not drawn.
|
||||
* @param selectionHighlightPoint drawing logic to draw the highlight at the point when it is selected
|
||||
* in [SelectionHighlightPoint] If null, the point won't be highlighted on selection
|
||||
* @param shadowUnderLine drawing logic for the section under the line in [ShadowUnderLine].
|
||||
* @param selectionHighlightPopUp All prams related to selection popup to be added here in [SelectionHighlightPopUp]
|
||||
*/
|
||||
data class Line(
|
||||
val dataPoints: List<Point>,
|
||||
val lineStyle: LineStyle = LineStyle(),
|
||||
val intersectionPoint: IntersectionPoint? = null,
|
||||
val selectionHighlightPoint: SelectionHighlightPoint? = null,
|
||||
val shadowUnderLine: ShadowUnderLine? = null,
|
||||
val selectionHighlightPopUp: SelectionHighlightPopUp? = null
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package co.yml.charts.ui.linechart.model
|
||||
|
||||
import co.yml.charts.common.model.PlotData
|
||||
import co.yml.charts.common.model.PlotType
|
||||
|
||||
/**
|
||||
* LinePlotData is a data class that holds line graph related data and styling components
|
||||
* @param plotType : Defines the type of plot/graph
|
||||
* @param lines : Data related to the list of lines to be drawn.
|
||||
*/
|
||||
data class LinePlotData(
|
||||
override val plotType: PlotType = PlotType.Line,
|
||||
val lines: List<Line>
|
||||
) : PlotData {
|
||||
companion object {
|
||||
fun default() =
|
||||
LinePlotData(lines = listOf())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package co.yml.charts.ui.linechart.model
|
||||
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultBlendMode
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
|
||||
/**
|
||||
* Handles styling for the path drawn in the line graph
|
||||
*
|
||||
* @param color Defines the color of the path or line.
|
||||
* @param width Defines the width of the path/line stroke.
|
||||
* @param alpha Defines the alpha of the path/line.
|
||||
* @param style Defines if the path/line is filled or stroke.
|
||||
* @param colorFilter ColorFilter to apply to the [color] when drawn into the destination.
|
||||
* @param blendMode All prams related to selection popup to be added here in [SelectionHighlightPopUp]
|
||||
*/
|
||||
data class LineStyle(
|
||||
val lineType: LineType = LineType.SmoothCurve(isDotted = false),
|
||||
val color: Color = Color.Black,
|
||||
val width: Float = 8f,
|
||||
val alpha: Float = 1.0f,
|
||||
val style: DrawStyle = Stroke(width = width),
|
||||
val colorFilter: ColorFilter? = null,
|
||||
val blendMode: BlendMode = DefaultBlendMode
|
||||
)
|
||||
|
||||
/**
|
||||
* Represent different types of line to be drawn
|
||||
* @see LineType.Straight Draws straight lines with no curves, just a connection from
|
||||
* one point to another.
|
||||
* @see LineType.SmoothCurve Draws curved lines using cubic paths
|
||||
* It has @param isDotted true if line has to be dotted else false.
|
||||
* Also @param intervals Array of "on" and "off" distances for the dashed line segments
|
||||
* phase - Pixel offset into the intervals array
|
||||
*/
|
||||
sealed class LineType {
|
||||
abstract val isDotted: Boolean
|
||||
abstract var intervals: FloatArray
|
||||
|
||||
data class Straight(
|
||||
override val isDotted: Boolean = false,
|
||||
override var intervals: FloatArray = floatArrayOf(30f, 10f)
|
||||
) : LineType()
|
||||
|
||||
data class SmoothCurve(
|
||||
override val isDotted: Boolean = false,
|
||||
override var intervals: FloatArray = floatArrayOf(30f, 10f)
|
||||
) : LineType()
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package co.yml.charts.ui.linechart.model
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
/**
|
||||
* Represents the point when it is selected on the line graph
|
||||
*
|
||||
* @param color The color or fill to be applied to the circle
|
||||
* @param radius The radius of the circle
|
||||
* @param alpha Opacity to be applied to the circle from 0.0f to 1.0f representing
|
||||
* fully transparent to fully opaque respectively
|
||||
* @param style Whether or not the circle is stroked or filled in
|
||||
* @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
|
||||
* @param blendMode Blending algorithm to be applied to the brush
|
||||
* @param draw override this to change the default drawCircle implementation. You are provided
|
||||
* with the actual point [Offset].
|
||||
*/
|
||||
data class SelectionHighlightPoint(
|
||||
val color: Color = Color.Red,
|
||||
val radius: Dp = 6.dp,
|
||||
val alpha: Float = 1.0f,
|
||||
val style: DrawStyle = Fill,
|
||||
val colorFilter: ColorFilter? = null,
|
||||
val blendMode: BlendMode = DrawScope.DefaultBlendMode,
|
||||
val draw: DrawScope.(Offset) -> Unit = { point ->
|
||||
drawCircle(
|
||||
color,
|
||||
radius.toPx(),
|
||||
point,
|
||||
alpha,
|
||||
style,
|
||||
colorFilter,
|
||||
blendMode
|
||||
)
|
||||
},
|
||||
val isHighlightLineRequired: Boolean = true,
|
||||
val drawHighlightLine: DrawScope.(Offset, Offset) -> Unit = { start, end ->
|
||||
drawLine(
|
||||
color,
|
||||
start,
|
||||
end,
|
||||
(radius / 2).toPx(),
|
||||
Stroke.DefaultCap,
|
||||
PathEffect.dashPathEffect(floatArrayOf(40f, 20f)),
|
||||
alpha,
|
||||
colorFilter,
|
||||
blendMode
|
||||
)
|
||||
}
|
||||
)
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package co.yml.charts.ui.linechart.model
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextPaint
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.common.extensions.getTextBackgroundRect
|
||||
import co.yml.charts.common.model.Point
|
||||
|
||||
/**
|
||||
* SelectionHighlightPopUp is a data class used to draw the pop on the given selected point on a line
|
||||
* to identify the dimensions of the selected point.All the styling related params are included here
|
||||
* @param backgroundColor : Defines the background color of the popup.
|
||||
* @param backgroundAlpha : Defines the alpha of the background color.
|
||||
* @param backgroundCornerRadius : Defines the corner radius of the background.
|
||||
* @param backgroundColorFilter : ColorFilter to apply to the color when drawn into the destination.
|
||||
* @param backgroundBlendMode :Blending algorithm to be applied to the path when it is drawn.
|
||||
* @param backgroundStyle : Whether or not the path is stroked or filled in.
|
||||
* @param labelSize : The size of the popUp label in [TextUnit].
|
||||
* @param labelColor : The color of the label text.
|
||||
* @param labelAlignment : The alignment of the label text.
|
||||
* @param labelTypeface : The style of the label text.
|
||||
* @param paddingBetweenPopUpAndPoint : The padding between the anchor position/ popup
|
||||
* start position and the selected point on the line.
|
||||
* @param popUpLabel : The text that can be shown on the popup given 2 input params x and y values
|
||||
* @param draw : Draw the popUp marker on the selected point with 2 inputs [Offset] i.e selectedPoint
|
||||
* and [Point] i.e the input data w.r.t selected point
|
||||
*/
|
||||
data class SelectionHighlightPopUp(
|
||||
val backgroundColor: Color = Color.White,
|
||||
val backgroundAlpha: Float = 0.7f,
|
||||
val backgroundCornerRadius: CornerRadius = CornerRadius(5f),
|
||||
val backgroundColorFilter: ColorFilter? = null,
|
||||
val backgroundBlendMode: BlendMode = DrawScope.DefaultBlendMode,
|
||||
val backgroundStyle: DrawStyle = Fill,
|
||||
val paddingBetweenPopUpAndPoint: Dp = 20.dp,
|
||||
val labelSize: TextUnit = 14.sp,
|
||||
val labelColor: Color = Color.Black,
|
||||
val labelAlignment: Paint.Align = Paint.Align.CENTER,
|
||||
val labelTypeface: Typeface = Typeface.DEFAULT,
|
||||
val popUpLabel: (Float, Float) -> (String) = { x, y ->
|
||||
val xLabel = "x : ${x.toInt()} "
|
||||
val yLabel = "y : ${String.format("%.2f", y)}"
|
||||
"$xLabel $yLabel"
|
||||
},
|
||||
val draw: DrawScope.(Offset, Point) -> Unit = { selectedOffset, identifiedPoint ->
|
||||
val highlightTextPaint = TextPaint().apply {
|
||||
textSize = labelSize.toPx()
|
||||
color = labelColor.toArgb()
|
||||
textAlign = labelAlignment
|
||||
typeface = labelTypeface
|
||||
}
|
||||
val label = popUpLabel(identifiedPoint.x, identifiedPoint.y)
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
val background = getTextBackgroundRect(
|
||||
selectedOffset.x,
|
||||
selectedOffset.y,
|
||||
label,
|
||||
highlightTextPaint
|
||||
)
|
||||
drawRoundRect(
|
||||
color = backgroundColor,
|
||||
topLeft = Offset(
|
||||
background.left.toFloat(),
|
||||
background.top.toFloat() - paddingBetweenPopUpAndPoint.toPx()
|
||||
),
|
||||
size = Size(background.width().toFloat(), background.height().toFloat()),
|
||||
alpha = backgroundAlpha,
|
||||
cornerRadius = backgroundCornerRadius,
|
||||
colorFilter = backgroundColorFilter,
|
||||
blendMode = backgroundBlendMode,
|
||||
style = backgroundStyle
|
||||
)
|
||||
drawText(
|
||||
label,
|
||||
selectedOffset.x,
|
||||
selectedOffset.y - paddingBetweenPopUpAndPoint.toPx(),
|
||||
highlightTextPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
package co.yml.charts.ui.linechart.model
|
||||
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
|
||||
/**
|
||||
* Controls the drawing behaviour of the area/section under the line.
|
||||
*
|
||||
* @param color Color to be applied to the path
|
||||
* @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing
|
||||
* fully transparent to fully opaque respectively
|
||||
* @param style Whether or not the path is stroked or filled in
|
||||
* @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
|
||||
* @param blendMode Blending algorithm to be applied to the path when it is drawn
|
||||
* @param draw override this to change the default drawPath implementation. You are provided with
|
||||
* the [Path] of the line
|
||||
* @param drawMultiColor override this to change the default drawPath implementation. You are provided with
|
||||
* the [Path] of the line, custom [Color] and [Brush]
|
||||
*/
|
||||
data class ShadowUnderLine(
|
||||
val color: Color = Color.Black,
|
||||
val brush: Brush? = null,
|
||||
val alpha: Float = 0.1f,
|
||||
val style: DrawStyle = Fill,
|
||||
val colorFilter: ColorFilter? = null,
|
||||
val blendMode: BlendMode = DrawScope.DefaultBlendMode,
|
||||
val draw: DrawScope.(Path) -> Unit = { path ->
|
||||
if (brush != null) {
|
||||
drawPath(path, brush, alpha, style, colorFilter, blendMode)
|
||||
} else {
|
||||
drawPath(path, color, alpha, style, colorFilter, blendMode)
|
||||
}
|
||||
},
|
||||
val drawMultiColor: DrawScope.(Path, Color, Brush?) -> Unit = { path, multiColor, multiColorBrush ->
|
||||
if (multiColorBrush != null) {
|
||||
drawPath(path, multiColorBrush, alpha, style, colorFilter, blendMode)
|
||||
} else {
|
||||
drawPath(path, multiColor, alpha, style, colorFilter, blendMode)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
package co.yml.charts.ui.piechart
|
||||
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import co.yml.charts.common.model.PlotType
|
||||
import co.yml.charts.ui.piechart.charts.DonutPieChart
|
||||
import co.yml.charts.ui.piechart.charts.PieChart
|
||||
import co.yml.charts.ui.piechart.models.PieChartConfig
|
||||
import co.yml.charts.ui.piechart.models.PieChartData
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
object ChartWrapper {
|
||||
|
||||
/**
|
||||
* Wrapper compose method for drawing Pie Chart and Donut chart.
|
||||
* @param modifier : All modifier related property
|
||||
* @param plotType: Type of the chart (Pie or Donut)
|
||||
* @param pieChartData: data list for the pie chart
|
||||
* @param onSliceClick: Callback when any slice is clicked.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun DrawChart(
|
||||
modifier: Modifier,
|
||||
plotType: PlotType,
|
||||
pieChartData: PieChartData,
|
||||
pieChartConfig: PieChartConfig,
|
||||
onSliceClick: (PieChartData.Slice) -> Unit = {}
|
||||
) {
|
||||
when (plotType) {
|
||||
is PlotType.Pie -> {
|
||||
PieChart(
|
||||
modifier = modifier,
|
||||
pieChartData = pieChartData,
|
||||
pieChartConfig = pieChartConfig,
|
||||
onSliceClick = onSliceClick
|
||||
)
|
||||
}
|
||||
is PlotType.Donut -> {
|
||||
DonutPieChart(
|
||||
modifier = modifier,
|
||||
pieChartData = pieChartData,
|
||||
pieChartConfig = pieChartConfig,
|
||||
onSliceClick = onSliceClick
|
||||
)
|
||||
}
|
||||
else -> { // T0DO Handle if required for other types
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package co.yml.charts.ui.piechart
|
||||
|
||||
|
||||
object PieChartConstants {
|
||||
const val DEFAULT_PADDING = 10
|
||||
const val DEFAULT_START_ANGLE = 270f
|
||||
const val DEFAULT_STROKE_WIDTH = 100f
|
||||
const val DEFAULT_SLICE_LABEL_TEXT_SIZE = 12
|
||||
const val MINIMUM_PERCENTAGE_FOR_SLICE_LABELS = 5
|
||||
const val TOTAL_ANGLE = 360
|
||||
const val ONE_HUNDRED = 100
|
||||
const val DESCRIPTION = "Double tap to know the chart in detail"
|
||||
const val NO_SELECTED_SLICE = -1
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package co.yml.charts.ui.piechart.charts
|
||||
|
||||
import android.graphics.Paint
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.NativeCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
|
||||
import co.yml.charts.common.components.accessibility.SliceInfo
|
||||
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
|
||||
import co.yml.charts.common.model.PlotType
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.NO_SELECTED_SLICE
|
||||
import co.yml.charts.ui.piechart.models.PieChartConfig
|
||||
import co.yml.charts.ui.piechart.models.PieChartData
|
||||
import co.yml.charts.ui.piechart.utils.convertTouchEventPointToAngle
|
||||
import co.yml.charts.ui.piechart.utils.proportion
|
||||
import co.yml.charts.ui.piechart.utils.sweepAngles
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Compose function for Drawing Donut chart
|
||||
* @param modifier : All modifier related property
|
||||
* @param pieChartData: data list for the pie chart
|
||||
* @param pieChartConfig: configuration for the pie chart
|
||||
* @param onSliceClick(pieChartData.Slice)->Unit: The event that captures the click
|
||||
*/
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun DonutPieChart(
|
||||
modifier: Modifier,
|
||||
pieChartData: PieChartData,
|
||||
pieChartConfig: PieChartConfig,
|
||||
onSliceClick: (PieChartData.Slice) -> Unit = {}
|
||||
) {
|
||||
// Sum of all the values
|
||||
val sumOfValues = pieChartData.totalLength
|
||||
|
||||
// Calculate each proportion value
|
||||
val proportions = pieChartData.slices.proportion(sumOfValues)
|
||||
|
||||
// Convert each proportions to angle
|
||||
val sweepAngles = proportions.sweepAngles()
|
||||
|
||||
val progressSize = mutableListOf<Float>()
|
||||
progressSize.add(sweepAngles.first())
|
||||
|
||||
for (x in 1 until sweepAngles.size) {
|
||||
progressSize.add(sweepAngles[x] + progressSize[x - 1])
|
||||
}
|
||||
|
||||
var activePie by rememberSaveable {
|
||||
mutableStateOf(NO_SELECTED_SLICE)
|
||||
}
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val isTalkBackEnabled by LocalContext.current.collectIsTalkbackEnabledAsState()
|
||||
if (accessibilitySheetState.isVisible && isTalkBackEnabled
|
||||
&& pieChartConfig.accessibilityConfig.shouldHandleBackWhenTalkBackPopUpShown
|
||||
) {
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
accessibilitySheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
modifier = modifier
|
||||
) {
|
||||
val boxModifier = if (pieChartConfig.isClickOnSliceEnabled) {
|
||||
modifier
|
||||
.aspectRatio(ratio = 1f)
|
||||
.background(pieChartConfig.backgroundColor)
|
||||
.semantics {
|
||||
contentDescription = pieChartConfig.accessibilityConfig.chartDescription
|
||||
}
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
modifier
|
||||
.aspectRatio(1f)
|
||||
.semantics {
|
||||
contentDescription = pieChartConfig.accessibilityConfig.chartDescription
|
||||
}
|
||||
}
|
||||
BoxWithConstraints(
|
||||
modifier = boxModifier
|
||||
) {
|
||||
|
||||
val sideSize = Integer.min(constraints.maxWidth, constraints.maxHeight)
|
||||
val padding = (sideSize * pieChartConfig.chartPadding) / 100f
|
||||
val size = Size(sideSize.toFloat() - padding, sideSize.toFloat() - padding)
|
||||
|
||||
val pathPortion = remember {
|
||||
Animatable(initialValue = 0f)
|
||||
}
|
||||
|
||||
if (pieChartConfig.isAnimationEnable) {
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
pathPortion.animateTo(
|
||||
1f, animationSpec = tween(pieChartConfig.animationDuration)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val canvasModifier = if (pieChartConfig.isClickOnSliceEnabled) {
|
||||
Modifier
|
||||
.width(sideSize.dp)
|
||||
.height(sideSize.dp)
|
||||
.pointerInput(true) {
|
||||
detectTapGestures {
|
||||
val clickedAngle = convertTouchEventPointToAngle(
|
||||
sideSize.toFloat(),
|
||||
sideSize.toFloat(),
|
||||
it.x,
|
||||
it.y
|
||||
)
|
||||
progressSize.forEachIndexed { index, item ->
|
||||
if (clickedAngle <= item) {
|
||||
activePie = if (activePie != index)
|
||||
index
|
||||
else
|
||||
NO_SELECTED_SLICE
|
||||
onSliceClick(pieChartData.slices[index])
|
||||
return@detectTapGestures
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
.width(sideSize.dp)
|
||||
.height(sideSize.dp)
|
||||
}
|
||||
Canvas(
|
||||
modifier = canvasModifier
|
||||
|
||||
) {
|
||||
|
||||
var sAngle = pieChartConfig.startAngle
|
||||
|
||||
sweepAngles.forEachIndexed { index, arcProgress ->
|
||||
drawPie(
|
||||
color = pieChartData.slices[index].color,
|
||||
startAngle = sAngle,
|
||||
arcProgress = if (pieChartConfig.isAnimationEnable)
|
||||
arcProgress * pathPortion.value else arcProgress,
|
||||
size = size,
|
||||
padding = padding,
|
||||
isDonut = pieChartData.plotType == PlotType.Donut,
|
||||
strokeWidth = pieChartConfig.strokeWidth,
|
||||
isActive = activePie == index,
|
||||
pieChartConfig = pieChartConfig
|
||||
)
|
||||
sAngle += arcProgress
|
||||
}
|
||||
when {
|
||||
activePie != -1 && pieChartConfig.labelVisible -> {
|
||||
val selectedSlice = pieChartData.slices[activePie]
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
val fontSize = pieChartConfig.labelFontSize.toPx()
|
||||
var isValue = false
|
||||
val textToDraw = when (pieChartConfig.labelType) {
|
||||
PieChartConfig.LabelType.PERCENTAGE -> "${
|
||||
proportions[activePie].roundToInt()
|
||||
}%"
|
||||
PieChartConfig.LabelType.VALUE -> {
|
||||
isValue = true
|
||||
selectedSlice.value.toString()
|
||||
}
|
||||
}
|
||||
val labelColor = when (pieChartConfig.labelColorType) {
|
||||
PieChartConfig.LabelColorType.SPECIFIED_COLOR -> pieChartConfig.labelColor
|
||||
PieChartConfig.LabelColorType.SLICE_COLOR -> selectedSlice.color
|
||||
}
|
||||
val shouldShowUnit = isValue && pieChartConfig.sumUnit.isNotEmpty()
|
||||
drawLabel(
|
||||
canvas = this,
|
||||
pieChartConfig = pieChartConfig,
|
||||
labelColor = labelColor,
|
||||
shouldShowUnit = shouldShowUnit,
|
||||
fontSize = fontSize,
|
||||
textToDraw = textToDraw,
|
||||
sideSize = sideSize
|
||||
)
|
||||
}
|
||||
}
|
||||
activePie == -1 && pieChartConfig.isSumVisible -> {
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
val fontSize = pieChartConfig.labelFontSize.toPx()
|
||||
val textToDraw = "$sumOfValues"
|
||||
drawLabel(
|
||||
canvas = this,
|
||||
pieChartConfig = pieChartConfig,
|
||||
labelColor = pieChartConfig.labelColor,
|
||||
shouldShowUnit = pieChartConfig.sumUnit.isNotEmpty(),
|
||||
fontSize = fontSize,
|
||||
textToDraw = textToDraw,
|
||||
sideSize = sideSize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isTalkBackEnabled) {
|
||||
with(pieChartConfig) {
|
||||
AccessibilityBottomSheetDialog(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = Color.White,
|
||||
content = {
|
||||
LazyColumn {
|
||||
items(pieChartData.slices.size) { index ->
|
||||
SliceInfo(
|
||||
pieChartData.slices[index],
|
||||
proportions[index].roundToInt(),
|
||||
accessibilityConfig.descriptionTextSize
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
popUpTopRightButtonTitle = accessibilityConfig.popUpTopRightButtonTitle,
|
||||
popUpTopRightButtonDescription = accessibilityConfig.popUpTopRightButtonDescription,
|
||||
sheetState = accessibilitySheetState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawLabel(
|
||||
canvas: NativeCanvas,
|
||||
pieChartConfig: PieChartConfig,
|
||||
labelColor: Color,
|
||||
shouldShowUnit: Boolean,
|
||||
fontSize: Float,
|
||||
textToDraw: String,
|
||||
sideSize: Int
|
||||
) {
|
||||
val paint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = labelColor.toArgb()
|
||||
textSize = fontSize
|
||||
textAlign = Paint.Align.CENTER
|
||||
typeface = pieChartConfig.labelTypeface
|
||||
}
|
||||
val x = (sideSize / 2).toFloat()
|
||||
var y: Float = (sideSize / 2).toFloat() + fontSize / 3
|
||||
if (shouldShowUnit)
|
||||
y -= (paint.fontSpacing / 4)
|
||||
canvas.drawText(
|
||||
textToDraw,
|
||||
x, y,
|
||||
paint
|
||||
)
|
||||
y += paint.fontSpacing
|
||||
canvas.drawText(
|
||||
pieChartConfig.sumUnit,
|
||||
x,
|
||||
y,
|
||||
paint
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package co.yml.charts.ui.piechart.charts
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import co.yml.charts.ui.piechart.models.PieChartConfig
|
||||
|
||||
/**
|
||||
* Extension for drawing arcs
|
||||
* @param color : Color for slice
|
||||
* @param startAngle : StartAngle for the slice, from where to start draw
|
||||
* @param arcProgress : Process of slice
|
||||
* @param size : Size of the chart
|
||||
* @param strokeWidth : StrokeWidth for the pie chart
|
||||
* @param padding : Padding from top left
|
||||
* @param isDonut : DonutPieChart or PieChart to indicate
|
||||
* @param isActive : DonutPieChart zoom slice if IsActive
|
||||
*/
|
||||
|
||||
fun DrawScope.drawPie(
|
||||
color: Color,
|
||||
startAngle: Float,
|
||||
arcProgress: Float,
|
||||
size: Size,
|
||||
strokeWidth: Float = 100f,
|
||||
padding: Float,
|
||||
isDonut: Boolean = false,
|
||||
isActive: Boolean = false,
|
||||
pieChartConfig: PieChartConfig
|
||||
) {
|
||||
drawArc(
|
||||
color = color,
|
||||
startAngle = startAngle,
|
||||
sweepAngle = arcProgress,
|
||||
useCenter = !isDonut,
|
||||
size = size,
|
||||
alpha = if (isActive) pieChartConfig.activeSliceAlpha else pieChartConfig.inActiveSliceAlpha,
|
||||
style = if (isDonut) Stroke(
|
||||
width = if (isActive) (strokeWidth + 20f) else strokeWidth,
|
||||
) else Fill,
|
||||
|
||||
topLeft = Offset(padding / 2, padding / 2)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package co.yml.charts.ui.piechart.charts
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.text.TextPaint
|
||||
import android.text.TextUtils
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.withRotation
|
||||
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
|
||||
import co.yml.charts.common.components.accessibility.SliceInfo
|
||||
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
|
||||
import co.yml.charts.common.extensions.getTextHeight
|
||||
import co.yml.charts.common.model.PlotType
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.MINIMUM_PERCENTAGE_FOR_SLICE_LABELS
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.NO_SELECTED_SLICE
|
||||
import co.yml.charts.ui.piechart.models.PieChartConfig
|
||||
import co.yml.charts.ui.piechart.models.PieChartData
|
||||
import co.yml.charts.ui.piechart.utils.convertTouchEventPointToAngle
|
||||
import co.yml.charts.ui.piechart.utils.getSliceCenterPoints
|
||||
import co.yml.charts.ui.piechart.utils.proportion
|
||||
import co.yml.charts.ui.piechart.utils.sweepAngles
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
/**
|
||||
* Compose function used to Draw the Pie Chart.
|
||||
* @param modifier : All modifier related property
|
||||
* @param pieChartData: data list for the pie chart
|
||||
* @param pieChartConfig: configuration for the pie chart
|
||||
* @param onSliceClick(pieChartData.Slice)->Unit: The event that captures the click
|
||||
*/
|
||||
@Composable
|
||||
fun PieChart(
|
||||
modifier: Modifier,
|
||||
pieChartData: PieChartData,
|
||||
pieChartConfig: PieChartConfig,
|
||||
onSliceClick: (PieChartData.Slice) -> Unit = {}
|
||||
) {
|
||||
// Sum of all the values
|
||||
val sumOfValues = pieChartData.totalLength
|
||||
|
||||
// Calculate each proportion value
|
||||
val proportions = pieChartData.slices.proportion(sumOfValues)
|
||||
|
||||
// Convert each proportions to angle
|
||||
val sweepAngles = proportions.sweepAngles()
|
||||
|
||||
val progressSize = mutableListOf<Float>()
|
||||
progressSize.add(sweepAngles.first())
|
||||
|
||||
for (x in 1 until sweepAngles.size) {
|
||||
progressSize.add(sweepAngles[x] + progressSize[x - 1])
|
||||
}
|
||||
|
||||
var activePie by rememberSaveable { mutableStateOf(NO_SELECTED_SLICE) }
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val isTalkBackEnabled by LocalContext.current.collectIsTalkbackEnabledAsState()
|
||||
if (accessibilitySheetState.isVisible && isTalkBackEnabled && pieChartConfig.accessibilityConfig.shouldHandleBackWhenTalkBackPopUpShown) {
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
accessibilitySheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
val boxModifier = if (pieChartConfig.isClickOnSliceEnabled) {
|
||||
modifier
|
||||
.aspectRatio(ratio = 1f)
|
||||
.background(pieChartConfig.backgroundColor)
|
||||
.semantics {
|
||||
contentDescription = pieChartConfig.accessibilityConfig.chartDescription
|
||||
}
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
modifier
|
||||
.aspectRatio(1f)
|
||||
.semantics {
|
||||
contentDescription = pieChartConfig.accessibilityConfig.chartDescription
|
||||
}
|
||||
}
|
||||
BoxWithConstraints(
|
||||
modifier = boxModifier
|
||||
) {
|
||||
|
||||
val sideSize = Integer.min(constraints.maxWidth, constraints.maxHeight)
|
||||
val padding = (sideSize * pieChartConfig.chartPadding) / 100f
|
||||
val size = Size(sideSize.toFloat() - padding, sideSize.toFloat() - padding)
|
||||
|
||||
val pathPortion = remember {
|
||||
Animatable(initialValue = 0f)
|
||||
}
|
||||
if (pieChartConfig.isAnimationEnable) {
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
pathPortion.animateTo(
|
||||
1f, animationSpec = tween(pieChartConfig.animationDuration)
|
||||
)
|
||||
}
|
||||
}
|
||||
val canvasModifier = if (pieChartConfig.isClickOnSliceEnabled) {
|
||||
Modifier
|
||||
.width(sideSize.dp)
|
||||
.height(sideSize.dp)
|
||||
.pointerInput(true) {
|
||||
detectTapGestures {
|
||||
val clickedAngle = convertTouchEventPointToAngle(
|
||||
sideSize.toFloat(),
|
||||
sideSize.toFloat(),
|
||||
it.x,
|
||||
it.y
|
||||
)
|
||||
progressSize.forEachIndexed { index, item ->
|
||||
if (clickedAngle <= item) {
|
||||
activePie = if (activePie != index)
|
||||
index
|
||||
else
|
||||
NO_SELECTED_SLICE
|
||||
onSliceClick(pieChartData.slices[index])
|
||||
return@detectTapGestures
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
.width(sideSize.dp)
|
||||
.height(sideSize.dp)
|
||||
}
|
||||
Canvas(modifier = canvasModifier) {
|
||||
|
||||
var sAngle = pieChartConfig.startAngle
|
||||
|
||||
val sliceLabelPaint = TextPaint().apply {
|
||||
isAntiAlias = true
|
||||
textSize = pieChartConfig.sliceLabelTextSize.toPx()
|
||||
textAlign = Paint.Align.CENTER
|
||||
color = pieChartConfig.sliceLabelTextColor.toArgb()
|
||||
typeface = pieChartConfig.sliceLabelTypeface
|
||||
}
|
||||
|
||||
sweepAngles.forEachIndexed { index, arcProgress ->
|
||||
drawPie(
|
||||
color = pieChartData.slices[index].color,
|
||||
startAngle = sAngle,
|
||||
arcProgress = if (pieChartConfig.isAnimationEnable) arcProgress * pathPortion.value else arcProgress,
|
||||
size = size,
|
||||
padding = padding,
|
||||
isDonut = (pieChartData.plotType == PlotType.Pie).not(),
|
||||
isActive = activePie == index,
|
||||
pieChartConfig = pieChartConfig
|
||||
)
|
||||
|
||||
// if percentage is less than 5 width of slice will be very small
|
||||
if (pieChartConfig.showSliceLabels && proportions[index] >= MINIMUM_PERCENTAGE_FOR_SLICE_LABELS) {
|
||||
|
||||
val (arcCenter, x, y) = getSliceCenterPoints(
|
||||
sAngle,
|
||||
arcProgress,
|
||||
size,
|
||||
padding
|
||||
)
|
||||
|
||||
// find the height of text
|
||||
val height = pieChartData.slices[index].label.getTextHeight(sliceLabelPaint)
|
||||
|
||||
var label = pieChartData.slices[index].label
|
||||
|
||||
val ellipsizedText by lazy {
|
||||
TextUtils.ellipsize(
|
||||
label,
|
||||
sliceLabelPaint,
|
||||
pieChartConfig.sliceMinTextWidthToEllipsize.toPx(),
|
||||
pieChartConfig.sliceLabelEllipsizeAt
|
||||
).toString()
|
||||
}
|
||||
|
||||
drawIntoCanvas {
|
||||
it.nativeCanvas.withRotation(
|
||||
arcCenter, x, y
|
||||
) {
|
||||
if (pieChartConfig.labelVisible) {
|
||||
label = "$label ${proportions[index].roundToInt()}%"
|
||||
}
|
||||
it.nativeCanvas.drawText(
|
||||
if (pieChartConfig.isEllipsizeEnabled) ellipsizedText
|
||||
else label, x, y + abs(height) / 2, sliceLabelPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sAngle += arcProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isTalkBackEnabled) {
|
||||
with(pieChartConfig) {
|
||||
AccessibilityBottomSheetDialog(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = Color.White,
|
||||
content = {
|
||||
LazyColumn {
|
||||
items(pieChartData.slices.size) { index ->
|
||||
SliceInfo(
|
||||
pieChartData.slices[index], proportions[index].roundToInt(),
|
||||
accessibilityConfig.descriptionTextSize
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
popUpTopRightButtonTitle = accessibilityConfig.popUpTopRightButtonTitle,
|
||||
popUpTopRightButtonDescription = accessibilityConfig.popUpTopRightButtonDescription,
|
||||
sheetState = accessibilitySheetState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package co.yml.charts.ui.piechart.models
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.common.model.AccessibilityConfig
|
||||
import co.yml.charts.ui.piechart.PieChartConstants
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.DEFAULT_PADDING
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.DEFAULT_SLICE_LABEL_TEXT_SIZE
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.DEFAULT_START_ANGLE
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.DEFAULT_STROKE_WIDTH
|
||||
|
||||
/**
|
||||
* PieChartConfig data class used to mention all config related param required to draw PieChart.
|
||||
* @param startAngle: Starting angle
|
||||
* @param showSliceLabels: Control the labels visibility
|
||||
* @param sliceLabelTextSize: Text size of the labels
|
||||
* @param sliceLabelTextColor: Text color of the labels
|
||||
* @param sliceLabelTypeface: Typeface of the labels
|
||||
* @param isAnimationEnable: Boolean Flag for enabling animation
|
||||
* @param animationDuration: Duration of animation
|
||||
* @param strokeWidth: Stroke width of Donut Chart
|
||||
* @param labelFontSize: Label text font size
|
||||
* @param labelTypeface: Label text typeface
|
||||
* @param labelVisible: Label text visibility
|
||||
* @param labelType: Type of label (percentage or value of slice), value available only for Donut Chart
|
||||
* @param labelColor: Label text color
|
||||
* @param labelColorType: Label text color according to the specified in color in labelColor or the slice color, only for Donut Chart
|
||||
* @param activeSliceAlpha: Opacity of the active slice
|
||||
* @param inActiveSliceAlpha: Opacity of the inactive slice
|
||||
* @param isEllipsizeEnabled: Boolean flag for enabling ellipsize
|
||||
* @param sliceMinTextWidthToEllipsize: Minimum width of the label post which label will be ellipsized
|
||||
* @param sliceLabelEllipsizeAt: Position at which the label will be truncated or ellipsized
|
||||
* @param chartPadding: Padding for the Pie chart/Donut Chart
|
||||
* @param accessibilityConfig: Configs related to accessibility service defined here in [AccessibilityConfig]
|
||||
* @param isSumVisible: When no slice is selected show the sum of values, only used for Donut Chart
|
||||
* @param isClickOnSliceEnabled: Enable/Disable the click on slice
|
||||
* @param sumUnit: The unit of the sum of values, only used for Donut Chart
|
||||
*/
|
||||
data class PieChartConfig(
|
||||
val startAngle: Float = DEFAULT_START_ANGLE,
|
||||
val showSliceLabels: Boolean = true,
|
||||
val sliceLabelTextSize: TextUnit = DEFAULT_SLICE_LABEL_TEXT_SIZE.sp,
|
||||
val sliceLabelTextColor: Color = Color.White,
|
||||
val sliceLabelTypeface: Typeface = Typeface.DEFAULT,
|
||||
val isAnimationEnable: Boolean = false,
|
||||
@IntRange(from = 1) val animationDuration: Int = 500,
|
||||
val strokeWidth: Float = DEFAULT_STROKE_WIDTH,
|
||||
val labelFontSize: TextUnit = 24.sp,
|
||||
val labelTypeface: Typeface = Typeface.DEFAULT,
|
||||
val labelVisible: Boolean = false,
|
||||
val labelType: LabelType = LabelType.PERCENTAGE,
|
||||
val labelColor: Color = Color.White,
|
||||
val labelColorType: LabelColorType = LabelColorType.SPECIFIED_COLOR,
|
||||
val backgroundColor: Color = Color.White,
|
||||
val activeSliceAlpha: Float = .8f,
|
||||
val inActiveSliceAlpha: Float = 1f,
|
||||
val isEllipsizeEnabled: Boolean = false,
|
||||
val sliceMinTextWidthToEllipsize: Dp = 80.dp,
|
||||
val sliceLabelEllipsizeAt: TextUtils.TruncateAt = TextUtils.TruncateAt.END,
|
||||
val chartPadding: Int = DEFAULT_PADDING,
|
||||
val accessibilityConfig: AccessibilityConfig = AccessibilityConfig(
|
||||
chartDescription = PieChartConstants.DESCRIPTION
|
||||
),
|
||||
val isSumVisible: Boolean = false,
|
||||
val sumUnit: String = "",
|
||||
val isClickOnSliceEnabled: Boolean = true
|
||||
){
|
||||
enum class LabelType {
|
||||
PERCENTAGE,
|
||||
VALUE
|
||||
}
|
||||
|
||||
enum class LabelColorType {
|
||||
SPECIFIED_COLOR,
|
||||
SLICE_COLOR
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package co.yml.charts.ui.piechart.models
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import co.yml.charts.common.model.PlotData
|
||||
import co.yml.charts.common.model.PlotType
|
||||
import co.yml.charts.ui.piechart.models.PieChartData.Slice
|
||||
import co.yml.charts.ui.piechart.utils.sum
|
||||
|
||||
/**
|
||||
* PieChartData is a data class to mention all the data needed to draw slices in the pie/donut chart.
|
||||
* @param slices: Defines the list of slices [Slice] to be drawn.
|
||||
* @param plotType: Defines the type of the chart.
|
||||
*/
|
||||
data class PieChartData(val slices: List<Slice>, override val plotType: PlotType) : PlotData {
|
||||
val totalLength: Float
|
||||
get() {
|
||||
return slices.sum()
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice is data class to mention data of each arc in a 360 degree chart.
|
||||
* @param label: Name of the arc.
|
||||
* @param value: Value of the arc.
|
||||
* @param color: Color of the arc.
|
||||
* @param sliceDescription: Description of the arc for accessibility service.
|
||||
*/
|
||||
data class Slice(
|
||||
val label: String,
|
||||
val value: Float,
|
||||
val color: Color,
|
||||
val sliceDescription: (Int) -> String = { slicePercentage ->
|
||||
"Slice name : $label \nPercentage : $slicePercentage %"
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package co.yml.charts.ui.piechart.utils
|
||||
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.geometry.center
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.ONE_HUNDRED
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.TOTAL_ANGLE
|
||||
import co.yml.charts.ui.piechart.models.PieChartData
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* Returns the angle for given touch point
|
||||
* @param width: Width of the chart.
|
||||
* @param height: Height of the chart.
|
||||
* @param xPos: X offset of the tap point.
|
||||
* @param yPos: Y offset of the tap point.
|
||||
*/
|
||||
fun convertTouchEventPointToAngle(
|
||||
width: Float,
|
||||
height: Float,
|
||||
xPos: Float,
|
||||
yPos: Float
|
||||
): Double {
|
||||
val x = xPos - (width * 0.5f)
|
||||
val y = yPos - (height * 0.5f)
|
||||
|
||||
var angle = Math.toDegrees(atan2(y.toDouble(), x.toDouble()) + Math.PI / 2)
|
||||
angle = if (angle < 0) angle + 360 else angle
|
||||
return angle
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of all the arc values
|
||||
*/
|
||||
fun List<PieChartData.Slice>.sum(): Float {
|
||||
return this.map { it.value }.sum()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the center points of the slice
|
||||
* @param sAngle Start angle of the point
|
||||
* @param arcProgress Progress angle of the point
|
||||
* @param size Size of the canvas
|
||||
* @param padding padding of the canvas
|
||||
*/
|
||||
fun getSliceCenterPoints(sAngle: Float, arcProgress: Float, size: Size, padding: Float):
|
||||
Triple<Float, Float, Float> {
|
||||
val arcCenter = sAngle + (arcProgress / 2)
|
||||
// Middle point radius is half of the radius of the pie chart
|
||||
val pointRadius = size.width / 4
|
||||
|
||||
/* Calculate the x & y co-ordinates to show the label/percentage tex
|
||||
* find points using angle and radius
|
||||
*https://en.wikipedia.org/wiki/Polar_coordinate_system#Converting_between_polar_and_Cartesian_coordinates
|
||||
* */
|
||||
val x =
|
||||
(pointRadius * cos(Math.toRadians(arcCenter.toDouble()))) +
|
||||
size.center.x + padding / 2
|
||||
val y =
|
||||
(pointRadius * sin(Math.toRadians(arcCenter.toDouble()))) +
|
||||
size.center.y + padding / 2
|
||||
|
||||
return Triple(arcCenter,x.toFloat(),y.toFloat())
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the calculated proportion value of each arc
|
||||
* @param total: Total of the the slices.
|
||||
*/
|
||||
fun List<PieChartData.Slice>.proportion(total:Float): List<Float> {
|
||||
return this.map {
|
||||
it.value * ONE_HUNDRED / total
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of sweep angles
|
||||
*/
|
||||
fun List<Float>.sweepAngles(): List<Float> {
|
||||
return this.map {
|
||||
TOTAL_ANGLE * it / ONE_HUNDRED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,988 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package co.yml.charts.ui.wavechart
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.text.TextPaint
|
||||
import android.text.TextUtils
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.withRotation
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.axis.YAxis
|
||||
import co.yml.charts.axis.getXAxisScale
|
||||
import co.yml.charts.chartcontainer.container.ScrollableCanvasContainer
|
||||
import co.yml.charts.common.components.ItemDivider
|
||||
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
|
||||
import co.yml.charts.common.components.accessibility.LinePointInfo
|
||||
import co.yml.charts.common.extensions.*
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.common.utils.ChartConstants
|
||||
import co.yml.charts.ui.barchart.drawUnderScrollMask
|
||||
import co.yml.charts.ui.linechart.*
|
||||
import co.yml.charts.ui.linechart.model.LineStyle
|
||||
import co.yml.charts.ui.linechart.model.LineType
|
||||
import co.yml.charts.ui.wavechart.model.AxisPosition
|
||||
import co.yml.charts.ui.wavechart.model.Wave
|
||||
import co.yml.charts.ui.wavechart.model.WaveChartData
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.*
|
||||
import co.yml.charts.ui.linechart.getMaxElementInYAxis as getMaxElementInLineYAxis
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* [WaveChart] compose method used for drawing a Wave Chart.
|
||||
* @param modifier :All modifier related property.
|
||||
* Data class [WaveChartData] to save all params needed to draw the wave chart.
|
||||
* @param waveChartData : Add data related to wave chart.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun WaveChart(modifier: Modifier, waveChartData: WaveChartData) {
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val isTalkBackEnabled by LocalContext.current.collectIsTalkbackEnabledAsState()
|
||||
if (accessibilitySheetState.isVisible && isTalkBackEnabled
|
||||
&& waveChartData.accessibilityConfig.shouldHandleBackWhenTalkBackPopUpShown
|
||||
) {
|
||||
BackHandler {
|
||||
scope.launch {
|
||||
accessibilitySheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(modifier = modifier.testTag("wave_chart")) {
|
||||
with(waveChartData) {
|
||||
var columnWidth by remember { mutableStateOf(0f) }
|
||||
var rowHeight by remember { mutableStateOf(0f) }
|
||||
var xOffset by remember { mutableStateOf(0f) }
|
||||
val bgColor = MaterialTheme.colors.surface
|
||||
var isTapped by remember { mutableStateOf(false) }
|
||||
var tapOffset by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
var selectionTextVisibility by remember { mutableStateOf(false) }
|
||||
var identifiedPoint by remember { mutableStateOf(Point(0f, 0f)) }
|
||||
val wave = wavePlotData.lines.first()
|
||||
rowHeight =
|
||||
LocalDensity.current.run { ChartConstants.DEFAULT_YAXIS_BOTTOM_PADDING.dp.toPx() }
|
||||
// Update must required values
|
||||
val xAxisData = xAxisData.copy(
|
||||
axisBottomPadding = bottomPadding,
|
||||
)
|
||||
val yAxisData = yAxisData.copy(
|
||||
axisBottomPadding = LocalDensity.current.run { rowHeight.toDp() },
|
||||
)
|
||||
|
||||
val (xMin, xMax, xAxisScale) = getXAxisScale(wave.dataPoints, xAxisData.steps)
|
||||
val (yMin, _, yAxisScale) = getYAxisScale(wave.dataPoints, yAxisData.steps)
|
||||
val maxElementInYAxis = getMaxElementInLineYAxis(yAxisScale, yAxisData.steps)
|
||||
|
||||
ScrollableCanvasContainer(modifier = modifier
|
||||
.semantics {
|
||||
contentDescription = waveChartData.accessibilityConfig.chartDescription
|
||||
}
|
||||
.testTag("scrollable_container")
|
||||
.clickable {
|
||||
if (isTalkBackEnabled) {
|
||||
scope.launch {
|
||||
accessibilitySheetState.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
calculateMaxDistance = { xZoom ->
|
||||
xOffset = xAxisData.axisStepSize.toPx() * xZoom
|
||||
getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
paddingRight.toPx(),
|
||||
size.width,
|
||||
containerPaddingEnd.toPx()
|
||||
)
|
||||
},
|
||||
containerBackgroundColor = backgroundColor,
|
||||
isPinchZoomEnabled = isZoomAllowed,
|
||||
drawXAndYAxis = { _, _ ->
|
||||
YAxis(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.fillMaxHeight()
|
||||
.wrapContentWidth()
|
||||
.onGloballyPositioned {
|
||||
columnWidth = it.size.width.toFloat()
|
||||
}, yAxisData = yAxisData
|
||||
)
|
||||
},
|
||||
onDraw = { scrollOffset, xZoom ->
|
||||
val yBottom = size.height - rowHeight
|
||||
val yOffset = ((yBottom - paddingTop.toPx()).div(maxElementInYAxis))
|
||||
xOffset = xAxisData.axisStepSize.toPx() * xZoom
|
||||
val xLeft = columnWidth // To add extra space if needed
|
||||
val pointsData = getMappingPointsToGraph(
|
||||
wave.dataPoints, xMin, xOffset, xLeft, scrollOffset, yBottom, yMin, yOffset
|
||||
)
|
||||
val yPointOfOrigin = yBottom - ((0.minus(yMin)) * yOffset)
|
||||
val tapPointLocks = mutableMapOf<Int, Pair<Point, Offset>>()
|
||||
|
||||
var xPos = xLeft - scrollOffset
|
||||
|
||||
// Draw guide lines
|
||||
gridLines?.let {
|
||||
drawGridLines(
|
||||
yBottom,
|
||||
yAxisData.axisTopPadding.toPx(),
|
||||
xLeft,
|
||||
paddingRight,
|
||||
scrollOffset,
|
||||
pointsData.size,
|
||||
xZoom,
|
||||
xAxisScale,
|
||||
yAxisData.steps,
|
||||
xAxisData.axisStepSize,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
// Draw cubic line using the points and form a wave graph
|
||||
drawStraightOrCubicLineForWave(
|
||||
pointsData, wave.waveStyle, yPointOfOrigin, wave
|
||||
)
|
||||
|
||||
// Draw Lines and Points and AreaUnderLine
|
||||
// Draw area under curve
|
||||
drawShadowUnderLineAndIntersectionPoint(
|
||||
pointsData,
|
||||
wave
|
||||
)
|
||||
|
||||
// Draw the X-axis
|
||||
for (index in 0..xAxisData.steps) {
|
||||
drawXAxisLabel(
|
||||
xAxisData,
|
||||
index,
|
||||
xAxisScale,
|
||||
yPointOfOrigin,
|
||||
xPos
|
||||
)
|
||||
drawAxisLineWithPointers(
|
||||
xPos,
|
||||
xAxisData,
|
||||
xZoom,
|
||||
xAxisScale,
|
||||
yPointOfOrigin,
|
||||
index != xAxisData.steps
|
||||
)
|
||||
xPos += (xAxisData.axisStepSize.toPx() * (xZoom * xAxisScale))
|
||||
}
|
||||
|
||||
// Draw column to make graph look scrollable under Yaxis
|
||||
drawUnderScrollMask(columnWidth, paddingRight, bgColor)
|
||||
|
||||
pointsData.forEachIndexed { index, point ->
|
||||
if (isTapped && point.isTapped(tapOffset.x, xOffset)) {
|
||||
// Dealing with only one line graph hence tapPointLocks[0]
|
||||
tapPointLocks[0] = wave.dataPoints[index] to point
|
||||
}
|
||||
}
|
||||
|
||||
val selectedOffset = tapPointLocks.values.firstOrNull()?.second
|
||||
if (selectionTextVisibility && selectedOffset.isNotNull()) {
|
||||
drawHighlightText(
|
||||
identifiedPoint,
|
||||
selectedOffset ?: Offset(0f, 0f),
|
||||
wave.selectionHighlightPopUp
|
||||
)
|
||||
}
|
||||
if (isTapped) {
|
||||
val x = tapPointLocks.values.firstOrNull()?.second?.x
|
||||
if (x != null) identifiedPoint =
|
||||
tapPointLocks.values.map { it.first }.first()
|
||||
drawHighLightOnSelectedPoint(
|
||||
tapPointLocks,
|
||||
columnWidth,
|
||||
paddingRight,
|
||||
yBottom,
|
||||
wave.selectionHighlightPoint
|
||||
)
|
||||
}
|
||||
},
|
||||
onPointClicked = { offset: Offset, _: Float ->
|
||||
isTapped = true
|
||||
selectionTextVisibility = true
|
||||
tapOffset = offset
|
||||
},
|
||||
onScroll = {
|
||||
isTapped = false
|
||||
selectionTextVisibility = false
|
||||
},
|
||||
onZoomInAndOut = {
|
||||
isTapped = false
|
||||
selectionTextVisibility = false
|
||||
})
|
||||
}
|
||||
if (isTalkBackEnabled) {
|
||||
with(waveChartData) {
|
||||
AccessibilityBottomSheetDialog(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = Color.White,
|
||||
content = {
|
||||
val linePoints = wavePlotData.lines.firstOrNull()?.dataPoints
|
||||
LazyColumn {
|
||||
items(linePoints?.size ?: 0) { index ->
|
||||
Column {
|
||||
LinePointInfo(
|
||||
xAxisData.axisLabelDescription(
|
||||
xAxisData.labelData(
|
||||
index
|
||||
)
|
||||
),
|
||||
linePoints?.get(index)?.description ?: "",
|
||||
wavePlotData.lines.firstOrNull()?.waveStyle?.color
|
||||
?: Color.Transparent,
|
||||
accessibilityConfig.titleTextSize,
|
||||
accessibilityConfig.descriptionTextSize
|
||||
)
|
||||
ItemDivider(
|
||||
thickness = accessibilityConfig.dividerThickness,
|
||||
dividerColor = accessibilityConfig.dividerColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
popUpTopRightButtonTitle = accessibilityConfig.popUpTopRightButtonTitle,
|
||||
popUpTopRightButtonDescription = accessibilityConfig.popUpTopRightButtonDescription,
|
||||
sheetState = accessibilitySheetState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawShadowUnderLineAndIntersectionPoint extension method used for drawing a
|
||||
* shadow below the line graph points and also drawing intersection points on the line graph.
|
||||
* @param pointsData : List of the points on the Line graph.
|
||||
* @param line : line on which shadow & intersectionPoints has to be drawn.
|
||||
*/
|
||||
private fun DrawScope.drawShadowUnderLineAndIntersectionPoint(
|
||||
pointsData: MutableList<Offset>,
|
||||
line: Wave
|
||||
) {
|
||||
if (line.intersectionPoint.isNotNull()) {
|
||||
pointsData.forEach { offset ->
|
||||
drawPointOnLine(offset, line.intersectionPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DrawScope.drawAxisLineWithPointers extension method used for drawing the axis line and the
|
||||
* axis pointers.
|
||||
* @param xPos : X coordinate of the axis line
|
||||
* @param axisData : Axis data.
|
||||
* @param zoomScale : Zoom scale
|
||||
* @param xAxisScale : The scale of increment of X-Axis
|
||||
* @param yPos : Y coordinate of the axis line
|
||||
* @param canDrawEndLine : Boolean letting us know if it is the end point.
|
||||
*/
|
||||
private fun DrawScope.drawAxisLineWithPointers(
|
||||
xPos: Float,
|
||||
axisData: AxisData,
|
||||
zoomScale: Float,
|
||||
xAxisScale: Float,
|
||||
yPos: Float,
|
||||
canDrawEndLine: Boolean, // Added check to avoid drawing an extra line post the last point
|
||||
) {
|
||||
with(axisData) {
|
||||
if (axisConfig.isAxisLineRequired) {
|
||||
if (canDrawEndLine) {
|
||||
val axisStepWidth = (axisStepSize.toPx() * (zoomScale * xAxisScale))
|
||||
drawLine(
|
||||
axisLineColor,
|
||||
Offset(xPos, yPos),
|
||||
if (shouldDrawAxisLineTillEnd) {
|
||||
Offset((xPos + (axisStepWidth / 2) + axisStepWidth), yPos)
|
||||
} else {
|
||||
Offset(xPos + axisStepWidth, yPos)
|
||||
}, strokeWidth = axisLineThickness.toPx()
|
||||
)
|
||||
}
|
||||
drawLine(
|
||||
axisLineColor,
|
||||
Offset(xPos, yPos),
|
||||
Offset(xPos, yPos + indicatorLineWidth.toPx()),
|
||||
strokeWidth = axisLineThickness.toPx()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DrawScope.drawXAxisLabel extension method used for drawing the axis line labels.
|
||||
* @param xPos : X coordinate of the axis line
|
||||
* @param axisData : Axis data.
|
||||
* @param index : Index of label data.
|
||||
* @param xAxisScale : The scale of increment of X-Axis
|
||||
* @param yPos : Y coordinate of the axis line
|
||||
*/
|
||||
private fun DrawScope.drawXAxisLabel(
|
||||
axisData: AxisData,
|
||||
index: Int,
|
||||
xAxisScale: Float,
|
||||
yPos: Float,
|
||||
xPos: Float
|
||||
) {
|
||||
with(axisData) {
|
||||
val xAxisTextPaint = TextPaint().apply {
|
||||
textSize = axisLabelFontSize.toPx()
|
||||
color = axisLabelColor.toArgb()
|
||||
textAlign = Paint.Align.LEFT
|
||||
typeface = axisData.typeface
|
||||
}
|
||||
val xLabel = labelData((index * xAxisScale).toInt())
|
||||
val labelHeight = xLabel.getTextHeight(xAxisTextPaint)
|
||||
val labelWidth = xLabel.getTextWidth(xAxisTextPaint)
|
||||
val ellipsizedText = TextUtils.ellipsize(
|
||||
xLabel,
|
||||
xAxisTextPaint,
|
||||
axisStepSize.toPx(),
|
||||
axisConfig.ellipsizeAt
|
||||
)
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
val x = xPos - (labelWidth / 2)
|
||||
val y =
|
||||
yPos + labelHeight / 2 + indicatorLineWidth.toPx() + labelAndAxisLinePadding.toPx()
|
||||
withRotation(axisLabelAngle, x, y) {
|
||||
drawText(
|
||||
if (axisConfig.shouldEllipsizeAxisLabel) ellipsizedText.toString() else xLabel,
|
||||
x,
|
||||
y,
|
||||
xAxisTextPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* DrawScope.drawStraightOrCubicLine extension method used for drawing a straight/cubic line for a given Point(x,y).
|
||||
* @param pointsData : List of points to be drawn on the canvas
|
||||
* @param lineStyle : All styles related to the path are included in [LineStyle].
|
||||
* @param yPointOfOrigin : y position of x Axis
|
||||
* @param wave : Wave data
|
||||
*/
|
||||
fun DrawScope.drawStraightOrCubicLineForWave(
|
||||
pointsData: MutableList<Offset>,
|
||||
lineStyle: LineStyle,
|
||||
yPointOfOrigin: Float = 0f,
|
||||
wave: Wave
|
||||
): Path {
|
||||
val path = Path()
|
||||
|
||||
when (lineStyle.lineType) {
|
||||
is LineType.Straight -> {
|
||||
drawLinePath(
|
||||
path,
|
||||
pointsData,
|
||||
lineStyle,
|
||||
wave,
|
||||
yPointOfOrigin
|
||||
)
|
||||
}
|
||||
is LineType.SmoothCurve -> {
|
||||
//draw the path using wave points
|
||||
drawWavePath(
|
||||
path,
|
||||
pointsData,
|
||||
lineStyle,
|
||||
wave,
|
||||
yPointOfOrigin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to draw the wave
|
||||
* @param path : Represent the single cubic bezier curve path using the given points
|
||||
* @param pointsData : curve points
|
||||
* @param lineStyle : styling for the path drawn in the line graph
|
||||
* @param wave : Wave data
|
||||
* @param yPointOfOrigin : y position of x Axis
|
||||
*/
|
||||
fun DrawScope.drawWavePath(
|
||||
path: Path,
|
||||
pointsData: MutableList<Offset>,
|
||||
lineStyle: LineStyle,
|
||||
wave: Wave,
|
||||
yPointOfOrigin: Float
|
||||
): Path {
|
||||
for (i in 1 until pointsData.size) {
|
||||
//find control points to draw the cubic curve
|
||||
val (cubicPoints1, cubicPoints2) = findControlPoints(
|
||||
pointsData[i - 1],
|
||||
pointsData[i]
|
||||
)
|
||||
|
||||
path.moveTo(
|
||||
pointsData[i - 1].x,
|
||||
pointsData[i - 1].y
|
||||
)
|
||||
//draw cubic bezier curve
|
||||
path.cubicTo(
|
||||
cubicPoints1.x,
|
||||
cubicPoints1.y,
|
||||
cubicPoints2.x,
|
||||
cubicPoints2.y,
|
||||
pointsData[i].x,
|
||||
pointsData[i].y
|
||||
)
|
||||
|
||||
//Color Filling part//
|
||||
val pointPosition = findPointPosition(
|
||||
pointsData[i - 1].y,
|
||||
pointsData[i].y,
|
||||
yPointOfOrigin
|
||||
)
|
||||
|
||||
if (pointPosition == AxisPosition.INTERSECT) {
|
||||
val curvePoints =
|
||||
getCurvePoints(pointsData[i - 1], cubicPoints1, cubicPoints2, pointsData[i])
|
||||
|
||||
fillIntersectionCurvePath(
|
||||
curvePoints,
|
||||
yPointOfOrigin,
|
||||
wave
|
||||
)
|
||||
} else {
|
||||
fillCurvePath(
|
||||
yPointOfOrigin,
|
||||
pointPosition,
|
||||
wave,
|
||||
pointsData[i - 1],
|
||||
pointsData[i],
|
||||
cubicPoints1,
|
||||
cubicPoints2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
with(lineStyle) {
|
||||
drawPath(
|
||||
path,
|
||||
color = color,
|
||||
style = getDrawStyleForPath(lineStyle.lineType, lineStyle),
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
blendMode = blendMode
|
||||
)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
fun DrawScope.drawLinePath(
|
||||
path: Path,
|
||||
pointsData: MutableList<Offset>,
|
||||
lineStyle: LineStyle,
|
||||
wave: Wave,
|
||||
yPointOfOrigin: Float
|
||||
) {
|
||||
path.moveTo(pointsData.first().x, pointsData.first().y)
|
||||
|
||||
// slop and y intercept of x axis line
|
||||
val (m1, b1) = findSlopeAndYIntercept(
|
||||
Offset(0f, yPointOfOrigin),
|
||||
Offset(size.width, yPointOfOrigin)
|
||||
)
|
||||
|
||||
for (i in 1 until pointsData.size) {
|
||||
//Draw the line with out filling color
|
||||
path.lineTo(pointsData[i].x, pointsData[i].y)
|
||||
|
||||
with(lineStyle) {
|
||||
drawPath(
|
||||
path,
|
||||
color = color,
|
||||
style = getDrawStyleForPath(lineStyle.lineType, lineStyle),
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
blendMode = blendMode
|
||||
)
|
||||
}
|
||||
|
||||
//Color Filling part//
|
||||
val pointPosition = findPointPosition(
|
||||
pointsData[i - 1].y,
|
||||
pointsData[i].y,
|
||||
yPointOfOrigin
|
||||
)
|
||||
|
||||
if (pointPosition == AxisPosition.INTERSECT) {
|
||||
val (m2, b2) = findSlopeAndYIntercept(pointsData[i - 1], pointsData[i])
|
||||
|
||||
//find intersection points of the line
|
||||
val intersectionPoint =
|
||||
findLineIntersection(m1, b1, m2, b2) ?: Offset(0f, 0f)
|
||||
|
||||
fillIntersectionLinePath(
|
||||
pointsData[i - 1],
|
||||
pointsData[i],
|
||||
yPointOfOrigin,
|
||||
intersectionPoint,
|
||||
wave
|
||||
)
|
||||
} else {
|
||||
fillLinePath(
|
||||
yPointOfOrigin,
|
||||
pointPosition,
|
||||
wave,
|
||||
pointsData[i - 1],
|
||||
pointsData[i]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get the points which helps to construct the bezier curve
|
||||
* @param startPoint : startPoint of the curve
|
||||
* @param cubicPoint1 : cubicPoint1 of the curve
|
||||
* @param cubicPoint2 : cubicPoint2 of the curve
|
||||
* @param endPoint : endPoint of the curve
|
||||
*/
|
||||
fun getCurvePoints(
|
||||
startPoint: Offset,
|
||||
cubicPoint1: Offset,
|
||||
cubicPoint2: Offset,
|
||||
endPoint: Offset
|
||||
): List<Offset> {
|
||||
val start = 0.0
|
||||
val end = 1.0
|
||||
val step = 0.01
|
||||
|
||||
//generate t values for constructing the cubic curve
|
||||
val values = generateSequence(start) { (it + step).roundTwoDecimal() }
|
||||
.takeWhile { it <= end }
|
||||
|
||||
val curvePoints = mutableListOf<Offset>()
|
||||
for (t in values) {
|
||||
val point =
|
||||
findCubicCurvePoints(
|
||||
t,
|
||||
startPoint,
|
||||
cubicPoint1,
|
||||
cubicPoint2,
|
||||
endPoint
|
||||
)
|
||||
curvePoints.add(point)
|
||||
}
|
||||
|
||||
return curvePoints.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fill the line path
|
||||
* @param yPointOfOrigin y offset point on xAxis
|
||||
* @param pointPosition : line point position
|
||||
* @param wave : Wave data
|
||||
* @param startPoint: startPoint of the curve
|
||||
* @param endPoint : endPoint of the curve
|
||||
* @param cubicPoints1: control point 1
|
||||
* @param cubicPoints2: control point 2
|
||||
*/
|
||||
fun DrawScope.fillCurvePath(
|
||||
yPointOfOrigin: Float,
|
||||
pointPosition: AxisPosition,
|
||||
wave: Wave,
|
||||
startPoint: Offset,
|
||||
endPoint: Offset,
|
||||
cubicPoints1: Offset,
|
||||
cubicPoints2: Offset
|
||||
) {
|
||||
val curvePath = Path()
|
||||
curvePath.moveTo(startPoint.x, startPoint.y)
|
||||
|
||||
curvePath.cubicTo(
|
||||
cubicPoints1.x,
|
||||
cubicPoints1.y,
|
||||
cubicPoints2.x,
|
||||
cubicPoints2.y,
|
||||
endPoint.x,
|
||||
endPoint.y
|
||||
)
|
||||
|
||||
curvePath.lineTo(endPoint.x, yPointOfOrigin)
|
||||
curvePath.lineTo(startPoint.x, yPointOfOrigin)
|
||||
|
||||
val fillColor: Color
|
||||
val fillBrush: Brush?
|
||||
if (pointPosition == AxisPosition.BOTTOM) {
|
||||
fillColor = wave.waveFillColor.bottomColor
|
||||
fillBrush = wave.waveFillColor.bottomBrush
|
||||
} else {
|
||||
fillColor = wave.waveFillColor.topColor
|
||||
fillBrush = wave.waveFillColor.topBrush
|
||||
}
|
||||
|
||||
wave.shadowUnderLine?.drawMultiColor?.let { it(this, curvePath, fillColor, fillBrush) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fill the wave path using given color
|
||||
* @param curvePoints : points which create the bezier curve
|
||||
* @param yPointOfOrigin : y position of x Axis
|
||||
* @param wave : Wave data
|
||||
*/
|
||||
fun DrawScope.fillIntersectionCurvePath(
|
||||
curvePoints: List<Offset>,
|
||||
yPointOfOrigin: Float,
|
||||
wave: Wave
|
||||
) {
|
||||
val bottomPath = Path()
|
||||
val topPath = Path()
|
||||
|
||||
val startPoint = curvePoints.first()
|
||||
val endPoint = curvePoints.last()
|
||||
|
||||
val isStartPointBottom =
|
||||
findPointPosition(startPoint.y, yPointOfOrigin, yPointOfOrigin) == AxisPosition.BOTTOM
|
||||
|
||||
val intersectionPoint = getCurveIntersectionPoint(curvePoints, yPointOfOrigin)
|
||||
|
||||
if (isStartPointBottom) {
|
||||
bottomPath.moveTo(startPoint.x, startPoint.y)
|
||||
topPath.moveTo(intersectionPoint.x, intersectionPoint.y)
|
||||
} else {
|
||||
bottomPath.moveTo(intersectionPoint.x, intersectionPoint.y)
|
||||
topPath.moveTo(startPoint.x, startPoint.y)
|
||||
}
|
||||
|
||||
for (i in 1 until curvePoints.size) {
|
||||
if (curvePoints[i - 1].y > yPointOfOrigin) {
|
||||
//BOTTOM
|
||||
bottomPath.lineTo(curvePoints[i - 1].x, curvePoints[i - 1].y)
|
||||
bottomPath.lineTo(curvePoints[i].x, curvePoints[i].y)
|
||||
} else {
|
||||
//TOP
|
||||
topPath.lineTo(curvePoints[i - 1].x, curvePoints[i - 1].y)
|
||||
topPath.lineTo(curvePoints[i].x, curvePoints[i].y)
|
||||
}
|
||||
}
|
||||
|
||||
// Fill rest of the bottom path
|
||||
if (isStartPointBottom) {
|
||||
bottomPath.lineTo(intersectionPoint.x, yPointOfOrigin)
|
||||
bottomPath.lineTo(startPoint.x, yPointOfOrigin)
|
||||
|
||||
topPath.lineTo(endPoint.x, yPointOfOrigin)
|
||||
topPath.moveTo(intersectionPoint.x, yPointOfOrigin)
|
||||
} else {
|
||||
bottomPath.lineTo(endPoint.x, yPointOfOrigin)
|
||||
bottomPath.moveTo(intersectionPoint.x, yPointOfOrigin)
|
||||
|
||||
topPath.lineTo(intersectionPoint.x, yPointOfOrigin)
|
||||
topPath.lineTo(startPoint.x, yPointOfOrigin)
|
||||
}
|
||||
wave.shadowUnderLine?.drawMultiColor?.let {
|
||||
it(
|
||||
this,
|
||||
bottomPath,
|
||||
wave.waveFillColor.bottomColor,
|
||||
wave.waveFillColor.bottomBrush
|
||||
)
|
||||
}
|
||||
wave.shadowUnderLine?.drawMultiColor?.let {
|
||||
it(
|
||||
this,
|
||||
topPath,
|
||||
wave.waveFillColor.topColor,
|
||||
wave.waveFillColor.topBrush
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fill the line path
|
||||
* @param yPointOfOrigin y offset point on xAxis
|
||||
* @param pointPosition : line point position
|
||||
* @param wave : Wave data
|
||||
* @param startPoint: startPoint of the line
|
||||
* @param endPoint : endPoint of the line
|
||||
*/
|
||||
fun DrawScope.fillLinePath(
|
||||
yPointOfOrigin: Float,
|
||||
pointPosition: AxisPosition,
|
||||
wave: Wave,
|
||||
startPoint: Offset,
|
||||
endPoint: Offset
|
||||
) {
|
||||
val linePath = Path()
|
||||
linePath.moveTo(startPoint.x, startPoint.y)
|
||||
linePath.lineTo(endPoint.x, endPoint.y)
|
||||
|
||||
linePath.lineTo(endPoint.x, yPointOfOrigin)
|
||||
linePath.lineTo(startPoint.x, yPointOfOrigin)
|
||||
|
||||
val fillColor: Color
|
||||
val fillBrush: Brush?
|
||||
if (pointPosition == AxisPosition.BOTTOM) {
|
||||
fillColor = wave.waveFillColor.bottomColor
|
||||
fillBrush = wave.waveFillColor.bottomBrush
|
||||
} else {
|
||||
fillColor = wave.waveFillColor.topColor
|
||||
fillBrush = wave.waveFillColor.topBrush
|
||||
}
|
||||
|
||||
wave.shadowUnderLine?.drawMultiColor?.let { it(this, linePath, fillColor, fillBrush) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fill the line path when two lines are intersecting
|
||||
* @param startPoint: startPoint of the line
|
||||
* @param endPoint : endPoint of the line
|
||||
* @param yPointOfOrigin : y position of x Axis
|
||||
* @param intersectionPoint : intersectionPoint of lines
|
||||
* @param wave : Wave data
|
||||
*/
|
||||
fun DrawScope.fillIntersectionLinePath(
|
||||
startPoint: Offset,
|
||||
endPoint: Offset,
|
||||
yPointOfOrigin: Float,
|
||||
intersectionPoint: Offset,
|
||||
wave: Wave
|
||||
) {
|
||||
val bottomPath = Path()
|
||||
val topPath = Path()
|
||||
|
||||
val isStartPointBottom =
|
||||
findPointPosition(startPoint.y, yPointOfOrigin, yPointOfOrigin) == AxisPosition.BOTTOM
|
||||
|
||||
//move to corresponding start point
|
||||
if (isStartPointBottom) {
|
||||
bottomPath.moveTo(startPoint.x, startPoint.y)
|
||||
topPath.moveTo(intersectionPoint.x, intersectionPoint.y)
|
||||
} else {
|
||||
bottomPath.moveTo(intersectionPoint.x, intersectionPoint.y)
|
||||
topPath.moveTo(startPoint.x, startPoint.y)
|
||||
}
|
||||
|
||||
// Fill rest of the bottom path
|
||||
if (isStartPointBottom) {
|
||||
bottomPath.lineTo(intersectionPoint.x, intersectionPoint.y)
|
||||
bottomPath.lineTo(intersectionPoint.x, yPointOfOrigin)
|
||||
bottomPath.lineTo(startPoint.x, yPointOfOrigin)
|
||||
|
||||
topPath.lineTo(endPoint.x, endPoint.y)
|
||||
topPath.lineTo(endPoint.x, yPointOfOrigin)
|
||||
topPath.moveTo(intersectionPoint.x, yPointOfOrigin)
|
||||
} else {
|
||||
bottomPath.lineTo(endPoint.x, endPoint.y)
|
||||
bottomPath.lineTo(endPoint.x, yPointOfOrigin)
|
||||
bottomPath.moveTo(intersectionPoint.x, yPointOfOrigin)
|
||||
|
||||
topPath.lineTo(intersectionPoint.x, intersectionPoint.y)
|
||||
topPath.lineTo(intersectionPoint.x, yPointOfOrigin)
|
||||
topPath.lineTo(startPoint.x, yPointOfOrigin)
|
||||
}
|
||||
|
||||
wave.shadowUnderLine?.drawMultiColor?.let {
|
||||
it(
|
||||
this,
|
||||
bottomPath,
|
||||
wave.waveFillColor.bottomColor,
|
||||
wave.waveFillColor.bottomBrush
|
||||
)
|
||||
}
|
||||
wave.shadowUnderLine?.drawMultiColor?.let {
|
||||
it(
|
||||
this,
|
||||
topPath,
|
||||
wave.waveFillColor.topColor,
|
||||
wave.waveFillColor.topBrush
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get the intersection point between the curve and line
|
||||
* @param curvePoints : list of curve points
|
||||
* @param yPointOfOrigin : y offset point on xAxis
|
||||
*/
|
||||
fun getCurveIntersectionPoint(curvePoints: List<Offset>, yPointOfOrigin: Float): Offset {
|
||||
var intersectionPoint = curvePoints.firstOrNull { it.y == yPointOfOrigin }
|
||||
if (intersectionPoint == null) {
|
||||
intersectionPoint = findNearestValue(curvePoints, yPointOfOrigin)
|
||||
}
|
||||
|
||||
return intersectionPoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to find the closest value for a given input
|
||||
* @param list : list of offsets
|
||||
* @param intersectionPoint : point on xAxis
|
||||
*/
|
||||
fun findNearestValue(list: List<Offset>, intersectionPoint: Float): Offset {
|
||||
var nearestValue = list[0]
|
||||
var smallestDifference = abs(intersectionPoint - list[0].y)
|
||||
|
||||
for (value in list) {
|
||||
val difference = abs(intersectionPoint - value.y)
|
||||
if (difference < smallestDifference) {
|
||||
smallestDifference = difference
|
||||
nearestValue = value
|
||||
}
|
||||
}
|
||||
|
||||
return nearestValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get the control points of the give curve
|
||||
* @param startPoint : start point of the curve
|
||||
* @param endPoint : endPoint of the curve
|
||||
*/
|
||||
fun findControlPoints(startPoint: Offset, endPoint: Offset): Pair<Offset, Offset> {
|
||||
// Calculate the difference between the x-coordinates of the two points
|
||||
val dx = endPoint.x - startPoint.x
|
||||
// Set the value of K to control the shape of the curve
|
||||
val k = 0.3f
|
||||
// Calculate the control points of the curve
|
||||
val control1X = startPoint.x + k * dx
|
||||
val control1Y = startPoint.y
|
||||
val control2X = endPoint.x - k * dx
|
||||
val control2Y = endPoint.y
|
||||
|
||||
return Pair(Offset(control1X, control1Y), Offset(control2X, control2Y))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to find the position of the start and end point of the bezier curve
|
||||
* @param p1 : start point
|
||||
* @param p2 : end point
|
||||
* @param yPointOfOrigin : point where x axis reside
|
||||
*/
|
||||
fun findPointPosition(
|
||||
p1: Float,
|
||||
p2: Float,
|
||||
yPointOfOrigin: Float
|
||||
): AxisPosition {
|
||||
return if (p1 > yPointOfOrigin && p2 > yPointOfOrigin) {
|
||||
AxisPosition.BOTTOM
|
||||
} else if (p1 < yPointOfOrigin && p2 < yPointOfOrigin) {
|
||||
AxisPosition.TOP
|
||||
} else if (p1 == yPointOfOrigin) {
|
||||
if (p2 > yPointOfOrigin) {
|
||||
AxisPosition.BOTTOM
|
||||
} else {
|
||||
AxisPosition.TOP
|
||||
}
|
||||
} else if (p2 == yPointOfOrigin) {
|
||||
if (p1 > yPointOfOrigin) {
|
||||
AxisPosition.BOTTOM
|
||||
} else {
|
||||
AxisPosition.TOP
|
||||
}
|
||||
} else {
|
||||
AxisPosition.INTERSECT
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return slop and y intercept of the line
|
||||
* @param p1 : points on a line 1
|
||||
* @param p2 : points on a line 2
|
||||
*/
|
||||
fun findSlopeAndYIntercept(p1: Offset, p2: Offset): Pair<Float, Float> {
|
||||
val m = (p2.y - p1.y) / (p2.x - p1.x)
|
||||
val b = p1.y - m * p1.x
|
||||
return Pair(m, b)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to compute the intersection point of the lines
|
||||
* @param m1 : slop of line 1
|
||||
* @param b1 : y intercept of line 1
|
||||
* @param m2 : slop of line 2
|
||||
* @param b2 : y intercept of line 2
|
||||
*/
|
||||
fun findLineIntersection(m1: Float, b1: Float, m2: Float, b2: Float): Offset? {
|
||||
if (m1 == m2) {
|
||||
return null // Lines are parallel
|
||||
}
|
||||
|
||||
val x = (b2 - b1) / (m1 - m2)
|
||||
val y = m1 * x + b1
|
||||
|
||||
return Offset(x, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Using De Casteljau’s algorithm to find the intermediate points on the curve
|
||||
* @param t : value between 0 and 1
|
||||
* @param p0: start point of the curve
|
||||
* @param p1: control point 1
|
||||
* @param p1: control point 2
|
||||
* @param p3: end point of the curve
|
||||
* @return a point which lies on the curve based on t value
|
||||
*/
|
||||
fun findCubicCurvePoints(
|
||||
t: Double,
|
||||
p0: Offset,
|
||||
p1: Offset,
|
||||
p2: Offset,
|
||||
p3: Offset
|
||||
): Offset {
|
||||
// First level of interpolation
|
||||
//calculate the points on the line segments
|
||||
val p01 = interpolate(t, p0, p1)
|
||||
val p12 = interpolate(t, p1, p2)
|
||||
val p23 = interpolate(t, p2, p3)
|
||||
|
||||
// Second level of interpolation
|
||||
val p012 = interpolate(t, p01, p12)
|
||||
val p123 = interpolate(t, p12, p23)
|
||||
|
||||
// Third level of interpolation
|
||||
val p0123 = interpolate(t, p012, p123)
|
||||
|
||||
return p0123
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to returns the interpolated point between p0 and p1 at the given t value.
|
||||
* @param t: value between 0 and 1
|
||||
* @param p0 : Curve point
|
||||
* @param p1: Curve point
|
||||
*/
|
||||
fun interpolate(t: Double, p0: Offset, p1: Offset): Offset {
|
||||
val x = (1 - t) * p0.x + t * p1.x
|
||||
val y = (1 - t) * p0.y + t * p1.y
|
||||
return Offset(x.toFloat(), y.toFloat())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package co.yml.charts.ui.wavechart.model
|
||||
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.model.AccessibilityConfig
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.linechart.model.*
|
||||
import co.yml.charts.ui.wavechart.model.AxisPosition.INTERSECT
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* WaveChartData data class that contains all params user need to define to draw a wave graph.
|
||||
* @param wavePlotData: The path to be drawn on the graph represented by a wave.
|
||||
* @param xAxisData: All the configurations related to X-Axis to be defined here in [AxisData]
|
||||
* @param yAxisData: All the configurations related to Y-Axis to be defined here in [AxisData]
|
||||
* @param isZoomAllowed: True if zoom in for all vertical graph components is allowed else false.
|
||||
* @param paddingTop: Padding from the top of the canvas to start of the graph container.
|
||||
* @param paddingRight: Padding from the end of the canvas to end of the graph container.
|
||||
* @param bottomPadding: Padding from the bottom of the canvas to bottom of the graph container.
|
||||
* @param containerPaddingEnd: Container inside padding end after the last point of the graph.
|
||||
* @param backgroundColor: Background color of the Y & X components,
|
||||
* @param gridLines: This enables graph to draw horizontal and vertical grid lines
|
||||
* @param accessibilityConfig: Configs related to accessibility service defined here in [AccessibilityConfig]
|
||||
*/
|
||||
data class WaveChartData(
|
||||
val wavePlotData: WavePlotData,
|
||||
val xAxisData: AxisData = AxisData.Builder().build(),
|
||||
val yAxisData: AxisData = AxisData.Builder().build(),
|
||||
val isZoomAllowed: Boolean = true,
|
||||
val paddingTop: Dp = 30.dp,
|
||||
val bottomPadding: Dp = 10.dp,
|
||||
val paddingRight: Dp = 10.dp,
|
||||
val containerPaddingEnd: Dp = 15.dp,
|
||||
val backgroundColor: Color = Color.White,
|
||||
val gridLines: GridLines? = null,
|
||||
val accessibilityConfig: AccessibilityConfig = AccessibilityConfig()
|
||||
)
|
||||
|
||||
/**
|
||||
* Represent a Wave in the [co.yml.charts.ui.wavechart]
|
||||
*
|
||||
* @param dataPoints list of points [Point] in the wave
|
||||
* @param waveStyle Adds styling options in [LineStyle] to the wave path drawn.
|
||||
* @param intersectionPoint drawing logic to draw the point itself in [IntersectionPoint].
|
||||
* If null, the point is not drawn.
|
||||
* @param selectionHighlightPoint drawing logic to draw the highlight at the point when it is selected
|
||||
* in [SelectionHighlightPoint] If null, the point won't be highlighted on selection
|
||||
* @param shadowUnderLine drawing logic for the section under the wave in [ShadowUnderLine].
|
||||
* @param selectionHighlightPopUp All prams related to selection popup to be added here in [SelectionHighlightPopUp]
|
||||
*/
|
||||
data class Wave(
|
||||
val dataPoints: List<Point>,
|
||||
val waveStyle: LineStyle = LineStyle(),
|
||||
val intersectionPoint: IntersectionPoint? = null,
|
||||
val selectionHighlightPoint: SelectionHighlightPoint? = null,
|
||||
val shadowUnderLine: ShadowUnderLine? = null,
|
||||
val selectionHighlightPopUp: SelectionHighlightPopUp? = null,
|
||||
val waveFillColor: WaveFillColor = WaveFillColor()
|
||||
)
|
||||
|
||||
/**
|
||||
* hold the color values
|
||||
*/
|
||||
data class WaveFillColor(
|
||||
val topColor: Color = Color.Black,
|
||||
val bottomColor: Color = Color.Black,
|
||||
val topBrush: Brush? = null,
|
||||
val bottomBrush: Brush? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents the start and end point positions of the bezier curve
|
||||
* @property INTERSECT : Means both lines are not in the same region
|
||||
*/
|
||||
enum class AxisPosition {
|
||||
TOP,
|
||||
BOTTOM,
|
||||
INTERSECT
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package co.yml.charts.ui.wavechart.model
|
||||
|
||||
import co.yml.charts.common.model.PlotData
|
||||
import co.yml.charts.common.model.PlotType
|
||||
|
||||
/**
|
||||
* WavePlotData is a data class that holds line graph related data and styling components
|
||||
* @param plotType : Defines the type of plot/graph
|
||||
* @param lines : Data related to the list of waves to be drawn.
|
||||
*/
|
||||
data class WavePlotData(
|
||||
override val plotType: PlotType = PlotType.Wave,
|
||||
val lines: List<Wave>
|
||||
) : PlotData {
|
||||
companion object {
|
||||
fun default() =
|
||||
WavePlotData(lines = listOf())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package co.yml.charts.axis
|
||||
|
||||
import co.yml.charts.common.model.Point
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class XGraphExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `Given list of points exact xMin and xMax value should be calculated`() {
|
||||
// Given
|
||||
val chartData = listOf(
|
||||
Point(0f, 10f), Point(1f, 30f), Point(2f, 15f), Point(3f, 50f)
|
||||
)
|
||||
val steps = 2
|
||||
|
||||
// When
|
||||
val (xMin, xMax, scale) = getXAxisScale(chartData, steps)
|
||||
|
||||
//Then
|
||||
assertEquals(xMin.toInt(), 0)
|
||||
assertEquals(xMax.toInt(), 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given list of points when drawing axis using the scale the value should be withing xMin and xMax`() {
|
||||
// Given
|
||||
val chartData = listOf(
|
||||
Point(0f, 10f), Point(1f, 30f), Point(2f, 15f), Point(3f, 50f)
|
||||
)
|
||||
val steps = 2
|
||||
|
||||
// When
|
||||
val (xMin, xMax, scale) = getXAxisScale(chartData, steps)
|
||||
|
||||
//Then
|
||||
for (i in 0 until steps) {
|
||||
val value = i * scale
|
||||
assertTrue(value >= xMin)
|
||||
assertTrue(value <= xMax)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given empty list of points all the values should be zero`() {
|
||||
// Given
|
||||
val chartData = listOf<Point>()
|
||||
val steps = 2
|
||||
|
||||
// When
|
||||
val (xMin, xMax, scale) = getXAxisScale(chartData, steps)
|
||||
|
||||
//Then
|
||||
assertEquals(xMin.toInt(), 0)
|
||||
assertEquals(xMax.toInt(), 0)
|
||||
assertEquals(scale.toInt(), 0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package co.yml.charts.axis
|
||||
|
||||
import co.yml.charts.common.model.Point
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class YChartsExtensionsTest {
|
||||
|
||||
@MockK
|
||||
private val axisData: AxisData = mockk()
|
||||
|
||||
@Test
|
||||
fun `When getAxisInitValues extension is invoked should not return values lesser than zero`() {
|
||||
// Given
|
||||
every { axisData.steps }.returns(5)
|
||||
|
||||
// When
|
||||
val values = getAxisInitValues(
|
||||
axisData,
|
||||
687f, // 250.dp.toPx()
|
||||
20f,
|
||||
20f
|
||||
)
|
||||
|
||||
// Then
|
||||
assertTrue(values.first > 0)
|
||||
assertTrue(values.second > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given yAxisData segmentHeight should be less than yaxisHeight `() {
|
||||
// Given
|
||||
every { axisData.steps }.returns(5)
|
||||
|
||||
// When
|
||||
val values = getAxisInitValues(
|
||||
axisData,
|
||||
687f, // 250.dp.toPx()
|
||||
20f,
|
||||
20f
|
||||
)
|
||||
|
||||
// Then
|
||||
assertTrue(values.second < values.first)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Given list of points exact yMin and yMax value should be calculated`() {
|
||||
// Given
|
||||
val chartData = listOf(
|
||||
Point(10f, 0f), Point(30f, 1f), Point(15f, 2f), Point(50f, 3f)
|
||||
)
|
||||
val steps = 2
|
||||
|
||||
// When
|
||||
val (yMin, yMax, yAxisScale) = getYAxisScale(chartData, steps)
|
||||
//Then
|
||||
Assert.assertEquals(yMin.toInt(), 0)
|
||||
Assert.assertEquals(yMax.toInt(), 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given list of points when drawing axis using the scale the value should be withing xMin and xMax`() {
|
||||
// Given
|
||||
val chartData = listOf(
|
||||
Point(10f, 0f), Point(30f, 1f), Point(15f, 2f), Point(50f, 3f)
|
||||
)
|
||||
val steps = 2
|
||||
|
||||
// When
|
||||
val (yMin, yMax, yAxisScale) = getYAxisScale(chartData, steps)
|
||||
|
||||
//Then
|
||||
for (i in 0 until steps) {
|
||||
val value = i * yAxisScale
|
||||
assertTrue(value >= yMin)
|
||||
assertTrue(value <= yMax)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Given empty list of points all the values should be zero`() {
|
||||
// Given
|
||||
val chartData = listOf<Point>()
|
||||
val steps = 2
|
||||
|
||||
// When
|
||||
val (yMin, yMax, yAxisScale) = getYAxisScale(chartData, steps)
|
||||
|
||||
//Then
|
||||
Assert.assertEquals(yMin.toInt(), 0)
|
||||
Assert.assertEquals(yMax.toInt(), 0)
|
||||
Assert.assertEquals(yAxisScale.toInt(), 0)
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package co.yml.charts.chartcontainer
|
||||
|
||||
import co.yml.charts.chartcontainer.container.checkAndGetMaxScrollOffset
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ScrollableCanvasContainerTest {
|
||||
|
||||
@Test
|
||||
fun `Given negative scroll offset canvas should not scroll`() {
|
||||
// Given
|
||||
val scrollOffset = -20f
|
||||
val maxScrollOffset = 200f
|
||||
|
||||
// When
|
||||
val calculatedScrollOffset = checkAndGetMaxScrollOffset(scrollOffset, maxScrollOffset)
|
||||
|
||||
// Then
|
||||
assertEquals(calculatedScrollOffset, 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given scroll offset canvas should support scroll if within maxScrollOffset`() {
|
||||
// Given
|
||||
val scrollOffset = 20f
|
||||
val maxScrollOffset = 200f
|
||||
|
||||
// When
|
||||
val calculatedScrollOffset = checkAndGetMaxScrollOffset(scrollOffset, maxScrollOffset)
|
||||
|
||||
// Then
|
||||
assertEquals(calculatedScrollOffset, 20f)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Given scroll offset more then maxScrollOffset then scroll offset should be reset to same`() {
|
||||
// Given
|
||||
val scrollOffset = 220f
|
||||
val maxScrollOffset = 200f
|
||||
|
||||
// When
|
||||
val calculatedScrollOffset = checkAndGetMaxScrollOffset(scrollOffset, maxScrollOffset)
|
||||
|
||||
// Then
|
||||
assertEquals(calculatedScrollOffset, maxScrollOffset)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package co.yml.charts.extensions
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import co.yml.charts.common.extensions.*
|
||||
import co.yml.charts.common.model.Point
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ExtensionsTest{
|
||||
@Test
|
||||
fun `Find proper maximum and minimum X values from the given points`() {
|
||||
val pointList = listOf(
|
||||
Point(0f, 10f), Point(1f, 20f), Point(2f, 30f), Point(3f, 40f)
|
||||
)
|
||||
|
||||
val (xMin, xMax) = getXMaxAndMinPoints(pointList)
|
||||
|
||||
assertEquals(xMin, 0f)
|
||||
assertEquals(xMax, 3f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find proper maximum and minimum Y values from the given points`() {
|
||||
val pointList = listOf(
|
||||
Point(0f, 10f), Point(1f, 20f), Point(2f, 30f), Point(3f, 40f)
|
||||
)
|
||||
|
||||
val (yMin, yMax) = getYMaxAndMinPoints(pointList)
|
||||
|
||||
assertEquals(yMin, 10f)
|
||||
assertEquals(yMax, 40f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given empty list maximum and minimum points should be zero`() {
|
||||
val pointList = emptyList<Point>()
|
||||
|
||||
val (yMin, yMax) = getYMaxAndMinPoints(pointList)
|
||||
|
||||
assertEquals(yMin, 0f)
|
||||
assertEquals(yMax, 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find the maximum Y point from the steps size and max value`() {
|
||||
val stepSize = 10
|
||||
val maxValue = 45f
|
||||
|
||||
val maximumYValue = getMaxElementInYAxis(maxValue,stepSize)
|
||||
|
||||
assertEquals(maximumYValue, 50)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a point and proper drag lock status should be calculated`() {
|
||||
val offset = Offset(30f, 20f)
|
||||
val dragOffsetX = 25f
|
||||
val xOffset = 20f
|
||||
|
||||
val isDragLocked = offset.isDragLocked(dragOffsetX, xOffset)
|
||||
assertEquals(isDragLocked, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find the maximum X point from the steps size and max value`() {
|
||||
val stepSize = 10
|
||||
val maxValue = 45f
|
||||
|
||||
val maximumYValue = getMaxElementInXAxis(maxValue, stepSize)
|
||||
|
||||
assertEquals(maximumYValue, 50)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Return true when tapped point within the vertical bar bound`() {
|
||||
val middleOffset = Offset(747.5f, 727.8f)
|
||||
val tapOffset = Offset(745f, 807f)
|
||||
val xOffset = 122.5f
|
||||
val bottom = 950f
|
||||
val tapPadding = 35f
|
||||
|
||||
val isTapped = middleOffset.isTapped(tapOffset, xOffset, bottom, tapPadding)
|
||||
|
||||
assertEquals(isTapped, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Return false when tapped point outside the vertical bar bound`() {
|
||||
val middleOffset = Offset(747.5f, 727.8f)
|
||||
val tapOffset = Offset(500f, 807f)
|
||||
val xOffset = 122.5f
|
||||
val bottom = 950f
|
||||
val tapPadding = 35f
|
||||
|
||||
val isTapped = middleOffset.isTapped(tapOffset, xOffset, bottom, tapPadding)
|
||||
|
||||
assertEquals(isTapped, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Return true when tapped point within the horizontal bar bound`() {
|
||||
val middleOffset = Offset(250f, 708.8f)
|
||||
val tapOffset = Offset(360.7f, 723.6f)
|
||||
val yOffset = 122.5f
|
||||
val left = 250f
|
||||
val tapPadding = 35f
|
||||
val xAxisWidth = 636.75f
|
||||
|
||||
val isTapped = middleOffset.isYAxisTapped(tapOffset, yOffset, left, tapPadding, xAxisWidth)
|
||||
|
||||
assertEquals(isTapped, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Return false when tapped point outside the horizontal bar bound`() {
|
||||
val middleOffset = Offset(250f, 708.8f)
|
||||
val tapOffset = Offset(360.7f, 800f)
|
||||
val yOffset = 122.5f
|
||||
val left = 250f
|
||||
val tapPadding = 35f
|
||||
val xAxisWidth = 636.75f
|
||||
|
||||
val isTapped = middleOffset.isYAxisTapped(tapOffset, yOffset, left, tapPadding, xAxisWidth)
|
||||
|
||||
assertEquals(isTapped, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Return true when tapped point within the selected stacked bar bound`() {
|
||||
val middleOffset = Offset(250f, 500f)
|
||||
val tapOffset = Offset(260f, 550f)
|
||||
val barWidth = 25f
|
||||
val barHeight = 600f
|
||||
val tapPadding = 35f
|
||||
|
||||
val isTapped = middleOffset.isStackedBarTapped(tapOffset, barWidth, barHeight, tapPadding)
|
||||
|
||||
assertEquals(isTapped, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Return true when tapped point outside the the selected stacked bar bound`() {
|
||||
val middleOffset = Offset(250f, 500f)
|
||||
val tapOffset = Offset(260f, 770f)
|
||||
val barWidth = 25f
|
||||
val barHeight = 600f
|
||||
val tapPadding = 35f
|
||||
|
||||
val isTapped = middleOffset.isStackedBarTapped(tapOffset, barWidth, barHeight, tapPadding)
|
||||
|
||||
assertEquals(isTapped, false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package co.yml.charts.ui.bargraph
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import co.yml.charts.axis.DataCategoryOptions
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.ui.barchart.getDrawHorizontalOffset
|
||||
import co.yml.charts.ui.barchart.getDrawOffset
|
||||
import co.yml.charts.ui.barchart.getFullBarDetails
|
||||
import co.yml.charts.ui.barchart.getMaxScrollDistance
|
||||
import co.yml.charts.ui.barchart.models.BarData
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class BarChartTest {
|
||||
@Test
|
||||
fun `Given a container with input values valid scroll offset should be returned`() {
|
||||
// Given
|
||||
val columnWidth = 104f
|
||||
val xMax = 100f
|
||||
val xMin = 0f
|
||||
val xOffset = 200f
|
||||
val xLeft = 10f
|
||||
val paddingRight = 40f
|
||||
val canvasWidth = 500f
|
||||
|
||||
// When
|
||||
val scrollDistance = getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
paddingRight,
|
||||
canvasWidth
|
||||
)
|
||||
|
||||
assertTrue(scrollDistance > 0)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Given a total drawing width lesser than container width container should not be able to scrolled`() {
|
||||
// Given
|
||||
val columnWidth = 20f
|
||||
val xMax = 20f
|
||||
val xMin = 0f
|
||||
val xOffset = 20f
|
||||
val xLeft = 10f
|
||||
val paddingRight = 40f
|
||||
val canvasWidth = 500f
|
||||
|
||||
// When
|
||||
val scrollDistance = getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
paddingRight,
|
||||
canvasWidth
|
||||
)
|
||||
|
||||
assertEquals(scrollDistance, 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a point drawOffset should be positive`() {
|
||||
val point = Point(100f, 20f)
|
||||
|
||||
val drawOffset = getDrawOffset(
|
||||
point = point,
|
||||
xMin = 0f,
|
||||
xOffset = 10f,
|
||||
xLeft = 20f,
|
||||
scrollOffset = 50f,
|
||||
yBottom = 1500f,
|
||||
yOffset = 20f,
|
||||
yMin = 0f,
|
||||
startDrawPadding = 20f,
|
||||
barWidth = 40f,
|
||||
zoomScale = 1f
|
||||
)
|
||||
assertTrue(drawOffset.x > 0 && drawOffset.y > 0)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Given scroll offset,xleft,xmin are zero drawOffset should be product of xOffset and xValue`() {
|
||||
val point = Point(1f, 20f)
|
||||
val xOffset = 20f
|
||||
|
||||
val drawOffset = getDrawOffset(
|
||||
point = point,
|
||||
xMin = 0f,
|
||||
xOffset = xOffset,
|
||||
xLeft = 0f,
|
||||
scrollOffset = 0f,
|
||||
yBottom = 1500f,
|
||||
yOffset = 20f,
|
||||
yMin = 0f,
|
||||
startDrawPadding = 20f,
|
||||
barWidth = 40f,
|
||||
zoomScale = 1f
|
||||
)
|
||||
|
||||
assertEquals(drawOffset.x, xOffset * point.x)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a point horizontal drawOffset should be positive`() {
|
||||
val point = Point(250f, 700f)
|
||||
|
||||
val drawOffset = getDrawHorizontalOffset(
|
||||
point = point,
|
||||
xLeft = 250f,
|
||||
scrollOffset = 50f,
|
||||
yBottom = 950f,
|
||||
yOffset = 192.5f,
|
||||
yMin = 0f,
|
||||
yMax = 9f,
|
||||
yStart = 150f,
|
||||
dataCategoryOptions = DataCategoryOptions(),
|
||||
zoomScale = 1f
|
||||
)
|
||||
|
||||
assertTrue(drawOffset.x > 0 && drawOffset.y > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given scroll offset,yStart,ymin are zero drawOffset should be product of yOffset and yValue`() {
|
||||
val point = Point(250f, 700f)
|
||||
val yOffset = 192.5f
|
||||
|
||||
val drawOffset = getDrawHorizontalOffset(
|
||||
point = point,
|
||||
xLeft = 250f,
|
||||
scrollOffset = 0f,
|
||||
yBottom = 950f,
|
||||
yOffset = 192.5f,
|
||||
yMin = 0f,
|
||||
yMax = 9f,
|
||||
yStart = 0f,
|
||||
dataCategoryOptions = DataCategoryOptions(),
|
||||
zoomScale = 1f
|
||||
)
|
||||
|
||||
assertEquals(drawOffset.y, yOffset * point.y)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given individual stacked bar data points return it as full bar points`() {
|
||||
// Given
|
||||
val totalPaddingBtwBars = 10f
|
||||
val yOffset = 50f
|
||||
val yBottom = 100f
|
||||
val xPointOffSet = 100f
|
||||
val barDetails = listOf<BarData>(
|
||||
BarData(Point(50f, 100f)),
|
||||
BarData(Point(50f, 200f)),
|
||||
BarData(Point(50f, 300f))
|
||||
)
|
||||
|
||||
//When
|
||||
val fullBarDetails =
|
||||
getFullBarDetails(barDetails, totalPaddingBtwBars, yOffset, yBottom, xPointOffSet)
|
||||
|
||||
//Then
|
||||
assertEquals(fullBarDetails.first, BarData(Point(50f, 600f)))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Given individual stacked bar data points return its full bar offset`() {
|
||||
// Given
|
||||
val totalPaddingBtwBars = 10f
|
||||
val yOffset = 5f
|
||||
val yBottom = 1200f
|
||||
val xPointOffSet = 100f
|
||||
val barDetails = listOf<BarData>(
|
||||
BarData(Point(50f, 100f)),
|
||||
BarData(Point(50f, 200f)),
|
||||
BarData(Point(50f, 300f))
|
||||
)
|
||||
|
||||
//When
|
||||
val fullBarDetails =
|
||||
getFullBarDetails(barDetails, totalPaddingBtwBars, yOffset, yBottom, xPointOffSet)
|
||||
|
||||
//Then
|
||||
assertEquals(fullBarDetails.second, Offset(100f, -1810f))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package co.yml.charts.ui.bubblechart
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import co.yml.charts.ui.linechart.getCubicPoints
|
||||
import co.yml.charts.ui.linechart.getMappingPointsToGraph
|
||||
import co.yml.charts.ui.linechart.getMaxScrollDistance
|
||||
import co.yml.charts.common.model.Point
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class BubbleChartTest {
|
||||
|
||||
@Test
|
||||
fun `Given few points data n-1 size of cubicPoints1 & cubicPoints2 should be returned`() {
|
||||
// Given
|
||||
val pointsData = listOf(Offset(1f, 30f), Offset(2f, 10f), Offset(3f, 55f))
|
||||
|
||||
// When
|
||||
val pairCubicPoints = getCubicPoints(pointsData)
|
||||
|
||||
//Then
|
||||
Assert.assertEquals(pointsData.size - 1, pairCubicPoints.first.size)
|
||||
Assert.assertEquals(pointsData.size - 1, pairCubicPoints.second.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given two points calculated cubicPoints1 value should be returned`() {
|
||||
// Given
|
||||
val pointsData = listOf(Offset(1f, 30f), Offset(2f, 10f))
|
||||
|
||||
// When
|
||||
val pairCubicPoints = getCubicPoints(pointsData)
|
||||
|
||||
//Then
|
||||
val expectedCubic1Point = Offset(
|
||||
(pointsData[1].x + pointsData.first().x) / 2,
|
||||
pointsData.first().y
|
||||
)
|
||||
|
||||
Assert.assertEquals(expectedCubic1Point, pairCubicPoints.first.first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given two points calculated cubicPoints2 value should be returned`() {
|
||||
// Given
|
||||
val pointsData = listOf(Offset(1f, 30f), Offset(2f, 10f))
|
||||
|
||||
// When
|
||||
val pairCubicPoints = getCubicPoints(pointsData)
|
||||
|
||||
//Then
|
||||
val expectedCubic1Point = Offset(
|
||||
(pointsData[1].x + pointsData.first().x) / 2,
|
||||
pointsData[1].y
|
||||
)
|
||||
|
||||
Assert.assertEquals(expectedCubic1Point, pairCubicPoints.second.first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a container with input values valid scroll offset should be returned`() {
|
||||
// Given
|
||||
val columnWidth = 104f
|
||||
val xMax = 100f
|
||||
val xMin = 0f
|
||||
val xOffset = 200f
|
||||
val paddingRight = 40f
|
||||
val canvasWidth = 500f
|
||||
|
||||
// When
|
||||
val scrollDistance = getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
paddingRight,
|
||||
canvasWidth
|
||||
)
|
||||
|
||||
Assert.assertTrue(scrollDistance > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a total drawing width lesser than container width container should not be able to scrolled`() {
|
||||
// Given
|
||||
val columnWidth = 20f
|
||||
val xMax = 20f
|
||||
val xMin = 0f
|
||||
val xOffset = 20f
|
||||
val paddingRight = 40f
|
||||
val canvasWidth = 500f
|
||||
|
||||
// When
|
||||
val scrollDistance = getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
paddingRight,
|
||||
canvasWidth
|
||||
)
|
||||
|
||||
Assert.assertEquals(scrollDistance, 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given input points size the transformed list size should be same`() {
|
||||
// Given
|
||||
val pointsData = listOf(Point(1f, 30f), Point(2f, 10f), Point(3f, 55f))
|
||||
val xLeft = 20f
|
||||
val scrollOffset = 0f
|
||||
val xMin = 0f
|
||||
val xOffset = 20f
|
||||
val yBottom = 40f
|
||||
val yMin = 0f
|
||||
val yOffset = 20f
|
||||
|
||||
//When
|
||||
val transformedPointsList = getMappingPointsToGraph(
|
||||
pointsData, xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yMin,
|
||||
yOffset
|
||||
)
|
||||
|
||||
// Then
|
||||
Assert.assertEquals(pointsData.size, transformedPointsList.size)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a point x and transformed point x to w_r_t container should be same `() {
|
||||
// Given
|
||||
val pointData = listOf(Point(1f, 30f))
|
||||
val xLeft = 20f
|
||||
val scrollOffset = 0f
|
||||
val xMin = 0f
|
||||
val xOffset = 20f
|
||||
val yBottom = 40f
|
||||
val yMin = 0f
|
||||
val yOffset = 20f
|
||||
|
||||
//When
|
||||
val transformedXPoint = getMappingPointsToGraph(
|
||||
pointData, xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yMin,
|
||||
yOffset
|
||||
).first().x
|
||||
|
||||
//Then
|
||||
val expectedXPoint = ((pointData.first().x - xMin) * xOffset) + xLeft - scrollOffset
|
||||
Assert.assertEquals(expectedXPoint, transformedXPoint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a point y and transformed point y to w_r_t container should be same `() {
|
||||
// Given
|
||||
val pointData = listOf(Point(1f, 30f))
|
||||
val xLeft = 20f
|
||||
val scrollOffset = 0f
|
||||
val xMin = 0f
|
||||
val xOffset = 20f
|
||||
val yBottom = 40f
|
||||
val yMin = 0f
|
||||
val yOffset = 20f
|
||||
|
||||
//When
|
||||
val transformedYPoint = getMappingPointsToGraph(
|
||||
pointData, xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yMin,
|
||||
yOffset
|
||||
).first().y
|
||||
|
||||
//Then
|
||||
val expectedYPoint = yBottom - ((pointData.first().y - yMin) * yOffset)
|
||||
Assert.assertEquals(expectedYPoint, transformedYPoint)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package co.yml.charts.ui.linegraph
|
||||
|
||||
import co.yml.charts.ui.linechart.getYAxisScale
|
||||
import co.yml.charts.common.model.Point
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class LineChartExtensionsTest {
|
||||
@Test
|
||||
fun `Given list of points exact yMin and yMax value should be calculated`() {
|
||||
// Given
|
||||
val chartData = listOf(
|
||||
Point(0f, 10f), Point(1f, 30f), Point(2f, 15f), Point(3f, 50f)
|
||||
)
|
||||
val steps = 2
|
||||
|
||||
// When
|
||||
val (yMin, yMax, scale) = getYAxisScale(chartData, steps)
|
||||
|
||||
//Then
|
||||
Assert.assertEquals(yMin.toInt(), 10)
|
||||
Assert.assertEquals(yMax.toInt(), 50)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given list of points when drawing axis using the scale the value should be withing yMin and yMax`() {
|
||||
// Given
|
||||
val chartData = listOf(
|
||||
Point(0f, 10f), Point(1f, 30f), Point(2f, 15f), Point(3f, 50f)
|
||||
)
|
||||
val steps = 2
|
||||
|
||||
// When
|
||||
val (yMin, yMax, scale) = getYAxisScale(chartData, steps)
|
||||
|
||||
//Then
|
||||
for (i in 1 until steps) {
|
||||
val value = i * scale
|
||||
Assert.assertTrue(value >= yMin)
|
||||
Assert.assertTrue(value <= yMax)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given empty list of points all the values should be zero`() {
|
||||
// Given
|
||||
val chartData = listOf<Point>()
|
||||
val steps = 2
|
||||
|
||||
// When
|
||||
val (yMin, yMax, scale) = getYAxisScale(chartData, steps)
|
||||
|
||||
//Then
|
||||
Assert.assertEquals(yMin.toInt(), 0)
|
||||
Assert.assertEquals(yMax.toInt(), 0)
|
||||
Assert.assertEquals(scale.toInt(), 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package co.yml.charts.ui.linegraph
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import co.yml.charts.ui.linechart.getCubicPoints
|
||||
import co.yml.charts.ui.linechart.getMappingPointsToGraph
|
||||
import co.yml.charts.ui.linechart.getMaxScrollDistance
|
||||
import co.yml.charts.common.model.Point
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class LineChartTest {
|
||||
|
||||
@Test
|
||||
fun `Given few points data n-1 size of cubicPoints1 & cubicPoints2 should be returned`() {
|
||||
// Given
|
||||
val pointsData = listOf(Offset(1f, 30f), Offset(2f, 10f), Offset(3f, 55f))
|
||||
|
||||
// When
|
||||
val pairCubicPoints = getCubicPoints(pointsData)
|
||||
|
||||
//Then
|
||||
Assert.assertEquals(pointsData.size - 1, pairCubicPoints.first.size)
|
||||
Assert.assertEquals(pointsData.size - 1, pairCubicPoints.second.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given two points calculated cubicPoints1 value should be returned`() {
|
||||
// Given
|
||||
val pointsData = listOf(Offset(1f, 30f), Offset(2f, 10f))
|
||||
|
||||
// When
|
||||
val pairCubicPoints = getCubicPoints(pointsData)
|
||||
|
||||
//Then
|
||||
val expectedCubic1Point = Offset(
|
||||
(pointsData[1].x + pointsData.first().x) / 2,
|
||||
pointsData.first().y
|
||||
)
|
||||
|
||||
Assert.assertEquals(expectedCubic1Point, pairCubicPoints.first.first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given two points calculated cubicPoints2 value should be returned`() {
|
||||
// Given
|
||||
val pointsData = listOf(Offset(1f, 30f), Offset(2f, 10f))
|
||||
|
||||
// When
|
||||
val pairCubicPoints = getCubicPoints(pointsData)
|
||||
|
||||
//Then
|
||||
val expectedCubic1Point = Offset(
|
||||
(pointsData[1].x + pointsData.first().x) / 2,
|
||||
pointsData[1].y
|
||||
)
|
||||
|
||||
Assert.assertEquals(expectedCubic1Point, pairCubicPoints.second.first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a container with input values valid scroll offset should be returned`() {
|
||||
// Given
|
||||
val columnWidth = 104f
|
||||
val xMax = 100f
|
||||
val xMin = 0f
|
||||
val xOffset = 200f
|
||||
val paddingRight = 40f
|
||||
val canvasWidth = 500f
|
||||
|
||||
// When
|
||||
val scrollDistance = getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
paddingRight,
|
||||
canvasWidth
|
||||
)
|
||||
|
||||
Assert.assertTrue(scrollDistance > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a total drawing width lesser than container width container should not be able to scrolled`() {
|
||||
// Given
|
||||
val columnWidth = 20f
|
||||
val xMax = 20f
|
||||
val xMin = 0f
|
||||
val xOffset = 20f
|
||||
val paddingRight = 40f
|
||||
val canvasWidth = 500f
|
||||
|
||||
// When
|
||||
val scrollDistance = getMaxScrollDistance(
|
||||
columnWidth,
|
||||
xMax,
|
||||
xMin,
|
||||
xOffset,
|
||||
paddingRight,
|
||||
canvasWidth
|
||||
)
|
||||
|
||||
Assert.assertEquals(scrollDistance, 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given input points size the transformed list size should be same`() {
|
||||
// Given
|
||||
val pointsData = listOf(Point(1f, 30f), Point(2f, 10f), Point(3f, 55f))
|
||||
val xLeft = 20f
|
||||
val scrollOffset = 0f
|
||||
val xMin = 0f
|
||||
val xOffset = 20f
|
||||
val yBottom = 40f
|
||||
val yMin = 0f
|
||||
val yOffset = 20f
|
||||
|
||||
//When
|
||||
val transformedPointsList = getMappingPointsToGraph(
|
||||
pointsData, xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yMin,
|
||||
yOffset
|
||||
)
|
||||
|
||||
// Then
|
||||
Assert.assertEquals(pointsData.size, transformedPointsList.size)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a point x and transformed point x to w_r_t container should be same `() {
|
||||
// Given
|
||||
val pointData = listOf(Point(1f, 30f))
|
||||
val xLeft = 20f
|
||||
val scrollOffset = 0f
|
||||
val xMin = 0f
|
||||
val xOffset = 20f
|
||||
val yBottom = 40f
|
||||
val yMin = 0f
|
||||
val yOffset = 20f
|
||||
|
||||
//When
|
||||
val transformedXPoint = getMappingPointsToGraph(
|
||||
pointData, xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yMin,
|
||||
yOffset
|
||||
).first().x
|
||||
|
||||
//Then
|
||||
val expectedXPoint = ((pointData.first().x - xMin) * xOffset) + xLeft - scrollOffset
|
||||
Assert.assertEquals(expectedXPoint, transformedXPoint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a point y and transformed point y to w_r_t container should be same `() {
|
||||
// Given
|
||||
val pointData = listOf(Point(1f, 30f))
|
||||
val xLeft = 20f
|
||||
val scrollOffset = 0f
|
||||
val xMin = 0f
|
||||
val xOffset = 20f
|
||||
val yBottom = 40f
|
||||
val yMin = 0f
|
||||
val yOffset = 20f
|
||||
|
||||
//When
|
||||
val transformedYPoint = getMappingPointsToGraph(
|
||||
pointData, xMin,
|
||||
xOffset,
|
||||
xLeft,
|
||||
scrollOffset,
|
||||
yBottom,
|
||||
yMin,
|
||||
yOffset
|
||||
).first().y
|
||||
|
||||
//Then
|
||||
val expectedYPoint = yBottom - ((pointData.first().y - yMin) * yOffset)
|
||||
Assert.assertEquals(expectedYPoint, transformedYPoint)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package co.yml.charts.ui.wavechart
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import co.yml.charts.ui.wavechart.model.AxisPosition
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import kotlin.math.abs
|
||||
|
||||
class WaveChartTest {
|
||||
|
||||
@Test
|
||||
fun `Given two points on a line find slop and y intercept`() {
|
||||
//Give
|
||||
val p1 = Offset(-3f, 2f)
|
||||
val p2 = Offset(-15f, -6f)
|
||||
|
||||
//When
|
||||
val (m1, b1) = findSlopeAndYIntercept(p1, p2)
|
||||
|
||||
//Then
|
||||
assertEquals(2f / 3f, m1)
|
||||
assertEquals(4f, b1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given two points on a line where y values are 0 then return 0 as slop and y intercept`() {
|
||||
//Give
|
||||
val p1 = Offset(-3f, 0f)
|
||||
val p2 = Offset(-15f, 0f)
|
||||
|
||||
//When
|
||||
val (m1, b1) = findSlopeAndYIntercept(p1, p2)
|
||||
|
||||
//Then
|
||||
assertEquals(0f, abs(m1))
|
||||
assertEquals(0f, abs(b1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given slops and y intercepts of two lines find the intersection point`() {
|
||||
//Give
|
||||
val m1 = 2f
|
||||
val b1 = 3f
|
||||
val m2 = -3f
|
||||
val b2 = 8f
|
||||
|
||||
//When
|
||||
val intersection = findLineIntersection(m1, b1, m2, b2)
|
||||
|
||||
//Then
|
||||
assertEquals(Offset(1f, 5f), intersection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given slops and y intercepts of two lines return null when slops are equal`() {
|
||||
//Give
|
||||
val m1 = 2f
|
||||
val b1 = 3f
|
||||
val m2 = 2f
|
||||
val b2 = 8f
|
||||
|
||||
//When
|
||||
val intersection = findLineIntersection(m1, b1, m2, b2)
|
||||
|
||||
//Then
|
||||
assertEquals(null, intersection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given t and two points find intermediate point`() {
|
||||
//Given
|
||||
val t = 0.5
|
||||
val p0 = Offset(10f, 10f)
|
||||
val p1 = Offset(20f, 30f)
|
||||
|
||||
//When
|
||||
val interpolate = interpolate(t, p0, p1)
|
||||
|
||||
//Then
|
||||
assertEquals(Offset(15f, 20f), interpolate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given 4 control points on a cubic curve find intermediate point based on t value`() {
|
||||
//Given
|
||||
val t = 0.5
|
||||
val p0 = Offset(10f, 10f)
|
||||
val p1 = Offset(20f, 30f)
|
||||
val p2 = Offset(40f, 30f)
|
||||
val p3 = Offset(50f, 10f)
|
||||
|
||||
//When
|
||||
val curvePoint = findCubicCurvePoints(t, p0, p1, p2, p3)
|
||||
|
||||
//Then
|
||||
assertEquals(Offset(30f, 25f), curvePoint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a list of Offsets find the closest y value for the given point`() {
|
||||
//Given
|
||||
val list = listOf(Offset(10f, 10f), Offset(15f, 15f), Offset(18f, 18f), Offset(20f, 20f))
|
||||
val target = 16f
|
||||
|
||||
//When
|
||||
val nearestValue = findNearestValue(list, target)
|
||||
|
||||
//Then
|
||||
assertEquals(Offset(15f, 15f), nearestValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given two points calculated cubicPoints value should be returned`() {
|
||||
// Given
|
||||
val startPoint = Offset(1f, 30f)
|
||||
val endPoint = Offset(2f, 10f)
|
||||
|
||||
// When
|
||||
val pairCubicPoints = findControlPoints(startPoint, endPoint)
|
||||
|
||||
//Then
|
||||
val expectedCubic1Point = Offset(1.3f, 30f)
|
||||
val expectedCubic2Point = Offset(1.7f, 10f)
|
||||
|
||||
assertEquals(expectedCubic1Point, pairCubicPoints.first)
|
||||
assertEquals(expectedCubic2Point, pairCubicPoints.second)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Given two points calculated cubicPoints1 y value should be returned`() {
|
||||
// Given
|
||||
val startPoint = Offset(1f, 30f)
|
||||
val endPoint = Offset(2f, 10f)
|
||||
|
||||
// When
|
||||
val pairCubicPoints = findControlPoints(startPoint, endPoint)
|
||||
|
||||
//Then
|
||||
assertEquals(30f, pairCubicPoints.first.y)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given y values of two points position should return bottom`() {
|
||||
// Given
|
||||
val startPoint = 650f
|
||||
val endPoint = 700f
|
||||
val xAxisYPosition = 500f
|
||||
|
||||
// When
|
||||
val position = findPointPosition(startPoint, endPoint, xAxisYPosition)
|
||||
|
||||
//Then
|
||||
assertEquals(AxisPosition.BOTTOM, position)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given y values of two points of curve position should return top`() {
|
||||
// Given
|
||||
val startPoint = 450f
|
||||
val endPoint = 400f
|
||||
val xAxisYPosition = 500f
|
||||
|
||||
// When
|
||||
val position = findPointPosition(startPoint, endPoint, xAxisYPosition)
|
||||
|
||||
//Then
|
||||
assertEquals(AxisPosition.TOP, position)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given y values of two points of curve position should return intersect`() {
|
||||
// Given
|
||||
val startPoint = 450f
|
||||
val endPoint = 550f
|
||||
val xAxisYPosition = 500f
|
||||
|
||||
// When
|
||||
val position = findPointPosition(startPoint, endPoint, xAxisYPosition)
|
||||
|
||||
//Then
|
||||
assertEquals(AxisPosition.INTERSECT, position)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given y values of two points of curve position should return top when start point is equal to xAxis y value`() {
|
||||
// Given
|
||||
val startPoint = 500f
|
||||
val endPoint = 550f
|
||||
val xAxisYPosition = 500f
|
||||
|
||||
// When
|
||||
val position = findPointPosition(startPoint, endPoint, xAxisYPosition)
|
||||
|
||||
//Then
|
||||
assertEquals(AxisPosition.BOTTOM, position)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given y values of two points of curve position should return bottom when end point is equal to xAxis y value`() {
|
||||
// Given
|
||||
val startPoint = 450f
|
||||
val endPoint = 500f
|
||||
val xAxisYPosition = 500f
|
||||
|
||||
// When
|
||||
val position = findPointPosition(startPoint, endPoint, xAxisYPosition)
|
||||
|
||||
//Then
|
||||
assertEquals(AxisPosition.TOP, position)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given curve points return the point intersect on xAxis y position`() {
|
||||
// Given
|
||||
val curvePoints = listOf(
|
||||
Offset(100f, 150f),
|
||||
Offset(200f, 250f),
|
||||
Offset(300f, 350f),
|
||||
Offset(400f, 450f),
|
||||
Offset(500f, 650f),
|
||||
Offset(600f, 750f)
|
||||
)
|
||||
val xAxisYPosition = 500f
|
||||
|
||||
// When
|
||||
val intersectionPoint = getCurveIntersectionPoint(curvePoints, xAxisYPosition)
|
||||
|
||||
//Then
|
||||
assertEquals(Offset(400f, 450f), intersectionPoint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given control points of a cubic curve find the points on the curve`() {
|
||||
// Given
|
||||
val p0 = Offset(0f, 0f)
|
||||
val p1 = Offset(1f, 2f)
|
||||
val p2 = Offset(3f, 4f)
|
||||
val p3 = Offset(4f, 3f)
|
||||
|
||||
// When
|
||||
val controlPoints = getCurvePoints(p0, p1, p2, p3)
|
||||
|
||||
//Then
|
||||
assertEquals(Offset(4f, 3f), controlPoints.last())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package co.yml.charts.utils
|
||||
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.DEFAULT_PADDING
|
||||
import co.yml.charts.ui.piechart.PieChartConstants.DEFAULT_START_ANGLE
|
||||
import co.yml.charts.ui.piechart.models.PieChartData
|
||||
import co.yml.charts.ui.piechart.utils.convertTouchEventPointToAngle
|
||||
import co.yml.charts.ui.piechart.utils.getSliceCenterPoints
|
||||
import co.yml.charts.ui.piechart.utils.proportion
|
||||
import co.yml.charts.ui.piechart.utils.sum
|
||||
import co.yml.charts.ui.piechart.utils.sweepAngles
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class PieChartUtilsTest {
|
||||
|
||||
private val sliceList = listOf(
|
||||
PieChartData.Slice("A", 10f, Color.Cyan),
|
||||
PieChartData.Slice("B", 20f, Color.Red),
|
||||
PieChartData.Slice("C", 40f, Color.Black),
|
||||
PieChartData.Slice("D", 30f, Color.Blue)
|
||||
)
|
||||
|
||||
|
||||
@Test
|
||||
fun `Slice total calculation is correct`() {
|
||||
val sum = sliceList.sum()
|
||||
assertEquals(100f, sum)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Proportion calculation is correct`() {
|
||||
val sum = sliceList.sum()
|
||||
val proportions = sliceList.proportion(sum)
|
||||
assertEquals(10f, proportions.first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Sweep angle calculation is correct`() {
|
||||
val sum = sliceList.sum()
|
||||
val proportions = sliceList.proportion(sum)
|
||||
val sweepAngles = proportions.sweepAngles()
|
||||
assertEquals(36f, sweepAngles.first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Arch center calculation is correct and x and y are positive`() {
|
||||
|
||||
val sideSize = 200
|
||||
val padding = (sideSize * DEFAULT_PADDING) / 100f
|
||||
val size = Size(sideSize.toFloat() - padding, sideSize.toFloat() - padding)
|
||||
|
||||
val (arcCenter, x, y) = getSliceCenterPoints(
|
||||
DEFAULT_START_ANGLE,
|
||||
30f,
|
||||
size,
|
||||
padding
|
||||
)
|
||||
assertEquals(285f, arcCenter)
|
||||
assertTrue(x > 0)
|
||||
assertTrue(y > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Calculated angle is in the correct range`() {
|
||||
val angle = convertTouchEventPointToAngle(200f, 200f, 10f, 10f)
|
||||
assertTrue(angle in 0.0..360.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,31 @@
|
||||
plugins {
|
||||
id("ycharts.android.application")
|
||||
id("ycharts.android.application.compose")
|
||||
id("ycharts.android.test")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "co.yml.ycharts.app"
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
defaultConfig {
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
applicationId = "co.yml.ycharts.app"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(mapOf("path" to ":YChartsLib")))
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package co.yml.ycharts.app
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("co.yml.ycharts.app", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.YCharts"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name="co.yml.ycharts.app.presentation.BarChartActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_bar_chart"
|
||||
android:theme="@style/Theme.YCharts" />
|
||||
<activity
|
||||
android:name="co.yml.ycharts.app.presentation.LineChartActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_line_chart"
|
||||
android:theme="@style/Theme.YCharts" />
|
||||
<activity
|
||||
android:name="co.yml.ycharts.app.presentation.WaveChartActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_wave_chart"
|
||||
android:theme="@style/Theme.YCharts" />
|
||||
<activity
|
||||
android:name="co.yml.ycharts.app.presentation.PieChartActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_pie_chart"
|
||||
android:theme="@style/Theme.YCharts" />
|
||||
<activity
|
||||
android:name="co.yml.ycharts.app.presentation.DonutChartActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_donut_chart"
|
||||
android:theme="@style/Theme.YCharts" />
|
||||
<activity
|
||||
android:name="co.yml.ycharts.app.presentation.CombinedLineAndBarChartActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_combined_line_bar_chart"
|
||||
android:theme="@style/Theme.YCharts" />
|
||||
<activity
|
||||
android:name="co.yml.ycharts.app.presentation.BubbleChartActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_bubble_chart"
|
||||
android:theme="@style/Theme.YCharts" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.YCharts">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,150 @@
|
||||
package co.yml.ycharts.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.ycharts.app.presentation.*
|
||||
import co.yml.ycharts.app.ui.theme.YChartsTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
YChartsTheme {
|
||||
ChartsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ChartsMenu() {
|
||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = YChartsTheme.colors.background,
|
||||
topBar = { AppBar() }) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
ChartButton(title = getString(R.string.title_bar_chart), onClick = {
|
||||
startActivity(
|
||||
Intent(
|
||||
this@MainActivity, BarChartActivity::class.java
|
||||
)
|
||||
)
|
||||
addActivityInOutAnim()
|
||||
})
|
||||
ChartButton(title = getString(R.string.title_line_chart), onClick = {
|
||||
startActivity(
|
||||
Intent(
|
||||
this@MainActivity, LineChartActivity::class.java
|
||||
)
|
||||
)
|
||||
addActivityInOutAnim()
|
||||
})
|
||||
|
||||
ChartButton(title = getString(R.string.title_wave_chart), onClick = {
|
||||
startActivity(
|
||||
Intent(
|
||||
this@MainActivity, WaveChartActivity::class.java
|
||||
)
|
||||
)
|
||||
addActivityInOutAnim()
|
||||
})
|
||||
|
||||
ChartButton(title = getString(R.string.title_pie_chart), onClick = {
|
||||
startActivity(
|
||||
Intent(
|
||||
this@MainActivity, PieChartActivity::class.java
|
||||
)
|
||||
)
|
||||
addActivityInOutAnim()
|
||||
})
|
||||
ChartButton(title = getString(R.string.title_donut_chart), onClick = {
|
||||
startActivity(
|
||||
Intent(
|
||||
this@MainActivity, DonutChartActivity::class.java
|
||||
)
|
||||
)
|
||||
addActivityInOutAnim()
|
||||
})
|
||||
ChartButton(title = getString(R.string.title_bar_with_line_chart), onClick = {
|
||||
startActivity(
|
||||
Intent(
|
||||
this@MainActivity, CombinedLineAndBarChartActivity::class.java
|
||||
)
|
||||
)
|
||||
addActivityInOutAnim()
|
||||
})
|
||||
ChartButton(title = getString(R.string.bubble_chart), onClick = {
|
||||
startActivity(
|
||||
Intent(
|
||||
this@MainActivity, BubbleChartActivity::class.java
|
||||
)
|
||||
)
|
||||
addActivityInOutAnim()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun addActivityInOutAnim() {
|
||||
overridePendingTransition(
|
||||
R.anim.move_right_in_activity, R.anim.move_left_out_activity
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppBar() {
|
||||
TopAppBar(modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = YChartsTheme.colors.button,
|
||||
elevation = 6.dp,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.app_name),
|
||||
color = YChartsTheme.colors.text,
|
||||
textAlign = TextAlign.Center,
|
||||
style = YChartsTheme.typography.header
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChartButton(title: String, onClick: () -> Unit) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp, start = 10.dp)
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
onClick = onClick,
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = YChartsTheme.colors.button)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = YChartsTheme.typography.button,
|
||||
color = YChartsTheme.colors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DefaultPreview() {
|
||||
ChartButton(title = "Chart", onClick = {})
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
package co.yml.ycharts.app.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.axis.DataCategoryOptions
|
||||
import co.yml.charts.common.components.Legends
|
||||
import co.yml.charts.common.extensions.getMaxElementInYAxis
|
||||
import co.yml.charts.common.model.LegendsConfig
|
||||
import co.yml.charts.common.utils.DataUtils
|
||||
import co.yml.charts.ui.barchart.BarChart
|
||||
import co.yml.charts.ui.barchart.GroupBarChart
|
||||
import co.yml.charts.ui.barchart.StackedBarChart
|
||||
import co.yml.charts.ui.barchart.models.*
|
||||
import co.yml.ycharts.app.R
|
||||
import kotlin.random.Random
|
||||
import co.yml.ycharts.app.ui.compositions.AppBarWithBackButton
|
||||
import co.yml.ycharts.app.ui.theme.YChartsTheme
|
||||
|
||||
/**
|
||||
* Bar chart activity
|
||||
*
|
||||
* @constructor Create empty Bar chart activity
|
||||
*/
|
||||
class BarChartActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
YChartsTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = YChartsTheme.colors.background,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
stringResource(id = R.string.title_bar_chart),
|
||||
onBackPressed = {
|
||||
onBackPressed()
|
||||
})
|
||||
})
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
LazyColumn(content = {
|
||||
items(6) { item ->
|
||||
when (item) {
|
||||
0 ->{ Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.barchart_solid_colors),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
BarchartWithSolidBars()
|
||||
}
|
||||
1 -> { Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.barchart_gradient_colors),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
BarchartWithGradientBars()}
|
||||
2 ->{
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.barchart_background_color),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
BarchartWithBackgroundColor()
|
||||
}
|
||||
3 ->{
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.horizontal_bar_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
HorizontalBarChart()
|
||||
}
|
||||
|
||||
4 ->{
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.grouped_bar_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
VerticalGroupBarChart()
|
||||
}
|
||||
5 ->{
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.stacked_barchart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
VerticalStackedBarChart()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Barchart with solid bars
|
||||
*
|
||||
*/
|
||||
@Composable
|
||||
private fun BarchartWithSolidBars() {
|
||||
val maxRange = 50
|
||||
val barData = DataUtils.getBarChartData(50, maxRange, BarChartType.VERTICAL, DataCategoryOptions())
|
||||
val yStepSize = 10
|
||||
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(barData.size - 1)
|
||||
.bottomPadding(40.dp)
|
||||
.axisLabelAngle(20f)
|
||||
.startDrawPadding(48.dp)
|
||||
.labelData { index -> barData[index].label }
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
val barChartData = BarChartData(
|
||||
chartData = barData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
barStyle = BarStyle(
|
||||
paddingBetweenBars = 20.dp,
|
||||
barWidth = 25.dp
|
||||
),
|
||||
showYAxis = true,
|
||||
showXAxis = true,
|
||||
horizontalExtraSpace = 10.dp,
|
||||
)
|
||||
BarChart(modifier = Modifier.height(350.dp), barChartData = barChartData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Barchart with gradient bars
|
||||
*
|
||||
*/
|
||||
@Composable
|
||||
private fun BarchartWithGradientBars() {
|
||||
val maxRange = 100
|
||||
val barData = DataUtils.getGradientBarChartData(50, 100)
|
||||
val yStepSize = 10
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(barData.size - 1)
|
||||
.bottomPadding(40.dp)
|
||||
.axisLabelAngle(20f)
|
||||
.startDrawPadding(48.dp)
|
||||
.labelData { index -> barData[index].label }
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
val barChartData = BarChartData(
|
||||
chartData = barData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
barStyle = BarStyle(paddingBetweenBars = 20.dp,
|
||||
barWidth = 35.dp,
|
||||
isGradientEnabled = true,
|
||||
selectionHighlightData = SelectionHighlightData(
|
||||
highlightBarColor = Color.Red,
|
||||
highlightTextBackgroundColor = Color.Green,
|
||||
popUpLabel = { _, y -> " Value : $y " }
|
||||
)),
|
||||
showYAxis = true,
|
||||
showXAxis = true,
|
||||
horizontalExtraSpace = 20.dp
|
||||
)
|
||||
BarChart(modifier = Modifier.height(350.dp), barChartData = barChartData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Barchart with background color
|
||||
*
|
||||
*/
|
||||
@Composable
|
||||
private fun BarchartWithBackgroundColor() {
|
||||
val maxRange = 100
|
||||
val backgroundColor = Color.LightGray
|
||||
val barData = DataUtils.getBarChartData(50, 100, BarChartType.VERTICAL, DataCategoryOptions())
|
||||
val yStepSize = 10
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(barData.size - 1)
|
||||
.bottomPadding(40.dp)
|
||||
.startDrawPadding(48.dp)
|
||||
.axisLabelAngle(20f)
|
||||
.labelData { index -> barData[index].label }
|
||||
.backgroundColor(backgroundColor)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.backgroundColor(backgroundColor)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
val barChartData = BarChartData(
|
||||
chartData = barData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
barStyle = BarStyle(paddingBetweenBars = 20.dp,
|
||||
barWidth = 35.dp,
|
||||
selectionHighlightData = SelectionHighlightData(
|
||||
highlightBarColor = Color.Red,
|
||||
highlightTextBackgroundColor = Color.Green,
|
||||
popUpLabel = { _, y -> " Value : $y " }
|
||||
)),
|
||||
showYAxis = true,
|
||||
showXAxis = true,
|
||||
horizontalExtraSpace = 20.dp,
|
||||
backgroundColor = backgroundColor
|
||||
)
|
||||
BarChart(modifier = Modifier.height(350.dp), barChartData = barChartData)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerticalBarChart() {
|
||||
val maxRange = 50
|
||||
val barData =
|
||||
DataUtils.getBarChartData(50, maxRange, BarChartType.VERTICAL, DataCategoryOptions())
|
||||
val yStepSize = 10
|
||||
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(barData.size - 1)
|
||||
.bottomPadding(12.dp)
|
||||
.axisLabelAngle(20f)
|
||||
.startDrawPadding(48.dp)
|
||||
.shouldDrawAxisLineTillEnd(false)
|
||||
.labelData { index -> barData[index].label }
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
val barChartData = BarChartData(
|
||||
chartData = barData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
barStyle = BarStyle(
|
||||
paddingBetweenBars = 20.dp,
|
||||
barWidth = 25.dp
|
||||
),
|
||||
showYAxis = true,
|
||||
showXAxis = true,
|
||||
horizontalExtraSpace = 10.dp,
|
||||
drawBar = { drawScope, barData, drawOffset, height, barChartType, barStyle ->
|
||||
with(drawScope) {
|
||||
with(barStyle) {
|
||||
drawRect(
|
||||
color = barData.color,
|
||||
topLeft = drawOffset,
|
||||
size = if (barChartType == BarChartType.VERTICAL) Size(
|
||||
barWidth.toPx(),
|
||||
height
|
||||
) else Size(height, barWidth.toPx()),
|
||||
style = barDrawStyle,
|
||||
blendMode = barBlendMode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
BarChart(modifier = Modifier.height(350.dp), barChartData = barChartData)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HorizontalBarChart() {
|
||||
val maxRange = 30
|
||||
val barData =
|
||||
DataUtils.getBarChartData(
|
||||
10,
|
||||
maxRange,
|
||||
BarChartType.HORIZONTAL,
|
||||
DataCategoryOptions(isDataCategoryInYAxis = true)
|
||||
)
|
||||
val xStepSize = 10
|
||||
|
||||
val xAxisData = AxisData.Builder()
|
||||
.steps(xStepSize)
|
||||
.bottomPadding(12.dp)
|
||||
.endPadding(40.dp)
|
||||
.labelData { index -> (index * (maxRange / xStepSize)).toString() }
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(barData.size - 1)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.setDataCategoryOptions(
|
||||
DataCategoryOptions(
|
||||
isDataCategoryInYAxis = true,
|
||||
isDataCategoryStartFromBottom = false
|
||||
)
|
||||
)
|
||||
.startDrawPadding(48.dp)
|
||||
.labelData { index -> barData[index].label }
|
||||
.build()
|
||||
val barChartData = BarChartData(
|
||||
chartData = barData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
barStyle = BarStyle(
|
||||
isGradientEnabled = false,
|
||||
paddingBetweenBars = 20.dp,
|
||||
barWidth = 35.dp,
|
||||
selectionHighlightData = SelectionHighlightData(
|
||||
highlightBarColor = Color.Red,
|
||||
highlightTextBackgroundColor = Color.Green,
|
||||
popUpLabel = { x, _ -> " Value : $x " },
|
||||
barChartType = BarChartType.HORIZONTAL
|
||||
),
|
||||
),
|
||||
showYAxis = true,
|
||||
showXAxis = true,
|
||||
horizontalExtraSpace = 20.dp,
|
||||
barChartType = BarChartType.HORIZONTAL
|
||||
)
|
||||
BarChart(
|
||||
modifier = Modifier.height(350.dp),
|
||||
barChartData = barChartData
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerticalGroupBarChart() {
|
||||
val maxRange = 100
|
||||
val barSize = 3
|
||||
val groupBarData = DataUtils.getGroupBarChartData(50, maxRange, barSize)
|
||||
val yStepSize = 10
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.bottomPadding(5.dp)
|
||||
.startDrawPadding(48.dp)
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
val colorPaletteList = DataUtils.getColorPaletteList(barSize)
|
||||
val legendsConfig = LegendsConfig(
|
||||
legendLabelList = DataUtils.getLegendsLabelData(colorPaletteList),
|
||||
gridColumnCount = 3
|
||||
)
|
||||
val groupBarPlotData = BarPlotData(
|
||||
groupBarList = groupBarData,
|
||||
barStyle = BarStyle(barWidth = 35.dp),
|
||||
barColorPaletteList = colorPaletteList
|
||||
)
|
||||
val groupBarChartData = GroupBarChartData(
|
||||
barPlotData = groupBarPlotData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
groupSeparatorConfig = GroupSeparatorConfig(0.dp)
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.height(450.dp)
|
||||
) {
|
||||
GroupBarChart(
|
||||
modifier = Modifier
|
||||
.height(400.dp),
|
||||
groupBarChartData = groupBarChartData
|
||||
)
|
||||
Legends(
|
||||
legendsConfig = legendsConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerticalStackedBarChart() {
|
||||
val barSize = 3
|
||||
val listSize = 10
|
||||
val groupBarData = DataUtils.getGroupBarChartData(listSize, 100, barSize)
|
||||
val yStepSize = 10
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(listSize - 1)
|
||||
.startDrawPadding(48.dp)
|
||||
.labelData { index -> "C $index" }
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index ->
|
||||
val valueList = mutableListOf<Float>()
|
||||
groupBarData.map { groupBar ->
|
||||
var yMax = 0f
|
||||
groupBar.barList.forEach {
|
||||
yMax += it.point.y
|
||||
}
|
||||
valueList.add(yMax)
|
||||
}
|
||||
val maxElementInYAxis = getMaxElementInYAxis(valueList.maxOrNull() ?: 0f, yStepSize)
|
||||
|
||||
(index * (maxElementInYAxis / yStepSize)).toString()
|
||||
}
|
||||
.topPadding(36.dp)
|
||||
.build()
|
||||
val colorPaletteList = DataUtils.getColorPaletteList(barSize)
|
||||
val legendsConfig = LegendsConfig(
|
||||
legendLabelList = DataUtils.getLegendsLabelData(colorPaletteList),
|
||||
gridColumnCount = 3
|
||||
)
|
||||
val groupBarPlotData = BarPlotData(
|
||||
groupBarList = groupBarData,
|
||||
barStyle = BarStyle(
|
||||
barWidth = 35.dp,
|
||||
selectionHighlightData = SelectionHighlightData(
|
||||
isHighlightFullBar = true,
|
||||
groupBarPopUpLabel = { name, value ->
|
||||
"Name : C$name Value : ${String.format("%.2f", value)}"
|
||||
}
|
||||
)
|
||||
),
|
||||
barColorPaletteList = colorPaletteList
|
||||
)
|
||||
val groupBarChartData = GroupBarChartData(
|
||||
barPlotData = groupBarPlotData,
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
paddingBetweenStackedBars = 4.dp,
|
||||
drawBar = { drawScope, barChartData, barStyle, drawOffset, height, barIndex ->
|
||||
with(drawScope) {
|
||||
drawRect(
|
||||
color = colorPaletteList[barIndex],
|
||||
topLeft = drawOffset,
|
||||
size = Size(barStyle.barWidth.toPx(), height),
|
||||
style = barStyle.barDrawStyle,
|
||||
blendMode = barStyle.barBlendMode
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.height(500.dp)
|
||||
) {
|
||||
StackedBarChart(
|
||||
modifier = Modifier
|
||||
.height(400.dp),
|
||||
groupBarChartData = groupBarChartData
|
||||
)
|
||||
Legends(
|
||||
legendsConfig = legendsConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package co.yml.ycharts.app.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.extensions.formatToSinglePrecision
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.common.utils.DataUtils
|
||||
import co.yml.charts.ui.bubblechart.BubbleChart
|
||||
import co.yml.charts.ui.bubblechart.model.BubbleChartData
|
||||
import co.yml.charts.ui.linechart.model.GridLines
|
||||
import co.yml.ycharts.app.R
|
||||
import co.yml.ycharts.app.ui.compositions.AppBarWithBackButton
|
||||
import co.yml.ycharts.app.ui.theme.YChartsTheme
|
||||
|
||||
/**
|
||||
* Line chart activity
|
||||
*
|
||||
* @constructor Create empty Line chart activity
|
||||
*/
|
||||
class BubbleChartActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
YChartsTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = YChartsTheme.colors.background,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
stringResource(id = R.string.bubble_chart),
|
||||
onBackPressed = {
|
||||
onBackPressed()
|
||||
})
|
||||
})
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
LazyColumn(content = {
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
text = getString(R.string.gradient_bubble_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
BubbleChartWithGrid(
|
||||
pointsData = DataUtils.getRandomPoints(
|
||||
200,
|
||||
start = 30,
|
||||
maxRange = 100
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
text = getString(R.string.solid_bubble_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
SolidBubbleChart(
|
||||
pointsData = DataUtils.getRandomPoints(
|
||||
200,
|
||||
start = 30,
|
||||
maxRange = 900
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bubble chart with grid lines
|
||||
*
|
||||
* @param pointsData
|
||||
*/
|
||||
@Composable
|
||||
private fun BubbleChartWithGrid(pointsData: List<Point>) {
|
||||
val steps = 5
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> pointsData[i].x.toInt().toString() }
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.build()
|
||||
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.labelData { i ->
|
||||
// Add yMin to get the negative axis values to the scale
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}.build()
|
||||
|
||||
val data = BubbleChartData(
|
||||
DataUtils.getBubbleChartDataWithGradientStyle(pointsData),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines()
|
||||
)
|
||||
|
||||
BubbleChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(500.dp),
|
||||
bubbleChartData = data
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bubble chart with grid lines
|
||||
*
|
||||
* @param pointsData
|
||||
*/
|
||||
@Composable
|
||||
private fun SolidBubbleChart(pointsData: List<Point>) {
|
||||
val steps = 5
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i ->pointsData[i].x.toInt().toString()}
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.build()
|
||||
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.labelData { i ->
|
||||
// Add yMin to get the negative axis values to the scale
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}.build()
|
||||
|
||||
val data = BubbleChartData(
|
||||
DataUtils.getBubbleChartDataWithSolidStyle(pointsData),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines()
|
||||
)
|
||||
|
||||
BubbleChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(500.dp),
|
||||
bubbleChartData = data
|
||||
)
|
||||
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
@file:OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
package co.yml.ycharts.app.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.ycharts.app.ui.compositions.AppBarWithBackButton
|
||||
import co.yml.ycharts.app.ui.theme.YChartsTheme
|
||||
import co.yml.charts.ui.barchart.models.BarPlotData
|
||||
import co.yml.charts.ui.barchart.models.BarStyle
|
||||
import co.yml.charts.ui.combinedchart.CombinedChart
|
||||
import co.yml.charts.ui.combinedchart.model.CombinedChartData
|
||||
import co.yml.charts.common.components.Legends
|
||||
import co.yml.charts.common.model.LegendsConfig
|
||||
import co.yml.charts.common.utils.DataUtils
|
||||
import co.yml.charts.ui.linechart.model.IntersectionPoint
|
||||
import co.yml.charts.ui.linechart.model.Line
|
||||
import co.yml.charts.ui.linechart.model.LinePlotData
|
||||
import co.yml.charts.ui.linechart.model.LineStyle
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPoint
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPopUp
|
||||
import co.yml.ycharts.app.R
|
||||
|
||||
/**
|
||||
* Combined line and bar chart activity
|
||||
*
|
||||
* @constructor Create empty Combined line and bar chart activity
|
||||
*/
|
||||
class CombinedLineAndBarChartActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
YChartsTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = YChartsTheme.colors.background,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
stringResource(id = R.string.title_bar_with_line_chart),
|
||||
onBackPressed = {
|
||||
onBackPressed()
|
||||
})
|
||||
})
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
LazyColumn(content = {
|
||||
items(2) { item ->
|
||||
when (item) {
|
||||
0 ->{
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.combined_bar_line_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
BarWithLineChart()
|
||||
}
|
||||
1->{
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.combined_chart_with_background),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
BarWithLineChartAndBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bar with line chart
|
||||
*
|
||||
*/
|
||||
@Composable
|
||||
fun BarWithLineChart() {
|
||||
val maxRange = 100
|
||||
val groupBarData = DataUtils.getGroupBarChartData(50, 100, 3)
|
||||
val yStepSize = 10
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.bottomPadding(5.dp)
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
val linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
DataUtils.getLineChartData(50, maxRange = 100),
|
||||
lineStyle = LineStyle(color = Color.Blue),
|
||||
intersectionPoint = IntersectionPoint(),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp()
|
||||
),
|
||||
Line(
|
||||
DataUtils.getLineChartData(50, maxRange = 100),
|
||||
lineStyle = LineStyle(color = Color.Black),
|
||||
intersectionPoint = IntersectionPoint(),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp()
|
||||
)
|
||||
)
|
||||
)
|
||||
val colorPaletteList = DataUtils.getColorPaletteList(3)
|
||||
val legendsConfig = LegendsConfig(
|
||||
legendLabelList = DataUtils.getLegendsLabelData(colorPaletteList),
|
||||
gridColumnCount = 3
|
||||
)
|
||||
val barPlotData = BarPlotData(
|
||||
groupBarList = groupBarData,
|
||||
barStyle = BarStyle(barWidth = 35.dp),
|
||||
barColorPaletteList = colorPaletteList
|
||||
)
|
||||
val combinedChartData = CombinedChartData(
|
||||
combinedPlotDataList = listOf(barPlotData, linePlotData),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.height(500.dp)
|
||||
) {
|
||||
CombinedChart(
|
||||
modifier = Modifier
|
||||
.height(400.dp),
|
||||
combinedChartData = combinedChartData
|
||||
)
|
||||
Legends(
|
||||
legendsConfig = legendsConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bar with line chart and background
|
||||
*
|
||||
*/
|
||||
@Composable
|
||||
fun BarWithLineChartAndBackground() {
|
||||
val maxRange = 100
|
||||
val groupBarData = DataUtils.getGroupBarChartData(50, 100, 3)
|
||||
val yStepSize = 10
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.bottomPadding(5.dp)
|
||||
.backgroundColor(Color.Yellow)
|
||||
.labelData { index -> index.toString() }
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(yStepSize)
|
||||
.backgroundColor(Color.Yellow)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.axisOffset(20.dp)
|
||||
.labelData { index -> (index * (maxRange / yStepSize)).toString() }
|
||||
.build()
|
||||
val linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
DataUtils.getLineChartData(50, maxRange = 100),
|
||||
lineStyle = LineStyle(color = Color.Blue),
|
||||
intersectionPoint = IntersectionPoint(),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp()
|
||||
),
|
||||
Line(
|
||||
DataUtils.getLineChartData(50, maxRange = 100),
|
||||
lineStyle = LineStyle(color = Color.Black),
|
||||
intersectionPoint = IntersectionPoint(),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp()
|
||||
)
|
||||
)
|
||||
)
|
||||
val colorPaletteList = DataUtils.getColorPaletteList(3)
|
||||
val legendsConfig = LegendsConfig(
|
||||
legendLabelList = DataUtils.getLegendsLabelData(colorPaletteList),
|
||||
gridColumnCount = 3
|
||||
)
|
||||
val barPlotData = BarPlotData(
|
||||
groupBarList = groupBarData,
|
||||
barStyle = BarStyle(barWidth = 35.dp),
|
||||
barColorPaletteList = colorPaletteList
|
||||
)
|
||||
val combinedChartData = CombinedChartData(
|
||||
combinedPlotDataList = listOf(barPlotData, linePlotData),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
backgroundColor = Color.Yellow
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.height(500.dp)
|
||||
) {
|
||||
CombinedChart(
|
||||
modifier = Modifier
|
||||
.height(400.dp),
|
||||
combinedChartData = combinedChartData
|
||||
)
|
||||
Legends(
|
||||
legendsConfig = legendsConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package co.yml.ycharts.app.presentation
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.yml.charts.common.components.Legends
|
||||
import co.yml.charts.common.utils.DataUtils
|
||||
import co.yml.charts.ui.piechart.charts.DonutPieChart
|
||||
import co.yml.charts.ui.piechart.models.PieChartConfig
|
||||
import co.yml.charts.ui.piechart.utils.proportion
|
||||
import co.yml.ycharts.app.R
|
||||
import co.yml.ycharts.app.ui.compositions.AppBarWithBackButton
|
||||
import co.yml.ycharts.app.ui.theme.YChartsTheme
|
||||
|
||||
/**
|
||||
* Donut chart activity
|
||||
*
|
||||
* @constructor Create empty Donut chart activity
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
class DonutChartActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
YChartsTheme {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = YChartsTheme.colors.background,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
stringResource(id = R.string.title_donut_chart),
|
||||
onBackPressed = {
|
||||
onBackPressed()
|
||||
})
|
||||
})
|
||||
{
|
||||
val context = LocalContext.current
|
||||
LazyColumn(content = {
|
||||
items(2) { item ->
|
||||
when (item) {
|
||||
0 -> {
|
||||
Text(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
text = getString(R.string.simple_donut_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
SimpleDonutChart(context)
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
Text(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
text = getString(R.string.multiple_donuts),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
MultipleSmallDonutCharts(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple donut chart
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
private fun SimpleDonutChart(context: Context) {
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val data = DataUtils.getDonutChartData()
|
||||
// Sum of all the values
|
||||
val sumOfValues = data.totalLength
|
||||
|
||||
// Calculate each proportion value
|
||||
val proportions = data.slices.proportion(sumOfValues)
|
||||
val pieChartConfig =
|
||||
PieChartConfig(
|
||||
labelVisible = true,
|
||||
strokeWidth = 120f,
|
||||
labelColor = Color.Black,
|
||||
activeSliceAlpha = .9f,
|
||||
isEllipsizeEnabled = true,
|
||||
labelTypeface = Typeface.defaultFromStyle(Typeface.BOLD),
|
||||
isAnimationEnable = true,
|
||||
chartPadding = 25,
|
||||
labelFontSize = 42.sp,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(500.dp)
|
||||
) {
|
||||
Legends(legendsConfig = DataUtils.getLegendsConfigFromPieChartData(pieChartData = data, 3))
|
||||
DonutPieChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp),
|
||||
data,
|
||||
pieChartConfig
|
||||
) { slice ->
|
||||
Toast.makeText(context, slice.label, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple small donut charts
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
private fun MultipleSmallDonutCharts(context: Context) {
|
||||
val accessibilitySheetState =
|
||||
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val data = DataUtils.getDonutChartData()
|
||||
// Sum of all the values
|
||||
val sumOfValues = data.totalLength
|
||||
|
||||
// Calculate each proportion value
|
||||
val proportions = data.slices.proportion(sumOfValues)
|
||||
val firstPieChartConfig =
|
||||
PieChartConfig(
|
||||
labelVisible = true,
|
||||
strokeWidth = 50f,
|
||||
labelColor = Color.Black,
|
||||
backgroundColor = Color.Yellow,
|
||||
activeSliceAlpha = .9f,
|
||||
isEllipsizeEnabled = true,
|
||||
labelTypeface = Typeface.defaultFromStyle(Typeface.BOLD),
|
||||
isAnimationEnable = true,
|
||||
chartPadding = 25,
|
||||
labelFontSize = 16.sp
|
||||
)
|
||||
val secondPieChartConfig =
|
||||
PieChartConfig(
|
||||
labelVisible = true,
|
||||
strokeWidth = 50f,
|
||||
labelColor = Color.Black,
|
||||
activeSliceAlpha = .9f,
|
||||
isEllipsizeEnabled = true,
|
||||
backgroundColor = Color.Black,
|
||||
labelTypeface = Typeface.defaultFromStyle(Typeface.BOLD),
|
||||
isAnimationEnable = true,
|
||||
chartPadding = 25,
|
||||
labelFontSize = 16.sp,
|
||||
isSumVisible = true,
|
||||
sumUnit = "unit",
|
||||
labelColorType = PieChartConfig.LabelColorType.SLICE_COLOR,
|
||||
labelType = PieChartConfig.LabelType.VALUE
|
||||
)
|
||||
val thirdPieChartConfig =
|
||||
PieChartConfig(
|
||||
labelVisible = true,
|
||||
strokeWidth = 50f,
|
||||
labelColor = Color.Black,
|
||||
activeSliceAlpha = .9f,
|
||||
backgroundColor = Color.LightGray,
|
||||
isEllipsizeEnabled = true,
|
||||
labelTypeface = Typeface.defaultFromStyle(Typeface.BOLD),
|
||||
isAnimationEnable = true,
|
||||
chartPadding = 25,
|
||||
labelFontSize = 16.sp
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)
|
||||
) {
|
||||
Legends(legendsConfig = DataUtils.getLegendsConfigFromPieChartData(pieChartData = data, 3))
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
item {
|
||||
DonutPieChart(
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(100.dp),
|
||||
data,
|
||||
firstPieChartConfig
|
||||
) { slice ->
|
||||
Toast.makeText(context, slice.label, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Spacer(modifier = Modifier.width(30.dp))
|
||||
}
|
||||
item {
|
||||
DonutPieChart(
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(100.dp),
|
||||
data,
|
||||
secondPieChartConfig
|
||||
) { slice ->
|
||||
Toast.makeText(context, slice.label, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Spacer(modifier = Modifier.width(30.dp))
|
||||
}
|
||||
item {
|
||||
DonutPieChart(
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(100.dp),
|
||||
data,
|
||||
thirdPieChartConfig
|
||||
) { slice ->
|
||||
Toast.makeText(context, slice.label, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Spacer(modifier = Modifier.width(30.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,642 @@
|
||||
package co.yml.ycharts.app.presentation
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.components.Legends
|
||||
import co.yml.charts.common.extensions.formatToSinglePrecision
|
||||
import co.yml.charts.common.model.LegendsConfig
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.common.utils.DataUtils
|
||||
import co.yml.charts.ui.linechart.LineChart
|
||||
import co.yml.charts.ui.linechart.model.GridLines
|
||||
import co.yml.charts.ui.linechart.model.IntersectionPoint
|
||||
import co.yml.charts.ui.linechart.model.Line
|
||||
import co.yml.charts.ui.linechart.model.LineChartData
|
||||
import co.yml.charts.ui.linechart.model.LinePlotData
|
||||
import co.yml.charts.ui.linechart.model.LineStyle
|
||||
import co.yml.charts.ui.linechart.model.LineType
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPoint
|
||||
import co.yml.charts.ui.linechart.model.SelectionHighlightPopUp
|
||||
import co.yml.charts.ui.linechart.model.ShadowUnderLine
|
||||
import co.yml.ycharts.app.R
|
||||
import co.yml.ycharts.app.ui.compositions.AppBarWithBackButton
|
||||
import co.yml.ycharts.app.ui.theme.YChartsTheme
|
||||
|
||||
/**
|
||||
* Line chart activity
|
||||
*
|
||||
* @constructor Create empty Line chart activity
|
||||
*/
|
||||
class LineChartActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
YChartsTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = YChartsTheme.colors.background,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
stringResource(id = R.string.title_line_chart),
|
||||
onBackPressed = {
|
||||
onBackPressed()
|
||||
})
|
||||
})
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
LazyColumn(content = {
|
||||
item {
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.linechart_default_style),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
SingleLineChartWithGridLines(
|
||||
DataUtils.getLineChartData(
|
||||
100,
|
||||
start = 50,
|
||||
maxRange = 100
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Divider(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp))
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.linechart_straight_line_style),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
StraightLinechart(DataUtils.getLineChartData(50, maxRange = 200))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Divider(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp))
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.linechart_dotted_style),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
DottedLinechart(DataUtils.getLineChartData(
|
||||
200,
|
||||
start = -50,
|
||||
maxRange = 50
|
||||
))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Divider(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp))
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.linechart_multiple_tones),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
MultipleToneLinechart(DataUtils.getLineChartData(
|
||||
200,
|
||||
start = -50,
|
||||
maxRange = 50
|
||||
))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Divider(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp))
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.combined_line_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
CombinedLinechart(DataUtils.getLineChartData(
|
||||
200,
|
||||
start = -50,
|
||||
maxRange = 50
|
||||
))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Divider(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp))
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.combined_line_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
CombinedLinechartWithBackground(DataUtils.getLineChartData(
|
||||
200,
|
||||
start = -50,
|
||||
maxRange = 50
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single line chart with grid lines
|
||||
*
|
||||
* @param pointsData
|
||||
*/
|
||||
@Composable
|
||||
private fun SingleLineChartWithGridLines(pointsData: List<Point>) {
|
||||
val steps = 5
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.topPadding(105.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> pointsData[i].x.toInt().toString() }
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.labelData { i ->
|
||||
// Add yMin to get the negative axis values to the scale
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}.build()
|
||||
val data = LineChartData(
|
||||
linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
dataPoints = pointsData,
|
||||
LineStyle(),
|
||||
IntersectionPoint(),
|
||||
SelectionHighlightPoint(),
|
||||
ShadowUnderLine(),
|
||||
SelectionHighlightPopUp()
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines()
|
||||
)
|
||||
LineChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
lineChartData = data
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Straight linechart
|
||||
*
|
||||
* @param pointsData
|
||||
*/
|
||||
@Composable
|
||||
private fun StraightLinechart(pointsData: List<Point>) {
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(40.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> (1900 + i).toString() }
|
||||
.axisLabelAngle(20f)
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.axisLabelColor(Color.Blue)
|
||||
.axisLineColor(Color.DarkGray)
|
||||
.typeFace(Typeface.DEFAULT_BOLD)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(10)
|
||||
.labelData { i -> "${(i * 20)}k" }
|
||||
.labelAndAxisLinePadding(30.dp)
|
||||
.axisLabelColor(Color.Blue)
|
||||
.axisLineColor(Color.DarkGray)
|
||||
.typeFace(Typeface.DEFAULT_BOLD)
|
||||
.build()
|
||||
val data = LineChartData(
|
||||
linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
dataPoints = pointsData,
|
||||
lineStyle = LineStyle(lineType = LineType.Straight(), color = Color.Blue),
|
||||
intersectionPoint = IntersectionPoint(color = Color.Red),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(popUpLabel = { x, y ->
|
||||
val xLabel = "x : ${(1900 + x).toInt()} "
|
||||
val yLabel = "y : ${String.format("%.2f", y)}"
|
||||
"$xLabel $yLabel"
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
LineChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
lineChartData = data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple tone linechart
|
||||
*
|
||||
* @param pointsData
|
||||
*/
|
||||
@Composable
|
||||
private fun MultipleToneLinechart(pointsData: List<Point>) {
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(40.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> (1900 + i).toString() }
|
||||
.axisLabelAngle(20f)
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.axisLabelColor(Color.Blue)
|
||||
.axisLineColor(Color.DarkGray)
|
||||
.typeFace(Typeface.DEFAULT_BOLD)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(10)
|
||||
.labelData { i -> "${(i * 20)}k" }
|
||||
.labelAndAxisLinePadding(30.dp)
|
||||
.axisLabelColor(Color.Blue)
|
||||
.axisLineColor(Color.DarkGray)
|
||||
.typeFace(Typeface.DEFAULT_BOLD)
|
||||
.build()
|
||||
val data = LineChartData(
|
||||
linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
dataPoints = pointsData,
|
||||
lineStyle = LineStyle(lineType = LineType.Straight(), color = Color.Blue),
|
||||
intersectionPoint = IntersectionPoint(color = Color.Red),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(popUpLabel = { x, y ->
|
||||
val xLabel = "x : ${(1900 + x).toInt()} "
|
||||
val yLabel = "y : ${String.format("%.2f", y)}"
|
||||
"$xLabel $yLabel"
|
||||
})
|
||||
), Line(
|
||||
dataPoints = pointsData.subList(0, 10),
|
||||
lineStyle = LineStyle(lineType = LineType.Straight(), color = Color.Magenta),
|
||||
intersectionPoint = IntersectionPoint(color = Color.Red),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(popUpLabel = { x, y ->
|
||||
val xLabel = "x : ${(1900 + x).toInt()} "
|
||||
val yLabel = "y : ${String.format("%.2f", y)}"
|
||||
"$xLabel $yLabel"
|
||||
})
|
||||
), Line(
|
||||
dataPoints = pointsData.subList(15, 30),
|
||||
lineStyle = LineStyle(lineType = LineType.Straight(), color = Color.Yellow),
|
||||
intersectionPoint = IntersectionPoint(color = Color.Red),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(popUpLabel = { x, y ->
|
||||
val xLabel = "x : ${(1900 + x).toInt()} "
|
||||
val yLabel = "y : ${String.format("%.2f", y)}"
|
||||
"$xLabel $yLabel"
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
LineChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
lineChartData = data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dotted linechart
|
||||
*
|
||||
* @param pointsData
|
||||
*/
|
||||
@Composable
|
||||
private fun DottedLinechart(pointsData: List<Point>) {
|
||||
val steps = 10
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(40.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> i.toString() }
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.axisLineColor(Color.Red)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.labelData { i ->
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}
|
||||
.axisLineColor(Color.Red)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.build()
|
||||
val data = LineChartData(
|
||||
linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
dataPoints = pointsData,
|
||||
lineStyle = LineStyle(
|
||||
lineType = LineType.SmoothCurve(isDotted = true),
|
||||
color = Color.Green
|
||||
),
|
||||
shadowUnderLine = ShadowUnderLine(
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.Green,
|
||||
Color.Transparent
|
||||
)
|
||||
), alpha = 0.3f
|
||||
),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(
|
||||
color = Color.Green
|
||||
),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(
|
||||
backgroundColor = Color.Black,
|
||||
backgroundStyle = Stroke(2f),
|
||||
labelColor = Color.Red,
|
||||
labelTypeface = Typeface.DEFAULT_BOLD
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
LineChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
lineChartData = data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined linechart
|
||||
*
|
||||
* @param pointsData
|
||||
*/
|
||||
@Composable
|
||||
private fun CombinedLinechart(pointsData: List<Point>) {
|
||||
val steps = 5
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> i.toString() }
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.labelData { i ->
|
||||
// Add yMin to get the negative axis values to the scale
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}.build()
|
||||
val colorPaletteList = listOf<Color>(Color.Blue,Color.Yellow,Color.Magenta,Color.DarkGray)
|
||||
val legendsConfig = LegendsConfig(
|
||||
legendLabelList = DataUtils.getLegendsLabelData(colorPaletteList),
|
||||
gridColumnCount = 4
|
||||
)
|
||||
val data = LineChartData(
|
||||
linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
dataPoints = pointsData,
|
||||
lineStyle = LineStyle(
|
||||
lineType = LineType.SmoothCurve(isDotted = true),
|
||||
color = colorPaletteList.first()
|
||||
),
|
||||
shadowUnderLine = ShadowUnderLine(
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.Green,
|
||||
Color.Transparent
|
||||
)
|
||||
), alpha = 0.3f
|
||||
),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(
|
||||
color = Color.Green
|
||||
),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(
|
||||
backgroundColor = Color.Black,
|
||||
backgroundStyle = Stroke(2f),
|
||||
labelColor = Color.Red,
|
||||
labelTypeface = Typeface.DEFAULT_BOLD
|
||||
)
|
||||
),
|
||||
Line(
|
||||
dataPoints = pointsData.subList(10,20),
|
||||
lineStyle = LineStyle(lineType = LineType.SmoothCurve(), color = colorPaletteList[1]),
|
||||
intersectionPoint = IntersectionPoint(color = Color.Red),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(popUpLabel = { x, y ->
|
||||
val xLabel = "x : ${(1900 + x).toInt()} "
|
||||
val yLabel = "y : ${String.format("%.2f", y)}"
|
||||
"$xLabel $yLabel"
|
||||
})
|
||||
),
|
||||
Line(
|
||||
dataPoints = DataUtils.getLineChartData(
|
||||
20,
|
||||
start = 0,
|
||||
maxRange = 50
|
||||
),
|
||||
LineStyle(color = colorPaletteList[2]),
|
||||
IntersectionPoint(),
|
||||
SelectionHighlightPoint(),
|
||||
shadowUnderLine = ShadowUnderLine(
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.Cyan,
|
||||
Color.Blue
|
||||
)
|
||||
), alpha = 0.5f
|
||||
),
|
||||
SelectionHighlightPopUp()
|
||||
),
|
||||
Line(
|
||||
dataPoints = pointsData.subList(10, 20),
|
||||
LineStyle(color = colorPaletteList[3]),
|
||||
IntersectionPoint(),
|
||||
SelectionHighlightPoint(),
|
||||
ShadowUnderLine(),
|
||||
SelectionHighlightPopUp()
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines()
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.height(400.dp)) {
|
||||
LineChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
lineChartData = data
|
||||
)
|
||||
Legends(
|
||||
legendsConfig = legendsConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined linechart with background
|
||||
*
|
||||
* @param pointsData
|
||||
*/
|
||||
@Composable
|
||||
private fun CombinedLinechartWithBackground(pointsData: List<Point>) {
|
||||
val steps = 5
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.backgroundColor(Color.Yellow)
|
||||
.labelData { i -> i.toString() }
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.backgroundColor(Color.Yellow)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.labelData { i ->
|
||||
// Add yMin to get the negative axis values to the scale
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}.build()
|
||||
val colorPaletteList = listOf<Color>(Color.Blue,Color.Yellow,Color.Magenta,Color.DarkGray)
|
||||
val legendsConfig = LegendsConfig(
|
||||
legendLabelList = DataUtils.getLegendsLabelData(colorPaletteList),
|
||||
gridColumnCount = 4
|
||||
)
|
||||
val data = LineChartData(
|
||||
linePlotData = LinePlotData(
|
||||
lines = listOf(
|
||||
Line(
|
||||
dataPoints = pointsData,
|
||||
lineStyle = LineStyle(
|
||||
lineType = LineType.SmoothCurve(isDotted = true),
|
||||
color = colorPaletteList.first()
|
||||
),
|
||||
shadowUnderLine = ShadowUnderLine(
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.Green,
|
||||
Color.Transparent
|
||||
)
|
||||
), alpha = 0.3f
|
||||
),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(
|
||||
color = Color.Green
|
||||
),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(
|
||||
backgroundColor = Color.Black,
|
||||
backgroundStyle = Stroke(2f),
|
||||
labelColor = Color.Red,
|
||||
labelTypeface = Typeface.DEFAULT_BOLD
|
||||
)
|
||||
),
|
||||
Line(
|
||||
dataPoints = pointsData.subList(10,20),
|
||||
lineStyle = LineStyle(lineType = LineType.SmoothCurve(), color = colorPaletteList[1]),
|
||||
intersectionPoint = IntersectionPoint(color = Color.Red),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(popUpLabel = { x, y ->
|
||||
val xLabel = "x : ${(1900 + x).toInt()} "
|
||||
val yLabel = "y : ${String.format("%.2f", y)}"
|
||||
"$xLabel $yLabel"
|
||||
})
|
||||
),
|
||||
Line(
|
||||
dataPoints = DataUtils.getLineChartData(
|
||||
20,
|
||||
start = 0,
|
||||
maxRange = 50
|
||||
),
|
||||
LineStyle(color = colorPaletteList[2]),
|
||||
IntersectionPoint(),
|
||||
SelectionHighlightPoint(),
|
||||
shadowUnderLine = ShadowUnderLine(
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.Cyan,
|
||||
Color.Blue
|
||||
)
|
||||
), alpha = 0.5f
|
||||
),
|
||||
SelectionHighlightPopUp()
|
||||
),
|
||||
Line(
|
||||
dataPoints = pointsData.subList(10, 20),
|
||||
LineStyle(color = colorPaletteList[3]),
|
||||
IntersectionPoint(),
|
||||
SelectionHighlightPoint(),
|
||||
ShadowUnderLine(),
|
||||
SelectionHighlightPopUp()
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines(),
|
||||
backgroundColor = Color.Yellow
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.height(400.dp)
|
||||
) {
|
||||
LineChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
lineChartData = data
|
||||
)
|
||||
Legends(
|
||||
legendsConfig = legendsConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package co.yml.ycharts.app.presentation
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.common.components.Legends
|
||||
import co.yml.charts.common.utils.DataUtils
|
||||
import co.yml.charts.ui.piechart.charts.PieChart
|
||||
import co.yml.charts.ui.piechart.models.PieChartConfig
|
||||
import co.yml.ycharts.app.R
|
||||
import co.yml.ycharts.app.ui.compositions.AppBarWithBackButton
|
||||
import co.yml.ycharts.app.ui.theme.YChartsTheme
|
||||
|
||||
/**
|
||||
* Pie chart activity
|
||||
*
|
||||
* @constructor Create empty Pie chart activity
|
||||
*/
|
||||
class PieChartActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
YChartsTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = YChartsTheme.colors.background,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
stringResource(id = R.string.title_pie_chart),
|
||||
onBackPressed = {
|
||||
onBackPressed()
|
||||
})
|
||||
})
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
LazyColumn(content = {
|
||||
items(3) { item ->
|
||||
when (item) {
|
||||
0 -> {
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.simple_piechart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
SimplePiechart(LocalContext.current)
|
||||
}
|
||||
1 -> {
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.piechart_with_lables),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
PiechartWithSliceLables(LocalContext.current)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple piechart
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
@Composable
|
||||
private fun SimplePiechart(context: Context) {
|
||||
val pieChartData = DataUtils.getPieChartData()
|
||||
val pieChartConfig =
|
||||
PieChartConfig(
|
||||
labelVisible = true,
|
||||
activeSliceAlpha = .9f,
|
||||
isEllipsizeEnabled = true,
|
||||
sliceLabelEllipsizeAt = TextUtils.TruncateAt.MIDDLE,
|
||||
isAnimationEnable = true,
|
||||
chartPadding = 30,
|
||||
backgroundColor = Color.Black,
|
||||
showSliceLabels = false,
|
||||
animationDuration = 1500
|
||||
)
|
||||
Column(modifier = Modifier.height(500.dp)) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Legends(legendsConfig = DataUtils.getLegendsConfigFromPieChartData(pieChartData, 3))
|
||||
PieChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp),
|
||||
pieChartData,
|
||||
pieChartConfig
|
||||
) { slice ->
|
||||
Toast.makeText(context, slice.label, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Piechart with slice lables
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
@Composable
|
||||
private fun PiechartWithSliceLables(context: Context) {
|
||||
val pieChartData = DataUtils.getPieChartData2()
|
||||
|
||||
val pieChartConfig =
|
||||
PieChartConfig(
|
||||
activeSliceAlpha = .9f,
|
||||
isEllipsizeEnabled = true,
|
||||
sliceLabelEllipsizeAt = TextUtils.TruncateAt.MIDDLE,
|
||||
sliceLabelTypeface = Typeface.defaultFromStyle(Typeface.ITALIC),
|
||||
isAnimationEnable = true,
|
||||
chartPadding = 20,
|
||||
showSliceLabels = true,
|
||||
labelVisible = true
|
||||
)
|
||||
Column(modifier = Modifier.height(500.dp)) {
|
||||
Legends(legendsConfig = DataUtils.getLegendsConfigFromPieChartData(pieChartData, 3))
|
||||
PieChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp),
|
||||
pieChartData,
|
||||
pieChartConfig
|
||||
) { slice ->
|
||||
Toast.makeText(context, slice.label, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package co.yml.ycharts.app.presentation
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.yml.charts.axis.AxisData
|
||||
import co.yml.charts.common.extensions.formatToSinglePrecision
|
||||
import co.yml.charts.common.model.Point
|
||||
import co.yml.charts.common.utils.DataUtils
|
||||
import co.yml.charts.ui.linechart.model.*
|
||||
import co.yml.charts.ui.wavechart.WaveChart
|
||||
import co.yml.charts.ui.wavechart.model.Wave
|
||||
import co.yml.charts.ui.wavechart.model.WaveChartData
|
||||
import co.yml.charts.ui.wavechart.model.WaveFillColor
|
||||
import co.yml.charts.ui.wavechart.model.WavePlotData
|
||||
import co.yml.ycharts.app.R
|
||||
import co.yml.ycharts.app.ui.compositions.AppBarWithBackButton
|
||||
import co.yml.ycharts.app.ui.theme.YChartsTheme
|
||||
|
||||
class WaveChartActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
YChartsTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = YChartsTheme.colors.background,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
stringResource(id = R.string.title_wave_chart),
|
||||
onBackPressed = {
|
||||
onBackPressed()
|
||||
})
|
||||
})
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
LazyColumn(content = {
|
||||
items(3) { item ->
|
||||
when (item) {
|
||||
0 -> {
|
||||
Text(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
text = getString(R.string.simple_wave_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
WaveGraph1(
|
||||
DataUtils.getWaveChartData(
|
||||
15.0,
|
||||
50.0,
|
||||
0.5
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
Text(
|
||||
modifier=Modifier.padding(12.dp),
|
||||
text = getString(R.string.sine_wave_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
WaveGraph2(
|
||||
DataUtils.getWaveChartData(
|
||||
15.0,
|
||||
50.0,
|
||||
0.2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
2 -> {
|
||||
Text(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
text = getString(R.string.amplitude_wave_chart),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
WaveGraph3(
|
||||
DataUtils.getWaveChartData(
|
||||
20.0,
|
||||
5.0,
|
||||
1.0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WaveGraph1(pointsData: List<Point>) {
|
||||
val steps = 5
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(30.dp)
|
||||
.startDrawPadding(48.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> i.toString() }
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.labelData { i ->
|
||||
// Add yMin to get the negative axis values to the scale
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}.build()
|
||||
val data = WaveChartData(
|
||||
wavePlotData = WavePlotData(
|
||||
lines = listOf(
|
||||
Wave(
|
||||
dataPoints = pointsData,
|
||||
waveStyle = LineStyle(color = Color.Black),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(),
|
||||
shadowUnderLine = ShadowUnderLine(),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(),
|
||||
waveFillColor = WaveFillColor(topColor = Color.Green, bottomColor = Color.Red),
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData,
|
||||
gridLines = GridLines()
|
||||
)
|
||||
WaveChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
waveChartData = data
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WaveGraph2(pointsData: List<Point>) {
|
||||
val steps = 10
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(40.dp)
|
||||
.startDrawPadding(48.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> (i).toString() }
|
||||
.axisLabelAngle(20f)
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.axisLabelColor(Color.Blue)
|
||||
.axisLineColor(Color.DarkGray)
|
||||
.typeFace(Typeface.DEFAULT_BOLD)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(10)
|
||||
.labelData { i ->
|
||||
// Add yMin to get the negative axis values to the scale
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}
|
||||
.labelAndAxisLinePadding(30.dp)
|
||||
.axisLabelColor(Color.Blue)
|
||||
.axisLineColor(Color.DarkGray)
|
||||
.typeFace(Typeface.DEFAULT_BOLD)
|
||||
.build()
|
||||
val data = WaveChartData(
|
||||
wavePlotData = WavePlotData(
|
||||
lines = listOf(
|
||||
Wave(
|
||||
dataPoints = pointsData,
|
||||
waveStyle = LineStyle(lineType = LineType.SmoothCurve(), color = Color.Blue),
|
||||
shadowUnderLine = ShadowUnderLine(alpha = 0.5f),
|
||||
waveFillColor = WaveFillColor(
|
||||
topColor = Color.Yellow,
|
||||
bottomColor = Color.Magenta
|
||||
),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(popUpLabel = { x, y ->
|
||||
val xLabel = "x : ${(1900 + x).toInt()} "
|
||||
val yLabel = "y : ${String.format("%.2f", y)}"
|
||||
"$xLabel $yLabel"
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
WaveChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
waveChartData = data
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WaveGraph3(pointsData: List<Point>) {
|
||||
val steps = 10
|
||||
val xAxisData = AxisData.Builder()
|
||||
.axisStepSize(40.dp)
|
||||
.startDrawPadding(48.dp)
|
||||
.steps(pointsData.size - 1)
|
||||
.labelData { i -> i.toString() }
|
||||
.labelAndAxisLinePadding(15.dp)
|
||||
.axisLineColor(Color.Red)
|
||||
.build()
|
||||
val yAxisData = AxisData.Builder()
|
||||
.steps(steps)
|
||||
.labelData { i ->
|
||||
val yMin = pointsData.minOf { it.y }
|
||||
val yMax = pointsData.maxOf { it.y }
|
||||
val yScale = (yMax - yMin) / steps
|
||||
((i * yScale) + yMin).formatToSinglePrecision()
|
||||
}
|
||||
.axisLineColor(Color.Red)
|
||||
.labelAndAxisLinePadding(20.dp)
|
||||
.bottomPadding(15.dp)
|
||||
.build()
|
||||
val data = WaveChartData(
|
||||
wavePlotData = WavePlotData(
|
||||
lines = listOf(
|
||||
Wave(
|
||||
dataPoints = pointsData,
|
||||
waveStyle = LineStyle(
|
||||
lineType = LineType.Straight(isDotted = true),
|
||||
color = Color.Green
|
||||
),
|
||||
shadowUnderLine = ShadowUnderLine(
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.Green,
|
||||
Color.Transparent
|
||||
)
|
||||
), alpha = 0.3f
|
||||
),
|
||||
selectionHighlightPoint = SelectionHighlightPoint(
|
||||
color = Color.Green
|
||||
),
|
||||
selectionHighlightPopUp = SelectionHighlightPopUp(
|
||||
backgroundColor = Color.Black,
|
||||
backgroundStyle = Stroke(2f),
|
||||
labelColor = Color.Red,
|
||||
labelTypeface = Typeface.DEFAULT_BOLD
|
||||
),
|
||||
waveFillColor = WaveFillColor(
|
||||
topColor = Color.Green,
|
||||
bottomColor = Color.Red,
|
||||
topBrush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.Green,
|
||||
Color.Blue
|
||||
)
|
||||
),
|
||||
bottomBrush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.Green,
|
||||
Color.Red
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
xAxisData = xAxisData,
|
||||
yAxisData = yAxisData
|
||||
)
|
||||
WaveChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
waveChartData = data
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user