diff --git a/.editorconfig b/.editorconfig index 2c679cde8..0f2a4eb72 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,6 @@ trim_trailing_whitespace = true [*.{yml,yaml}] indent_size = 2 -[*.{kts, kt}] +[*.{kts,kt}] ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = true diff --git a/.gitattributes b/.gitattributes index 411c07777..919851d56 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ * text=auto eol=lf - *.bat text eol=crlf *.jar binary +**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..ea8b8cfc8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: patrykandpatrick diff --git a/.github/workflows/release-update.yml b/.github/workflows/release-update.yml index 8f3c66377..e1dfb1617 100644 --- a/.github/workflows/release-update.yml +++ b/.github/workflows/release-update.yml @@ -39,8 +39,9 @@ jobs: cd ${{ github.workspace }}/vico git remote set-url origin https://patrykandpatrickbot:${{ secrets.BOT_PAT }}@github.com/patrykandpatrick/vico.git ./gradlew dokkaHtmlMultiModule - rm -r ${{ github.workspace }}/patrykandpatrick.com/src/vico/api/* - cp -R ${{ github.workspace }}/vico/vico/build/dokka/htmlMultiModule/. ${{ github.workspace }}/patrykandpatrick.com/src/vico/api + API_REFERENCE_DESTINATION=${{ github.workspace }}/patrykandpatrick.com/src/vico/api/$(if $IS_PRERELEASE; then echo $VERSION_NAME; else echo main; fi) + rm -fr $API_REFERENCE_DESTINATION/* + cp -R ${{ github.workspace }}/vico/vico/build/dokka/htmlMultiModule/. $API_REFERENCE_DESTINATION cd ${{ github.workspace }}/patrykandpatrick.com git remote set-url origin https://patrykandpatrickbot:${{ secrets.BOT_PAT }}@github.com/patrykandpatrick/patrykandpatrick.com.git git add src/vico/api @@ -62,7 +63,7 @@ jobs: mv vico/view/src/main/java/com/patrykandpatryk/vico/views vico/view/src/main/java/com/patrykandpatryk/vico/view find . -not -path "*/.*" -type f | while read path; do sed -i "s/com\.patrykandpatrick/com\.patrykandpatryk/" "$path"; done find . -not -path "*/.*" -type f | while read path; do sed -i "s/vico\.views/vico\.view/" "$path"; done - sed -i s/:vico:views/:vico:view/ *.gradle + find -type f \( -name "*.gradle" -o -name "*.gradle.kts" \) -exec sed -i s/vico:views/vico:view/ "{}" + sed -i "s/\(username\s*=\s*\)\"patrykandpatrick\"/\1\"patrykandpatryk\"/" common-scripts.gradle ./gradlew publish env: diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c78fb51bf..a6b3f4732 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -5,13 +5,22 @@ on: jobs: run-tests: runs-on: ubuntu-latest - continue-on-error: true steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3.3.0 + - uses: actions/checkout@v4 + with: + lfs: true + - uses: actions/setup-java@v3 with: java-version: 17 distribution: adopt - - run: | - chmod +x gradlew - ./gradlew test + - run: chmod +x gradlew + - id: unit-tests + run: ./gradlew vico:core:testDebug + - id: paparazzi + if: ${{ success() || steps.unit-tests.conclusion == 'failure' }} + run: ./gradlew sample:verifyPaparazziDebug + - uses: actions/upload-artifact@v3 + if: ${{ !cancelled() && steps.paparazzi.conclusion == 'failure' }} + with: + name: Paparazzi deltas + path: sample/build/paparazzi/failures/delta-*.png diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b589d56e9..312bf2eab 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/detekt-config.yml b/.idea/detekt-config.yml index 2553969aa..25b3e15ba 100644 --- a/.idea/detekt-config.yml +++ b/.idea/detekt-config.yml @@ -10,6 +10,8 @@ comments: - "**/jsTest/**" - "**/iosTest/**" - "**/com/patrykandpatrick/vico/sample/**" + ignoreAnnotated: + - "RestrictTo" UndocumentedPublicFunction: active: true excludes: @@ -20,6 +22,8 @@ comments: - "**/jsTest/**" - "**/iosTest/**" - "**/com/patrykandpatrick/vico/sample/**" + ignoreAnnotated: + - "RestrictTo" UndocumentedPublicProperty: active: true excludes: @@ -30,21 +34,10 @@ comments: - "**/jsTest/**" - "**/iosTest/**" - "**/com/patrykandpatrick/vico/sample/**" + ignoreAnnotated: + - "RestrictTo" complexity: - LongMethod: - threshold: 90 - LongParameterList: - active: false - TooManyFunctions: - thresholdInFiles: 999 - ignoreDeprecated: true - ignorePrivate: true - ignoreOverridden: true - thresholdInInterfaces: 20 - CyclomaticComplexMethod: - threshold: 20 - ComplexCondition: - threshold: 6 + active: false style: MagicNumber: excludes: diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 26566b239..508184b24 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - diff --git a/README.md b/README.md index 61faea934..cd4a75bc2 100644 --- a/README.md +++ b/README.md @@ -16,4 +16,4 @@ There are three primary learning resources for Vico: [a setup guide](https://patrykandpatrick.com/vico/wiki/getting-started) and outlines Vico’s features and structure. - [The sample app](https://github.com/patrykandpatrick/vico/tree/master/sample) demonstrates how to use Vico in Jetpack Compose and the view system. The charts showcased at the top of this document are implemented in the sample app. -- [The API reference](https://patrykandpatrick.com/vico/api) is a comprehensive manual for Vico’s API. +- [The API reference](https://patrykandpatrick.com/vico/wiki/#api-reference) is a comprehensive manual for Vico’s API. diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 79f7299b6..000000000 --- a/build.gradle +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2023 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import io.gitlab.arturbosch.detekt.Detekt - -buildscript { - - apply from: "versions.gradle" - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - - dependencies { - - classpath libs.buildTools - classpath libs.kotlinGradlePlugin - } -} - -plugins { - - alias libs.plugins.detekt - alias libs.plugins.dokka apply false -} - -dependencies { - - detektPlugins libs.detektFormatting -} - -detekt { - - buildUponDefaultConfig = true - source = files("sample", "vico/core", "vico/compose", "vico/views", "vico/compose-m3", "vico/compose-m2") - config = files(".idea/detekt-config.yml") -} - -tasks.register("detektFix", Detekt) { - - autoCorrect = true - buildUponDefaultConfig = true - source = files("sample", "vico/core", "vico/compose", "vico/views", "vico/compose-m3", "vico/compose-m2") - config.setFrom(files(".idea/detekt-config.yml")) -} - -allprojects { - - repositories { - - google() - mavenLocal() - mavenCentral() - } -} - -tasks.withType(Detekt).configureEach { - - jvmTarget = JavaVersion.VERSION_1_8.toString() - - reports { - html { - enabled = true - destination = file("build/reports/detekt/detekt.html") - } - } -} - -task clean(type: Delete) { - - delete rootProject.buildDir -} - -tasks.register("assembleVico") { task -> - task.dependsOn "clean" - allprojects.forEach { project -> - if (project.parent != null && project.parent.name.contains("vico")) { - task.dependsOn "${project.parent.name}:${project.name}:assembleRelease" - } - } -} - -project(":vico").tasks.configureEach { subtask -> - - if (subtask.name.contains("publishVicoCorePublication")) { - subtask.mustRunAfter(":assembleVico") - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..eeb60f332 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.gitlab.arturbosch.detekt.Detekt + +buildscript { + dependencies { + classpath(libs.buildTools) + classpath(libs.kotlinGradlePlugin) + classpath(libs.paparazziGradlePlugin) + } +} + +plugins { + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) apply false +} + +apply("versions.gradle") + +dependencies { detektPlugins(libs.detektFormatting) } + +tasks.register("clean") { delete(rootProject.layout.buildDirectory) } + +tasks.register("detektFix") { autoCorrect = true } + +tasks.withType().configureEach { + source = fileTree(projectDir) + config = files(".idea/detekt-config.yml") + buildUponDefaultConfig = true + reports.html { + required = true + outputLocation = file("build/reports/detekt/detekt.html") + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e117d741..94bf0bfc5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,24 +1,27 @@ [versions] accompanist = "0.32.0" -activity = "1.7.2" -agp = "8.3.0-alpha03" +activity = "1.8.0" +agp = "8.3.0-alpha14" +androidXAnnotation = "1.7.0" androidXCore = "1.12.0" appcompat = "1.6.1" -composeBom = "2023.09.00" -composeCompiler = "1.5.3" +composeBom = "2023.10.01" +composeCompiler = "1.5.4" coroutines = "1.7.3" -detekt = "1.23.1" -dokka = "1.9.0" +detekt = "1.23.3" +dokka = "1.9.10" jUnit = "4.13.2" jUnitExt = "1.1.5" -kotlin = "1.9.10" +kotlin = "1.9.20" lifecycle = "2.6.2" -material = "1.9.0" +material = "1.10.0" mockK = "1.13.7" +paparazziGradlePlugin = "1.3.1" testCore = "1.5.0" [libraries] activityCompose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" } +androidXAnnotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidXAnnotation" } androidXCore = { group = "androidx.core", name = "core-ktx", version.ref = "androidXCore" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } buildTools = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } @@ -39,6 +42,7 @@ kotlinTest = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref lifecycleRuntime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } mockK = { group = "io.mockk", name = "mockk", version.ref = "mockK" } +paparazziGradlePlugin = { module = "app.cash.paparazzi:paparazzi-gradle-plugin", version.ref = "paparazziGradlePlugin" } systemUIController = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } testCore = { group = "androidx.test", name = "core-ktx", version.ref = "testCore" } viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34e8..3fa8f862f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/logo-styles.css b/logo-styles.css index 03a8326f2..4d8c451b9 100644 --- a/logo-styles.css +++ b/logo-styles.css @@ -1,15 +1,3 @@ -.library-name a { - position: relative; - margin-left: 55px; -} - -.library-name a::before { - content: ""; - background: url("https://raw.githubusercontent.com/patrykandpatrick/vico/master/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png") center no-repeat; - background-size: contain; - position: absolute; - width: 50px; - height: 50px; - top: -16px; - left: -55px; +:root { + --dokka-logo-image-url: url("https://raw.githubusercontent.com/patrykandpatrick/vico/master/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png"); } diff --git a/sample/build.gradle b/sample/build.gradle index fc47559a7..679895904 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -17,14 +17,14 @@ plugins { id "com.android.application" id "kotlin-android" + id "app.cash.paparazzi" } -apply from: "$rootDir/common-scripts.gradle" - android { defaultConfig { minSdk library.compose_min_sdk targetSdk library.target_sdk + compileSdk library.target_sdk versionCode library.version_code versionName library.version_name } @@ -46,6 +46,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + composeOptions { kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() } diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/annotation/ChartPreview.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/annotation/ChartPreview.kt new file mode 100644 index 000000000..bde8b5ecb --- /dev/null +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/annotation/ChartPreview.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.sample.previews.annotation + +import android.content.res.Configuration +import androidx.compose.ui.tooling.preview.Preview + +@Preview(widthDp = 300, heightDp = 200) +@Preview(widthDp = 300, heightDp = 200, uiMode = Configuration.UI_MODE_NIGHT_YES) +public annotation class ChartPreview diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/composables/column/ColumnCharts.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/composables/column/ColumnCharts.kt new file mode 100644 index 000000000..6e3015af3 --- /dev/null +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/composables/column/ColumnCharts.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.sample.previews.composables.column + +import androidx.compose.runtime.Composable +import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis +import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis +import com.patrykandpatrick.vico.compose.chart.Chart +import com.patrykandpatrick.vico.compose.chart.column.columnChart +import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec +import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition +import com.patrykandpatrick.vico.core.scroll.InitialScroll +import com.patrykandpatrick.vico.sample.previews.annotation.ChartPreview +import com.patrykandpatrick.vico.sample.previews.resource.PreviewSurface +import com.patrykandpatrick.vico.sample.previews.resource.mediumEntryModel +import com.patrykandpatrick.vico.sample.previews.resource.shortEntryModel + +@ChartPreview +@Composable +public fun DefaultColumnChart( + model: ChartEntryModel = shortEntryModel, + oldModel: ChartEntryModel? = null, + scrollable: Boolean = true, + initialScroll: InitialScroll = InitialScroll.Start, + autoScrollCondition: AutoScrollCondition = AutoScrollCondition.Never, +) { + PreviewSurface { + Chart( + chart = columnChart(), + model = model, + oldModel = oldModel, + startAxis = rememberStartAxis(), + bottomAxis = rememberBottomAxis(), + chartScrollSpec = rememberChartScrollSpec( + isScrollEnabled = scrollable, + initialScroll = initialScroll, + autoScrollCondition = autoScrollCondition, + ), + ) + } +} + +@ChartPreview +@Composable +public fun DefaultColumnChartLongScrollable() { + DefaultColumnChart(model = mediumEntryModel) +} + +@ChartPreview +@Composable +public fun DefaultColumnChartLongScrollableEnd() { + DefaultColumnChart(model = mediumEntryModel, initialScroll = InitialScroll.End) +} + +@ChartPreview +@Composable +public fun DefaultColumnChartLongNonScrollable() { + DefaultColumnChart(model = mediumEntryModel, scrollable = false) +} diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/composables/line/LineCharts.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/composables/line/LineCharts.kt new file mode 100644 index 000000000..77eae7c41 --- /dev/null +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/composables/line/LineCharts.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.sample.previews.composables.line + +import androidx.compose.runtime.Composable +import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis +import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis +import com.patrykandpatrick.vico.compose.chart.Chart +import com.patrykandpatrick.vico.compose.chart.line.lineChart +import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec +import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.scroll.InitialScroll +import com.patrykandpatrick.vico.sample.previews.annotation.ChartPreview +import com.patrykandpatrick.vico.sample.previews.resource.PreviewSurface +import com.patrykandpatrick.vico.sample.previews.resource.mediumEntryModel +import com.patrykandpatrick.vico.sample.previews.resource.shortEntryModel + +@ChartPreview +@Composable +public fun DefaultLineChart( + model: ChartEntryModel = shortEntryModel, + scrollable: Boolean = true, + initialScroll: InitialScroll = InitialScroll.Start, +) { + PreviewSurface { + Chart( + chart = lineChart(), + model = model, + startAxis = rememberStartAxis(), + bottomAxis = rememberBottomAxis(), + chartScrollSpec = rememberChartScrollSpec(isScrollEnabled = scrollable, initialScroll = initialScroll), + ) + } +} + +@ChartPreview +@Composable +public fun DefaultLineChartLongScrollable() { + DefaultLineChart(model = mediumEntryModel) +} + +@ChartPreview +@Composable +public fun DefaultLineChartLongScrollableEnd() { + DefaultLineChart(model = mediumEntryModel, initialScroll = InitialScroll.End) +} + +@ChartPreview +@Composable +public fun DefaultLineChartLongNonScrollable() { + DefaultLineChart(model = mediumEntryModel, scrollable = false) +} diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/resource/PreviewSurface.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/resource/PreviewSurface.kt new file mode 100644 index 000000000..1f6c83bc3 --- /dev/null +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/resource/PreviewSurface.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.sample.previews.resource + +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import com.patrykandpatrick.vico.sample.utils.VicoTheme + +@Composable +public fun PreviewSurface(content: @Composable () -> Unit) { + VicoTheme { + Surface { + content() + } + } +} diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/resource/SampleModels.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/resource/SampleModels.kt new file mode 100644 index 000000000..90134f735 --- /dev/null +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/previews/resource/SampleModels.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.sample.previews.resource + +import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.entryModelOf + +public val shortEntryModel: ChartEntryModel = entryModelOf(1, 2, 4, 8, 3) + +public val mediumEntryModel: ChartEntryModel = entryModelOf(1, 2, 4, 8, 3, 10, 4, 7, 2, 6, 4, 8) diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/ShowcaseViewModel.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/ShowcaseViewModel.kt index 5477d4b84..48844068b 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/ShowcaseViewModel.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/ShowcaseViewModel.kt @@ -21,10 +21,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer -import com.patrykandpatrick.vico.core.entry.composed.plus import com.patrykandpatrick.vico.core.util.RandomEntriesGenerator import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay @@ -49,8 +47,7 @@ internal class ShowcaseViewModel : ViewModel() { internal val multiDataSetChartEntryModelProducer: ChartEntryModelProducer = ChartEntryModelProducer() - internal val composedChartEntryModelProducer: ComposedChartEntryModelProducer = - multiDataSetChartEntryModelProducer + chartEntryModelProducer + internal val composedChartEntryModelProducer = ComposedChartEntryModelProducer.build() var uiSystem by mutableStateOf(UISystem.Compose) private set @@ -58,13 +55,15 @@ internal class ShowcaseViewModel : ViewModel() { init { viewModelScope.launch { while (currentCoroutineContext().isActive) { - chartEntryModelProducer.setEntries(generator.generateRandomEntries()) - multiDataSetChartEntryModelProducer.setEntries( - entries = List(size = MULTI_ENTRIES_COMBINED) { - generator.generateRandomEntries() - }, - ) + val randomSeries = generator.generateRandomEntries() + val randomDataSet = List(MULTI_ENTRIES_COMBINED) { generator.generateRandomEntries() } + chartEntryModelProducer.setEntries(randomSeries) + multiDataSetChartEntryModelProducer.setEntries(randomDataSet) customStepChartEntryModelProducer.setEntries(customStepGenerator.generateRandomEntries()) + composedChartEntryModelProducer.runTransaction { + add(randomDataSet) + add(randomSeries) + } delay(UPDATE_FREQUENCY) } } diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart2.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart2.kt index 4b12520bc..77f63b773 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart2.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart2.kt @@ -133,8 +133,8 @@ private val thresholdLineLabelPadding = private val thresholdLineLabelMargins = dimensionsOf(thresholdLineLabelMarginValue) private val startAxisValueFormatter = PercentageFormatAxisValueFormatter() private val horizontalLayout = HorizontalLayout.FullWidth( - startPaddingDp = DefaultDimens.COLUMN_OUTSIDE_SPACING.half, - endPaddingDp = DefaultDimens.COLUMN_OUTSIDE_SPACING.half, + scalableStartPaddingDp = DefaultDimens.COLUMN_OUTSIDE_SPACING.half, + scalableEndPaddingDp = DefaultDimens.COLUMN_OUTSIDE_SPACING.half, ) private val startAxisItemPlacer = AxisItemPlacer.Vertical.default(MAX_START_AXIS_ITEM_COUNT) private val bottomAxisItemPlacer = AxisItemPlacer.Horizontal.default(BOTTOM_AXIS_ITEM_SPACING, BOTTOM_AXIS_ITEM_OFFSET) diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart4.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart4.kt index 533c14e4c..0265dc2d8 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart4.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart4.kt @@ -34,7 +34,6 @@ import com.patrykandpatrick.vico.core.chart.composed.plus import com.patrykandpatrick.vico.core.chart.copy import com.patrykandpatrick.vico.core.component.shape.LineComponent import com.patrykandpatrick.vico.core.component.shape.Shapes -import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer import com.patrykandpatrick.vico.databinding.Chart4Binding import com.patrykandpatrick.vico.sample.showcase.UISystem @@ -42,7 +41,7 @@ import com.patrykandpatrick.vico.sample.showcase.rememberChartStyle import com.patrykandpatrick.vico.sample.showcase.rememberMarker @Composable -internal fun Chart4(uiSystem: UISystem, chartEntryModelProducer: ComposedChartEntryModelProducer) { +internal fun Chart4(uiSystem: UISystem, chartEntryModelProducer: ComposedChartEntryModelProducer) { when (uiSystem) { UISystem.Compose -> ComposeChart4(chartEntryModelProducer) UISystem.Views -> ViewChart4(chartEntryModelProducer) @@ -50,7 +49,7 @@ internal fun Chart4(uiSystem: UISystem, chartEntryModelProducer: ComposedChartEn } @Composable -private fun ComposeChart4(chartEntryModelProducer: ComposedChartEntryModelProducer) { +private fun ComposeChart4(chartEntryModelProducer: ComposedChartEntryModelProducer) { ProvideChartStyle(rememberChartStyle(columnChartColors, lineChartColors)) { val defaultColumns = currentChartStyle.columnChart.columns val defaultLines = currentChartStyle.lineChart.lines @@ -82,7 +81,7 @@ private fun ComposeChart4(chartEntryModelProducer: ComposedChartEntryModelProduc } @Composable -private fun ViewChart4(chartEntryModelProducer: ComposedChartEntryModelProducer) { +private fun ViewChart4(chartEntryModelProducer: ComposedChartEntryModelProducer) { val marker = rememberMarker() AndroidViewBinding(Chart4Binding::inflate) { with(chartView) { diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart8.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart8.kt index 700d27e4d..062e631dc 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart8.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart8.kt @@ -32,7 +32,6 @@ import com.patrykandpatrick.vico.core.chart.column.ColumnChart import com.patrykandpatrick.vico.core.chart.composed.ComposedChart import com.patrykandpatrick.vico.core.chart.composed.plus import com.patrykandpatrick.vico.core.chart.line.LineChart -import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer import com.patrykandpatrick.vico.databinding.Chart8Binding import com.patrykandpatrick.vico.sample.showcase.UISystem @@ -40,7 +39,7 @@ import com.patrykandpatrick.vico.sample.showcase.rememberChartStyle import com.patrykandpatrick.vico.sample.showcase.rememberMarker @Composable -internal fun Chart8(uiSystem: UISystem, chartEntryModelProducer: ComposedChartEntryModelProducer) { +internal fun Chart8(uiSystem: UISystem, chartEntryModelProducer: ComposedChartEntryModelProducer) { when (uiSystem) { UISystem.Compose -> ComposeChart8(chartEntryModelProducer) UISystem.Views -> ViewChart8(chartEntryModelProducer) @@ -48,7 +47,7 @@ internal fun Chart8(uiSystem: UISystem, chartEntryModelProducer: ComposedChartEn } @Composable -private fun ComposeChart8(chartEntryModelProducer: ComposedChartEntryModelProducer) { +private fun ComposeChart8(chartEntryModelProducer: ComposedChartEntryModelProducer) { ProvideChartStyle(rememberChartStyle(columnChartColors, lineChartColors)) { val columnChart = columnChart( mergeMode = ColumnChart.MergeMode.Stack, @@ -67,7 +66,7 @@ private fun ComposeChart8(chartEntryModelProducer: ComposedChartEntryModelProduc } @Composable -private fun ViewChart8(chartEntryModelProducer: ComposedChartEntryModelProducer) { +private fun ViewChart8(chartEntryModelProducer: ComposedChartEntryModelProducer) { val marker = rememberMarker() AndroidViewBinding(Chart8Binding::inflate) { with(chartView) { diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/utils/VicoTheme.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/utils/VicoTheme.kt index 2263f09a4..df72fb89a 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/utils/VicoTheme.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/utils/VicoTheme.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color @Composable -internal fun VicoTheme(content: @Composable () -> Unit) { +fun VicoTheme(content: @Composable () -> Unit) { val darkColorScheme = darkColorScheme( surface = Color(color = DARK_SURFACE), background = Color.Black, diff --git a/sample/src/test/kotlin/com/patrykandpatrick/vico/sample/PaparazziTest.kt b/sample/src/test/kotlin/com/patrykandpatrick/vico/sample/PaparazziTest.kt new file mode 100644 index 000000000..1c05aa2ed --- /dev/null +++ b/sample/src/test/kotlin/com/patrykandpatrick/vico/sample/PaparazziTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.sample + +import androidx.compose.runtime.Composable +import app.cash.paparazzi.Paparazzi +import com.patrykandpatrick.vico.sample.paparazzi.lightConfig +import com.patrykandpatrick.vico.sample.paparazzi.nightConfig +import com.patrykandpatrick.vico.sample.previews.composables.column.DefaultColumnChart +import com.patrykandpatrick.vico.sample.previews.composables.column.DefaultColumnChartLongNonScrollable +import com.patrykandpatrick.vico.sample.previews.composables.column.DefaultColumnChartLongScrollable +import com.patrykandpatrick.vico.sample.previews.composables.column.DefaultColumnChartLongScrollableEnd +import com.patrykandpatrick.vico.sample.previews.composables.line.DefaultLineChart +import com.patrykandpatrick.vico.sample.previews.composables.line.DefaultLineChartLongNonScrollable +import com.patrykandpatrick.vico.sample.previews.composables.line.DefaultLineChartLongScrollable +import com.patrykandpatrick.vico.sample.previews.composables.line.DefaultLineChartLongScrollableEnd +import org.junit.Rule +import org.junit.Test + +public class PaparazziTest { + + private val defaultCharts = listOf Unit>>( + "LineChart" to { DefaultLineChart() }, + "LineChart Long Scrollable" to { DefaultLineChartLongScrollable() }, + "LineChart Long Scrollable with initial scroll end" to { DefaultLineChartLongScrollableEnd() }, + "LineChart Long Not Scrollable" to { DefaultLineChartLongNonScrollable() }, + "ColumnChart" to { DefaultColumnChart() }, + "ColumnChart Long Scrollable" to { DefaultColumnChartLongScrollable() }, + "ColumnChart Long Scrollable with initial scroll end" to { DefaultColumnChartLongScrollableEnd() }, + "ColumnChart Long Not Scrollable" to { DefaultColumnChartLongNonScrollable() }, + ) + + @get:Rule + public val paparazzi: Paparazzi = Paparazzi(deviceConfig = lightConfig) + + private fun List Unit>>.snapshotAll() { + forEach { (name, composable) -> + paparazzi.snapshot(name) { + composable() + } + } + } + + @Test + public fun `Test default charts in NOT NIGHT`() { + defaultCharts.snapshotAll() + } + + @Test + public fun `Test default charts in NIGHT`() { + paparazzi.unsafeUpdateConfig(nightConfig) + defaultCharts.snapshotAll() + } +} diff --git a/settings.gradle b/sample/src/test/kotlin/com/patrykandpatrick/vico/sample/paparazzi/PaparazziExtensions.kt similarity index 62% rename from settings.gradle rename to sample/src/test/kotlin/com/patrykandpatrick/vico/sample/paparazzi/PaparazziExtensions.kt index 16f8ebed6..9e6fa8696 100644 --- a/settings.gradle +++ b/sample/src/test/kotlin/com/patrykandpatrick/vico/sample/paparazzi/PaparazziExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,11 @@ * limitations under the License. */ -rootProject.name = "Vico" +package com.patrykandpatrick.vico.sample.paparazzi -include ":sample" -include ":vico" -include ":vico:views" -include ":vico:compose" -include ":vico:core" -include ":vico:compose-m2" -include ":vico:compose-m3" +import app.cash.paparazzi.DeviceConfig +import com.android.resources.NightMode + +internal val lightConfig = DeviceConfig(screenWidth = 1080, screenHeight = 800) + +internal val nightConfig = lightConfig.copy(nightMode = NightMode.NIGHT) diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart.png new file mode 100644 index 000000000..ecd6726c3 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f99772eebdc358be43ba37e27cdde42029b2fa56c1c39244e159c4a0a033481 +size 87121 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart_long_not_scrollable.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart_long_not_scrollable.png new file mode 100644 index 000000000..c5859b95d --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart_long_not_scrollable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1569faa59ad765fe2205a474cbf48e8add925142488ccf822a44365f047323a0 +size 106875 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart_long_scrollable.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart_long_scrollable.png new file mode 100644 index 000000000..c5859b95d --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart_long_scrollable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1569faa59ad765fe2205a474cbf48e8add925142488ccf822a44365f047323a0 +size 106875 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart_long_scrollable_with_initial_scroll_end.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart_long_scrollable_with_initial_scroll_end.png new file mode 100644 index 000000000..c5859b95d --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_columnchart_long_scrollable_with_initial_scroll_end.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1569faa59ad765fe2205a474cbf48e8add925142488ccf822a44365f047323a0 +size 106875 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart.png new file mode 100644 index 000000000..c0d44adbc --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1983764174bb1fdcb5f440fcb56043b5fa703517ea49d9880a0d86bc0220e61a +size 110811 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart_long_not_scrollable.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart_long_not_scrollable.png new file mode 100644 index 000000000..d6ce43e40 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart_long_not_scrollable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20a9a746115244d4ffcc7cfb231a5de42b94d522003f3d0a6aa5addd4bc30b37 +size 153408 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart_long_scrollable.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart_long_scrollable.png new file mode 100644 index 000000000..d6ce43e40 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart_long_scrollable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20a9a746115244d4ffcc7cfb231a5de42b94d522003f3d0a6aa5addd4bc30b37 +size 153408 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart_long_scrollable_with_initial_scroll_end.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart_long_scrollable_with_initial_scroll_end.png new file mode 100644 index 000000000..d6ce43e40 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NIGHT_linechart_long_scrollable_with_initial_scroll_end.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20a9a746115244d4ffcc7cfb231a5de42b94d522003f3d0a6aa5addd4bc30b37 +size 153408 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart.png new file mode 100644 index 000000000..80f976113 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f28913e1d05e9e7b17431b06b4020b8811fd136e27100f25187e1a5c0c99c02d +size 80073 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart_long_not_scrollable.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart_long_not_scrollable.png new file mode 100644 index 000000000..fe5d4cee3 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart_long_not_scrollable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:350f5b976bb4aeb5a3f354423e7465afea05a1d4e403406f53574de10dfebd54 +size 97131 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart_long_scrollable.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart_long_scrollable.png new file mode 100644 index 000000000..fe5d4cee3 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart_long_scrollable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:350f5b976bb4aeb5a3f354423e7465afea05a1d4e403406f53574de10dfebd54 +size 97131 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart_long_scrollable_with_initial_scroll_end.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart_long_scrollable_with_initial_scroll_end.png new file mode 100644 index 000000000..fe5d4cee3 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_columnchart_long_scrollable_with_initial_scroll_end.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:350f5b976bb4aeb5a3f354423e7465afea05a1d4e403406f53574de10dfebd54 +size 97131 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart.png new file mode 100644 index 000000000..9ea1dce0c --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3847e8b30f494ba84100b58848cbb0e5c7a130d343b6397884d1542b9d000faf +size 95928 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart_long_not_scrollable.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart_long_not_scrollable.png new file mode 100644 index 000000000..6249d11a8 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart_long_not_scrollable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15b6ed782073a30065b585166a259470e3fbf129812fdccf750b63c48ce5089c +size 127565 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart_long_scrollable.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart_long_scrollable.png new file mode 100644 index 000000000..6249d11a8 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart_long_scrollable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15b6ed782073a30065b585166a259470e3fbf129812fdccf750b63c48ce5089c +size 127565 diff --git a/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart_long_scrollable_with_initial_scroll_end.png b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart_long_scrollable_with_initial_scroll_end.png new file mode 100644 index 000000000..6249d11a8 --- /dev/null +++ b/sample/src/test/snapshots/images/com.patrykandpatrick.vico.sample_PaparazziTest_Test default charts in NOT NIGHT_linechart_long_scrollable_with_initial_scroll_end.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15b6ed782073a30065b585166a259470e3fbf129812fdccf750b63c48ce5089c +size 127565 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..821eeb011 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.gradle.api.initialization.resolve.RepositoriesMode + +pluginManagement.repositories { + google() + gradlePluginPortal() + mavenCentral() +} + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositories { + google() + mavenCentral() + mavenLocal() + } +} + +rootProject.name = "Vico" + +include("sample", "vico", "vico:compose", "vico:compose-m2", "vico:compose-m3", "vico:core", "vico:views") diff --git a/versions.gradle b/versions.gradle index 3d5f6f6e3..ff543d727 100644 --- a/versions.gradle +++ b/versions.gradle @@ -18,7 +18,7 @@ ext { library = [ groupId : "com.patrykandpatrick.vico", - version_name : "1.12.0", + version_name : "1.13.0", version_code : 1, target_sdk : 34, min_sdk : 16, diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/Charts.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/Charts.kt index 64bc7bff7..1e5b796a5 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/Charts.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/Charts.kt @@ -16,6 +16,7 @@ package com.patrykandpatrick.vico.compose.chart +import android.annotation.SuppressLint import android.graphics.RectF import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.Canvas @@ -23,11 +24,12 @@ import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableFloatState import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -49,7 +51,9 @@ import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState import com.patrykandpatrick.vico.compose.extension.chartTouchEvent import com.patrykandpatrick.vico.compose.gesture.OnZoom import com.patrykandpatrick.vico.compose.layout.getMeasureContext -import com.patrykandpatrick.vico.compose.state.MutableSharedState +import com.patrykandpatrick.vico.compose.state.component1 +import com.patrykandpatrick.vico.compose.state.component2 +import com.patrykandpatrick.vico.compose.state.component3 import com.patrykandpatrick.vico.compose.style.currentChartStyle import com.patrykandpatrick.vico.core.DEF_MAX_ZOOM import com.patrykandpatrick.vico.core.DEF_MIN_ZOOM @@ -58,6 +62,7 @@ import com.patrykandpatrick.vico.core.axis.AxisManager import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisRenderer import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.chartDrawContext import com.patrykandpatrick.vico.core.chart.draw.drawMarker import com.patrykandpatrick.vico.core.chart.draw.getAutoZoom @@ -65,6 +70,9 @@ import com.patrykandpatrick.vico.core.chart.draw.getMaxScrollDistance import com.patrykandpatrick.vico.core.chart.edges.FadingEdges import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp +import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.chart.values.toChartValuesProvider import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartModelProducer import com.patrykandpatrick.vico.core.extension.set @@ -75,6 +83,9 @@ import com.patrykandpatrick.vico.core.marker.Marker import com.patrykandpatrick.vico.core.marker.MarkerVisibilityChangeListener import com.patrykandpatrick.vico.core.model.Point import com.patrykandpatrick.vico.core.scroll.ScrollListener +import com.patrykandpatrick.vico.core.util.ValueWrapper +import com.patrykandpatrick.vico.core.util.getValue +import com.patrykandpatrick.vico.core.util.setValue import kotlinx.coroutines.launch /** @@ -103,6 +114,7 @@ import kotlinx.coroutines.launch * @param horizontalLayout defines how the chart’s content is positioned horizontally. * @param getXStep overrides the _x_ step (the difference between the _x_ values of neighboring major entries). If this * is null, the default _x_ step ([ChartEntryModel.xGcd]) is used. + * @param placeholder shown when no [ChartEntryModel] is available. */ @Composable public fun Chart( @@ -125,20 +137,19 @@ public fun Chart( chartScrollState: ChartScrollState = rememberChartScrollState(), horizontalLayout: HorizontalLayout = HorizontalLayout.segmented(), getXStep: ((Model) -> Float)? = null, + placeholder: @Composable BoxScope.() -> Unit = {}, ) { - val modelState: MutableSharedState = chartModelProducer.collectAsState( - chartKey = chart, - producerKey = chartModelProducer, - animationSpec = diffAnimationSpec, - runInitialAnimation = runInitialAnimation, - ) + val chartValuesManager = remember(chart) { ChartValuesManager() } + val chartEntryModelWrapper by chartModelProducer + .collectAsState(chart, chartModelProducer, diffAnimationSpec, runInitialAnimation, chartValuesManager, getXStep) + val (chartEntryModel, previousChartEntryModel, chartValuesProvider) = chartEntryModelWrapper ChartBox(modifier = modifier) { - modelState.value?.also { model -> + if (chartEntryModel != null) { ChartImpl( chart = chart, - model = model, - oldModel = modelState.previousValue, + model = chartEntryModel, + oldModel = previousChartEntryModel, startAxis = startAxis, topAxis = topAxis, endAxis = endAxis, @@ -152,81 +163,14 @@ public fun Chart( autoScaleUp = autoScaleUp, chartScrollState = chartScrollState, horizontalLayout = horizontalLayout, - getXStep = getXStep, + chartValuesProvider = chartValuesProvider, ) + } else { + placeholder() } } } -/** - * Displays a chart. - * - * This function accepts a [ChartEntryModel]. For dynamic data, use the function overload that accepts a - * [ChartModelProducer] instance. - * - * @param chart the chart itself (excluding axes, markers, etc.). You can use [lineChart] or [columnChart], or provide a - * custom [Chart] implementation. - * @param model the [ChartEntryModel] for the chart. - * @param modifier the modifier to be applied to the chart. - * @param startAxis the axis displayed at the start of the chart. - * @param topAxis the axis displayed at the top of the chart. - * @param endAxis the axis displayed at the end of the chart. - * @param bottomAxis the axis displayed at the bottom of the chart. - * @param marker appears when the chart is touched, highlighting the entry or entries nearest to the touch point. - * @param markerVisibilityChangeListener allows for listening to [marker] visibility changes. - * @param legend an optional legend for the chart. - * @param isZoomEnabled whether zooming in and out is enabled. - * @param fadingEdges applies a horizontal fade to the edges of the chart area for scrollable charts. - * @param autoScaleUp defines whether the content of the chart should be scaled up when the dimensions are such that, at - * a scale factor of 1, an empty space would be visible near the end edge of the chart. - * @param chartScrollState houses information on the chart’s scroll state. Allows for programmatic scrolling. - * @param horizontalLayout defines how the chart’s content is positioned horizontally. - * @param getXStep overrides the _x_ step (the difference between the _x_ values of neighboring major entries). If this - * is null, the default _x_ step ([ChartEntryModel.xGcd]) is used. - */ -@Deprecated(message = "Use `chartScrollSpec` to enable or disable scrolling.", level = DeprecationLevel.ERROR) -@Composable -public fun Chart( - chart: Chart, - model: Model, - modifier: Modifier = Modifier, - startAxis: AxisRenderer? = null, - topAxis: AxisRenderer? = null, - endAxis: AxisRenderer? = null, - bottomAxis: AxisRenderer? = null, - marker: Marker? = null, - markerVisibilityChangeListener: MarkerVisibilityChangeListener? = null, - legend: Legend? = null, - isHorizontalScrollEnabled: Boolean, - isZoomEnabled: Boolean = true, - fadingEdges: FadingEdges? = null, - autoScaleUp: AutoScaleUp = AutoScaleUp.Full, - chartScrollState: ChartScrollState = rememberChartScrollState(), - horizontalLayout: HorizontalLayout = HorizontalLayout.segmented(), - getXStep: ((Model) -> Float)? = null, -) { - ChartBox(modifier = modifier) { - ChartImpl( - chart = chart, - model = model, - startAxis = startAxis, - topAxis = topAxis, - endAxis = endAxis, - bottomAxis = bottomAxis, - marker = marker, - markerVisibilityChangeListener = markerVisibilityChangeListener, - legend = legend, - isZoomEnabled = isZoomEnabled, - chartScrollSpec = rememberChartScrollSpec(isScrollEnabled = isHorizontalScrollEnabled), - fadingEdges = fadingEdges, - autoScaleUp = autoScaleUp, - chartScrollState = chartScrollState, - horizontalLayout = horizontalLayout, - getXStep = getXStep, - ) - } -} - /** * Displays a chart. * @@ -257,6 +201,7 @@ public fun Chart( * is null, the default _x_ step ([ChartEntryModel.xGcd]) is used. */ @Composable +@SuppressLint("RememberReturnType") public fun Chart( chart: Chart, model: Model, @@ -277,6 +222,11 @@ public fun Chart( horizontalLayout: HorizontalLayout = HorizontalLayout.segmented(), getXStep: ((Model) -> Float)? = null, ) { + val chartValuesManager = remember(chart) { ChartValuesManager() } + remember(chartValuesManager, model, getXStep) { + chartValuesManager.resetChartValues() + chart.updateChartValues(chartValuesManager, model, getXStep?.invoke(model)) + } ChartBox(modifier = modifier) { ChartImpl( chart = chart, @@ -295,7 +245,7 @@ public fun Chart( autoScaleUp = autoScaleUp, chartScrollState = chartScrollState, horizontalLayout = horizontalLayout, - getXStep = getXStep, + chartValuesProvider = chartValuesManager.toChartValuesProvider(), ) } } @@ -319,7 +269,7 @@ internal fun ChartImpl( autoScaleUp: AutoScaleUp, chartScrollState: ChartScrollState = rememberChartScrollState(), horizontalLayout: HorizontalLayout, - getXStep: ((Model) -> Float)?, + chartValuesProvider: ChartValuesProvider, ) { val axisManager = remember { AxisManager() } val bounds = remember { RectF() } @@ -331,6 +281,7 @@ internal fun ChartImpl( bounds, horizontalLayout, with(LocalContext.current) { ::spToPx }, + chartValuesProvider, ) val scrollListener = rememberScrollListener(markerTouchPoint) val lastMarkerEntryModels = remember { mutableStateOf(emptyList()) } @@ -342,6 +293,8 @@ internal fun ChartImpl( val elevationOverlayColor = currentChartStyle.elevationOverlayColor.toArgb() val (wasMarkerVisible, setWasMarkerVisible) = remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() + var previousModelID by remember { ValueWrapper(model.id) } + val horizontalDimensions = remember { MutableHorizontalDimensions() } val onZoom = rememberZoomState( zoom = zoom, @@ -351,14 +304,6 @@ internal fun ChartImpl( chartBounds = chart.bounds, ) - LaunchedEffect(key1 = model.id) { - chartScrollSpec.performAutoScroll( - model = model, - oldModel = oldModel, - chartScrollState = chartScrollState, - ) - } - Canvas( modifier = Modifier .fillMaxSize() @@ -374,20 +319,29 @@ internal fun ChartImpl( ), ) { bounds.set(left = 0, top = 0, right = size.width, bottom = size.height) - chart.updateChartValues(measureContext.chartValuesManager, model, getXStep?.invoke(model)) - val horizontalDimensions = chart.getHorizontalDimensions(measureContext, model) + horizontalDimensions.clear() + chart.updateHorizontalDimensions(measureContext, horizontalDimensions, model) - val chartBounds = virtualLayout.setBounds( - context = measureContext, - contentBounds = bounds, - chart = chart, - legend = legend, - horizontalDimensions = horizontalDimensions, - marker, - ) + startAxis?.updateHorizontalDimensions(measureContext, horizontalDimensions) + topAxis?.updateHorizontalDimensions(measureContext, horizontalDimensions) + endAxis?.updateHorizontalDimensions(measureContext, horizontalDimensions) + bottomAxis?.updateHorizontalDimensions(measureContext, horizontalDimensions) - if (chartBounds.isEmpty) return@Canvas + if ( + virtualLayout + .setBounds( + context = measureContext, + contentBounds = bounds, + chart = chart, + legend = legend, + horizontalDimensions = horizontalDimensions, + marker, + ) + .isEmpty + ) { + return@Canvas + } var finalZoom = zoom.floatValue @@ -402,6 +356,11 @@ internal fun ChartImpl( zoom = finalZoom, ) + if (model.id != previousModelID) { + coroutineScope.launch { chartScrollSpec.performAutoScroll(model, oldModel, chartScrollState) } + previousModelID = model.id + } + chartScrollState.handleInitialScroll(initialScroll = chartScrollSpec.initialScroll) val chartDrawContext = chartDrawContext( @@ -452,7 +411,7 @@ internal fun ChartBox( content: @Composable BoxScope.() -> Unit, ) { Box( - modifier = modifier.height(DefaultDimens.CHART_HEIGHT.dp), + modifier = modifier.height(DefaultDimens.CHART_HEIGHT.dp).fillMaxWidth(), content = content, ) } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnChart.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnChart.kt index 133f77dcb..b55bec4b4 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnChart.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnChart.kt @@ -25,6 +25,7 @@ import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisRenderer import com.patrykandpatrick.vico.core.chart.column.ColumnChart import com.patrykandpatrick.vico.core.chart.column.ColumnChart.MergeMode +import com.patrykandpatrick.vico.core.chart.column.ColumnChartDrawingModel import com.patrykandpatrick.vico.core.chart.composed.ComposedChart import com.patrykandpatrick.vico.core.chart.decoration.Decoration import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider @@ -33,6 +34,8 @@ import com.patrykandpatrick.vico.core.component.shape.LineComponent import com.patrykandpatrick.vico.core.component.text.TextComponent import com.patrykandpatrick.vico.core.component.text.VerticalPosition import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.diff.DefaultDrawingModelInterpolator +import com.patrykandpatrick.vico.core.entry.diff.DrawingModelInterpolator import com.patrykandpatrick.vico.core.formatter.ValueFormatter import com.patrykandpatrick.vico.core.marker.Marker @@ -54,6 +57,7 @@ import com.patrykandpatrick.vico.core.marker.Marker * @param axisValuesOverrider overrides the minimum and maximum x-axis and y-axis values. * @param targetVerticalAxisPosition if this is set, any [AxisRenderer] with an [AxisPosition] equal to the provided * value will use the [ChartValues] provided by this chart. This is meant to be used with [ComposedChart]. + * @param drawingModelInterpolator interpolates the [ColumnChart]’s [ColumnChartDrawingModel]s. * * @see Chart * @see ColumnChart @@ -72,6 +76,8 @@ public fun columnChart( dataLabelValueFormatter: ValueFormatter = currentChartStyle.columnChart.dataLabelValueFormatter, dataLabelRotationDegrees: Float = currentChartStyle.columnChart.dataLabelRotationDegrees, axisValuesOverrider: AxisValuesOverrider? = null, + drawingModelInterpolator: DrawingModelInterpolator = + remember { DefaultDrawingModelInterpolator() }, ): ColumnChart = remember { ColumnChart() }.apply { this.columns = columns this.spacingDp = spacing.value @@ -83,68 +89,7 @@ public fun columnChart( this.dataLabelRotationDegrees = dataLabelRotationDegrees this.axisValuesOverrider = axisValuesOverrider this.targetVerticalAxisPosition = targetVerticalAxisPosition - decorations?.also(::setDecorations) - persistentMarkers?.also(::setPersistentMarkers) -} - -/** - * Creates a [ColumnChart]. - * - * @param columns the [LineComponent] instances to use for columns. This list is iterated through as many times - * as necessary for each column collection. If the list contains a single element, all columns have the same appearance. - * @param spacing the distance between neighboring column collections. - * @param innerSpacing the distance between neighboring grouped columns. - * @param mergeMode defines how columns should be drawn in column collections. - * @param minX the minimum value shown on the x-axis. If not null, this overrides [ChartEntryModel.minX]. - * @param maxX the maximum value shown on the x-axis. If not null, this overrides [ChartEntryModel.maxX]. - * @param minY the minimum value shown on the y-axis. If not null, this overrides [ChartEntryModel.minY]. - * @param maxY the maximum value shown on the y-axis. If not null, this overrides [ChartEntryModel.maxY]. - * @param decorations the list of [Decoration]s that will be added to the [ColumnChart]. - * @param persistentMarkers maps x-axis values to persistent [Marker]s. - * @param targetVerticalAxisPosition if this is set, any [AxisRenderer] with an [AxisPosition] equal to the provided - * value will use the [ChartValues] provided by this chart. This is meant to be used with [ComposedChart]. - * @param dataLabel an optional [TextComponent] to use for data labels. - * @param dataLabelVerticalPosition the vertical position of data labels relative to the top of their - * respective columns. - * @param dataLabelValueFormatter the [ValueFormatter] to use for data labels. - * @param dataLabelRotationDegrees the rotation of data labels (in degrees). - * - * @see Chart - * @see ColumnChart - */ -@Deprecated(message = "Axis values should be overridden `AxisValuesOverrider`.", level = DeprecationLevel.ERROR) -@Suppress("DEPRECATION_ERROR") -@Composable -public fun columnChart( - columns: List = currentChartStyle.columnChart.columns, - spacing: Dp = currentChartStyle.columnChart.outsideSpacing, - innerSpacing: Dp = currentChartStyle.columnChart.innerSpacing, - mergeMode: MergeMode = currentChartStyle.columnChart.mergeMode, - minX: Float? = null, - maxX: Float? = null, - minY: Float? = null, - maxY: Float? = null, - decorations: List? = null, - persistentMarkers: Map? = null, - targetVerticalAxisPosition: AxisPosition.Vertical? = null, - dataLabel: TextComponent? = currentChartStyle.columnChart.dataLabel, - dataLabelVerticalPosition: VerticalPosition = currentChartStyle.columnChart.dataLabelVerticalPosition, - dataLabelValueFormatter: ValueFormatter = currentChartStyle.columnChart.dataLabelValueFormatter, - dataLabelRotationDegrees: Float = currentChartStyle.columnChart.dataLabelRotationDegrees, -): ColumnChart = remember { ColumnChart() }.apply { - this.columns = columns - this.spacingDp = spacing.value - this.innerSpacingDp = innerSpacing.value - this.mergeMode = mergeMode - this.minX = minX - this.maxX = maxX - this.minY = minY - this.maxY = maxY - this.targetVerticalAxisPosition = targetVerticalAxisPosition - this.dataLabel = dataLabel - this.dataLabelVerticalPosition = dataLabelVerticalPosition - this.dataLabelValueFormatter = dataLabelValueFormatter - this.dataLabelRotationDegrees = dataLabelRotationDegrees + this.drawingModelInterpolator = drawingModelInterpolator decorations?.also(::setDecorations) persistentMarkers?.also(::setPersistentMarkers) } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/entry/ChartEntryModelExtensions.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/entry/ChartEntryModelExtensions.kt index bfefefd3e..0c0b2f859 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/entry/ChartEntryModelExtensions.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/entry/ChartEntryModelExtensions.kt @@ -21,45 +21,33 @@ import androidx.compose.animation.core.animate import androidx.compose.animation.core.tween import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalInspectionMode -import com.patrykandpatrick.vico.compose.state.MutableSharedState -import com.patrykandpatrick.vico.compose.state.mutableSharedStateOf +import com.patrykandpatrick.vico.compose.state.ChartEntryModelWrapper +import com.patrykandpatrick.vico.compose.state.ChartEntryModelWrapperState import com.patrykandpatrick.vico.core.Animation -import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel +import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.chart.values.toChartValuesProvider import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartModelProducer -import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking /** * The default [AnimationSpec] for difference animations. * - * @see collect + * @see collectAsState */ -public val defaultDiffAnimationSpec: AnimationSpec = tween( - durationMillis = Animation.DIFF_DURATION, -) - -/** - * Observes the data provided by this [ChartModelProducer] and launches an animation for each [ChartEntryModel] update. - * - * @see ChartModelProducer - */ -@Composable -public fun ChartModelProducer.collect( - chartKey: Any, - producerKey: Any, - animationSpec: AnimationSpec? = defaultDiffAnimationSpec, - runInitialAnimation: Boolean = true, -): Model? = collectAsState( - chartKey = chartKey, - producerKey = producerKey, - animationSpec = animationSpec, - runInitialAnimation = runInitialAnimation, -).value +public val defaultDiffAnimationSpec: AnimationSpec = tween(durationMillis = Animation.DIFF_DURATION) /** * Observes the data provided by this [ChartModelProducer] and launches an animation for each [ChartEntryModel] update. @@ -68,52 +56,97 @@ public fun ChartModelProducer.collect( */ @Composable public fun ChartModelProducer.collectAsState( - chartKey: Any, + chart: Chart, producerKey: Any, animationSpec: AnimationSpec? = defaultDiffAnimationSpec, runInitialAnimation: Boolean = true, -): MutableSharedState { - val model: MutableSharedState = remember(key1 = chartKey, key2 = producerKey) { - mutableSharedStateOf(null) - } - + chartValuesManager: ChartValuesManager, + getXStep: ((Model) -> Float)?, + dispatcher: CoroutineDispatcher = Dispatchers.Default, +): State> { + val chartEntryModelWrapperState = remember(chart, producerKey) { ChartEntryModelWrapperState() } + val modelTransformerProvider = remember(chart) { chart.modelTransformerProvider } + val extraStore = remember(chart) { MutableExtraStore() } val scope = rememberCoroutineScope() val isInPreview = LocalInspectionMode.current - DisposableEffect(chartKey, producerKey, runInitialAnimation, isInPreview) { - var animationJob: Job? = null - val listener = { - if (animationSpec != null && !isInPreview && (model.value != null || runInitialAnimation)) { - animationJob?.cancel() - animationJob = scope.launch { + DisposableEffect(chart, producerKey, runInitialAnimation, isInPreview) { + var mainAnimationJob: Job? = null + var animationFrameJob: Job? = null + var finalAnimationFrameJob: Job? = null + var isAnimationRunning: Boolean + var isAnimationFrameGenerationRunning = false + var chartValuesProvider: ChartValuesProvider = ChartValuesProvider.Empty + val startAnimation: (transformModel: suspend (key: Any, fraction: Float) -> Unit) -> Unit = { transformModel -> + if (animationSpec != null && !isInPreview && + (chartEntryModelWrapperState.value.chartEntryModel != null || runInitialAnimation) + ) { + isAnimationRunning = true + mainAnimationJob = scope.launch(dispatcher) { animate( initialValue = Animation.range.start, targetValue = Animation.range.endInclusive, animationSpec = animationSpec, - ) { value, _ -> - if (animationJob?.isActive == true) { - progressModel(chartKey, value) + ) { fraction, _ -> + when { + !isAnimationRunning -> return@animate + !isAnimationFrameGenerationRunning -> { + isAnimationFrameGenerationRunning = true + animationFrameJob = scope.launch(dispatcher) { + transformModel(chart, fraction) + isAnimationFrameGenerationRunning = false + } + } + fraction == 1f -> { + finalAnimationFrameJob = scope.launch(dispatcher) { + animationFrameJob?.cancelAndJoin() + transformModel(chart, fraction) + isAnimationFrameGenerationRunning = false + } + } } } } } else { - progressModel(chartKey, Animation.range.endInclusive) + finalAnimationFrameJob = scope.launch(dispatcher) { + transformModel(chart, Animation.range.endInclusive) + } } } - registerForUpdates( - key = chartKey, - updateListener = listener, - getOldModel = { model.value }, - ) { updatedModel -> - model.value = updatedModel + scope.launch(dispatcher) { + registerForUpdates( + key = chart, + cancelAnimation = { + runBlocking { + mainAnimationJob?.cancelAndJoin() + animationFrameJob?.cancelAndJoin() + finalAnimationFrameJob?.cancelAndJoin() + } + isAnimationRunning = false + isAnimationFrameGenerationRunning = false + }, + startAnimation = startAnimation, + getOldModel = { chartEntryModelWrapperState.value.chartEntryModel }, + modelTransformerProvider = modelTransformerProvider, + extraStore = extraStore, + updateChartValues = { model -> + chartValuesManager.resetChartValues() + if (model != null) { + chart.updateChartValues(chartValuesManager, model, getXStep?.invoke(model)) + chartValuesManager.toChartValuesProvider() + } else { + ChartValuesProvider.Empty + }.also { provider -> chartValuesProvider = provider } + }, + ) { chartEntryModel -> + chartEntryModelWrapperState.set(chartEntryModel, chartValuesProvider) + } + } + onDispose { + mainAnimationJob?.cancel() + animationFrameJob?.cancel() + finalAnimationFrameJob?.cancel() + unregisterFromUpdates(chart) } - onDispose { unregisterFromUpdates(chartKey) } } - return model + return chartEntryModelWrapperState } - -/** - * Combines two [ChartEntryModel] implementations—the receiver and [other]—into a [ComposedChartEntryModel]. - */ -@Deprecated("Use `com.patrykandpatrick.vico.core.entry.composed.plus` instead.") -public operator fun Model.plus(other: Model): ComposedChartEntryModel = - ComposedChartEntryModelProducer.composedChartEntryModelOf(listOf(this, other)) diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/layout/HorizontalLayout.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/layout/HorizontalLayout.kt index 79a216c97..f18a0b464 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/layout/HorizontalLayout.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/layout/HorizontalLayout.kt @@ -29,6 +29,13 @@ public fun HorizontalLayout.Companion.segmented(): HorizontalLayout.Segmented = * Creates and returns an instance of [HorizontalLayout.FullWidth]. */ public fun HorizontalLayout.Companion.fullWidth( - startPadding: Dp = 0.dp, - endPadding: Dp = 0.dp, -): HorizontalLayout.FullWidth = HorizontalLayout.FullWidth(startPadding.value, endPadding.value) + scalableStartPadding: Dp = 0.dp, + scalableEndPadding: Dp = 0.dp, + unscalableStartPadding: Dp = 0.dp, + unscalableEndPadding: Dp = 0.dp, +): HorizontalLayout.FullWidth = HorizontalLayout.FullWidth( + scalableStartPadding.value, + scalableEndPadding.value, + unscalableStartPadding.value, + unscalableEndPadding.value, +) diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineChart.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineChart.kt index fac63fac5..85ee10d43 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineChart.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineChart.kt @@ -38,6 +38,7 @@ import com.patrykandpatrick.vico.core.chart.composed.ComposedChart import com.patrykandpatrick.vico.core.chart.decoration.Decoration import com.patrykandpatrick.vico.core.chart.line.LineChart import com.patrykandpatrick.vico.core.chart.line.LineChart.LineSpec +import com.patrykandpatrick.vico.core.chart.line.LineChartDrawingModel import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.component.Component @@ -46,6 +47,8 @@ import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders import com.patrykandpatrick.vico.core.component.text.TextComponent import com.patrykandpatrick.vico.core.component.text.VerticalPosition import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.diff.DefaultDrawingModelInterpolator +import com.patrykandpatrick.vico.core.entry.diff.DrawingModelInterpolator import com.patrykandpatrick.vico.core.formatter.DecimalFormatValueFormatter import com.patrykandpatrick.vico.core.formatter.ValueFormatter import com.patrykandpatrick.vico.core.marker.Marker @@ -61,6 +64,7 @@ import com.patrykandpatrick.vico.core.marker.Marker * @param axisValuesOverrider overrides the minimum and maximum x-axis and y-axis values. * @param targetVerticalAxisPosition if this is set, any [AxisRenderer] with an [AxisPosition] equal to the provided * value will use the [ChartValues] provided by this chart. This is meant to be used with [ComposedChart]. + * @param drawingModelInterpolator interpolates the [LineChart]’s [LineChartDrawingModel]s. * * @see Chart * @see ColumnChart @@ -73,54 +77,14 @@ public fun lineChart( persistentMarkers: Map? = null, axisValuesOverrider: AxisValuesOverrider? = null, targetVerticalAxisPosition: AxisPosition.Vertical? = null, + drawingModelInterpolator: DrawingModelInterpolator = + remember { DefaultDrawingModelInterpolator() }, ): LineChart = remember { LineChart() }.apply { this.lines = lines this.spacingDp = spacing.value this.axisValuesOverrider = axisValuesOverrider this.targetVerticalAxisPosition = targetVerticalAxisPosition - decorations?.also(::setDecorations) - persistentMarkers?.also(::setPersistentMarkers) -} - -/** - * Creates a [LineChart]. - * - * @param lines the [LineChart.LineSpec]s to use for the lines. This list is iterated through as many times as there - * are lines. - * @param spacing the distance between neighboring major entries’ points. - * @param minX the minimum value shown on the x-axis. If not null, this overrides [ChartEntryModel.minX]. - * @param maxX the maximum value shown on the x-axis. If not null, this overrides [ChartEntryModel.maxX]. - * @param minY the minimum value shown on the y-axis. If not null, this overrides [ChartEntryModel.minY]. - * @param maxY the maximum value shown on the y-axis. If not null, this overrides [ChartEntryModel.maxY]. - * @param decorations the list of [Decoration]s that will be added to the [LineChart]. - * @param persistentMarkers maps x-axis values to persistent [Marker]s. - * @param targetVerticalAxisPosition if this is set, any [AxisRenderer] with an [AxisPosition] equal to the provided - * value will use the [ChartValues] provided by this chart. This is meant to be used with [ComposedChart]. - * - * @see Chart - * @see ColumnChart - */ -@Deprecated(message = "Axis values should be overridden via `AxisValuesOverrider`.", level = DeprecationLevel.ERROR) -@Suppress("DEPRECATION_ERROR") -@Composable -public fun lineChart( - lines: List = currentChartStyle.lineChart.lines, - spacing: Dp = currentChartStyle.lineChart.spacing, - minX: Float? = null, - maxX: Float? = null, - minY: Float? = null, - maxY: Float? = null, - decorations: List? = null, - persistentMarkers: Map? = null, - targetVerticalAxisPosition: AxisPosition.Vertical? = null, -): LineChart = remember { LineChart() }.apply { - this.lines = lines - this.spacingDp = spacing.value - this.minX = minX - this.maxX = maxX - this.minY = minY - this.maxY = maxY - this.targetVerticalAxisPosition = targetVerticalAxisPosition + this.drawingModelInterpolator = drawingModelInterpolator decorations?.also(::setDecorations) persistentMarkers?.also(::setPersistentMarkers) } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollState.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollState.kt index 44a0b0ab5..1f74a6009 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollState.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollState.kt @@ -20,8 +20,7 @@ import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import com.patrykandpatrick.vico.compose.chart.Chart import com.patrykandpatrick.vico.core.extension.rangeWith @@ -35,8 +34,8 @@ import kotlin.math.abs */ public class ChartScrollState : ScrollableState, ScrollListenerHost { - private val _value: MutableState = mutableStateOf(0f) - private val _maxValue: MutableState = mutableStateOf(0f) + private val _value = mutableFloatStateOf(0f) + private val _maxValue = mutableFloatStateOf(0f) private val scrollListeners: MutableSet = mutableSetOf() private var initialScrollHandled: Boolean = false @@ -44,10 +43,10 @@ public class ChartScrollState : ScrollableState, ScrollListenerHost { * The current scroll amount (in pixels). */ public var value: Float - get() = _value.value + get() = _value.floatValue private set(newValue) { val oldValue = value - _value.value = newValue + _value.floatValue = newValue scrollListeners.forEach { scrollListener -> scrollListener.onValueChanged(oldValue, newValue) } } @@ -55,10 +54,10 @@ public class ChartScrollState : ScrollableState, ScrollListenerHost { * The maximum scroll amount (in pixels). */ public var maxValue: Float - get() = _maxValue.value + get() = _maxValue.floatValue internal set(newMaxValue) { val oldMaxValue = maxValue - _maxValue.value = newMaxValue + _maxValue.floatValue = newMaxValue if (abs(value) > abs(newMaxValue)) value = newMaxValue scrollListeners.forEach { scrollListener -> scrollListener.onMaxValueChanged(oldMaxValue, newMaxValue) } } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/layout/MeasureContextExtensions.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/layout/MeasureContextExtensions.kt index f7975b34e..b20687319 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/layout/MeasureContextExtensions.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/layout/MeasureContextExtensions.kt @@ -23,6 +23,8 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout +import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.context.MutableMeasureContext @@ -33,6 +35,7 @@ import com.patrykandpatrick.vico.core.context.MutableMeasureContext * @param canvasBounds the bounds of the canvas that will be used to draw the chart and its components. * @param horizontalLayout defines how the chart’s content is positioned horizontally. * @param spToPx converts dimensions from sp to px. + * @param chartValuesProvider provides the chart’s [ChartValues] instances. */ @Composable public fun getMeasureContext( @@ -40,17 +43,20 @@ public fun getMeasureContext( canvasBounds: RectF, horizontalLayout: HorizontalLayout, spToPx: (Float) -> Float, + chartValuesProvider: ChartValuesProvider, ): MutableMeasureContext = remember { MutableMeasureContext( canvasBounds = canvasBounds, density = 0f, isLtr = true, - isHorizontalScrollEnabled = isHorizontalScrollEnabled, - horizontalLayout = horizontalLayout, spToPx = spToPx, + chartValuesProvider = chartValuesProvider, ) }.apply { this.density = LocalDensity.current.density this.isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr this.isHorizontalScrollEnabled = isHorizontalScrollEnabled + this.horizontalLayout = horizontalLayout + this.spToPx = spToPx + this.chartValuesProvider = chartValuesProvider } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelWrapper.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelWrapper.kt new file mode 100644 index 000000000..dc5580210 --- /dev/null +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelWrapper.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.compose.state + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.entry.ChartEntryModel + +/** + * Holds a chart’s current [ChartEntryModel] ([chartEntryModel]), previous [ChartEntryModel] + * ([previousChartEntryModel]), and [ChartValuesProvider] ([chartValuesProvider]). + */ +@Immutable +public class ChartEntryModelWrapper( + public val chartEntryModel: T? = null, + public val previousChartEntryModel: T? = null, + public val chartValuesProvider: ChartValuesProvider = ChartValuesProvider.Empty, +) + +/** + * Returns [ChartEntryModelWrapper.chartEntryModel]. + */ +public operator fun ChartEntryModelWrapper.component1(): T? = chartEntryModel + +/** + * Returns [ChartEntryModelWrapper.previousChartEntryModel]. + */ +public operator fun ChartEntryModelWrapper.component2(): T? = previousChartEntryModel + +/** + * Returns [ChartEntryModelWrapper.chartValuesProvider]. + */ +public operator fun ChartEntryModelWrapper.component3(): ChartValuesProvider = + chartValuesProvider + +internal class ChartEntryModelWrapperState : State> { + private var previousChartEntryModel: T? = null + + override var value by mutableStateOf>(ChartEntryModelWrapper()) + private set + + fun set(chartEntryModel: T?, chartValuesProvider: ChartValuesProvider) { + val currentChartEntryModel = value.chartEntryModel + if (chartEntryModel?.id != currentChartEntryModel?.id) previousChartEntryModel = currentChartEntryModel + value = ChartEntryModelWrapper(chartEntryModel, previousChartEntryModel, chartValuesProvider) + } +} diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/MutableSharedState.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/MutableSharedState.kt deleted file mode 100644 index 118cdc83f..000000000 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/MutableSharedState.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.patrykandpatrick.vico.compose.state - -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf - -/** - * A [MutableState] implementation that stores both its current and previous values. - */ -public class MutableSharedState( - value: T, - previousValue: P, -) : MutableState { - - private val backingState = mutableStateOf(value) - - private var mutablePreviousValue: P = previousValue - - /** - * The previous value. - */ - public val previousValue: P - get() = mutablePreviousValue - - override var value: T - get() = backingState.value - set(value) { - if (value !== this.value) { - mutablePreviousValue = backingState.value - } - backingState.value = value - } - - override fun component1(): T = backingState.component1() - - override fun component2(): (T) -> Unit = backingState.component2() -} - -/** - * Creates a [MutableSharedState] instance with the provided current value and a previous value of `null`. - */ -public fun mutableSharedStateOf(value: T): MutableSharedState = - MutableSharedState( - value = value, - previousValue = null, - ) - -/** - * Creates a [MutableSharedState] with the provided current and previous values. - */ -public fun mutableSharedStateOf( - value: T, - previousValue: P, -): MutableSharedState = - MutableSharedState( - value = value, - previousValue = previousValue, - ) diff --git a/vico/core/build.gradle b/vico/core/build.gradle index d44f20257..8d01976c9 100644 --- a/vico/core/build.gradle +++ b/vico/core/build.gradle @@ -60,6 +60,8 @@ afterEvaluate { dependencies { + implementation libs.androidXAnnotation + implementation libs.coroutinesCore implementation libs.kotlinStdLib testImplementation libs.JUnit testImplementation libs.JUnitExt diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisItemPlacer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisItemPlacer.kt index 2aa9a50f7..b636ec48e 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisItemPlacer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisItemPlacer.kt @@ -45,6 +45,20 @@ public interface AxisItemPlacer { context: ChartDrawContext, ): Boolean = true + /** + * Returns a boolean indicating whether the [HorizontalAxis] should reserve room for a label for + * [ChartValues.minX]. If `true` is returned, indicating that this behavior is desired, then [getLabelValues] + * should request a label for [ChartValues.minX]. + */ + public fun getAddFirstLabelPadding(context: MeasureContext): Boolean + + /** + * Returns a boolean indicating whether the [HorizontalAxis] should reserve room for a label for + * [ChartValues.maxX]. If `true` is returned, indicating that this behavior is desired, then [getLabelValues] + * should request a label for [ChartValues.maxX]. + */ + public fun getAddLastLabelPadding(context: MeasureContext): Boolean + /** * Returns, as a list, the _x_ values for which labels are to be displayed, restricted to [visibleXRange] and * with two extra values on either side (if applicable). @@ -113,9 +127,15 @@ public interface AxisItemPlacer { * [HorizontalLayout.FullWidth], their corresponding ticks and guidelines) to skip from the start. * [shiftExtremeTicks] defines whether ticks whose _x_ values are bounds of the _x_-axis value range should * be shifted to the edges of the axis bounds, to be aligned with the vertical axes. + * [addExtremeLabelPadding] specifies whether, for [HorizontalLayout.FullWidth], padding should be added for + * the first and last labels, ensuring their visibility. */ - public fun default(spacing: Int = 1, offset: Int = 0, shiftExtremeTicks: Boolean = true): Horizontal = - DefaultHorizontalAxisItemPlacer(spacing, offset, shiftExtremeTicks) + public fun default( + spacing: Int = 1, + offset: Int = 0, + shiftExtremeTicks: Boolean = true, + addExtremeLabelPadding: Boolean = false, + ): Horizontal = DefaultHorizontalAxisItemPlacer(spacing, offset, shiftExtremeTicks, addExtremeLabelPadding) } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisRenderer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisRenderer.kt index 6935ab3c9..5a5d19e10 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisRenderer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisRenderer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,10 @@ package com.patrykandpatrick.vico.core.axis import android.graphics.RectF import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter +import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.dimensions.BoundsAware /** @@ -53,4 +55,9 @@ public interface AxisRenderer : BoundsAware, ChartInset * The bounds ([RectF]) passed here define the area where the [AxisRenderer] shouldn’t draw anything. */ public fun setRestrictedBounds(vararg bounds: RectF?) + + /** + * Updates the chart’s [MutableHorizontalDimensions] instance. + */ + public fun updateHorizontalDimensions(context: MeasureContext, horizontalDimensions: MutableHorizontalDimensions) } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/DefaultHorizontalAxisItemPlacer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/DefaultHorizontalAxisItemPlacer.kt index a769ec736..842e4b631 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/DefaultHorizontalAxisItemPlacer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/DefaultHorizontalAxisItemPlacer.kt @@ -28,17 +28,27 @@ internal class DefaultHorizontalAxisItemPlacer( private val spacing: Int, private val offset: Int, private val shiftExtremeTicks: Boolean, + private val addExtremeLabelPadding: Boolean, ) : AxisItemPlacer.Horizontal { override fun getShiftExtremeTicks(context: ChartDrawContext): Boolean = shiftExtremeTicks + override fun getAddFirstLabelPadding(context: MeasureContext) = + context.horizontalLayout is HorizontalLayout.FullWidth && addExtremeLabelPadding && offset == 0 + + override fun getAddLastLabelPadding(context: MeasureContext): Boolean { + val chartValues = context.chartValuesProvider.getChartValues() + return context.horizontalLayout is HorizontalLayout.FullWidth && addExtremeLabelPadding && + (chartValues.maxX - chartValues.minX - chartValues.xStep * offset) % (chartValues.xStep * spacing) == 0f + } + @Suppress("LoopWithTooManyJumpStatements") override fun getLabelValues( context: ChartDrawContext, visibleXRange: ClosedFloatingPointRange, fullXRange: ClosedFloatingPointRange, ): List { - val chartValues = context.chartValuesManager.getChartValues() + val chartValues = context.chartValuesProvider.getChartValues() val remainder = ((visibleXRange.start - chartValues.minX) / chartValues.xStep - offset) % spacing val firstValue = visibleXRange.start + (spacing - remainder) % spacing * chartValues.xStep val minXOffset = chartValues.minX % chartValues.xStep @@ -61,7 +71,7 @@ internal class DefaultHorizontalAxisItemPlacer( horizontalDimensions: HorizontalDimensions, fullXRange: ClosedFloatingPointRange, ): List { - val chartValues = context.chartValuesManager.getChartValues() + val chartValues = context.chartValuesProvider.getChartValues() return listOf(chartValues.minX, (chartValues.minX + chartValues.maxX).half, chartValues.maxX) } @@ -71,7 +81,7 @@ internal class DefaultHorizontalAxisItemPlacer( visibleXRange: ClosedFloatingPointRange, fullXRange: ClosedFloatingPointRange, ): List? { - val chartValues = context.chartValuesManager.getChartValues() + val chartValues = context.chartValuesProvider.getChartValues() return when (context.horizontalLayout) { is HorizontalLayout.Segmented -> { val remainder = (visibleXRange.start - fullXRange.start) % chartValues.xStep diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/HorizontalAxis.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/HorizontalAxis.kt index 321b863b4..80d298364 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/HorizontalAxis.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/HorizontalAxis.kt @@ -22,11 +22,13 @@ import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisRenderer import com.patrykandpatrick.vico.core.axis.setTo import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions +import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext import com.patrykandpatrick.vico.core.chart.insets.Insets import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.component.text.VerticalPosition import com.patrykandpatrick.vico.core.context.MeasureContext +import com.patrykandpatrick.vico.core.extension.ceil import com.patrykandpatrick.vico.core.extension.doubled import com.patrykandpatrick.vico.core.extension.getStart import com.patrykandpatrick.vico.core.extension.half @@ -89,7 +91,7 @@ public class HorizontalAxis( val clipRestoreCount = canvas.save() val tickMarkTop = if (position.isBottom) bounds.top else bounds.bottom - axisThickness - tickLength val tickMarkBottom = tickMarkTop + axisThickness + tickLength - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() canvas.clipRect( bounds.left - itemPlacer.getStartHorizontalAxisInset(this, horizontalDimensions, tickThickness), @@ -114,7 +116,8 @@ public class HorizontalAxis( layoutDirectionMultiplier val previousX = labelValues.getOrNull(index - 1) ?: (fullXRange.start.doubled - x) val nextX = labelValues.getOrNull(index + 1) ?: (fullXRange.endInclusive.doubled - x) - val maxWidth = (min(x - previousX, nextX - x) / chartValues.xStep * horizontalDimensions.xSpacing).toInt() + val maxWidth = + (min(x - previousX, nextX - x) / chartValues.xStep * horizontalDimensions.xSpacing).ceil.toInt() label?.drawText( context = context, @@ -186,7 +189,7 @@ public class HorizontalAxis( val clipRestoreCount = canvas.save() canvas.clipRect(chartBounds) - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() if (lineValues == null) { labelValues.forEach { x -> @@ -224,6 +227,33 @@ public class HorizontalAxis( override fun drawAboveChart(context: ChartDrawContext): Unit = Unit + override fun updateHorizontalDimensions( + context: MeasureContext, + horizontalDimensions: MutableHorizontalDimensions, + ) { + val chartValues = context.chartValuesProvider.getChartValues() + horizontalDimensions.ensureValuesAtLeast( + unscalableStartPadding = label + .takeIf { itemPlacer.getAddFirstLabelPadding(context) } + ?.getWidth( + context = context, + text = valueFormatter.formatValue(chartValues.minX, chartValues), + pad = true, + ) + ?.half + .orZero, + unscalableEndPadding = label + .takeIf { itemPlacer.getAddLastLabelPadding(context) } + ?.getWidth( + context = context, + text = valueFormatter.formatValue(chartValues.maxX, chartValues), + pad = true, + ) + ?.half + .orZero, + ) + } + override fun getInsets( context: MeasureContext, outInsets: Insets, @@ -238,7 +268,7 @@ public class HorizontalAxis( private fun MeasureContext.getFullXRange( horizontalDimensions: HorizontalDimensions, ): ClosedFloatingPointRange = with(horizontalDimensions) { - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() val start = chartValues.minX - startPadding / xSpacing * chartValues.xStep val end = chartValues.maxX + endPadding / xSpacing * chartValues.xStep start..end @@ -248,7 +278,7 @@ public class HorizontalAxis( context: MeasureContext, horizontalDimensions: HorizontalDimensions, ): Float = with(context) { - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() val fullXRange = getFullXRange(horizontalDimensions) when (val constraint = sizeConstraint) { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/DefaultVerticalAxisItemPlacer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/DefaultVerticalAxisItemPlacer.kt index 76618f059..06d723685 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/DefaultVerticalAxisItemPlacer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/DefaultVerticalAxisItemPlacer.kt @@ -49,7 +49,7 @@ internal class DefaultVerticalAxisItemPlacer( position: AxisPosition.Vertical, ): List { if (maxItemCount == 0) return emptyList() - val chartValues = context.chartValuesManager.getChartValues(position) + val chartValues = context.chartValuesProvider.getChartValues(position) return if (chartValues.minY * chartValues.maxY >= 0) { getSimpleLabelValues(axisHeight, maxLabelHeight, chartValues) } else { @@ -61,7 +61,7 @@ internal class DefaultVerticalAxisItemPlacer( context: MeasureContext, position: AxisPosition.Vertical, ): List { - val chartValues = context.chartValuesManager.getChartValues(position) + val chartValues = context.chartValuesProvider.getChartValues(position) return listOf(chartValues.minY, (chartValues.minY + chartValues.maxY).half, chartValues.maxY) } @@ -69,24 +69,30 @@ internal class DefaultVerticalAxisItemPlacer( verticalLabelPosition: VerticalAxis.VerticalLabelPosition, maxLabelHeight: Float, maxLineThickness: Float, - ) = when (verticalLabelPosition) { - VerticalAxis.VerticalLabelPosition.Top -> + ) = when { + maxItemCount == 0 -> 0f + + verticalLabelPosition == VerticalAxis.VerticalLabelPosition.Top -> maxLabelHeight + (if (shiftTopLines) maxLineThickness else -maxLineThickness).half - VerticalAxis.VerticalLabelPosition.Center -> + verticalLabelPosition == VerticalAxis.VerticalLabelPosition.Center -> (max(maxLabelHeight, maxLineThickness) + if (shiftTopLines) maxLineThickness else -maxLineThickness).half - VerticalAxis.VerticalLabelPosition.Bottom -> if (shiftTopLines) maxLineThickness else 0f + else -> if (shiftTopLines) maxLineThickness else 0f } override fun getBottomVerticalAxisInset( verticalLabelPosition: VerticalAxis.VerticalLabelPosition, maxLabelHeight: Float, maxLineThickness: Float, - ): Float = when (verticalLabelPosition) { - VerticalAxis.VerticalLabelPosition.Top -> maxLineThickness - VerticalAxis.VerticalLabelPosition.Center -> (maxOf(maxLabelHeight, maxLineThickness) + maxLineThickness).half - VerticalAxis.VerticalLabelPosition.Bottom -> maxLabelHeight + maxLineThickness.half + ): Float = when { + maxItemCount == 0 -> 0f + verticalLabelPosition == VerticalAxis.VerticalLabelPosition.Top -> maxLineThickness + + verticalLabelPosition == VerticalAxis.VerticalLabelPosition.Center -> + (maxOf(maxLabelHeight, maxLineThickness) + maxLineThickness).half + + else -> maxLabelHeight + maxLineThickness.half } private fun getSimpleLabelValues(axisHeight: Float, maxLabelHeight: Float, chartValues: ChartValues): List { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/VerticalAxis.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/VerticalAxis.kt index 89d6826ca..30337bcf9 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/VerticalAxis.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/VerticalAxis.kt @@ -27,6 +27,7 @@ import com.patrykandpatrick.vico.core.axis.vertical.VerticalAxis.HorizontalLabel import com.patrykandpatrick.vico.core.axis.vertical.VerticalAxis.HorizontalLabelPosition.Outside import com.patrykandpatrick.vico.core.axis.vertical.VerticalAxis.VerticalLabelPosition.Center import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions +import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext import com.patrykandpatrick.vico.core.chart.insets.HorizontalInsets import com.patrykandpatrick.vico.core.chart.insets.Insets @@ -103,7 +104,7 @@ public class VerticalAxis( context: ChartDrawContext, ): Unit = with(context) { var centerY: Float - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) val maxLabelHeight = getMaxLabelHeight() val lineValues = itemPlacer.getLineValues(this, bounds.height(), maxLabelHeight, position) ?: itemPlacer.getLabelValues(this, bounds.height(), maxLabelHeight, position) @@ -146,7 +147,7 @@ public class VerticalAxis( val tickRightX = tickLeftX + axisThickness + tickLength val labelX = if (areLabelsOutsideAtStartOrInsideAtEnd == isLtr) tickLeftX else tickRightX var tickCenterY: Float - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) labelValues.forEach { labelValue -> tickCenterY = bounds.bottom - bounds.height() * (labelValue - chartValues.minY) / chartValues.lengthY + @@ -182,6 +183,11 @@ public class VerticalAxis( } } + override fun updateHorizontalDimensions( + context: MeasureContext, + horizontalDimensions: MutableHorizontalDimensions, + ): Unit = Unit + private fun ChartDrawContext.drawLabel( label: TextComponent, labelText: CharSequence, @@ -289,21 +295,21 @@ public class VerticalAxis( } private fun MeasureContext.getMaxLabelHeight() = label?.let { label -> - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) itemPlacer .getHeightMeasurementLabelValues(this, position) .maxOfOrNull { value -> label.getHeight(this, valueFormatter.formatValue(value, chartValues)) } }.orZero private fun MeasureContext.getMaxLabelWidth(axisHeight: Float) = label?.let { label -> - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) itemPlacer .getWidthMeasurementLabelValues(this, axisHeight, getMaxLabelHeight(), position) .maxOfOrNull { value -> label.getWidth(this, valueFormatter.formatValue(value, chartValues)) } }.orZero private fun ChartDrawContext.getLineCanvasYCorrection(thickness: Float, y: Float): Float { - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) return if (y == chartValues.maxY && itemPlacer.getShiftTopLines(this)) -thickness.half else thickness.half } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt index bc1e9e4bd..a115306c2 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt @@ -110,7 +110,7 @@ public abstract class BaseChart : Chart, Boun context = context, bounds = bounds, markedEntries = markerModel, - chartValuesProvider = chartValuesManager, + chartValuesProvider = chartValuesProvider, ) } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/Chart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/Chart.kt index e206dc58f..c41514734 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/Chart.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/Chart.kt @@ -19,16 +19,20 @@ package com.patrykandpatrick.vico.core.chart import com.patrykandpatrick.vico.core.chart.column.ColumnChart import com.patrykandpatrick.vico.core.chart.composed.ComposedChart import com.patrykandpatrick.vico.core.chart.decoration.Decoration -import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions +import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter import com.patrykandpatrick.vico.core.chart.line.LineChart import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.dimensions.BoundsAware import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.diff.DrawingModel +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore import com.patrykandpatrick.vico.core.marker.Marker internal const val AXIS_VALUES_DEPRECATION_MESSAGE: String = "Axis values should be overridden via " + @@ -178,13 +182,13 @@ public interface Chart : BoundsAware, ChartInsetter { public fun removePersistentMarker(x: Float) /** - * Called to get the [HorizontalDimensions] of this chart. The [HorizontalDimensions] influence the look of various - * parts of the chart. - * - * @param context holds data used for component measurements. - * @param model holds data about the [Chart]’s entries. + * Updates the chart’s [MutableHorizontalDimensions] instance. */ - public fun getHorizontalDimensions(context: MeasureContext, model: Model): HorizontalDimensions + public fun updateHorizontalDimensions( + context: MeasureContext, + horizontalDimensions: MutableHorizontalDimensions, + model: Model, + ) /** * Updates the [ChartValues] stored in the provided [ChartValuesManager] instance to this [Chart]’s [ChartValues]. @@ -194,6 +198,46 @@ public interface Chart : BoundsAware, ChartInsetter { * @param xStep the overridden _x_ step (or `null` if no override has occurred). */ public fun updateChartValues(chartValuesManager: ChartValuesManager, model: Model, xStep: Float?) + + /** + * Provides the [Chart]’s [ModelTransformer]. + */ + public val modelTransformerProvider: ModelTransformerProvider + + /** + * Provides a [Chart]’s [ModelTransformer]. + */ + public interface ModelTransformerProvider { + /** + * Returns the [ModelTransformer]. + */ + public fun getModelTransformer(): ModelTransformer + } + + /** + * Transforms [Model]s into [DrawingModel]s. + */ + public abstract class ModelTransformer { + /** + * Used for writing to and reading from the host’s [ExtraStore]. + */ + protected abstract val key: ExtraStore.Key<*> + + /** + * Prepares the [Chart] for a difference animation. + */ + public abstract fun prepareForTransformation( + oldModel: Model?, + newModel: Model?, + extraStore: MutableExtraStore, + chartValuesProvider: ChartValuesProvider, + ) + + /** + * Carries out the pending difference animation. [fraction] is the animation progress. + */ + public abstract suspend fun transform(extraStore: MutableExtraStore, fraction: Float) + } } /** diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChart.kt index 5fefc0283..65ab4ba2c 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChart.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChart.kt @@ -20,8 +20,8 @@ import com.patrykandpatrick.vico.core.DefaultDimens import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisRenderer import com.patrykandpatrick.vico.core.chart.BaseChart +import com.patrykandpatrick.vico.core.chart.Chart import com.patrykandpatrick.vico.core.chart.composed.ComposedChart -import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext import com.patrykandpatrick.vico.core.chart.forEachInAbsolutelyIndexed @@ -29,6 +29,7 @@ import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.chart.put import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.component.shape.LineComponent import com.patrykandpatrick.vico.core.component.text.TextComponent import com.patrykandpatrick.vico.core.component.text.VerticalPosition @@ -36,6 +37,10 @@ import com.patrykandpatrick.vico.core.component.text.inBounds import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.entry.ChartEntry import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.diff.DefaultDrawingModelInterpolator +import com.patrykandpatrick.vico.core.entry.diff.DrawingModelInterpolator +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore import com.patrykandpatrick.vico.core.extension.doubled import com.patrykandpatrick.vico.core.extension.getRepeating import com.patrykandpatrick.vico.core.extension.getStart @@ -60,6 +65,7 @@ import kotlin.math.abs * respective columns. * @param dataLabelValueFormatter the [ValueFormatter] to use for data labels. * @param dataLabelRotationDegrees the rotation of data labels (in degrees). + * @param drawingModelInterpolator interpolates the [ColumnChart]’s [ColumnChartDrawingModel]s. */ public open class ColumnChart( public var columns: List, @@ -71,6 +77,10 @@ public open class ColumnChart( public var dataLabelVerticalPosition: VerticalPosition = VerticalPosition.Top, public var dataLabelValueFormatter: ValueFormatter = DecimalFormatValueFormatter(), public var dataLabelRotationDegrees: Float = 0f, + public var drawingModelInterpolator: DrawingModelInterpolator< + ColumnChartDrawingModel.ColumnInfo, + ColumnChartDrawingModel, + > = DefaultDrawingModelInterpolator(), ) : BaseChart() { /** @@ -105,6 +115,8 @@ public open class ColumnChart( */ protected val horizontalDimensions: MutableHorizontalDimensions = MutableHorizontalDimensions() + protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() + override val entryLocationMap: HashMap> = HashMap() override fun drawChart( @@ -113,8 +125,9 @@ public open class ColumnChart( ): Unit = with(context) { entryLocationMap.clear() drawChartInternal( - chartValues = chartValuesManager.getChartValues(axisPosition = targetVerticalAxisPosition), + chartValues = chartValuesProvider.getChartValues(axisPosition = targetVerticalAxisPosition), model = model, + drawingModel = model.extraStore.getOrNull(drawingModelKey), ) heightMap.clear() } @@ -122,6 +135,7 @@ public open class ColumnChart( protected open fun ChartDrawContext.drawChartInternal( chartValues: ChartValues, model: ChartEntryModel, + drawingModel: ColumnChartDrawingModel?, ) { val yRange = (chartValues.maxY - chartValues.minY).takeIf { it != 0f } ?: return val heightMultiplier = bounds.height() / yRange @@ -141,7 +155,8 @@ public open class ColumnChart( entryCollection.forEachInAbsolutelyIndexed(chartValues.minX..chartValues.maxX) { entryIndex, entry -> - height = abs(entry.y) * heightMultiplier + val columnInfo = drawingModel?.getOrNull(index)?.get(entry.x) + height = (columnInfo?.height ?: (abs(entry.y) / chartValues.lengthY)) * bounds.height() val xSpacingMultiplier = (entry.x - chartValues.minX) / chartValues.xStep check(xSpacingMultiplier % 1f == 0f) { "Each entry’s x value must be a multiple of the x step." } columnCenterX = drawingStart + @@ -150,20 +165,20 @@ public open class ColumnChart( when (mergeMode) { MergeMode.Stack -> { - val (stackedNegY, stackedPosY) = heightMap.getOrElse(entry.x) { 0f to 0f } + val (stackedNegHeight, stackedPosHeight) = heightMap.getOrElse(entry.x) { 0f to 0f } columnBottom = zeroLinePosition + if (entry.y < 0f) { - height + abs(stackedNegY) * heightMultiplier + height + stackedNegHeight } else { - -stackedPosY * heightMultiplier + -stackedPosHeight } columnTop = (columnBottom - height).coerceAtMost(columnBottom) heightMap[entry.x] = if (entry.y < 0f) { - stackedNegY + entry.y to stackedPosY + stackedNegHeight + height to stackedPosHeight } else { - stackedNegY to stackedPosY + entry.y + stackedNegHeight to stackedPosHeight + height } } @@ -185,7 +200,7 @@ public open class ColumnChart( ) ) { updateMarkerLocationMap(entry, columnSignificantY, columnCenterX, column, entryIndex) - column.drawVertical(this, columnTop, columnBottom, columnCenterX, zoom) + column.drawVertical(this, columnTop, columnBottom, columnCenterX, zoom, drawingModel?.opacity ?: 1f) } if (mergeMode == MergeMode.Grouped) { @@ -266,7 +281,7 @@ public open class ColumnChart( } val text = dataLabelValueFormatter.formatValue( value = dataLabelValue, - chartValues = chartValuesManager.getChartValues(axisPosition = targetVerticalAxisPosition), + chartValues = chartValuesProvider.getChartValues(axisPosition = targetVerticalAxisPosition), ) val dataLabelWidth = textComponent.getWidth( context = this, @@ -332,26 +347,44 @@ public open class ColumnChart( ) } - override fun getHorizontalDimensions( + override fun updateHorizontalDimensions( context: MeasureContext, + horizontalDimensions: MutableHorizontalDimensions, model: ChartEntryModel, - ): HorizontalDimensions = with(context) { - val columnCollectionWidth = getColumnCollectionWidth(if (model.entries.isNotEmpty()) model.entries.size else 1) - horizontalDimensions.apply { - xSpacing = columnCollectionWidth + spacingDp.pixels + ) { + with(context) { + val columnCollectionWidth = + getColumnCollectionWidth(if (model.entries.isNotEmpty()) model.entries.size else 1) + val xSpacing = columnCollectionWidth + spacingDp.pixels when (val horizontalLayout = horizontalLayout) { is HorizontalLayout.Segmented -> { - scalableStartPadding = xSpacing.half - scalableEndPadding = scalableStartPadding + horizontalDimensions.ensureValuesAtLeast( + xSpacing = xSpacing, + scalableStartPadding = xSpacing.half, + scalableEndPadding = xSpacing.half, + ) } is HorizontalLayout.FullWidth -> { - scalableStartPadding = columnCollectionWidth.half + horizontalLayout.startPaddingDp.pixels - scalableEndPadding = columnCollectionWidth.half + horizontalLayout.endPaddingDp.pixels + horizontalDimensions.ensureValuesAtLeast( + xSpacing = xSpacing, + scalableStartPadding = columnCollectionWidth.half + + horizontalLayout.scalableStartPaddingDp.pixels, + scalableEndPadding = columnCollectionWidth.half + horizontalLayout.scalableEndPaddingDp.pixels, + unscalableStartPadding = horizontalLayout.unscalableStartPaddingDp.pixels, + unscalableEndPadding = horizontalLayout.unscalableEndPaddingDp.pixels, + ) } } } } + override val modelTransformerProvider: Chart.ModelTransformerProvider = object : Chart.ModelTransformerProvider { + private val modelTransformer = + ColumnChartModelTransformer(drawingModelKey, { targetVerticalAxisPosition }, { drawingModelInterpolator }) + + override fun getModelTransformer(): Chart.ModelTransformer = modelTransformer + } + protected open fun MeasureContext.getColumnCollectionWidth( entryCollectionSize: Int, ): Float = when (mergeMode) { @@ -416,4 +449,41 @@ public open class ColumnChart( Stack -> model.stackedPositiveY } } + + protected class ColumnChartModelTransformer( + override val key: ExtraStore.Key, + private val getTargetVerticalAxisPosition: () -> AxisPosition.Vertical?, + private val getDrawingModelInterpolator: () -> DrawingModelInterpolator< + ColumnChartDrawingModel.ColumnInfo, + ColumnChartDrawingModel, + >, + ) : Chart.ModelTransformer() { + + override fun prepareForTransformation( + oldModel: ChartEntryModel?, + newModel: ChartEntryModel?, + extraStore: MutableExtraStore, + chartValuesProvider: ChartValuesProvider, + ) { + getDrawingModelInterpolator().setModels( + extraStore.getOrNull(key), + newModel?.toDrawingModel(chartValuesProvider.getChartValues(getTargetVerticalAxisPosition())), + ) + } + + override suspend fun transform(extraStore: MutableExtraStore, fraction: Float) { + getDrawingModelInterpolator() + .transform(fraction) + ?.let { extraStore[key] = it } + ?: extraStore.remove(key) + } + + private fun ChartEntryModel.toDrawingModel(chartValues: ChartValues): ColumnChartDrawingModel = entries + .map { series -> + series.associate { entry -> + entry.x to ColumnChartDrawingModel.ColumnInfo(abs(entry.y) / chartValues.lengthY) + } + } + .let(::ColumnChartDrawingModel) + } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChartDrawingModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChartDrawingModel.kt new file mode 100644 index 000000000..3c6c26b66 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChartDrawingModel.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.chart.column + +import com.patrykandpatrick.vico.core.entry.diff.DrawingModel +import com.patrykandpatrick.vico.core.extension.orZero + +/** + * Houses drawing information for a [ColumnChart]. [opacity] is the columns’ opacity. + */ +public class ColumnChartDrawingModel(entries: List>, public val opacity: Float = 1f) : + DrawingModel(entries) { + + override fun transform( + drawingInfo: List>, + from: DrawingModel?, + fraction: Float, + ): DrawingModel { + val oldOpacity = (from as ColumnChartDrawingModel?)?.opacity.orZero + return ColumnChartDrawingModel(drawingInfo, oldOpacity + (opacity - oldOpacity) * fraction) + } + + /** + * Houses positional information for a [ColumnChart]’s column. [height] expresses the column’s height as a fraction + * of the [ColumnChart]’s height. + */ + public class ColumnInfo(public val height: Float) : DrawingInfo { + override fun transform(from: DrawingInfo?, fraction: Float): DrawingInfo { + val oldHeight = (from as? ColumnInfo)?.height.orZero + return ColumnInfo(oldHeight + (height - oldHeight) * fraction) + } + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChart.kt index e9864cc2a..2c35bc98f 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChart.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChart.kt @@ -19,6 +19,7 @@ package com.patrykandpatrick.vico.core.chart.composed import com.patrykandpatrick.vico.core.chart.AXIS_VALUES_DEPRECATION_MESSAGE import com.patrykandpatrick.vico.core.chart.BaseChart import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.Chart.ModelTransformerProvider import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext @@ -26,8 +27,11 @@ import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter import com.patrykandpatrick.vico.core.chart.insets.HorizontalInsets import com.patrykandpatrick.vico.core.chart.insets.Insets import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore import com.patrykandpatrick.vico.core.extension.set import com.patrykandpatrick.vico.core.extension.updateAll import com.patrykandpatrick.vico.core.marker.Marker @@ -51,8 +55,6 @@ public class ComposedChart( private val tempInsets = Insets() - private val horizontalDimensions = MutableHorizontalDimensions() - override val entryLocationMap: TreeMap> = TreeMap() override val chartInsetters: Collection @@ -97,22 +99,14 @@ public class ComposedChart( } } - override fun getHorizontalDimensions( + override fun updateHorizontalDimensions( context: MeasureContext, + horizontalDimensions: MutableHorizontalDimensions, model: ComposedChartEntryModel, - ): HorizontalDimensions { - horizontalDimensions.clear() + ) { model.forEachModelWithChart { item, chart -> - val chartHorizontalDimensions = chart.getHorizontalDimensions(context, item) - horizontalDimensions.apply { - xSpacing = maxOf(xSpacing, chartHorizontalDimensions.xSpacing) - scalableStartPadding = maxOf(scalableStartPadding, chartHorizontalDimensions.scalableStartPadding) - scalableEndPadding = maxOf(scalableEndPadding, chartHorizontalDimensions.scalableEndPadding) - unscalableStartPadding = maxOf(unscalableStartPadding, chartHorizontalDimensions.unscalableStartPadding) - unscalableEndPadding = maxOf(unscalableEndPadding, chartHorizontalDimensions.unscalableEndPadding) - } + chart.updateHorizontalDimensions(context, horizontalDimensions, item) } - return horizontalDimensions } override fun updateChartValues( @@ -154,6 +148,43 @@ public class ComposedChart( ) } } + + override val modelTransformerProvider: ModelTransformerProvider = object : ModelTransformerProvider { + private val modelTransformer = + ComposedModelTransformer { charts.map { it.modelTransformerProvider.getModelTransformer() } } + + override fun getModelTransformer(): Chart.ModelTransformer = modelTransformer + } + + private class ComposedModelTransformer( + private val getModelTransformers: () -> List>, + ) : Chart.ModelTransformer() { + + override val key: ExtraStore.Key = ExtraStore.Key() + + override fun prepareForTransformation( + oldModel: T?, + newModel: T?, + extraStore: MutableExtraStore, + chartValuesProvider: ChartValuesProvider, + ) { + getModelTransformers().forEachIndexed { index, transformer -> + @Suppress("UNCHECKED_CAST") + transformer.prepareForTransformation( + (oldModel as ComposedChartEntryModel<*>?)?.composedEntryCollections?.getOrNull(index) as T?, + (newModel as ComposedChartEntryModel<*>?)?.composedEntryCollections?.getOrNull(index) as T?, + extraStore, + chartValuesProvider, + ) + } + } + + override suspend fun transform(extraStore: MutableExtraStore, fraction: Float) { + getModelTransformers().forEach { transformer -> + transformer.transform(extraStore, fraction) + } + } + } } private fun childChartsValue( diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/ThresholdLine.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/ThresholdLine.kt index 8a0375ab5..67cbd88ce 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/ThresholdLine.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/ThresholdLine.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -99,7 +99,7 @@ public data class ThresholdLine( context: ChartDrawContext, bounds: RectF, ): Unit = with(context) { - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() val valueRange = chartValues.maxY - chartValues.minY diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/HorizontalDimensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/HorizontalDimensions.kt index f0f0875ed..9b672ee11 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/HorizontalDimensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/HorizontalDimensions.kt @@ -50,25 +50,40 @@ public interface HorizontalDimensions { /** * The total start padding (in pixels). */ - public val startPadding: Float - get() = scalableStartPadding + unscalableStartPadding + public val startPadding: Float get() = scalableStartPadding + unscalableStartPadding /** * The total end padding (in pixels). */ - public val endPadding: Float - get() = scalableEndPadding + unscalableEndPadding + public val endPadding: Float get() = scalableEndPadding + unscalableEndPadding + + /** + * The total scalable horizontal padding (in pixels). + */ + public val scalablePadding: Float get() = scalableStartPadding + scalableEndPadding + + /** + * The total unscalable horizontal padding (in pixels). + */ + public val unscalablePadding: Float get() = unscalableStartPadding + unscalableEndPadding /** * The total horizontal padding (in pixels). */ - public val padding: Float - get() = startPadding + endPadding + public val padding: Float get() = startPadding + endPadding + + /** + * Given the chart’s maximum number of major entries, calculates the width of the [Chart]’s scalable content (in + * pixels). + */ + public fun getScalableContentWidth(maxMajorEntryCount: Int): Float = + xSpacing * (maxMajorEntryCount - 1) + scalablePadding /** * Given the chart’s maximum number of major entries, calculates the width of the [Chart]’s content (in pixels). */ - public fun getContentWidth(maxMajorEntryCount: Int): Float = xSpacing * (maxMajorEntryCount - 1) + padding + public fun getContentWidth(maxMajorEntryCount: Int): Float = + getScalableContentWidth(maxMajorEntryCount) + unscalablePadding /** * Creates a new [HorizontalDimensions] instance by multiplying this one’s scalable values by the given factor. diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/MutableHorizontalDimensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/MutableHorizontalDimensions.kt index 4c807eb3b..422c862c4 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/MutableHorizontalDimensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/MutableHorizontalDimensions.kt @@ -59,6 +59,23 @@ public data class MutableHorizontalDimensions( unscalableEndPadding = 0f, ) + /** + * Ensures that the stored values are no smaller than the provided ones. + */ + public fun ensureValuesAtLeast( + xSpacing: Float = 0f, + scalableStartPadding: Float = 0f, + scalableEndPadding: Float = 0f, + unscalableStartPadding: Float = 0f, + unscalableEndPadding: Float = 0f, + ): MutableHorizontalDimensions = set( + this.xSpacing.coerceAtLeast(xSpacing), + this.scalableStartPadding.coerceAtLeast(scalableStartPadding), + this.scalableEndPadding.coerceAtLeast(scalableEndPadding), + this.unscalableStartPadding.coerceAtLeast(unscalableStartPadding), + this.unscalableEndPadding.coerceAtLeast(unscalableEndPadding), + ) + /** * Clears the stored values. */ diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContext.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContext.kt index 9181c863a..d0da5bef0 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContext.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContext.kt @@ -66,7 +66,7 @@ public fun MeasureContext.getMaxScrollDistance( ): Float { val contentWidth = horizontalDimensions .run { if (zoom != null) scaled(zoom) else this } - .getContentWidth(chartValuesManager.getChartValues().getMaxMajorEntryCount()) + .getContentWidth(chartValuesProvider.getChartValues().getMaxMajorEntryCount()) return (layoutDirectionMultiplier * (contentWidth - chartWidth)).run { if (isLtr) coerceAtLeast(minimumValue = 0f) else coerceAtMost(maximumValue = 0f) @@ -90,12 +90,15 @@ public fun MeasureContext.getAutoZoom( chartBounds: RectF, autoScaleUp: AutoScaleUp, ): Float { - val contentWidth = horizontalDimensions.getContentWidth(chartValuesManager.getChartValues().getMaxMajorEntryCount()) + val scalableContentWidth = + horizontalDimensions.getScalableContentWidth(chartValuesProvider.getChartValues().getMaxMajorEntryCount()) + val reducedChartWidth = chartBounds.width() - horizontalDimensions.unscalablePadding + val fillingZoom = reducedChartWidth / scalableContentWidth return when { - contentWidth < chartBounds.width() -> - if (autoScaleUp == AutoScaleUp.Full) (chartBounds.width() / contentWidth).coerceAtMost(DEF_MAX_ZOOM) else 1f + scalableContentWidth < reducedChartWidth -> + if (autoScaleUp == AutoScaleUp.Full) fillingZoom.coerceAtMost(DEF_MAX_ZOOM) else 1f - !isHorizontalScrollEnabled -> chartBounds.width() / contentWidth + !isHorizontalScrollEnabled -> fillingZoom else -> 1f } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContextExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContextExtensions.kt index 175f72e73..8d2e9cd2a 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContextExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContextExtensions.kt @@ -93,12 +93,11 @@ public fun ChartDrawContext.drawMarker( markerTouchPoint ?.let(chart.entryLocationMap::getClosestMarkerEntryModel) ?.let { markerEntryModels -> - chartValuesManager.getChartValues() marker.draw( context = this, bounds = chart.bounds, markedEntries = markerEntryModels, - chartValuesProvider = chartValuesManager, + chartValuesProvider = chartValuesProvider, ) if (wasMarkerVisible.not()) { markerVisibilityChangeListener?.onMarkerShown( diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/layout/HorizontalLayout.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/layout/HorizontalLayout.kt index 80e2bfcb9..dde91350b 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/layout/HorizontalLayout.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/layout/HorizontalLayout.kt @@ -43,11 +43,16 @@ public sealed interface HorizontalLayout { /** * When this is applied, the [Chart]’s content takes up the [Chart]’s entire width (unless padding is added). * [HorizontalAxis] instances display a tick and a guideline for each label, with the tick, guideline, and label - * vertically centered relative to one another. [startPaddingDp] and [endPaddingDp] control the amount of empty - * space at the start and end of the [Chart], respectively. + * vertically centered relative to one another. [scalableStartPaddingDp], [scalableEndPaddingDp], + * [unscalableStartPaddingDp], and [unscalableEndPaddingDp] control the amount of empty space at the start and end + * of the [Chart]. Scalable padding values are multiplied by the zoom factor, unlike unscalable ones. */ - public class FullWidth(public val startPaddingDp: Float = 0f, public val endPaddingDp: Float = 0f) : - HorizontalLayout + public class FullWidth( + public val scalableStartPaddingDp: Float = 0f, + public val scalableEndPaddingDp: Float = 0f, + public val unscalableStartPaddingDp: Float = 0f, + public val unscalableEndPaddingDp: Float = 0f, + ) : HorizontalLayout public companion object } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChart.kt index b542618c6..9c873ba35 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChart.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChart.kt @@ -24,6 +24,7 @@ import com.patrykandpatrick.vico.core.DefaultDimens import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisRenderer import com.patrykandpatrick.vico.core.chart.BaseChart +import com.patrykandpatrick.vico.core.chart.Chart import com.patrykandpatrick.vico.core.chart.DefaultPointConnector import com.patrykandpatrick.vico.core.chart.composed.ComposedChart import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions @@ -37,6 +38,7 @@ import com.patrykandpatrick.vico.core.chart.line.LineChart.LineSpec.PointConnect import com.patrykandpatrick.vico.core.chart.put import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.component.Component import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShader import com.patrykandpatrick.vico.core.component.text.TextComponent @@ -46,11 +48,16 @@ import com.patrykandpatrick.vico.core.context.DrawContext import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.entry.ChartEntry import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.diff.DefaultDrawingModelInterpolator +import com.patrykandpatrick.vico.core.entry.diff.DrawingModelInterpolator +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore import com.patrykandpatrick.vico.core.extension.doubled import com.patrykandpatrick.vico.core.extension.getRepeating import com.patrykandpatrick.vico.core.extension.getStart import com.patrykandpatrick.vico.core.extension.half import com.patrykandpatrick.vico.core.extension.rangeWith +import com.patrykandpatrick.vico.core.extension.withOpacity import com.patrykandpatrick.vico.core.formatter.DecimalFormatValueFormatter import com.patrykandpatrick.vico.core.formatter.ValueFormatter import com.patrykandpatrick.vico.core.marker.Marker @@ -64,11 +71,16 @@ import kotlin.math.min * @param spacingDp the spacing between each [LineSpec.point] (in dp). * @param targetVerticalAxisPosition if this is set, any [AxisRenderer] with an [AxisPosition] equal to the provided * value will use the [ChartValues] provided by this chart. This is meant to be used with [ComposedChart]. + * @param drawingModelInterpolator interpolates the [LineChart]’s [LineChartDrawingModel]s. */ public open class LineChart( public var lines: List = listOf(LineSpec()), public var spacingDp: Float = DefaultDimens.POINT_SPACING, public var targetVerticalAxisPosition: AxisPosition.Vertical? = null, + public var drawingModelInterpolator: DrawingModelInterpolator< + LineChartDrawingModel.PointInfo, + LineChartDrawingModel, + > = DefaultDrawingModelInterpolator(), ) : BaseChart() { /** @@ -217,25 +229,22 @@ public open class LineChart( /** * Draws the line. */ - public fun drawLine(context: DrawContext, path: Path): Unit = with(context) { - linePaint.strokeWidth = lineThicknessDp.pixels - canvas.drawPath(path, linePaint) + public fun drawLine(context: DrawContext, path: Path, opacity: Float = 1f) { + with(context) { + linePaint.strokeWidth = lineThicknessDp.pixels + linePaint.withOpacity(opacity) { canvas.drawPath(path, it) } + } } /** * Draws the line background. */ - public fun drawBackgroundLine(context: DrawContext, bounds: RectF, path: Path): Unit = with(context) { - lineBackgroundPaint.shader = lineBackgroundShader - ?.provideShader( - context = context, - left = bounds.left, - top = bounds.top, - right = bounds.right, - bottom = bounds.bottom, - ) - - canvas.drawPath(path, lineBackgroundPaint) + public fun drawBackgroundLine(context: DrawContext, bounds: RectF, path: Path, opacity: Float = 1f) { + with(lineBackgroundPaint) { + shader = + lineBackgroundShader?.provideShader(context, bounds.left, bounds.top, bounds.right, bounds.bottom) + withOpacity(opacity) { context.canvas.drawPath(path, it) } + } } /** @@ -270,10 +279,7 @@ public open class LineChart( */ protected val lineBackgroundPath: Path = Path() - /** - * Holds information on the [LineChart]’s horizontal dimensions. - */ - protected val horizontalDimensions: MutableHorizontalDimensions = MutableHorizontalDimensions() + protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() override val entryLocationMap: HashMap> = HashMap() @@ -283,8 +289,12 @@ public open class LineChart( ): Unit = with(context) { resetTempData() + val drawingModel = model.extraStore.getOrNull(drawingModelKey) + model.entries.forEachIndexed { entryListIndex, entries -> + val pointInfoMap = drawingModel?.getOrNull(entryListIndex) + linePath.rewind() lineBackgroundPath.rewind() val component = lines.getRepeating(entryListIndex) @@ -299,6 +309,7 @@ public open class LineChart( forEachPointWithinBoundsIndexed( entries = entries, drawingStart = drawingStart, + pointInfoMap = pointInfoMap, ) { entryIndex, entry, x, y, _, _ -> if (linePath.isEmpty) { linePath.moveTo(x, y) @@ -345,14 +356,15 @@ public open class LineChart( if (component.hasLineBackgroundShader) { lineBackgroundPath.lineTo(prevX, bounds.bottom) lineBackgroundPath.close() - component.drawBackgroundLine(context, bounds, lineBackgroundPath) + component.drawBackgroundLine(context, bounds, lineBackgroundPath, drawingModel?.opacity ?: 1f) } - component.drawLine(context, linePath) + component.drawLine(context, linePath, drawingModel?.opacity ?: 1f) drawPointsAndDataLabels( lineSpec = component, entries = entries, drawingStart = drawingStart, + pointInfoMap = pointInfoMap, ) } } @@ -364,13 +376,15 @@ public open class LineChart( lineSpec: LineSpec, entries: List, drawingStart: Float, + pointInfoMap: Map?, ) { if (lineSpec.point == null && lineSpec.dataLabel == null) return - val chartValues = chartValuesManager.getChartValues(targetVerticalAxisPosition) + val chartValues = chartValuesProvider.getChartValues(targetVerticalAxisPosition) forEachPointWithinBoundsIndexed( entries = entries, drawingStart = drawingStart, + pointInfoMap = pointInfoMap, ) { _, chartEntry, x, y, previousX, nextX -> if (lineSpec.point != null) lineSpec.drawPoint(context = this, x = x, y = y) @@ -427,7 +441,7 @@ public open class LineChart( previousX: Float?, nextX: Float?, ): Int { - val chartValues = chartValuesManager.getChartValues(targetVerticalAxisPosition) + val chartValues = chartValuesProvider.getChartValues(targetVerticalAxisPosition) return when { previousX != null && nextX != null -> min(x - previousX, nextX - x) @@ -471,14 +485,13 @@ public open class LineChart( protected open fun ChartDrawContext.forEachPointWithinBoundsIndexed( entries: List, drawingStart: Float, + pointInfoMap: Map?, action: (index: Int, entry: ChartEntry, x: Float, y: Float, previousX: Float?, nextX: Float?) -> Unit, ) { - val chartValues = chartValuesManager.getChartValues(targetVerticalAxisPosition) + val chartValues = chartValuesProvider.getChartValues(targetVerticalAxisPosition) val minX = chartValues.minX val maxX = chartValues.maxX - val minY = chartValues.minY - val maxY = chartValues.maxY val xStep = chartValues.xStep var x: Float = Float.NEGATIVE_INFINITY @@ -488,8 +501,6 @@ public open class LineChart( var prevEntry: ChartEntry? = null var lastEntry: ChartEntry? = null - val heightMultiplier = bounds.height() / (maxY - minY) - val boundsStart = bounds.getStart(isLtr = isLtr) val boundsEnd = boundsStart + layoutDirectionMultiplier * bounds.width() @@ -497,7 +508,8 @@ public open class LineChart( horizontalDimensions.xSpacing * (entry.x - minX) / xStep fun getDrawY(entry: ChartEntry): Float = - bounds.bottom - (entry.y - minY) * heightMultiplier + bounds.bottom - (pointInfoMap?.get(entry.x)?.y ?: ((entry.y - chartValues.minY) / chartValues.lengthY)) * + bounds.height() entries.forEachInAbsolutelyIndexed(minX - xStep..maxX + xStep) { index, entry, next -> @@ -527,23 +539,30 @@ public open class LineChart( } } - override fun getHorizontalDimensions( + override fun updateHorizontalDimensions( context: MeasureContext, + horizontalDimensions: MutableHorizontalDimensions, model: ChartEntryModel, - ): HorizontalDimensions = with(context) { - val maxPointSize = lines.maxOf { it.pointSizeDpOrZero }.pixels - horizontalDimensions.apply { - xSpacing = maxPointSize + spacingDp.pixels + ) { + with(context) { + val maxPointSize = lines.maxOf { it.pointSizeDpOrZero }.pixels + val xSpacing = maxPointSize + spacingDp.pixels when (val horizontalLayout = horizontalLayout) { is HorizontalLayout.Segmented -> { - scalableStartPadding = xSpacing.half - scalableEndPadding = scalableStartPadding + horizontalDimensions.ensureValuesAtLeast( + xSpacing = xSpacing, + scalableStartPadding = xSpacing.half, + scalableEndPadding = xSpacing.half, + ) } is HorizontalLayout.FullWidth -> { - scalableStartPadding = horizontalLayout.startPaddingDp.pixels - scalableEndPadding = horizontalLayout.endPaddingDp.pixels - unscalableStartPadding = maxPointSize.half - unscalableEndPadding = unscalableStartPadding + horizontalDimensions.ensureValuesAtLeast( + xSpacing = xSpacing, + scalableStartPadding = horizontalLayout.scalableStartPaddingDp.pixels, + scalableEndPadding = horizontalLayout.scalableEndPaddingDp.pixels, + unscalableStartPadding = maxPointSize.half + horizontalLayout.unscalableStartPaddingDp.pixels, + unscalableEndPadding = maxPointSize.half + horizontalLayout.unscalableEndPaddingDp.pixels, + ) } } } @@ -573,4 +592,48 @@ public open class LineChart( }.pixels, ) } + + override val modelTransformerProvider: Chart.ModelTransformerProvider = object : Chart.ModelTransformerProvider { + private val modelTransformer = + LineChartModelTransformer(drawingModelKey, { targetVerticalAxisPosition }, { drawingModelInterpolator }) + + override fun getModelTransformer(): Chart.ModelTransformer = modelTransformer + } + + protected class LineChartModelTransformer( + override val key: ExtraStore.Key, + private val getTargetVerticalAxisPosition: () -> AxisPosition.Vertical?, + private val getDrawingModelInterpolator: () -> DrawingModelInterpolator< + LineChartDrawingModel.PointInfo, + LineChartDrawingModel, + >, + ) : Chart.ModelTransformer() { + + override fun prepareForTransformation( + oldModel: ChartEntryModel?, + newModel: ChartEntryModel?, + extraStore: MutableExtraStore, + chartValuesProvider: ChartValuesProvider, + ) { + getDrawingModelInterpolator().setModels( + extraStore.getOrNull(key), + newModel?.toDrawingModel(chartValuesProvider.getChartValues(getTargetVerticalAxisPosition())), + ) + } + + override suspend fun transform(extraStore: MutableExtraStore, fraction: Float) { + getDrawingModelInterpolator() + .transform(fraction) + ?.let { extraStore[key] = it } + ?: extraStore.remove(key) + } + + private fun ChartEntryModel.toDrawingModel(chartValues: ChartValues): LineChartDrawingModel = entries + .map { series -> + series.associate { entry -> + entry.x to LineChartDrawingModel.PointInfo((entry.y - chartValues.minY) / chartValues.lengthY) + } + } + .let(::LineChartDrawingModel) + } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChartDrawingModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChartDrawingModel.kt new file mode 100644 index 000000000..12a514be4 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChartDrawingModel.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.chart.line + +import com.patrykandpatrick.vico.core.entry.diff.DrawingModel +import com.patrykandpatrick.vico.core.extension.orZero + +/** + * Houses drawing information for a [LineChart]. [opacity] is the lines’ opacity. + */ +public class LineChartDrawingModel(pointInfo: List>, public val opacity: Float = 1f) : + DrawingModel(pointInfo) { + + override fun transform( + drawingInfo: List>, + from: DrawingModel?, + fraction: Float, + ): DrawingModel { + val oldOpacity = (from as LineChartDrawingModel?)?.opacity.orZero + return LineChartDrawingModel(drawingInfo, oldOpacity + (opacity - oldOpacity) * fraction) + } + + /** + * Houses positional information for a [LineChart]’s point. [y] expresses the distance of the point from the bottom + * of the [LineChart] as a fraction of the [LineChart]’s height. + */ + public class PointInfo(public val y: Float) : DrawingInfo { + override fun transform(from: DrawingInfo?, fraction: Float): DrawingInfo { + val oldY = (from as? PointInfo)?.y.orZero + return PointInfo(oldY + (y - oldY) * fraction) + } + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt index ec8659e4d..044e5df3a 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt @@ -38,27 +38,13 @@ import com.patrykandpatrick.vico.core.entry.ChartEntryModel */ public class ChartValuesManager : ChartValuesProvider { - private val chartValues: MutableMap = mutableMapOf() + internal val chartValues: MutableMap = mutableMapOf() - /** - * Returns the [ChartValues] associated with the given [axisPosition]. - * @param axisPosition if this is null, the main [ChartValues] instance is returned. Otherwise, the [ChartValues] - * instance associated with the given [AxisPosition.Vertical] is returned. - */ - public fun getChartValues(axisPosition: AxisPosition.Vertical? = null): MutableChartValues = + override fun getChartValues(axisPosition: AxisPosition.Vertical?): ChartValues = chartValues[axisPosition] ?.takeIf { it.hasValuesSet } ?: chartValues.getOrPut(null) { MutableChartValues() } - override fun getChartValues(): ChartValues = getChartValues(null) - - override fun getChartValuesForAxisPosition(axisPosition: AxisPosition.Vertical): ChartValues? = - if (chartValues.containsKey(axisPosition)) { - getChartValues(axisPosition).takeIf { it.hasValuesSet } - } else { - null - } - /** * Attempts to update the stored values to the provided values. * [MutableChartValues.minX] and [MutableChartValues.minY] can be updated to a lower value. @@ -105,3 +91,17 @@ public class ChartValuesManager : ChartValuesProvider { chartValues.values.forEach { it.reset() } } } + +/** + * Creates and returns a [ChartValuesProvider] implementation with this [ChartValuesManager]’s [ChartValues] + * instances. + */ +public fun ChartValuesManager.toChartValuesProvider(): ChartValuesProvider = object : ChartValuesProvider { + val chartValues = this@toChartValuesProvider + .chartValues + .map { (axisPosition, chartValues) -> axisPosition to chartValues.toImmutable() } + .toMap() + + override fun getChartValues(axisPosition: AxisPosition.Vertical?): ChartValues = + chartValues[axisPosition] ?: chartValues.getValue(null) +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt index 6cc40077a..9f5204e29 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt @@ -17,21 +17,23 @@ package com.patrykandpatrick.vico.core.chart.values import com.patrykandpatrick.vico.core.axis.AxisPosition -import com.patrykandpatrick.vico.core.chart.Chart /** - * Provides a [Chart]’s [ChartValues]. + * Provides a chart’s [ChartValues] instances. */ public interface ChartValuesProvider { /** - * Returns the [Chart]’s main [ChartValues]. + * Returns the [ChartValues] instance associated with the specified [AxisPosition.Vertical] subclass. If + * [axisPosition] is `null`, the chart’s main [ChartValues] instance is returned. */ - public fun getChartValues(): ChartValues + public fun getChartValues(axisPosition: AxisPosition.Vertical? = null): ChartValues /** - * Returns the [ChartValues] associated with the specified [AxisPosition.Vertical] subclass, or `null` if there is - * no such association. + * An empty [ChartValuesProvider] implementation. [getChartValues] throws an exception when called. */ - public fun getChartValuesForAxisPosition(axisPosition: AxisPosition.Vertical): ChartValues? + public companion object Empty : ChartValuesProvider { + override fun getChartValues(axisPosition: AxisPosition.Vertical?): ChartValues = + error("`ChartValuesProvider.Empty#getChartValues` shouldn’t be used.") + } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/MutableChartValues.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/MutableChartValues.kt index c85b85841..9245d2470 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/MutableChartValues.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/MutableChartValues.kt @@ -106,3 +106,15 @@ public class MutableChartValues : ChartValues { } } } + +/** + * Creates and returns an immutable copy of this [MutableChartValues] instance. + */ +public fun MutableChartValues.toImmutable(): ChartValues = object : ChartValues { + override val minX: Float = this@toImmutable.minX + override val maxX: Float = this@toImmutable.maxX + override val xStep: Float = this@toImmutable.xStep + override val minY: Float = this@toImmutable.minY + override val maxY: Float = this@toImmutable.maxY + override val chartEntryModel: ChartEntryModel = this@toImmutable.chartEntryModel.toImmutable() +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/Component.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/Component.kt index 296ea47a0..68b73ea55 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/Component.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/Component.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,5 +35,6 @@ public abstract class Component : Margins by DefaultMargins() { top: Float, right: Float, bottom: Float, + opacity: Float = 1f, ) } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/OverlayingComponent.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/OverlayingComponent.kt index e9a4cee7e..e6136980b 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/OverlayingComponent.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/OverlayingComponent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,14 +65,15 @@ public class OverlayingComponent( top: Float, right: Float, bottom: Float, + opacity: Float, ): Unit = with(context) { val leftWithMargin = left + margins.startDp.pixels val topWithMargin = top + margins.topDp.pixels val rightWithMargin = right - margins.endDp.pixels val bottomWithMargin = bottom - margins.bottomDp.pixels - outer.draw(context, leftWithMargin, topWithMargin, rightWithMargin, bottomWithMargin) - inner.draw(context, leftWithMargin, topWithMargin, rightWithMargin, bottomWithMargin) + outer.draw(context, leftWithMargin, topWithMargin, rightWithMargin, bottomWithMargin, opacity) + inner.draw(context, leftWithMargin, topWithMargin, rightWithMargin, bottomWithMargin, opacity) DebugHelper.drawDebugBounds( context = context, diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/shape/LineComponent.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/shape/LineComponent.kt index 566c0165c..de7f77740 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/shape/LineComponent.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/shape/LineComponent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,6 +57,7 @@ public open class LineComponent( right: Float, centerY: Float, thicknessScale: Float = 1f, + opacity: Float = 1f, ): Unit = with(context) { draw( context, @@ -64,6 +65,7 @@ public open class LineComponent( top = centerY - thickness * thicknessScale / 2, right = right, bottom = centerY + thickness * thicknessScale / 2, + opacity = opacity, ) } @@ -96,6 +98,7 @@ public open class LineComponent( bottom: Float, centerX: Float, thicknessScale: Float = 1f, + opacity: Float = 1f, ): Unit = with(context) { draw( context, @@ -103,6 +106,7 @@ public open class LineComponent( top = top, right = centerX + thickness * thicknessScale / 2, bottom = bottom, + opacity = opacity, ) } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/shape/ShapeComponent.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/shape/ShapeComponent.kt index 8ea9d2b2b..653cd3458 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/shape/ShapeComponent.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/shape/ShapeComponent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import com.patrykandpatrick.vico.core.dimensions.Dimensions import com.patrykandpatrick.vico.core.dimensions.emptyDimensions import com.patrykandpatrick.vico.core.extension.alpha import com.patrykandpatrick.vico.core.extension.half +import com.patrykandpatrick.vico.core.extension.withOpacity import kotlin.properties.Delegates /** @@ -85,6 +86,7 @@ public open class ShapeComponent( top: Float, right: Float, bottom: Float, + opacity: Float, ): Unit = with(context) { if (left == right || top == bottom) return // Skip drawing shape that will be invisible. path.rewind() @@ -108,8 +110,8 @@ public open class ShapeComponent( ) } - drawShape(paint) - if (strokeWidth > 0f && strokeColor.alpha > 0) drawShape(strokePaint) + paint.withOpacity(opacity, ::drawShape) + if (strokeWidth > 0f && strokeColor.alpha > 0) strokePaint.withOpacity(opacity, ::drawShape) DebugHelper.drawDebugBounds( context = context, diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MeasureContext.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MeasureContext.kt index 4285e421a..e25d9438a 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MeasureContext.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MeasureContext.kt @@ -17,10 +17,9 @@ package com.patrykandpatrick.vico.core.context import android.graphics.RectF -import com.patrykandpatrick.vico.core.chart.Chart import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.chart.values.ChartValues -import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider /** * [MeasureContext] holds data used by various chart components during the measuring and drawing phases. @@ -33,11 +32,11 @@ public interface MeasureContext : Extras { public val canvasBounds: RectF /** - * Manages the associated [Chart]’s [ChartValues]. + * Provides the chart’s [ChartValues] instances. * - * @see [ChartValuesManager] + * @see [ChartValuesProvider] */ - public val chartValuesManager: ChartValuesManager + public val chartValuesProvider: ChartValuesProvider /** * The pixel density. @@ -102,10 +101,9 @@ public interface MeasureContext : Extras { public fun toFontSize(sp: Float): Float = spToPx(sp) /** - * Removes all stored extras and resets [ChartValuesManager.chartValues]. + * Removes all stored extras. * * @see Extras.clearExtras - * @see ChartValuesManager.resetChartValues */ public fun reset() } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MutableMeasureContext.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MutableMeasureContext.kt index 1e354393c..6922d90fa 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MutableMeasureContext.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MutableMeasureContext.kt @@ -17,8 +17,9 @@ package com.patrykandpatrick.vico.core.context import android.graphics.RectF +import androidx.annotation.RestrictTo import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout -import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider /** * A [MeasureContext] implementation that facilitates the mutation of some of its properties. @@ -29,14 +30,13 @@ public data class MutableMeasureContext( override var isLtr: Boolean, override var isHorizontalScrollEnabled: Boolean = false, override var horizontalLayout: HorizontalLayout = HorizontalLayout.Segmented, - private var spToPx: (Float) -> Float, + @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public var spToPx: (Float) -> Float, + override var chartValuesProvider: ChartValuesProvider, ) : MeasureContext, Extras by DefaultExtras() { - override val chartValuesManager: ChartValuesManager = ChartValuesManager() - override fun reset() { clearExtras() - chartValuesManager.resetChartValues() } override fun spToPx(sp: Float): Float = spToPx.invoke(sp) diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/draw/DrawContextExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/draw/DrawContextExtensions.kt index 26fe2dd0e..29f24a168 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/draw/DrawContextExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/draw/DrawContextExtensions.kt @@ -20,7 +20,7 @@ import android.graphics.Canvas import android.graphics.RectF import com.patrykandpatrick.vico.core.DefaultColors import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout -import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.context.DefaultExtras import com.patrykandpatrick.vico.core.context.DrawContext import com.patrykandpatrick.vico.core.context.Extras @@ -54,7 +54,7 @@ public fun drawContext( override val density: Float = density override val isLtr: Boolean = isLtr override val isHorizontalScrollEnabled: Boolean = false - override val chartValuesManager: ChartValuesManager = ChartValuesManager() + override val chartValuesProvider: ChartValuesProvider = ChartValuesProvider.Empty override val horizontalLayout: HorizontalLayout = HorizontalLayout.Segmented override fun withOtherCanvas(canvas: Canvas, block: (DrawContext) -> Unit) { @@ -65,7 +65,6 @@ public fun drawContext( } override fun reset() { - chartValuesManager.resetChartValues() clearExtras() } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryExtensions.kt index dcc734aee..135e4cb8f 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryExtensions.kt @@ -88,24 +88,11 @@ internal inline val Iterable>.yRange: ClosedFloatingPointRa internal inline val Iterable>.xRange: ClosedFloatingPointRange get() = flatten().rangeOfOrNull { it.x } ?: 0f..0f -internal fun Iterable>.calculateXGcd(): Float { - var gcd: Float? = null - forEach { entryCollection -> - val iterator = entryCollection.iterator() - var currentEntry: ChartEntry - var previousEntry: ChartEntry? = null - while (iterator.hasNext()) { - currentEntry = iterator.next() - previousEntry?.let { prevEntry -> - val difference = abs(x = currentEntry.x - prevEntry.x) - gcd = gcd?.gcdWith(other = difference) ?: difference - } - previousEntry = currentEntry - } - if (gcd == -1f) gcd = 1f - } - return gcd ?: 1f -} +internal fun Iterable>.calculateXGcd() = flatten() + .zipWithNext { firstEntry, secondEntry -> abs(secondEntry.x - firstEntry.x) } + .fold(null) { gcd, delta -> gcd?.gcdWith(delta) ?: delta } + ?.also { require(it != 0f) { "The precision of the x values is too large. The maximum is two decimal places." } } + ?: 1f internal fun Iterable>.calculateStackedYRange(): ClosedFloatingPointRange = flatten().fold(HashMap>()) { map, entry -> diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt index 31e80788f..5ace93f79 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt @@ -22,6 +22,8 @@ import com.patrykandpatrick.vico.core.chart.line.LineChart import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer +import com.patrykandpatrick.vico.core.entry.diff.DrawingModel +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore /** * Contains the data for a [Chart]. Pre-calculates values needed for the rendering of the [Chart]. @@ -88,4 +90,15 @@ public interface ChartEntryModel { * The greatest common divisor of the _x_ values. */ public val xGcd: Float + + /** + * Houses auxiliary data, including [DrawingModel]s. + */ + public val extraStore: ExtraStore + get() = ExtraStore.empty + + /** + * Returns an immutable copy of this [ChartEntryModel]. + */ + public fun toImmutable(): ChartEntryModel = this } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModelProducer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModelProducer.kt index 0666fee59..9e63c2679 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModelProducer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModelProducer.kt @@ -16,138 +16,178 @@ package com.patrykandpatrick.vico.core.entry -import com.patrykandpatrick.vico.core.DEF_THREAD_POOL_SIZE -import com.patrykandpatrick.vico.core.entry.diff.DefaultDiffProcessor -import com.patrykandpatrick.vico.core.entry.diff.DiffProcessor -import com.patrykandpatrick.vico.core.extension.setToAllChildren -import java.util.concurrent.Executor -import java.util.concurrent.Executors +import androidx.annotation.WorkerThread +import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore +import com.patrykandpatrick.vico.core.extension.copy +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex /** * A [ChartModelProducer] implementation that generates [ChartEntryModel] instances. * - * @param entryCollections a two-dimensional list of [ChartEntry] instances used to generate the [ChartEntryModel]. - * @param backgroundExecutor an [Executor] used to generate instances of the [ChartEntryModel] off the main thread. - * @param diffProcessor the [DiffProcessor] to use for difference animations. + * @param entryCollections the initial data set (list of series). + * @param dispatcher the [CoroutineDispatcher] to be used for update handling. * * @see ChartModelProducer */ public class ChartEntryModelProducer( entryCollections: List>, - backgroundExecutor: Executor = Executors.newFixedThreadPool(DEF_THREAD_POOL_SIZE), - private val diffProcessor: DiffProcessor = DefaultDiffProcessor(), + dispatcher: CoroutineDispatcher = Dispatchers.Default, ) : ChartModelProducer { - private var cachedModel: ChartEntryModel? = null - - private var entriesHashCode: Int? = null - + private var series = emptyList>() + private var cachedInternalModel: InternalModel? = null + private val mutex = Mutex() + private val coroutineScope = CoroutineScope(dispatcher) private val updateReceivers: HashMap = HashMap() - - private val executor: Executor = backgroundExecutor - - /** - * A mutable two-dimensional list of the [ChartEntry] instances used to generate the [ChartEntryModel]. - */ - private val entries: ArrayList> = ArrayList() + private val extraStore = MutableExtraStore() public constructor( vararg entryCollections: List, - backgroundExecutor: Executor = Executors.newFixedThreadPool(DEF_THREAD_POOL_SIZE), - diffProcessor: DiffProcessor = DefaultDiffProcessor(), - ) : this(entryCollections.toList(), backgroundExecutor, diffProcessor) + dispatcher: CoroutineDispatcher = Dispatchers.Default, + ) : this(entryCollections.toList(), dispatcher) init { setEntries(entryCollections) } /** - * Updates the two-dimensional list of [ChartEntry] instances and notifies listeners about the update. - * - * @see entries - * @see registerForUpdates + * Requests that the data set be updated to the provided one. If the update is accepted, `true` is returned. If the + * update is rejected, which occurs when there’s already an update in progress, `false` is returned. For suspending + * behavior, use [setEntriesSuspending]. [updateExtras] allows for adding auxiliary data, which can later be + * retrieved via [ChartEntryModel.extraStore]. */ - public fun setEntries(entries: List>) { - this.entries.setToAllChildren(entries) - val entriesHashCode = entries.hashCode() - cachedModel = null - updateReceivers.values.forEach { updateReceiver -> - executor.execute { - this.entriesHashCode = entriesHashCode - updateReceiver.diffProcessor.setEntries( - old = updateReceiver.getOldModel()?.entries.orEmpty(), - new = entries, - ) - updateReceiver.listener() - } + public fun setEntries(entries: List>, updateExtras: (MutableExtraStore) -> Unit = {}): Boolean { + if (!mutex.tryLock()) return false + series = entries.copy() + updateExtras(extraStore) + cachedInternalModel = null + val deferredUpdates = updateReceivers.values.map { updateReceiver -> + coroutineScope.async { updateReceiver.handleUpdate() } } + coroutineScope.launch { + deferredUpdates.awaitAll() + mutex.unlock() + } + return true } /** - * Updates the two-dimensional list of [ChartEntry] instances and notifies listeners about the update. - * - * @see entries - * @see registerForUpdates + * Updates the data set. Unlike [setEntries], this function suspends the current coroutine and waits until an update + * can be run, meaning the update cannot be rejected. The returned [Deferred] implementation is marked as completed + * once the update has been processed. [updateExtras] allows for adding auxiliary data, which can later be retrieved + * via [ChartEntryModel.extraStore]. */ - public fun setEntries(vararg entries: List) { - setEntries(entries.toList()) + public suspend fun setEntriesSuspending( + entries: List>, + updateExtras: (MutableExtraStore) -> Unit = {}, + ): Deferred { + mutex.lock() + series = entries.copy() + updateExtras(extraStore) + cachedInternalModel = null + val completableDeferred = CompletableDeferred() + val deferredUpdates = updateReceivers.values.map { updateReceiver -> + coroutineScope.async { updateReceiver.handleUpdate() } + } + coroutineScope.launch { + deferredUpdates.awaitAll() + mutex.unlock() + completableDeferred.complete(Unit) + } + return completableDeferred } - override fun getModel(): ChartEntryModel = - cachedModel ?: getModel(entries).also { cachedModel = it } + /** + * Requests that the data set be updated to the provided one. If the update is accepted, `true` is returned. If the + * update is rejected, which occurs when there’s already an update in progress, `false` is returned. For suspending + * behavior, use [setEntriesSuspending]. [updateExtras] allows for adding auxiliary data, which can later be + * retrieved via [ChartEntryModel.extraStore]. + */ + public fun setEntries(vararg entries: List, updateExtras: (MutableExtraStore) -> Unit = {}): Boolean = + setEntries(entries.toList(), updateExtras) - override fun progressModel(key: Any, progress: Float) { - val (_, modelReceiver, diffProcessor) = updateReceivers[key] ?: return - executor.execute { - progressModelSynchronously(progress, modelReceiver, diffProcessor) + /** + * Updates the data set. Unlike [setEntries], this function suspends the current coroutine and waits until an update + * can be run, meaning the update cannot be rejected. The returned [Deferred] implementation is marked as completed + * once the update has been processed. [updateExtras] allows for adding auxiliary data, which can later be retrieved + * via [ChartEntryModel.extraStore]. + */ + public suspend fun setEntriesSuspending( + vararg entries: List, + updateExtras: (MutableExtraStore) -> Unit = {}, + ): Deferred = setEntriesSuspending(entries.toList(), updateExtras) + + private fun getInternalModel(extraStore: ExtraStore? = null) = + if (series.isEmpty()) { + null + } else { + val mergedExtraStore = this.extraStore.let { if (extraStore != null) it + extraStore else it } + cachedInternalModel?.copy(extraStore = mergedExtraStore) + ?: run { + val xRange = series.xRange + val yRange = series.yRange + val aggregateYRange = series.calculateStackedYRange() + InternalModel( + entries = series, + minX = xRange.start, + maxX = xRange.endInclusive, + minY = yRange.start, + maxY = yRange.endInclusive, + stackedPositiveY = aggregateYRange.endInclusive, + stackedNegativeY = aggregateYRange.start, + xGcd = series.calculateXGcd(), + id = series.hashCode(), + extraStore = mergedExtraStore, + ).also { cachedInternalModel = it } + } } - } - private fun progressModelSynchronously( - progress: Float, - modelReceiver: (ChartEntryModel) -> Unit, - diffProcessor: DiffProcessor, - ) { - val model = getModel( - entries = diffProcessor.progressDiff(progress), - yRange = diffProcessor.yRangeProgressDiff(progress), - stackedPositiveYRange = diffProcessor.stackedYRangeProgressDiff(progress), - ) - modelReceiver(model) - } + override fun getModel(): ChartEntryModel? = getInternalModel() - private fun getModel( - entries: List>, - yRange: ClosedFloatingPointRange = entries.yRange, - stackedPositiveYRange: ClosedFloatingPointRange = entries.calculateStackedYRange(), - ): ChartEntryModel = - Model( - entries = entries, - minX = entries.xRange.start, - maxX = entries.xRange.endInclusive, - minY = yRange.start, - maxY = yRange.endInclusive, - stackedPositiveY = stackedPositiveYRange.endInclusive, - stackedNegativeY = stackedPositiveYRange.start, - xGcd = entries.calculateXGcd(), - id = entriesHashCode ?: entries.hashCode().also { entriesHashCode = it }, - ) + override suspend fun transformModel(key: Any, fraction: Float) { + with(updateReceivers[key] ?: return) { + modelTransformer?.transform(extraStore, fraction) + val internalModel = getInternalModel(extraStore.copy()) + currentCoroutineContext().ensureActive() + onModelCreated(internalModel) + } + } + @WorkerThread override fun registerForUpdates( key: Any, - updateListener: () -> Unit, + cancelAnimation: () -> Unit, + startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, getOldModel: () -> ChartEntryModel?, - onModel: (ChartEntryModel) -> Unit, + modelTransformerProvider: Chart.ModelTransformerProvider?, + extraStore: MutableExtraStore, + updateChartValues: (ChartEntryModel?) -> ChartValuesProvider, + onModelCreated: (ChartEntryModel?) -> Unit, ) { - updateReceivers[key] = UpdateReceiver( - listener = updateListener, - onModel = onModel, - diffProcessor = diffProcessor, - getOldModel = getOldModel, - ) - executor.execute { - diffProcessor.setEntries(old = getOldModel()?.entries.orEmpty(), new = entries) - updateListener() + UpdateReceiver( + cancelAnimation, + startAnimation, + onModelCreated, + extraStore, + modelTransformerProvider?.getModelTransformer(), + getOldModel, + updateChartValues, + ).run { + updateReceivers[key] = this + handleUpdate() } } @@ -157,14 +197,28 @@ public class ChartEntryModelProducer( override fun isRegistered(key: Any): Boolean = updateReceivers.containsKey(key = key) - private data class UpdateReceiver( - val listener: () -> Unit, - val onModel: (ChartEntryModel) -> Unit, - val diffProcessor: DiffProcessor, + private inner class UpdateReceiver( + val cancelAnimation: () -> Unit, + val startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, + val onModelCreated: (ChartEntryModel?) -> Unit, + val extraStore: MutableExtraStore, + val modelTransformer: Chart.ModelTransformer?, val getOldModel: () -> ChartEntryModel?, - ) + val updateChartValues: (ChartEntryModel?) -> ChartValuesProvider, + ) { + fun handleUpdate() { + cancelAnimation() + modelTransformer?.prepareForTransformation( + oldModel = getOldModel(), + newModel = getModel(), + extraStore = extraStore, + chartValuesProvider = updateChartValues(getModel()), + ) + startAnimation(::transformModel) + } + } - internal data class Model( + private data class InternalModel( override val entries: List>, override val minX: Float, override val maxX: Float, @@ -174,5 +228,6 @@ public class ChartEntryModelProducer( override val stackedNegativeY: Float, override val xGcd: Float, override val id: Int, + override val extraStore: ExtraStore, ) : ChartEntryModel } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartModelProducer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartModelProducer.kt index bed0b763c..8f35b4e5b 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartModelProducer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartModelProducer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,39 +16,53 @@ package com.patrykandpatrick.vico.core.entry +import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore + /** - * A [Model] producer that can deliver generated [Model]s asynchronously. It supports difference animations. + * Generates [ChartEntryModel]s and handles difference animations. * - * @see ChartEntryModel + * @see ChartEntryModelProducer + * @see ComposedChartEntryModelProducer */ public interface ChartModelProducer { /** - * Returns the [ChartEntryModel] for this [ChartModelProducer] synchronously. + * Returns the [ChartEntryModel] or, if no [ChartEntryModel] is available, `null`. + */ + public fun getModel(): Model? + + /** + * Returns the [ChartEntryModel] or, if no [ChartEntryModel] is available, throws an exception. */ - public fun getModel(): Model + public fun requireModel(): Model = getModel()!! /** - * Calculates an intermediate list of entries for difference animations for the associated [key], where [progress] - * is the balance between the previous and current lists of entries. + * Creates an intermediate [ChartEntryModel] for difference animations. [fraction] is the balance between the + * initial and target [ChartEntryModel]s. */ - public fun progressModel(key: Any, progress: Float) + public suspend fun transformModel(key: Any, fraction: Float) /** - * Registers an update listener associated with a [key]. - * - * @param key associates a receiver with its listeners. It’s later used to perform difference animations with - * [progressModel]. - * @param updateListener is called immediately in this function, and when the [ChartModelProducer] receives a new - * list of entries. The [registerForUpdates] function caller may start an animator, which will order the - * [ChartModelProducer] to handle the difference animation with [progressModel]. - * @param onModel called when the [ChartModelProducer] has generated the [Model]. + * Registers an update listener associated with a [key]. [cancelAnimation] and [startAnimation] are + * called after a data update is requested, with [cancelAnimation] being called before the update starts + * being processed (at which point [transformModel] should stop being used), and [startAnimation] being + * called once the update has been processed (at which point it’s safe to use [transformModel]). [updateChartValues] + * updates the chart’s [ChartValues] and returns its [ChartValuesProvider]. [onModelCreated] is called when a new + * [Model] has been generated. */ public fun registerForUpdates( key: Any, - updateListener: () -> Unit, + cancelAnimation: () -> Unit, + startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, getOldModel: () -> Model?, - onModel: (Model) -> Unit, + modelTransformerProvider: Chart.ModelTransformerProvider?, + extraStore: MutableExtraStore, + updateChartValues: (Model?) -> ChartValuesProvider, + onModelCreated: (Model?) -> Unit, ) /** diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/EntryListExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/EntryListExtensions.kt index ddc4f711f..857e73536 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/EntryListExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/EntryListExtensions.kt @@ -27,7 +27,7 @@ public fun entryModelOf(vararg entries: Pair): ChartEntryModel = entries .map { (x, y) -> entryOf(x.toFloat(), y.toFloat()) } .let { entryList -> ChartEntryModelProducer(listOf(entryList)) } - .getModel() + .requireModel() /** * Creates a [ChartEntryModel] out of the provided array of numbers, treating each number’s index as the _x_ value, and @@ -37,7 +37,7 @@ public fun entryModelOf(vararg values: Number): ChartEntryModel = values .mapIndexed { index, value -> entryOf(index.toFloat(), value.toFloat()) } .let { entryList -> ChartEntryModelProducer(listOf(entryList)) } - .getModel() + .requireModel() /** * Creates a [ChartEntryModel] out of the provided list of list of [FloatEntry] instances. @@ -45,4 +45,4 @@ public fun entryModelOf(vararg values: Number): ChartEntryModel = * This can be used to create [LineChart]s with multiple lines and [ColumnChart]s with grouped or stacked columns. */ public fun entryModelOf(vararg values: List): ChartEntryModel = - ChartEntryModelProducer(values.toList()).getModel() + ChartEntryModelProducer(values.toList()).requireModel() diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedChartEntryModelProducer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedChartEntryModelProducer.kt index 843a1d8e0..885692e65 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedChartEntryModelProducer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedChartEntryModelProducer.kt @@ -16,127 +16,328 @@ package com.patrykandpatrick.vico.core.entry.composed -import com.patrykandpatrick.vico.core.DEF_THREAD_POOL_SIZE +import androidx.annotation.WorkerThread +import com.patrykandpatrick.vico.core.chart.Chart import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.entry.ChartEntry import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartModelProducer +import com.patrykandpatrick.vico.core.entry.calculateStackedYRange +import com.patrykandpatrick.vico.core.entry.calculateXGcd +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore +import com.patrykandpatrick.vico.core.entry.xRange +import com.patrykandpatrick.vico.core.entry.yRange +import com.patrykandpatrick.vico.core.extension.copy import com.patrykandpatrick.vico.core.extension.gcdWith -import java.util.SortedMap -import java.util.TreeMap -import java.util.concurrent.Executor -import java.util.concurrent.Executors +import com.patrykandpatrick.vico.core.extension.setAll +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex /** * A [ChartModelProducer] implementation that generates [ComposedChartEntryModel] instances. * - * @property chartModelProducers the list of [ChartModelProducer]s to be composed by this - * [ComposedChartEntryModelProducer]. - * @param backgroundExecutor an [Executor] used to generate instances of the [ComposedChartEntryModel] off the main - * thread. - * * @see ComposedChartEntryModel * @see ChartModelProducer */ -public class ComposedChartEntryModelProducer( - public val chartModelProducers: List>, - backgroundExecutor: Executor = Executors.newFixedThreadPool(DEF_THREAD_POOL_SIZE), -) : ChartModelProducer> { +public class ComposedChartEntryModelProducer private constructor(dispatcher: CoroutineDispatcher) : + ChartModelProducer> { - private val compositeModelReceivers: HashMap> = HashMap() + private var dataSets = emptyList>>() + private var cachedInternalComposedModel: InternalComposedModel? = null + private val mutex = Mutex() + private val coroutineScope = CoroutineScope(dispatcher) + private val updateReceivers = mutableMapOf() + private val extraStore = MutableExtraStore() - private val executor: Executor = backgroundExecutor + private fun setDataSets(dataSets: List>>): Boolean { + if (!mutex.tryLock()) return false + this.dataSets = dataSets.copy() + cachedInternalComposedModel = null + val deferredUpdates = updateReceivers.values.map { updateReceiver -> + coroutineScope.async { updateReceiver.handleUpdate() } + } + coroutineScope.launch { + deferredUpdates.awaitAll() + mutex.unlock() + } + return true + } - private var cachedModel: ComposedChartEntryModel? = null + private suspend fun setDataSetsSuspending(dataSets: List>>): Deferred { + mutex.lock() + this.dataSets = dataSets.copy() + cachedInternalComposedModel = null + val completableDeferred = CompletableDeferred() + val deferredUpdates = updateReceivers.values.map { updateReceiver -> + coroutineScope.async { updateReceiver.handleUpdate() } + } + coroutineScope.launch { + deferredUpdates.awaitAll() + mutex.unlock() + completableDeferred.complete(Unit) + } + return completableDeferred + } - public constructor( - vararg chartModelProducers: ChartModelProducer, - backgroundExecutor: Executor = Executors.newFixedThreadPool(DEF_THREAD_POOL_SIZE), - ) : this(chartModelProducers.toList(), backgroundExecutor) + private fun getInternalModel(extraStore: ExtraStore? = null) = + if (dataSets.isEmpty()) { + null + } else { + val mergedExtraStore = this.extraStore.let { if (extraStore != null) it + extraStore else it } + cachedInternalComposedModel + ?.let { composedModel -> + composedModel.copy( + composedEntryCollections = composedModel.composedEntryCollections + .map { model -> model.copy(extraStore = mergedExtraStore) }, + extraStore = mergedExtraStore, + ) + } + ?: run { + val models = dataSets.map { dataSet -> + val xRange = dataSet.xRange + val yRange = dataSet.yRange + val aggregateYRange = dataSet.calculateStackedYRange() + InternalModel( + entries = dataSet, + minX = xRange.start, + maxX = xRange.endInclusive, + minY = yRange.start, + maxY = yRange.endInclusive, + stackedPositiveY = aggregateYRange.endInclusive, + stackedNegativeY = aggregateYRange.start, + xGcd = dataSet.calculateXGcd(), + extraStore = mergedExtraStore, + ) + } + InternalComposedModel( + composedEntryCollections = models, + entries = models.map { it.entries }.flatten(), + minX = models.minOf { it.minX }, + maxX = models.maxOf { it.maxX }, + minY = models.minOf { it.minY }, + maxY = models.maxOf { it.maxY }, + stackedPositiveY = models.maxOf { it.stackedPositiveY }, + stackedNegativeY = models.minOf { it.stackedNegativeY }, + xGcd = models.fold(null) { gcd, model -> + gcd?.gcdWith(model.xGcd) ?: model.xGcd + } ?: 1f, + id = models.map { it.id }.hashCode(), + extraStore = mergedExtraStore, + ).also { cachedInternalComposedModel = it } + } + } - override fun getModel(): ComposedChartEntryModel = - cachedModel ?: composedChartEntryModelOf(chartModelProducers.map { it.getModel() }) - .also { cachedModel = it } + override fun getModel(): ComposedChartEntryModel? = getInternalModel() - override fun progressModel(key: Any, progress: Float) { - chartModelProducers.forEach { producer -> - producer.progressModel(key, progress) + override suspend fun transformModel(key: Any, fraction: Float) { + with(updateReceivers[key] ?: return) { + modelTransformer?.transform(extraStore, fraction) + val internalModel = getInternalModel(extraStore.copy()) + currentCoroutineContext().ensureActive() + onModelCreated(internalModel) } } + @WorkerThread override fun registerForUpdates( key: Any, - updateListener: () -> Unit, - getOldModel: () -> ComposedChartEntryModel?, - onModel: (ComposedChartEntryModel) -> Unit, + cancelAnimation: () -> Unit, + startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, + getOldModel: () -> ComposedChartEntryModel?, + modelTransformerProvider: Chart.ModelTransformerProvider?, + extraStore: MutableExtraStore, + updateChartValues: (ComposedChartEntryModel?) -> ChartValuesProvider, + onModelCreated: (ComposedChartEntryModel?) -> Unit, ) { - val receiver = CompositeModelReceiver(onModel, executor) - compositeModelReceivers[key] = receiver - chartModelProducers.forEachIndexed { index, producer -> - producer.registerForUpdates( - key = key, - updateListener = updateListener, - getOldModel = { getOldModel()?.composedEntryCollections?.getOrNull(index) }, - onModel = receiver.getModelReceiver(index), - ) + UpdateReceiver( + cancelAnimation, + startAnimation, + onModelCreated, + extraStore, + modelTransformerProvider?.getModelTransformer(), + getOldModel, + updateChartValues, + ).run { + updateReceivers[key] = this + handleUpdate() } } - private class CompositeModelReceiver( - private val onModel: (ComposedChartEntryModel) -> Unit, - private val executor: Executor, - ) { + override fun isRegistered(key: Any): Boolean = updateReceivers.containsKey(key) + + override fun unregisterFromUpdates(key: Any) { + updateReceivers.remove(key) + } + + /** + * Creates a [Transaction] instance. + */ + public fun createTransaction(): Transaction = Transaction() - private val modelReceivers: SortedMap = TreeMap() + /** + * Creates a [Transaction], runs [block], and calls [Transaction.commit], returning its output. For suspending + * behavior, use [runTransactionSuspending]. + */ + public fun runTransaction(block: Transaction.() -> Unit): Boolean = createTransaction().also(block).commit() - internal fun getModelReceiver(index: Int): (Model) -> Unit { - val modelReceiver: (Model) -> Unit = { model -> - onModelUpdate(index, model) - } - modelReceivers[index] = null - return modelReceiver + /** + * Creates a [Transaction], runs [block], and calls [Transaction.commitSuspending], returning its output. + */ + public suspend fun runTransactionSuspending(block: Transaction.() -> Unit): Deferred = + createTransaction().also(block).commitSuspending() + + /** + * Handles data updates. An initially empty list of data sets is created and can be updated via the class’s + * functions. Each data set corresponds to a single nested [Chart]. + */ + public inner class Transaction internal constructor() { + private val newDataSets = mutableListOf>>() + + /** + * Populates the new list of data sets with the current data sets. + */ + public fun populate() { + newDataSets.setAll(dataSets) } - private fun onModelUpdate(index: Int, model: Model) { - modelReceivers[index] = model - val models = modelReceivers.values.mapNotNull { it } - if (modelReceivers.values.size == models.size) { - executor.execute { - onModel(composedChartEntryModelOf(models)) - } - } + /** + * Replaces the data set at the specified index ([Pair.first]) with the provided data set ([Pair.second]). + */ + public fun set(pair: Pair>>) { + set(pair.first, pair.second) + } + + /** + * Removes the data set at the specified index. + */ + public fun removeAt(index: Int) { + newDataSets.removeAt(index) + } + + /** + * Replaces the data set at the specified index with the provided data set. + */ + public fun set(index: Int, dataSet: List>) { + newDataSets[index] = dataSet } + + /** + * Adds a data set. + */ + public fun add(dataSet: List>) { + newDataSets.add(dataSet) + } + + /** + * Adds a data set. + */ + public fun add(index: Int, dataSet: List>) { + newDataSets.add(index, dataSet) + } + + /** + * Adds a data set comprising the provided series. + */ + public fun add(vararg series: List) { + add(series.toList()) + } + + /** + * Clears the new list of data sets. + */ + public fun clear() { + newDataSets.clear() + } + + /** + * Allows for adding auxiliary values, which can later be retrieved via [ChartEntryModel.extraStore]. + */ + public fun updateExtras(block: (MutableExtraStore) -> Unit) { + block(extraStore) + } + + /** + * Requests a data update. If the update is accepted, `true` is returned. If the update is rejected, which + * occurs when there’s already an update in progress, `false` is returned. For suspending behavior, use + * [commitSuspending]. + */ + public fun commit(): Boolean = setDataSets(newDataSets) + + /** + * Runs a data update. Unlike [commit], this function suspends the current coroutine and waits until an update + * can be run, meaning the update cannot be rejected. The returned [Deferred] implementation is marked as + * completed once the update has been processed. + */ + public suspend fun commitSuspending(): Deferred = setDataSetsSuspending(newDataSets) } - override fun unregisterFromUpdates(key: Any) { - compositeModelReceivers.remove(key) - chartModelProducers.forEach { producer -> - producer.unregisterFromUpdates(key) + private inner class UpdateReceiver( + val cancelAnimation: () -> Unit, + val startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, + val onModelCreated: (ComposedChartEntryModel?) -> Unit, + val extraStore: MutableExtraStore, + val modelTransformer: Chart.ModelTransformer>?, + val getOldModel: () -> ComposedChartEntryModel?, + val updateChartValues: (ComposedChartEntryModel?) -> ChartValuesProvider, + ) { + fun handleUpdate() { + cancelAnimation() + modelTransformer?.prepareForTransformation( + oldModel = getOldModel(), + newModel = getModel(), + extraStore = extraStore, + chartValuesProvider = updateChartValues(getModel()), + ) + startAnimation(::transformModel) } } - override fun isRegistered(key: Any): Boolean = compositeModelReceivers.containsKey(key = key) + private data class InternalModel( + override val entries: List>, + override val minX: Float, + override val maxX: Float, + override val minY: Float, + override val maxY: Float, + override val stackedPositiveY: Float, + override val stackedNegativeY: Float, + override val xGcd: Float, + override val extraStore: ExtraStore, + ) : ChartEntryModel - public companion object { + private data class InternalComposedModel( + override val composedEntryCollections: List, + override val entries: List>, + override val minX: Float, + override val maxX: Float, + override val minY: Float, + override val maxY: Float, + override val stackedPositiveY: Float, + override val stackedNegativeY: Float, + override val xGcd: Float, + override val id: Int, + override val extraStore: ExtraStore, + ) : ComposedChartEntryModel + public companion object { /** - * Creates a [ComposedChartEntryModel] instance comprising the provided [Model]s. + * Creates a [ComposedChartEntryModelProducer], running an initial [Transaction]. [dispatcher] is the + * [CoroutineDispatcher] to be used for update handling. */ - public fun composedChartEntryModelOf( - models: List, - ): ComposedChartEntryModel = object : ComposedChartEntryModel { - override val composedEntryCollections: List = models - override val entries: List> = models.map { it.entries }.flatten() - override val minX: Float = models.minOf { it.minX } - override val maxX: Float = models.maxOf { it.maxX } - override val minY: Float = models.minOf { it.minY } - override val maxY: Float = models.maxOf { it.maxY } - override val stackedPositiveY: Float = models.maxOf { it.stackedPositiveY } - override val stackedNegativeY: Float = models.minOf { it.stackedNegativeY } - override val xGcd: Float = models.fold(null) { gcd, model -> - gcd?.gcdWith(model.xGcd) ?: model.xGcd - } ?: 1f - override val id: Int = models.map { it.id }.hashCode() - } + public fun build( + dispatcher: CoroutineDispatcher = Dispatchers.Default, + transaction: Transaction.() -> Unit = {}, + ): ComposedChartEntryModelProducer = + ComposedChartEntryModelProducer(dispatcher).also { it.runTransaction(transaction) } } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedEntryListExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedEntryListExtensions.kt index 87074a920..bc452ae0d 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedEntryListExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedEntryListExtensions.kt @@ -18,35 +18,18 @@ package com.patrykandpatrick.vico.core.entry.composed import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartEntryModel -import com.patrykandpatrick.vico.core.entry.ChartModelProducer -/** - * Combines two [ChartModelProducer] implementations into a [ComposedChartEntryModelProducer]. - */ -public operator fun ChartModelProducer.plus( - other: ChartModelProducer, -): ComposedChartEntryModelProducer = - ComposedChartEntryModelProducer(listOf(this, other)) - -/** - * Combines this [ComposedChartEntryModelProducer] and a [ChartModelProducer] - * into a single [ComposedChartEntryModelProducer]. - */ -public operator fun ComposedChartEntryModelProducer.plus( - other: ChartModelProducer, -): ComposedChartEntryModelProducer = - ComposedChartEntryModelProducer(chartModelProducers + other) - -/** - * Combines two [ComposedChartEntryModelProducer] instances into a single one. - */ -public operator fun ComposedChartEntryModelProducer.plus( - other: ComposedChartEntryModelProducer, -): ComposedChartEntryModelProducer = - ComposedChartEntryModelProducer(chartModelProducers + other.chartModelProducers) +private fun ComposedChartEntryModelProducer.Transaction.add(chartEntryModels: List) { + chartEntryModels.forEach { add(it.entries) } +} /** * Combines two [ChartEntryModel] implementations—the receiver and [other]—into a [ComposedChartEntryModel]. */ -public operator fun Model.plus(other: Model): ComposedChartEntryModel = - ComposedChartEntryModelProducer.composedChartEntryModelOf(listOf(this, other)) +public operator fun Model.plus(other: Model): ComposedChartEntryModel = + ComposedChartEntryModelProducer + .build { + if (this@plus is ComposedChartEntryModel<*>) add(composedEntryCollections) else add(entries) + if (other is ComposedChartEntryModel<*>) add(other.composedEntryCollections) else add(other.entries) + } + .requireModel() diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DefaultDiffProcessor.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DefaultDiffProcessor.kt deleted file mode 100644 index 52e797f57..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DefaultDiffProcessor.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.patrykandpatrick.vico.core.entry.diff - -import com.patrykandpatrick.vico.core.entry.ChartEntry -import com.patrykandpatrick.vico.core.entry.calculateStackedYRange -import com.patrykandpatrick.vico.core.entry.yRange -import com.patrykandpatrick.vico.core.extension.orZero -import com.patrykandpatrick.vico.core.extension.setToAllChildren -import java.util.TreeMap - -/** - * The default implementation of [DiffProcessor]. - */ -public class DefaultDiffProcessor : DiffProcessor { - - private val progressMaps = ArrayList>() - - private val oldEntries = ArrayList>() - private val newEntries = ArrayList>() - - private var oldYRange = 0f..0f - private var newYRange = 0f..0f - private var oldStackedYRange = 0f..0f - private var newStackedYRange = 0f..0f - - override fun setEntries( - old: List>, - new: List>, - ): Unit = synchronized(this) { - oldEntries.setToAllChildren(old) - newEntries.setToAllChildren(new) - updateProgressMap() - updateRanges() - } - - override fun setEntries(new: List>) { - setEntries(old = newEntries, new = new) - } - - override fun progressDiff(progress: Float): List> = synchronized(this) { - progressMaps.mapNotNull { map -> - map.mapNotNull { (_, model) -> - if (model.temporary && progress == 1f) null else model.progressDiff(progress) - }.takeIf { list -> list.isNotEmpty() } - } - } - - override fun yRangeProgressDiff(progress: Float): ClosedFloatingPointRange = when { - oldYRange == ZERO_TO_ZERO -> newYRange - newYRange == ZERO_TO_ZERO -> if (progress == 1f) newYRange else oldYRange - else -> RangeProgressModel( - oldRange = oldYRange, - newRange = newYRange, - ).progressDiff(progress) - } - - override fun stackedYRangeProgressDiff(progress: Float): ClosedFloatingPointRange = - RangeProgressModel( - oldRange = oldStackedYRange, - newRange = newStackedYRange, - ).progressDiff(progress) - - private fun updateRanges() { - oldYRange = oldEntries.yRange - newYRange = newEntries.yRange - oldStackedYRange = oldEntries.calculateStackedYRange() - newStackedYRange = newEntries.calculateStackedYRange() - } - - private fun updateProgressMap() { - progressMaps.clear() - val maxListSize = maxOf(oldEntries.size, newEntries.size) - for (i in 0..() - oldEntries - .getOrNull(i) - ?.forEach { chartEntry -> - map[chartEntry.x] = ChartEntryProgressModel( - oldY = chartEntry.y, - chartEntry = chartEntry, - ) - } - newEntries - .getOrNull(i) - ?.forEach { chartEntry -> - map[chartEntry.x] = ChartEntryProgressModel( - oldY = map[chartEntry.x]?.oldY, - newY = chartEntry.y, - temporary = false, - chartEntry = chartEntry, - ) - } - progressMaps.add(map) - } - } - - private data class RangeProgressModel( - val oldRange: ClosedFloatingPointRange, - val newRange: ClosedFloatingPointRange, - ) { - fun progressDiff(progress: Float): ClosedFloatingPointRange { - val minValue = ProgressModel( - oldY = oldRange.start, - newY = newRange.start, - ).progressDiff(progress) - - val maxValue = ProgressModel( - oldY = oldRange.endInclusive, - newY = newRange.endInclusive, - ).progressDiff(progress) - - return minValue..maxValue - } - } - - private data class ChartEntryProgressModel( - val oldY: Float? = null, - val newY: Float? = null, - val temporary: Boolean = true, - val chartEntry: ChartEntry, - ) { - fun progressDiff(progress: Float): ChartEntry = chartEntry.withY( - y = ProgressModel( - oldY = oldY, - newY = newY, - ).progressDiff(progress = progress), - ) - } - - private data class ProgressModel( - val oldY: Float? = null, - val newY: Float? = null, - ) { - fun progressDiff(progress: Float): Float { - val oldY = oldY.orZero - val newY = newY.orZero - return oldY + (newY - oldY) * progress - } - } - - private companion object { - val ZERO_TO_ZERO = 0f..0f - } -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DefaultDrawingModelInterpolator.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DefaultDrawingModelInterpolator.kt new file mode 100644 index 000000000..e0518b847 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DefaultDrawingModelInterpolator.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.entry.diff + +import com.patrykandpatrick.vico.core.extension.orZero +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlin.math.max + +/** + * The default [DrawingModelInterpolator] implementation. + */ +@Suppress("UNCHECKED_CAST") +public class DefaultDrawingModelInterpolator> : + DrawingModelInterpolator { + + private var transformationMaps = emptyList>>() + private var oldDrawingModel: R? = null + private var newDrawingModel: R? = null + + override fun setModels(old: R?, new: R?) { + synchronized(this) { + oldDrawingModel = old + newDrawingModel = new + updateTransformationMap() + } + } + + override suspend fun transform(fraction: Float): R? = newDrawingModel?.transform( + drawingInfo = transformationMaps.mapNotNull { map -> + map + .mapNotNull { (x, model) -> + currentCoroutineContext().ensureActive() + model.transform(fraction)?.let { drawingInfo -> x to drawingInfo } + } + .takeIf { list -> list.isNotEmpty() } + ?.toMap() + }, + from = oldDrawingModel, + fraction = fraction, + ) as R? + + private fun updateTransformationMap() { + transformationMaps = buildList { + repeat(max(oldDrawingModel?.size.orZero, newDrawingModel?.size.orZero)) { index -> + val map = mutableMapOf>() + oldDrawingModel + ?.getOrNull(index) + ?.forEach { (x, drawingInfo) -> map[x] = TransformationModel(drawingInfo) } + newDrawingModel + ?.getOrNull(index) + ?.forEach { (x, drawingInfo) -> map[x] = TransformationModel(map[x]?.old, drawingInfo) } + add(map) + } + } + } + + private class TransformationModel(val old: T?, val new: T? = null) { + fun transform(fraction: Float): T? = new?.transform(old, fraction) as T? + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DiffProcessor.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DiffProcessor.kt deleted file mode 100644 index 30a14f740..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DiffProcessor.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.patrykandpatrick.vico.core.entry.diff - -import com.patrykandpatrick.vico.core.entry.ChartEntry - -/** - * Processes the difference between two collections of [ChartEntry] instances and generates intermediate collections - * for use in difference animations. - */ -public interface DiffProcessor { - - /** - * Sets the initial and target collections of [ChartEntry] instances. - * @param old the initial collection. - * @param new the target collection. - */ - public fun setEntries(old: List>, new: List>) - - /** - * Reuses the current target collection of [ChartEntry] instances as the initial collection and sets a new target - * collection. - * @param new the target collection. - */ - public fun setEntries(new: List>) - - /** - * Creates an intermediate collection of [ChartEntry] instances for use in difference animations. - * @param progress the balance between the initial and target collections. A value of `0f` yields the initial - * collection, and a value of `1f` yields the target collection. - */ - public fun progressDiff(progress: Float): List> - - /** - * Creates an intermediate y-value range for use in difference animations. - * @param progress the balance between the initial and target range. A value of `Of` yields the initial range, and - * a value of `1f` yields the target range. - */ - public fun yRangeProgressDiff(progress: Float): ClosedFloatingPointRange - - /** - * Creates an intermediate stacked y-value range for use in difference animations. - * @param progress the balance between the initial and target range. A value of `Of` yields the initial range, and - * a value of `1f` yields the target range. - */ - public fun stackedYRangeProgressDiff(progress: Float): ClosedFloatingPointRange -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModel.kt new file mode 100644 index 000000000..b02ffbfaf --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModel.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.entry.diff + +import com.patrykandpatrick.vico.core.chart.Chart + +/** + * Houses drawing information for a [Chart]. + */ +public abstract class DrawingModel(private val drawingInfo: List>) : + List> by drawingInfo { + + /** + * Returns an intermediate [DrawingModel] between this one and [from]. The returned drawing model includes the + * provided [DrawingInfo] list. [fraction] is the balance between [from] and this [DrawingModel], with 0 + * corresponding to [from], and 1 corresponding to this [DrawingModel]. The returned object should be an instance + * of the [DrawingModel] subclass to which this function belongs. + */ + public abstract fun transform( + drawingInfo: List>, + from: DrawingModel?, + fraction: Float, + ): DrawingModel + + /** + * Houses positional information for a single [Chart] entity (e.g., a column or a point). + */ + public interface DrawingInfo { + /** + * Returns an intermediate [DrawingInfo] implementation between this one and [from]. [fraction] is the balance + * between [from] and this [DrawingInfo] implementation, with 0 corresponding to [from], and 1 corresponding to + * this [DrawingInfo] implementation. The returned object should be an instance of the [DrawingInfo] + * implementation to which this function belongs. + */ + public fun transform(from: DrawingInfo?, fraction: Float): DrawingInfo + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModelInterpolator.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModelInterpolator.kt new file mode 100644 index 000000000..28668325b --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModelInterpolator.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.entry.diff + +/** + * Interpolates two [DrawingModel]s. + */ +public interface DrawingModelInterpolator> { + /** + * Sets the initial and target [DrawingModel]s. + */ + public fun setModels(old: R?, new: R?) + + /** + * Interpolates the two [DrawingModel]s. [fraction] is the balance between the initial and target [DrawingModel]s, + * with 0 corresponding to the initial [DrawingModel], and 1 corresponding to the target [DrawingModel]. + */ + public suspend fun transform(fraction: Float): R? +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/ExtraStore.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/ExtraStore.kt new file mode 100644 index 000000000..f75d5868b --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/ExtraStore.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.entry.diff + +/** + * Houses auxiliary data. + */ +@Suppress("UNCHECKED_CAST") +public abstract class ExtraStore internal constructor() { + /** + * The underlying [Map]. + */ + protected abstract val mapDelegate: Map, Any> + + /** + * Returns the value associated with the provided key. + */ + public open operator fun get(key: Key): T = mapDelegate[key] as T + + /** + * Returns the value associated with the provided key, or `null` if there’s no such value. + */ + public fun getOrNull(key: Key): T? = mapDelegate[key] as? T + + /** + * Creates a copy of this [ExtraStore]. + */ + public abstract fun copy(): ExtraStore + + /** + * Copies this [ExtraStore]’s content to [destination]. + */ + public abstract fun copyContentTo(destination: MutableMap, Any>) + + /** + * Combines this [ExtraStore] and [other]. + */ + public abstract operator fun plus(other: ExtraStore): ExtraStore + + /** + * Used for writing to and reading from [ExtraStore]s. + */ + @Suppress("UNUSED") + public open class Key + + public companion object { + /** + * An empty [ExtraStore]. + */ + public val empty: ExtraStore = MutableExtraStore() + } +} + +/** + * A [ExtraStore] subclass that allows for data updates. + */ +public class MutableExtraStore internal constructor(mapDelegate: Map, Any>) : ExtraStore() { + override val mapDelegate: MutableMap, Any> = mapDelegate.toMutableMap() + + /** + * Creates an empty [MutableExtraStore]. + */ + public constructor() : this(emptyMap()) + + /** + * Saves the provided value to this [MutableExtraStore], associating the value with the given key. + */ + public operator fun set(key: Key, value: T) { + mapDelegate[key] = value + } + + /** + * Removes the value associated with the provided key. + */ + public fun > remove(key: Key) { + mapDelegate.remove(key) + } + + override fun copy(): ExtraStore = MutableExtraStore(mapDelegate) + + override fun copyContentTo(destination: MutableMap, Any>) { + destination.putAll(mapDelegate) + } + + override operator fun plus(other: ExtraStore): ExtraStore = MutableExtraStore( + buildMap { + putAll(mapDelegate) + other.copyContentTo(this) + }, + ) +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/CollectionExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/CollectionExtensions.kt index 9205ce23d..8cc4fdcba 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/CollectionExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/CollectionExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,12 @@ internal fun List.getRepeating(index: Int): T { return get(index % size.coerceAtLeast(1)) } +@JvmName("copyDouble") +internal fun List>.copy() = map { it.toList() } + +@JvmName("copyTriple") +internal fun List>>.copy() = map { it.copy() } + /** * Replaces all of the elements of this [MutableList] with the elements of the provided collection. */ @@ -56,6 +62,12 @@ private fun ArrayList>.ensureSize(size: Int) { } } +/** + * Creates a copy of this [ArrayList] and each child [ArrayList] contained in this [ArrayList]. + */ +public fun ArrayList>.copy(): List> = + List(size) { index -> ArrayList(get(index)) } + /** * Replaces all of the elements of this [MutableMap] with the elements of the provided map. */ diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/ColorExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/ColorExtensions.kt index 4f1d5fe31..5f2f1e1bb 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/ColorExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/ColorExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ private const val RED_BIT_SHIFT = 16 private const val GREEN_BIT_SHIFT = 8 private const val BLUE_BIT_SHIFT = 0 private const val COLOR_MASK = 0xff -private const val MAX_HEX_VALUE = 255f +internal const val MAX_HEX_VALUE = 255f /** * Copies this color, updating any or all of the color channels. diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/NumberExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/NumberExtensions.kt index 766fa64a6..4a209b08b 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/NumberExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/NumberExtensions.kt @@ -27,7 +27,7 @@ import kotlin.math.roundToInt */ public const val PI_RAD: Float = 180f -internal const val FLOAT_GCD_DECIMALS = 3 +internal const val FLOAT_GCD_DECIMALS = 2 private fun Float.round(decimals: Int): Float { val multiplier = 10f.pow(n = decimals) diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/PaintExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/PaintExtensions.kt index da1c8b9c0..20a135b6d 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/PaintExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/PaintExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,13 @@ import android.graphics.Paint private val fm: Paint.FontMetrics = Paint.FontMetrics() +internal fun Paint.withOpacity(opacity: Float, action: (Paint) -> Unit) { + val previousOpacity = this.alpha + color = color.copyColor(opacity * previousOpacity / MAX_HEX_VALUE) + action(this) + this.alpha = previousOpacity +} + /** * Returns the height of a single line of text. */ diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/RandomEntriesGenerator.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/RandomEntriesGenerator.kt index 2cb1a3447..120c3a094 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/RandomEntriesGenerator.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/RandomEntriesGenerator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,25 +52,23 @@ public class RandomEntriesGenerator( * The size of the collection is equal to the number of values in [xRange]. */ public fun randomEntryModel(): ChartEntryModel = - getChartEntryModelProducer().getModel() + getChartEntryModelProducer().requireModel() /** * Creates a [ComposedChartEntryModel] with three [ChartEntryModelProducer]s, each containing a collection of * [FloatEntry] instances with randomized y values. The size of each collection is equal to the number of values in * [xRange]. */ - public fun randomComposedEntryModel(): ComposedChartEntryModel = - ComposedChartEntryModelProducer( - getChartEntryModelProducer(), - getChartEntryModelProducer(), - getChartEntryModelProducer(), - ).getModel() + public fun randomComposedEntryModel(): ComposedChartEntryModel = ComposedChartEntryModelProducer + .build { repeat(MODEL_SIZE) { add(List(MODEL_SIZE) { generateRandomEntries() }) } } + .requireModel() private companion object { const val X_RANGE_TOP = 10 const val Y_RANGE_TOP = 20 + const val MODEL_SIZE = 3 fun RandomEntriesGenerator.getChartEntryModelProducer(): ChartModelProducer = - ChartEntryModelProducer(listOf(generateRandomEntries(), generateRandomEntries(), generateRandomEntries())) + ChartEntryModelProducer(List(MODEL_SIZE) { generateRandomEntries() }) } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/ValueWrapper.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/ValueWrapper.kt new file mode 100644 index 000000000..776ea0483 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/ValueWrapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.util + +import androidx.annotation.RestrictTo +import kotlin.reflect.KProperty + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class ValueWrapper(public var value: T) + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public operator fun ValueWrapper.getValue(thisObj: Any?, property: KProperty<*>): T = value + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public operator fun ValueWrapper.setValue(thisObj: Any?, property: KProperty<*>, value: T) { + this.value = value +} + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public operator fun ValueWrapper.component1(): T = value + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public operator fun ValueWrapper.component2(): (T) -> Unit = { value = it } diff --git a/vico/core/src/test/java/com/patrykandpatrick/vico/core/ChartValuesProducerTests.kt b/vico/core/src/test/java/com/patrykandpatrick/vico/core/ChartEntryModelProducerTest.kt similarity index 67% rename from vico/core/src/test/java/com/patrykandpatrick/vico/core/ChartValuesProducerTests.kt rename to vico/core/src/test/java/com/patrykandpatrick/vico/core/ChartEntryModelProducerTest.kt index a7d07d9bd..6df03efed 100644 --- a/vico/core/src/test/java/com/patrykandpatrick/vico/core/ChartValuesProducerTests.kt +++ b/vico/core/src/test/java/com/patrykandpatrick/vico/core/ChartEntryModelProducerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,11 @@ package com.patrykandpatrick.vico.core import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer -import com.patrykandpatrick.vico.core.entry.diff.DefaultDiffProcessor import com.patrykandpatrick.vico.core.entry.entriesOf import org.junit.Test import kotlin.test.assertEquals -public class ChartValuesProducerTests { +public class ChartEntryModelProducerTest { private val minX = 0f private val maxX = 3f @@ -35,25 +34,11 @@ public class ChartValuesProducerTests { @Test public fun `Test Min Max calculations`() { - val entryList = ChartEntryModelProducer(entries1, entries2, entries3).getModel() + val entryList = ChartEntryModelProducer(entries1, entries2, entries3).requireModel() assertEquals(minX, entryList.minX) assertEquals(maxX, entryList.maxX) assertEquals(minY, entryList.minY) assertEquals(maxY, entryList.maxY) assertEquals(10f, entryList.stackedPositiveY) } - - @Test - public fun `Test entry update while diff animation is running`() { - val first = entriesOf(0f to 2f, 1f to 0f) - val second = entriesOf(0f to 0f, 1f to 2f) - - val diffProcessor = DefaultDiffProcessor() - diffProcessor.setEntries(listOf(first)) - - assertEquals(first, diffProcessor.progressDiff(1f)[0]) - - diffProcessor.setEntries(listOf(second)) - assertEquals(entriesOf(0f to 1f, 1f to 1f), diffProcessor.progressDiff(.5f)[0]) - } } diff --git a/vico/core/src/test/java/com/patrykandpatrick/vico/core/chart/diff/DefaultDiffProcessorTest.kt b/vico/core/src/test/java/com/patrykandpatrick/vico/core/chart/diff/DefaultDiffProcessorTest.kt deleted file mode 100644 index 65ab94b0a..000000000 --- a/vico/core/src/test/java/com/patrykandpatrick/vico/core/chart/diff/DefaultDiffProcessorTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.patrykandpatrick.vico.core.chart.diff - -import com.patrykandpatrick.vico.core.entry.diff.DefaultDiffProcessor -import com.patrykandpatrick.vico.core.entry.entriesOf -import org.junit.Assert -import org.junit.Before -import org.junit.Test - -public class DefaultDiffProcessorTest { - - private val processor = DefaultDiffProcessor() - private val old = listOf(entriesOf(0f to 2f, 1f to 1f, 2f to 1f, 3f to 10f)) - private val new = listOf(entriesOf(0f to 2f, 2f to 2f, 3f to 5f)) - - @Before - public fun setEntries() { - processor.setEntries(old = old, new = new) - } - - @Test - public fun `Test 0 per cent progress`() { - Assert.assertEquals(old, processor.progressDiff(0f)) - } - - @Test - public fun `Test 50 per cent progress`() { - val expected = listOf(entriesOf(0f to 2f, 1f to 0.5f, 2f to 1.5f, 3f to 7.5f)) - Assert.assertEquals(expected, processor.progressDiff(0.5f)) - } - - @Test - public fun `Test 100 per cent progress`() { - Assert.assertEquals(new, processor.progressDiff(1f)) - } -} diff --git a/vico/views/src/main/java/com/patrykandpatrick/vico/views/chart/BaseChartView.kt b/vico/views/src/main/java/com/patrykandpatrick/vico/views/chart/BaseChartView.kt index d9da29340..1da1228dc 100644 --- a/vico/views/src/main/java/com/patrykandpatrick/vico/views/chart/BaseChartView.kt +++ b/vico/views/src/main/java/com/patrykandpatrick/vico/views/chart/BaseChartView.kt @@ -24,9 +24,12 @@ import android.util.AttributeSet import android.view.MotionEvent import android.view.ScaleGestureDetector import android.view.View +import android.view.ViewGroup import android.view.animation.Interpolator +import android.widget.FrameLayout import android.widget.OverScroller import androidx.core.view.ViewCompat +import androidx.core.view.isVisible import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.patrykandpatrick.vico.core.Animation import com.patrykandpatrick.vico.core.DEF_MAX_ZOOM @@ -36,6 +39,7 @@ import com.patrykandpatrick.vico.core.axis.AxisManager import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisRenderer import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.chartDrawContext import com.patrykandpatrick.vico.core.chart.draw.drawMarker import com.patrykandpatrick.vico.core.chart.draw.getAutoZoom @@ -43,11 +47,14 @@ import com.patrykandpatrick.vico.core.chart.draw.getMaxScrollDistance import com.patrykandpatrick.vico.core.chart.edges.FadingEdges import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp +import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.chart.values.toChartValuesProvider import com.patrykandpatrick.vico.core.component.shape.ShapeComponent -import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.context.MutableMeasureContext import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartModelProducer +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore import com.patrykandpatrick.vico.core.extension.set import com.patrykandpatrick.vico.core.extension.spToPx import com.patrykandpatrick.vico.core.layout.VirtualLayout @@ -62,7 +69,7 @@ import com.patrykandpatrick.vico.views.extension.defaultColors import com.patrykandpatrick.vico.views.extension.density import com.patrykandpatrick.vico.views.extension.dpInt import com.patrykandpatrick.vico.views.extension.isLtr -import com.patrykandpatrick.vico.views.extension.measureDimension +import com.patrykandpatrick.vico.views.extension.specMode import com.patrykandpatrick.vico.views.extension.specSize import com.patrykandpatrick.vico.views.extension.verticalPadding import com.patrykandpatrick.vico.views.gestures.ChartScaleGestureListener @@ -72,6 +79,15 @@ import com.patrykandpatrick.vico.views.gestures.movedYDistance import com.patrykandpatrick.vico.views.scroll.ChartScrollSpec import com.patrykandpatrick.vico.views.scroll.copy import com.patrykandpatrick.vico.views.theme.ThemeHandler +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlin.coroutines.EmptyCoroutineContext import kotlin.properties.Delegates.observable import kotlin.properties.ReadWriteProperty @@ -83,7 +99,7 @@ public abstract class BaseChartView internal constructo attrs: AttributeSet? = null, defStyleAttr: Int = 0, chartType: ThemeHandler.ChartType, -) : View(context, attrs, defStyleAttr), ScrollListenerHost { +) : FrameLayout(context, attrs, defStyleAttr), ScrollListenerHost { private val contentBounds = RectF() @@ -95,6 +111,8 @@ public abstract class BaseChartView internal constructo private val virtualLayout = VirtualLayout(axisManager) + private val chartValuesManager = ChartValuesManager() + private val motionEventHandler = MotionEventHandler( scroller = scroller, scrollHandler = scrollHandler, @@ -109,6 +127,7 @@ public abstract class BaseChartView internal constructo isLtr = context.isLtr, isHorizontalScrollEnabled = false, spToPx = context::spToPx, + chartValuesProvider = ChartValuesProvider.Empty, ) private val scaleGestureListener: ScaleGestureDetector.OnScaleGestureListener = @@ -123,7 +142,7 @@ public abstract class BaseChartView internal constructo ValueAnimator.ofFloat(Animation.range.start, Animation.range.endInclusive).apply { duration = Animation.DIFF_DURATION.toLong() interpolator = FastOutSlowInInterpolator() - addUpdateListener { progressModelOnAnimationProgress(it.animatedFraction) } + addUpdateListener { transformModelForAnimation(it.animatedFraction) } } private val scrollValueAnimator: ValueAnimator = @@ -132,6 +151,18 @@ public abstract class BaseChartView internal constructo interpolator = FastOutSlowInInterpolator() } + private val extraStore = MutableExtraStore() + + private var coroutineScope: CoroutineScope? = null + + private var animationFrameJob: Job? = null + + private var finalAnimationFrameJob: Job? = null + + private var isAnimationRunning = false + + private var isAnimationFrameGenerationRunning: Boolean = false + private var markerTouchPoint: Point? = null private var wasMarkerVisible: Boolean = false @@ -144,8 +175,14 @@ public abstract class BaseChartView internal constructo private var wasZoomOverridden = false + private var chartValuesProvider: ChartValuesProvider = ChartValuesProvider.Empty + + private var horizontalDimensions = MutableHorizontalDimensions() + internal val themeHandler: ThemeHandler = ThemeHandler(context, attrs, chartType) + protected var placeholder: View? = null + /** * The [AxisRenderer] for the start axis. */ @@ -210,11 +247,16 @@ public abstract class BaseChartView internal constructo */ public var runInitialAnimation: Boolean = true + /** + * The [CoroutineDispatcher] to be used for the handling of difference animations. + */ + public var dispatcher: CoroutineDispatcher = Dispatchers.Default + /** * The [Chart] displayed by this [View]. */ public var chart: Chart? by observable(null) { _, _, _ -> - tryInvalidate(chart, model) + tryInvalidate(chart = chart, model = model, updateChartValues = true) } /** @@ -236,31 +278,65 @@ public abstract class BaseChartView internal constructo } private fun registerForUpdates() { - entryProducer?.registerForUpdates( - key = this, - updateListener = { - if (model != null || runInitialAnimation) { - handler.post(animator::start) - } else { - progressModelOnAnimationProgress(progress = Animation.range.endInclusive) + coroutineScope?.launch(dispatcher) { + entryProducer?.registerForUpdates( + key = this@BaseChartView, + cancelAnimation = { + handler?.post(animator::cancel) + runBlocking { + animationFrameJob?.cancelAndJoin() + finalAnimationFrameJob?.cancelAndJoin() + } + isAnimationRunning = false + isAnimationFrameGenerationRunning = false + }, + startAnimation = { transformModel -> + if (model != null || runInitialAnimation) { + handler?.post { + isAnimationRunning = true + animator.start() + } + } else { + finalAnimationFrameJob = coroutineScope?.launch(dispatcher) { + transformModel(this@BaseChartView, Animation.range.endInclusive) + } + } + }, + getOldModel = { model }, + modelTransformerProvider = chart?.modelTransformerProvider, + extraStore = extraStore, + updateChartValues = { model -> + chartValuesManager.resetChartValues() + if (model != null) { + chart?.updateChartValues(chartValuesManager, model, getXStep?.invoke(model)) + chartValuesManager.toChartValuesProvider() + } else { + ChartValuesProvider.Empty + }.also { provider -> chartValuesProvider = provider } + }, + ) { model -> + val chartValuesProvider = chartValuesProvider + post { + setModel(model = model, updateChartValues = false) + measureContext.chartValuesProvider = chartValuesProvider + postInvalidateOnAnimation() } - }, - getOldModel = { model }, - ) { model -> - post { - setModel(model = model) - postInvalidateOnAnimation() } } } override fun onAttachedToWindow() { super.onAttachedToWindow() + coroutineScope = CoroutineScope(EmptyCoroutineContext) if (entryProducer?.isRegistered(key = this) != true) registerForUpdates() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() + coroutineScope?.cancel() + coroutineScope = null + animator.cancel() + isAnimationRunning = false entryProducer?.unregisterFromUpdates(key = this) } @@ -307,12 +383,17 @@ public abstract class BaseChartView internal constructo /** * Sets the [Model] for this [BaseChartView]’s [Chart] instance ([chart]). */ - public fun setModel(model: Model) { + public fun setModel(model: Model?) { + setModel(model = model, updateChartValues = true) + } + + private fun setModel(model: Model?, updateChartValues: Boolean) { val oldModel = this.model this.model = model - tryInvalidate(chart = chart, model = model) - if (ViewCompat.isAttachedToWindow(this) && oldModel?.id != model.id && isInEditMode.not()) { - handler.post { + updatePlaceholderVisibility() + tryInvalidate(chart, model, updateChartValues) + if (model != null && oldModel?.id != model.id && isInEditMode.not()) { + handler?.post { chartScrollSpec.performAutoScroll( model = model, oldModel = oldModel, @@ -322,15 +403,14 @@ public abstract class BaseChartView internal constructo } } - protected fun tryInvalidate(chart: Chart?, model: Model?) { - if (chart != null && model != null) { - measureContext.chartValuesManager.resetChartValues() - chart.updateChartValues(measureContext.chartValuesManager, model, getXStep?.invoke(model)) - - if (ViewCompat.isAttachedToWindow(this)) { - invalidate() - } + protected fun tryInvalidate(chart: Chart?, model: Model?, updateChartValues: Boolean) { + if (chart == null || model == null) return + if (updateChartValues) { + chartValuesManager.resetChartValues() + chart.updateChartValues(chartValuesManager, model, getXStep?.invoke(model)) + measureContext.chartValuesProvider = chartValuesManager.toChartValuesProvider() } + if (ViewCompat.isAttachedToWindow(this)) invalidate() } protected inline fun invalidatingObservable( @@ -339,7 +419,7 @@ public abstract class BaseChartView internal constructo ): ReadWriteProperty { onChange(initialValue) return observable(initialValue) { _, _, newValue -> - tryInvalidate(chart, model) + tryInvalidate(chart = chart, model = model, updateChartValues = false) onChange(newValue) } } @@ -381,114 +461,157 @@ public abstract class BaseChartView internal constructo markerTouchPoint = point } - override fun dispatchDraw(canvas: Canvas): Unit = withChartAndModel { chart, model -> - val chartBounds = updateBounds(measureContext, chart, model) - - if (chartBounds.isEmpty) return@withChartAndModel - - motionEventHandler.isHorizontalScrollEnabled = chartScrollSpec.isScrollEnabled - if (scroller.computeScrollOffset()) { - scrollHandler.handleScroll(scroller.currX.toFloat()) - ViewCompat.postInvalidateOnAnimation(this) - } + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + + withChartAndModel { chart, model -> + measureContext.clearExtras() + horizontalDimensions.clear() + chart.updateHorizontalDimensions(measureContext, horizontalDimensions, model) + + startAxis?.updateHorizontalDimensions(measureContext, horizontalDimensions) + topAxis?.updateHorizontalDimensions(measureContext, horizontalDimensions) + endAxis?.updateHorizontalDimensions(measureContext, horizontalDimensions) + bottomAxis?.updateHorizontalDimensions(measureContext, horizontalDimensions) + + if ( + virtualLayout + .setBounds( + context = measureContext, + contentBounds = contentBounds, + chart = chart, + legend = legend, + horizontalDimensions = horizontalDimensions, + marker, + ) + .isEmpty + ) { + return@withChartAndModel + } - val horizontalDimensions = chart.getHorizontalDimensions(measureContext, model) + motionEventHandler.isHorizontalScrollEnabled = chartScrollSpec.isScrollEnabled + if (scroller.computeScrollOffset()) { + scrollHandler.handleScroll(scroller.currX.toFloat()) + ViewCompat.postInvalidateOnAnimation(this) + } - var finalZoom = zoom + var finalZoom = zoom - if (!wasZoomOverridden || !chartScrollSpec.isScrollEnabled) { - finalZoom = measureContext.getAutoZoom(horizontalDimensions, chart.bounds, autoScaleUp) - if (chartScrollSpec.isScrollEnabled) zoom = finalZoom - } + if (!wasZoomOverridden || !chartScrollSpec.isScrollEnabled) { + finalZoom = measureContext.getAutoZoom(horizontalDimensions, chart.bounds, autoScaleUp) + if (chartScrollSpec.isScrollEnabled) zoom = finalZoom + } - scrollHandler.maxValue = measureContext.getMaxScrollDistance( - chartWidth = chart.bounds.width(), - horizontalDimensions = horizontalDimensions, - zoom = finalZoom, - ) + scrollHandler.maxValue = measureContext.getMaxScrollDistance( + chartWidth = chart.bounds.width(), + horizontalDimensions = horizontalDimensions, + zoom = finalZoom, + ) - scrollHandler.handleInitialScroll(initialScroll = chartScrollSpec.initialScroll) - - val chartDrawContext = chartDrawContext( - canvas = canvas, - elevationOverlayColor = elevationOverlayColor, - measureContext = measureContext, - markerTouchPoint = markerTouchPoint, - horizontalDimensions = horizontalDimensions, - chartBounds = chart.bounds, - horizontalScroll = scrollHandler.value, - zoom = finalZoom, - ) + scrollHandler.handleInitialScroll(initialScroll = chartScrollSpec.initialScroll) - val count = if (fadingEdges != null) chartDrawContext.saveLayer() else -1 + val chartDrawContext = chartDrawContext( + canvas = canvas, + elevationOverlayColor = elevationOverlayColor, + measureContext = measureContext, + markerTouchPoint = markerTouchPoint, + horizontalDimensions = horizontalDimensions, + chartBounds = chart.bounds, + horizontalScroll = scrollHandler.value, + zoom = finalZoom, + ) - axisManager.drawBehindChart(chartDrawContext) - chart.drawScrollableContent(chartDrawContext, model) + val count = if (fadingEdges != null) chartDrawContext.saveLayer() else -1 - fadingEdges?.apply { - applyFadingEdges(chartDrawContext, chart.bounds) - chartDrawContext.restoreCanvasToCount(count) - } + axisManager.drawBehindChart(chartDrawContext) + chart.drawScrollableContent(chartDrawContext, model) - axisManager.drawAboveChart(chartDrawContext) - chart.drawNonScrollableContent(chartDrawContext, model) - legend?.draw(chartDrawContext) + fadingEdges?.apply { + applyFadingEdges(chartDrawContext, chart.bounds) + chartDrawContext.restoreCanvasToCount(count) + } - marker?.also { marker -> - chartDrawContext.drawMarker( - marker = marker, - markerTouchPoint = markerTouchPoint, - chart = chart, - markerVisibilityChangeListener = markerVisibilityChangeListener, - wasMarkerVisible = wasMarkerVisible, - setWasMarkerVisible = { wasMarkerVisible = it }, - lastMarkerEntryModels = lastMarkerEntryModels, - onMarkerEntryModelsChange = { lastMarkerEntryModels = it }, - ) + axisManager.drawAboveChart(chartDrawContext) + chart.drawNonScrollableContent(chartDrawContext, model) + legend?.draw(chartDrawContext) + + marker?.also { marker -> + chartDrawContext.drawMarker( + marker = marker, + markerTouchPoint = markerTouchPoint, + chart = chart, + markerVisibilityChangeListener = markerVisibilityChangeListener, + wasMarkerVisible = wasMarkerVisible, + setWasMarkerVisible = { wasMarkerVisible = it }, + lastMarkerEntryModels = lastMarkerEntryModels, + onMarkerEntryModelsChange = { lastMarkerEntryModels = it }, + ) + } + measureContext.reset() } - measureContext.clearExtras() } - private fun progressModelOnAnimationProgress(progress: Float) { - entryProducer?.progressModel(this, progress) + private fun transformModelForAnimation(fraction: Float) { + when { + !isAnimationRunning -> return + !isAnimationFrameGenerationRunning -> { + isAnimationFrameGenerationRunning = true + animationFrameJob = coroutineScope?.launch(dispatcher) { + entryProducer?.transformModel(this@BaseChartView, fraction) + isAnimationFrameGenerationRunning = false + } + } + fraction == 1f -> { + finalAnimationFrameJob = coroutineScope?.launch(dispatcher) { + animationFrameJob?.cancelAndJoin() + entryProducer?.transformModel(this@BaseChartView, fraction) + isAnimationFrameGenerationRunning = false + } + } + } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val width = measureDimension(widthMeasureSpec.specSize, widthMeasureSpec) + val width = widthMeasureSpec.specSize.coerceAtLeast(suggestedMinimumWidth) + val defaultHeight = DefaultDimens.CHART_HEIGHT.dpInt + verticalPadding + val height = when (heightMeasureSpec.specMode) { + MeasureSpec.EXACTLY -> heightMeasureSpec.specSize + MeasureSpec.AT_MOST -> defaultHeight.coerceAtMost(heightMeasureSpec.specSize) + else -> defaultHeight + }.coerceAtLeast(suggestedMinimumHeight) + super.onMeasure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), + ) + contentBounds + .set(left = paddingLeft, top = paddingTop, right = width - paddingRight, bottom = height - paddingBottom) + } - val height = when (MeasureSpec.getMode(heightMeasureSpec)) { - MeasureSpec.UNSPECIFIED -> DefaultDimens.CHART_HEIGHT.dpInt + verticalPadding - MeasureSpec.AT_MOST -> minOf( - DefaultDimens.CHART_HEIGHT.dpInt + verticalPadding, - heightMeasureSpec.specSize, - ) + override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { + check(childCount == 0) { "Only one placeholder can be added." } + super.addView(child, index, params) + placeholder = child + updatePlaceholderVisibility() + } - else -> measureDimension(heightMeasureSpec.specSize, heightMeasureSpec) - } - setMeasuredDimension(width, height) + override fun onViewRemoved(child: View?) { + super.onViewRemoved(child) + placeholder = null + } - contentBounds.set( - paddingLeft, - paddingTop, - width - paddingRight, - height - paddingBottom, - ) + /** + * Updates the placeholder, which is shown when no [ChartEntryModel] is available. + */ + public fun setPlaceholder(view: View?, params: LayoutParams? = null) { + if (view === placeholder) return + removeAllViews() + if (view != null) addView(view, params) + placeholder = view + updatePlaceholderVisibility() } - private fun updateBounds( - context: MeasureContext, - chart: Chart, - model: Model, - ): RectF { - measureContext.clearExtras() - return virtualLayout.setBounds( - context = measureContext, - contentBounds = contentBounds, - chart = chart, - legend = legend, - horizontalDimensions = chart.getHorizontalDimensions(context = context, model = model), - marker, - ) + protected fun updatePlaceholderVisibility() { + placeholder?.isVisible = model == null } private inline fun withChartAndModel(block: (chart: Chart, model: Model) -> Unit) { diff --git a/vico/views/src/main/java/com/patrykandpatrick/vico/views/theme/ThemeHandler.kt b/vico/views/src/main/java/com/patrykandpatrick/vico/views/theme/ThemeHandler.kt index e7d3a83f5..b6a764e25 100644 --- a/vico/views/src/main/java/com/patrykandpatrick/vico/views/theme/ThemeHandler.kt +++ b/vico/views/src/main/java/com/patrykandpatrick/vico/views/theme/ThemeHandler.kt @@ -46,7 +46,6 @@ import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.extension.hasAnyFlagOf import com.patrykandpatrick.vico.core.extension.hasFlag import com.patrykandpatrick.vico.views.R -import java.lang.Exception internal class ThemeHandler( private val context: Context, @@ -116,17 +115,22 @@ internal class ThemeHandler( .getBoolean(R.styleable.BaseChartView_chartZoomEnabled, true) fadingEdges = typedArray.getFadingEdges() horizontalLayout = typedArray.getHorizontalLayout() - } - when (chartType) { - ChartType.Single -> - context.obtainStyledAttributes(attrs, R.styleable.ChartView).use { typedArray -> - chart = typedArray.getChart() - } - ChartType.Composed -> - context.obtainStyledAttributes(attrs, R.styleable.ComposedChartView).use { typedArray -> - composedChart = typedArray.getComposedChart() - } + when (chartType) { + ChartType.Single -> + context + .obtainStyledAttributes(attrs, R.styleable.ChartView) + .use { chartViewTypedArray -> + chart = getChart(chartViewTypedArray, typedArray) + } + + ChartType.Composed -> + context + .obtainStyledAttributes(attrs, R.styleable.ComposedChartView) + .use { composedChartViewTypedArray -> + composedChart = getComposedChart(composedChartViewTypedArray, typedArray) + } + } } } @@ -201,14 +205,14 @@ internal class ThemeHandler( horizontalLabelPosition = axisStyle .getInteger(R.styleable.Axis_verticalAxisHorizontalLabelPosition, 0) .let { value -> - val values = VerticalAxis.HorizontalLabelPosition.values() + val values = VerticalAxis.HorizontalLabelPosition.entries values[value % values.size] } verticalLabelPosition = axisStyle .getInteger(R.styleable.Axis_verticalAxisVerticalLabelPosition, 0) .let { value -> - val values = VerticalAxis.VerticalLabelPosition.values() + val values = VerticalAxis.VerticalLabelPosition.entries values[value % values.size] } @@ -220,17 +224,21 @@ internal class ThemeHandler( axisStyle.getInteger(R.styleable.Axis_horizontalAxisLabelSpacing, 1), axisStyle.getInteger(R.styleable.Axis_horizontalAxisLabelOffset, 0), axisStyle.getBoolean(R.styleable.Axis_shiftExtremeHorizontalAxisTicks, true), + axisStyle.getBoolean(R.styleable.Axis_addExtremeHorizontalAxisLabelPadding, false), ) } } }.also { axisStyle.recycle() } } - private fun TypedArray.getChart(): Chart? = - when (getInt(R.styleable.ChartView_chart, 0)) { - COLUMN_CHART -> getColumnChart(context, mergeMode = MergeMode.Grouped) - STACKED_COLUMN_CHART -> getColumnChart(context, mergeMode = MergeMode.Stack) - LINE_CHART -> getLineChart(context) + private fun getChart( + chartViewTypedArray: TypedArray, + baseTypedArray: TypedArray, + ): Chart? = + when (chartViewTypedArray.getInt(R.styleable.ChartView_chart, 0)) { + COLUMN_CHART -> baseTypedArray.getColumnChart(context, mergeMode = MergeMode.Grouped) + STACKED_COLUMN_CHART -> baseTypedArray.getColumnChart(context, mergeMode = MergeMode.Stack) + LINE_CHART -> baseTypedArray.getLineChart(context) else -> null } @@ -238,16 +246,29 @@ internal class ThemeHandler( when (getInt(R.styleable.BaseChartView_horizontalLayout, 0)) { 0 -> HorizontalLayout.Segmented else -> HorizontalLayout.FullWidth( - getRawDimension(context, R.styleable.BaseChartView_startContentPadding, 0f), - getRawDimension(context, R.styleable.BaseChartView_endContentPadding, 0f), + getRawDimension( + context, + R.styleable.BaseChartView_scalableStartContentPadding, + getRawDimension(context, R.styleable.BaseChartView_startContentPadding, 0f), + ), + getRawDimension( + context, + R.styleable.BaseChartView_scalableEndContentPadding, + getRawDimension(context, R.styleable.BaseChartView_endContentPadding, 0f), + ), + getRawDimension(context, R.styleable.BaseChartView_unscalableStartContentPadding, 0f), + getRawDimension(context, R.styleable.BaseChartView_unscalableEndContentPadding, 0f), ) } - private fun TypedArray.getComposedChart(): Chart>? { - val chartFlags = getInt(R.styleable.ComposedChartView_charts, 0) + private fun getComposedChart( + composedChartViewTypedArray: TypedArray, + baseTypedArray: TypedArray, + ): Chart>? { + val chartFlags = composedChartViewTypedArray.getInt(R.styleable.ComposedChartView_charts, 0) val columnChart: ColumnChart? = if (chartFlags.hasAnyFlagOf(COLUMN_CHART, STACKED_COLUMN_CHART)) { - getColumnChart( + baseTypedArray.getColumnChart( context, mergeMode = if (chartFlags.hasFlag(STACKED_COLUMN_CHART)) MergeMode.Stack else MergeMode.Grouped, ) @@ -255,7 +276,7 @@ internal class ThemeHandler( null } val lineChart = if (chartFlags.hasFlag(LINE_CHART)) { - getLineChart(context) + baseTypedArray.getLineChart(context) } else { null } diff --git a/vico/views/src/main/res/values/attrs.xml b/vico/views/src/main/res/values/attrs.xml index 758af58fb..ce777169a 100644 --- a/vico/views/src/main/res/values/attrs.xml +++ b/vico/views/src/main/res/values/attrs.xml @@ -68,7 +68,13 @@ + + + + + + @@ -99,6 +105,7 @@ +