diff --git a/demo/common-plot/src/commonMain/kotlin/demo/plot/common/model/EmptyGeomContext.kt b/demo/common-plot/src/commonMain/kotlin/demo/plot/common/model/EmptyGeomContext.kt index 7e499e4f756..80dd33752a5 100644 --- a/demo/common-plot/src/commonMain/kotlin/demo/plot/common/model/EmptyGeomContext.kt +++ b/demo/common-plot/src/commonMain/kotlin/demo/plot/common/model/EmptyGeomContext.kt @@ -54,6 +54,8 @@ class EmptyGeomContext : GeomContext { return 1.0 } + override fun removeNaMessages() = true + override fun consumeMessages(messages: List) { throw IllegalStateException("Not available in an empty geom context") } diff --git a/docs/dev/notebooks/na_messages.ipynb b/docs/dev/notebooks/na_messages.ipynb new file mode 100644 index 00000000000..f9bc8e5cdc1 --- /dev/null +++ b/docs/dev/notebooks/na_messages.ipynb @@ -0,0 +1,2178 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "524046f7-1406-4283-b8f7-085374a13479", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from lets_plot import *\n", + "\n", + "LetsPlot.setup_html()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c9e777c5-b196-4af8-83e8-bd1dbe00d9f7", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame({\n", + " \"id\": list(range(1, 11)),\n", + " \"x\": [4, np.nan, 1, 9, 6, 2, 10, np.nan, 7, 5],\n", + " \"y\": [7, 1, 9, 10, 4, np.nan, 3, np.nan, 6, 5],\n", + " \"start\": [0,0,0,0,0,0,0,0,0,0]\n", + "})" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1def5066-d800-47e6-b643-a7050254f781", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_point(na_rm=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3172d54c-8ea8-4849-b8c9-cfe8a412923e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_pointdensity(na_rm=False, stat=\"identity\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "634c648e-9842-43d4-b51f-ad138b3b66ee", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_qq(na_rm=False, stat=\"identity\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "363f307c-513d-4387-9eaa-5ad36a470aa5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_qq2(na_rm=False, stat=\"identity\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a6ad4016-cead-41d3-a021-f68b4bc064de", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_area(na_rm=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9a4d97f5-a7a6-4142-bfd4-afc6ca1b8664", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_jitter(na_rm=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "663bd9a3-464a-448d-8418-8393e4fbcc7d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_line(na_rm=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "68fc1a7c-1af6-4e76-b0b5-94b90149e87d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_map(na_rm=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "71073e5f-9970-44e7-83dc-83f4c688650f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_path(na_rm=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "433691b2-3cdb-449e-91c9-c90abafde839", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_polygon(na_rm=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "83cfd59a-3c3c-44b8-91a7-f7ca0116c19b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\")) + geom_ribbon(aes(ymin=\"start\", ymax=\"y\"), na_rm=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "9eae189e-93f7-485f-99dc-e6802018b78f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ggplot(df, aes(\"x\", \"y\")) + geom_step(na_rm=False)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.20" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/plot-base/src/commonTest/kotlin/org/jetbrains/letsPlot/core/plot/base/BogusContext.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/BogusContext.kt similarity index 96% rename from plot-base/src/commonTest/kotlin/org/jetbrains/letsPlot/core/plot/base/BogusContext.kt rename to plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/BogusContext.kt index bec272b3ae2..219ab86b7b1 100644 --- a/plot-base/src/commonTest/kotlin/org/jetbrains/letsPlot/core/plot/base/BogusContext.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/BogusContext.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023. JetBrains s.r.o. + * Copyright (c) 2026. JetBrains s.r.o. * Use of this source code is governed by the MIT license that can be found in the LICENSE file. */ @@ -55,6 +55,8 @@ object BogusContext : GeomContext { return 1.0 } + override fun removeNaMessages() = true + override fun consumeMessages(messages: List) { // do nothing } diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/BogusCoordinateSystem.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/BogusCoordinateSystem.kt new file mode 100644 index 00000000000..541ca5bdb01 --- /dev/null +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/BogusCoordinateSystem.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026. JetBrains s.r.o. + * Use of this source code is governed by the MIT license that can be found in the LICENSE file. + */ + +package org.jetbrains.letsPlot.core.plot.base + +import org.jetbrains.letsPlot.commons.geometry.DoubleVector + +object BogusCoordinateSystem : CoordinateSystem { + override val isLinear: Boolean + get() = error("Not available in a bogus coordinate system") + override val isPolar: Boolean + get() = error("Not available in a bogus coordinate system") + + override fun toClient(p: DoubleVector): DoubleVector? { + error("Not available in a bogus coordinate system") + } + + override fun fromClient(p: DoubleVector): DoubleVector? { + error("Not available in a bogus coordinate system") + } + + override fun unitSize(p: DoubleVector): DoubleVector { + error("Not available in a bogus coordinate system") + } + + override fun flip(): CoordinateSystem { + error("Not available in a bogus coordinate system") + } +} \ No newline at end of file diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/GeomContext.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/GeomContext.kt index e59b3302401..7ea5669966e 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/GeomContext.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/GeomContext.kt @@ -50,6 +50,8 @@ interface GeomContext { fun getScaleFactor(): Double + fun removeNaMessages(): Boolean + fun consumeMessages(messages: List) fun geomKind(): GeomKind diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/AreaGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/AreaGeom.kt index c7f21dab7ba..de2d8892929 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/AreaGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/AreaGeom.kt @@ -26,7 +26,7 @@ open class AreaGeom : GeomBase() { override fun rangeIncludesZero(aes: Aes<*>): Boolean = (aes == Aes.Y) - override fun prepareDataPoints(dataPoints: Iterable): Iterable { + override fun filterDataPoints(dataPoints: Iterable): Iterable { val data = GeomUtil.with_X(dataPoints) return GeomUtil.ordered_X(data) } @@ -38,21 +38,24 @@ open class AreaGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - val helper = LinesHelper(pos, coord, ctx) - helper.setResamplingEnabled(!coord.isLinear && !flat) + val linesHelper = LinesHelper(pos, coord, ctx) + linesHelper.setResamplingEnabled(!coord.isLinear && !flat) // Alpha is disabled for strokes (but still applies to fill). - helper.setAlphaEnabled(false) + linesHelper.setAlphaEnabled(false) val quantilesHelper = QuantilesHelper(pos, coord, ctx, quantiles) val targetCollectorHelper = TargetCollectorHelper(ctx) - val dataPoints = dataPoints(aesthetics) - val closePath = helper.meetsRadarPlotReq() + val source = aesthetics.dataPoints() + val dataPoints = filterDataPoints(source) + val filteredPointsIds = source.excludedIndicesComparedTo(dataPoints) + + val closePath = linesHelper.meetsRadarPlotReq() dataPoints.sortedByDescending(DataPointAesthetics::group).groupBy(DataPointAesthetics::group) .forEach { (_, groupDataPoints) -> quantilesHelper.splitByQuantiles(groupDataPoints, Aes.X).forEach { points -> - val bands = helper.renderBands( + val bands = linesHelper.renderBands( points, TO_LOCATION_X_Y, TO_LOCATION_X_ZERO_WITH_FINITE_Y, @@ -61,9 +64,9 @@ open class AreaGeom : GeomBase() { ) root.appendNodes(bands) - val upperPoints = helper.createPathData(points, TO_LOCATION_X_Y, closePath) + val upperPoints = linesHelper.createPathData(points, TO_LOCATION_X_Y, closePath) - val line = helper.renderPaths(upperPoints, filled = false) + val line = linesHelper.renderPaths(upperPoints, filled = false) root.appendNodes(line) targetCollectorHelper.addVariadicPaths(upperPoints) } @@ -72,6 +75,7 @@ open class AreaGeom : GeomBase() { createQuantileLines(groupDataPoints, quantilesHelper).forEach(root::add) } } + reportDroppedPoints((filteredPointsIds + linesHelper.getDroppedPointsIds()).size, ctx) } private fun createQuantileLines( diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/GeomBase.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/GeomBase.kt index ca0fe8ba5b4..d887cdba268 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/GeomBase.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/GeomBase.kt @@ -9,10 +9,10 @@ import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle import org.jetbrains.letsPlot.commons.interval.DoubleSpan import org.jetbrains.letsPlot.core.plot.base.* import org.jetbrains.letsPlot.core.plot.base.geom.legend.GenericLegendKeyElementFactory -import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetCollector import org.jetbrains.letsPlot.core.plot.base.render.LegendKeyElementFactory import org.jetbrains.letsPlot.core.plot.base.render.SvgRoot import org.jetbrains.letsPlot.core.plot.base.render.svg.LinePath +import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetCollector import org.jetbrains.letsPlot.datamodel.svg.dom.SvgGElement import org.jetbrains.letsPlot.datamodel.svg.dom.slim.SvgSlimElements import org.jetbrains.letsPlot.datamodel.svg.dom.slim.SvgSlimGroup @@ -23,9 +23,6 @@ abstract class GeomBase : Geom { override val legendKeyElementFactory: LegendKeyElementFactory get() = GenericLegendKeyElementFactory() - protected open val geomName: String = "unhandled_geom" - private var nullCounter = 0 - override fun build( root: SvgRoot, aesthetics: Aesthetics, @@ -34,9 +31,6 @@ abstract class GeomBase : Geom { ctx: GeomContext ) { buildIntern(root, aesthetics, pos, coord, ctx) - if (SHOW_NA_MESSAGES) { - ctx.consumeMessages(getMessages()) - } } open fun preferableNullDomain(aes: Aes<*>): DoubleSpan { @@ -47,23 +41,14 @@ abstract class GeomBase : Geom { return ctx.targetCollector } - open fun prepareDataPoints(dataPoints: Iterable): Iterable { + open fun filterDataPoints(dataPoints: Iterable): Iterable { return dataPoints } - protected fun dataPoints(aesthetics: Aesthetics): Iterable { - val source = aesthetics.dataPoints() - val result = prepareDataPoints(source) - nullCounter = source.count() - result.count() - return result - } - - fun addNulls(count: Int) { - nullCounter += count - } - - private fun getMessages(): List { - return if (nullCounter > 0) listOf("$geomName: removed $nullCounter data point(s)") else emptyList() + fun reportDroppedPoints(count: Int, ctx: GeomContext) { + if (!ctx.removeNaMessages() && count > 0) { + ctx.consumeMessages(listOf("${ctx.geomKind().name.lowercase()}: Removed $count data point(s) containing missing values or values outside the scale range.")) + } } protected abstract fun buildIntern( @@ -75,8 +60,6 @@ abstract class GeomBase : Geom { ) companion object { - private const val SHOW_NA_MESSAGES = false - fun wrap(slimGroup: SvgSlimGroup): SvgGElement { val g = SvgGElement() g.isPrebuiltSubtree = true @@ -110,5 +93,9 @@ abstract class GeomBase : Geom { add(path.rootGroup) } } + + fun Iterable.excludedIndicesComparedTo(other: Iterable): Set { + return mapTo(HashSet()) { it.index() } - other.mapTo(HashSet()) { it.index() } + } } } diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/LineGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/LineGeom.kt index c2552cbceae..6db94ecd6be 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/LineGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/LineGeom.kt @@ -9,9 +9,8 @@ import org.jetbrains.letsPlot.core.plot.base.DataPointAesthetics import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil open class LineGeom : PathGeom() { - override val geomName: String = "line" - override fun prepareDataPoints(dataPoints: Iterable): Iterable { + override fun filterDataPoints(dataPoints: Iterable): Iterable { val data = GeomUtil.with_X(dataPoints) return GeomUtil.ordered_X(data) } diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PathGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PathGeom.kt index ff5e9408e3f..4f25acd5fc5 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PathGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PathGeom.kt @@ -21,8 +21,6 @@ open class PathGeom : GeomBase() { var flat: Boolean = false var geodesic: Boolean = false - override val geomName: String = "path" - override val legendKeyElementFactory: LegendKeyElementFactory get() = HLineGeom.LEGEND_KEY_ELEMENT_FACTORY @@ -33,8 +31,11 @@ open class PathGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - val dataPoints = dataPoints(aesthetics) - val linesHelper = LinesHelper(pos, coord, ctx, ::addNulls) + val source = aesthetics.dataPoints() + val dataPoints = filterDataPoints(source) + val filteredPointsIds = source.excludedIndicesComparedTo(dataPoints) + + val linesHelper = LinesHelper(pos, coord, ctx) linesHelper.setResamplingEnabled(!coord.isLinear && !flat) val closePath = linesHelper.meetsRadarPlotReq() @@ -45,6 +46,7 @@ open class PathGeom : GeomBase() { val svgPath = linesHelper.renderPaths(pathData, filled = false) root.appendNodes(svgPath) + reportDroppedPoints((filteredPointsIds + linesHelper.getDroppedPointsIds()).size, ctx) } companion object { diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PointGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PointGeom.kt index 99214633a15..48bfc18b94e 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PointGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PointGeom.kt @@ -19,7 +19,6 @@ open class PointGeom : GeomBase() { var animation: Any? = null var sizeUnit: String? = null - override val geomName: String = "point" override val legendKeyElementFactory: LegendKeyElementFactory get() = PointLegendKeyElementFactory() @@ -66,7 +65,7 @@ open class PointGeom : GeomBase() { o.appendTo(slimGroup) goodPointsCount += 1 } - addNulls(count - goodPointsCount) + reportDroppedPoints(count - goodPointsCount, ctx) root.add(wrap(slimGroup)) } diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PolygonGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PolygonGeom.kt index 2984260e366..6381f9bceb8 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PolygonGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PolygonGeom.kt @@ -13,7 +13,7 @@ import org.jetbrains.letsPlot.core.plot.base.render.SvgRoot open class PolygonGeom : GeomBase() { - override fun prepareDataPoints(dataPoints: Iterable): Iterable { + override fun filterDataPoints(dataPoints: Iterable): Iterable { return GeomUtil.with_X_Y(dataPoints) } @@ -24,7 +24,10 @@ open class PolygonGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - val dataPoints = dataPoints(aesthetics) + val source = aesthetics.dataPoints() + val dataPoints = filterDataPoints(source) + val filteredPointsIds = source.excludedIndicesComparedTo(dataPoints) + val linesHelper = LinesHelper(pos, coord, ctx) linesHelper.setResamplingEnabled(coord.isPolar) @@ -34,6 +37,7 @@ open class PolygonGeom : GeomBase() { targetCollectorHelper.addPolygons(polygonData) root.add(svg) } + reportDroppedPoints((filteredPointsIds + linesHelper.getDroppedPointsIds()).size, ctx) } companion object { diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/RibbonGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/RibbonGeom.kt index 7392a49061b..b389e63bd38 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/RibbonGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/RibbonGeom.kt @@ -21,7 +21,7 @@ import org.jetbrains.letsPlot.core.plot.base.tooltip.TipLayoutHint.Kind.VERTICAL class RibbonGeom : GeomBase() { - override fun prepareDataPoints(dataPoints: Iterable): Iterable { + override fun filterDataPoints(dataPoints: Iterable): Iterable { val data = GeomUtil.with_X(dataPoints) return GeomUtil.ordered_X(data) } @@ -33,20 +33,24 @@ class RibbonGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - val dataPoints = dataPoints(aesthetics) - val helper = LinesHelper(pos, coord, ctx) + val source = aesthetics.dataPoints() + val dataPoints = filterDataPoints(source) + val filteredPointsIds = source.excludedIndicesComparedTo(dataPoints) - val paths = helper.createBands(dataPoints, TO_LOCATION_X_YMAX_WITH_FINITE_YMIN, TO_LOCATION_X_YMIN_WITH_FINITE_YMAX) + val linesHelper = LinesHelper(pos, coord, ctx) + + val paths = linesHelper.createBands(dataPoints, TO_LOCATION_X_YMAX_WITH_FINITE_YMIN, TO_LOCATION_X_YMIN_WITH_FINITE_YMAX) root.appendNodes(paths) //if you want to retain the side edges of ribbon: //comment out the following codes, and switch decorate method in LinesHelper.createBands - helper.setAlphaEnabled(false) + linesHelper.setAlphaEnabled(false) - root.appendNodes(helper.createLines(dataPoints, TO_LOCATION_X_YMAX)) - root.appendNodes(helper.createLines(dataPoints, TO_LOCATION_X_YMIN)) + root.appendNodes(linesHelper.createLines(dataPoints, TO_LOCATION_X_YMAX)) + root.appendNodes(linesHelper.createLines(dataPoints, TO_LOCATION_X_YMIN)) buildHints(aesthetics, pos, coord, ctx) + reportDroppedPoints((filteredPointsIds + linesHelper.getDroppedPointsIds()).size, ctx) } private fun buildHints(aesthetics: Aesthetics, pos: PositionAdjustment, coord: CoordinateSystem, ctx: GeomContext) { diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/StepGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/StepGeom.kt index c6b0552ae85..581acf8911b 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/StepGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/StepGeom.kt @@ -22,7 +22,7 @@ class StepGeom : LineGeom() { myDirection = Direction.toDirection(dir) } // commit name: - override fun prepareDataPoints(dataPoints: Iterable): Iterable { + override fun filterDataPoints(dataPoints: Iterable): Iterable { // filter out points with NaN x-values but keep +/-Infinity (for 'padded' mode) val data = dataPoints.filter { p: DataPointAesthetics -> val x = p.x() @@ -39,7 +39,10 @@ class StepGeom : LineGeom() { coord: CoordinateSystem, ctx: GeomContext ) { - val dataPoints = dataPoints(aesthetics) + val source = aesthetics.dataPoints() + val dataPoints = filterDataPoints(source) + val filteredPointsIds = source.excludedIndicesComparedTo(dataPoints) + val linesHelper = LinesHelper(pos, coord, ctx) val pathDataList = linesHelper.createPaths(dataPoints, toLocationFor(overallAesBounds(ctx))) @@ -54,6 +57,7 @@ class StepGeom : LineGeom() { val targetCollectorHelper = TargetCollectorHelper(ctx) targetCollectorHelper.addPaths(pathDataList) + reportDroppedPoints((filteredPointsIds + linesHelper.getDroppedPointsIds()).size, ctx) } private fun toLocationFor(viewPort: DoubleRectangle): (DataPointAesthetics) -> DoubleVector? { diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/util/GeomUtil.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/util/GeomUtil.kt index 1e1719cfbd1..6c6004155ac 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/util/GeomUtil.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/util/GeomUtil.kt @@ -8,7 +8,6 @@ package org.jetbrains.letsPlot.core.plot.base.geom.util import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle import org.jetbrains.letsPlot.commons.geometry.DoubleVector import org.jetbrains.letsPlot.commons.intern.gcommon.collect.Ordering -import org.jetbrains.letsPlot.commons.intern.splitByNull import org.jetbrains.letsPlot.core.commons.data.SeriesUtil import org.jetbrains.letsPlot.core.plot.base.Aes import org.jetbrains.letsPlot.core.plot.base.DataPointAesthetics @@ -175,17 +174,6 @@ object GeomUtil { return dataPoints.filter { p -> p.defined(aes0) && p.defined(aes1) && p.defined(aes2) && p.defined(aes3) } } - private fun createGroups( - dataPoints: Iterable, - sorted: Boolean = false - ): Map> { - val map = dataPoints.groupBy { it.group()!! } - return when { - sorted -> map.toList().sortedBy { (g, _) -> g }.toMap() - else -> map - } - } - fun createPathDataFromRectangle( dataPoints: Iterable, pointTransform: ((DataPointAesthetics) -> List?) @@ -205,50 +193,6 @@ object GeomUtil { } } - // Builds a list of PathData splitting by group and null points. - fun createPaths( - dataPoints: Iterable, - pointTransform: ((DataPointAesthetics) -> DoubleVector?), - sorted: Boolean, - closePath: Boolean = false, - nullsCounter: (Int) -> Unit, - ): List { - val groups = createGroups(dataPoints, sorted).let { groups -> - if (closePath) { - groups.mapValues { (_, group) -> group + group.first() } - } else { - groups - } - } - - var nulls = 0 - var singlePointPaths = 0 - val result = groups.values - .map { aesthetics -> toPathPoints(aesthetics, pointTransform) } - .also { a -> - nulls += a.flatten().count { it == null } - } - .map { pathPoints -> pathPoints.splitByNull() } - .flatten() - .mapNotNull { - if (it.size == 1) singlePointPaths++ - PathData.create(it) - } - - nullsCounter(nulls + singlePointPaths) - - return result - } - - private fun toPathPoints( - dataPoints: Iterable, - pointTransform: ((DataPointAesthetics) -> DoubleVector?) - ): List { - return dataPoints.map { aes -> - pointTransform(aes)?.let { p -> PathPoint(aes, p) } - } - } - fun rectToGeometry(minX: Double, minY: Double, maxX: Double, maxY: Double): List { return listOf( DoubleVector(minX, minY), diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/util/LinesHelper.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/util/LinesHelper.kt index 4dbe4d40a83..a588e1cf761 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/util/LinesHelper.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/util/LinesHelper.kt @@ -7,6 +7,7 @@ package org.jetbrains.letsPlot.core.plot.base.geom.util import org.jetbrains.letsPlot.commons.geometry.DoubleVector import org.jetbrains.letsPlot.commons.intern.splitBy +import org.jetbrains.letsPlot.commons.intern.splitByNull import org.jetbrains.letsPlot.commons.intern.typedGeometry.algorithms.* import org.jetbrains.letsPlot.commons.intern.typedGeometry.algorithms.AdaptiveResampler.Companion.PIXEL_PRECISION import org.jetbrains.letsPlot.commons.intern.util.VectorAdapter @@ -17,17 +18,16 @@ import org.jetbrains.letsPlot.core.plot.base.* import org.jetbrains.letsPlot.core.plot.base.aes.AesScaling import org.jetbrains.letsPlot.core.plot.base.aes.AestheticsUtil import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.createPathDataFromRectangle -import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.createPaths import org.jetbrains.letsPlot.core.plot.base.render.svg.LinePath import org.jetbrains.letsPlot.datamodel.svg.dom.SvgNode open class LinesHelper( pos: PositionAdjustment, coord: CoordinateSystem, - ctx: GeomContext, - private val counter: (Int) -> Unit = {} // todo: remove default counter + ctx: GeomContext ) : GeomHelper(pos, coord, ctx) { + private val myDroppedPointsIds = mutableSetOf() private var myAlphaEnabled = true protected var myResamplingEnabled = false protected var myResamplingPrecision = PIXEL_PRECISION @@ -83,7 +83,7 @@ open class LinesHelper( ): List { val domainData = createPaths( dataPoints, - locationTransform, sorted = true, closePath = closePath, nullsCounter = counter) + locationTransform, sorted = true, closePath = closePath) return toClientPaths(domainData) } @@ -91,7 +91,7 @@ open class LinesHelper( dataPoints: Iterable, locationTransform: (DataPointAesthetics) -> DoubleVector? = GeomUtil.TO_LOCATION_X_Y, ): List> { - val domainPathData = createPaths(dataPoints, locationTransform, sorted = true, closePath = false, nullsCounter = counter) + val domainPathData = createPaths(dataPoints, locationTransform, sorted = true, closePath = false) return createPolygon(domainPathData) } @@ -105,6 +105,10 @@ open class LinesHelper( return createPolygon(domainPathData) } + fun getDroppedPointsIds(): Set { + return myDroppedPointsIds + } + private fun createPolygon(domainPathData: Collection): List> { // split in domain space! after resampling coordinates may repeat and splitRings will return wrong results val domainPolygonData = domainPathData @@ -180,7 +184,59 @@ open class LinesHelper( dataPoints: Iterable, toLocation: (DataPointAesthetics) -> DoubleVector? ): List { - return createPaths(dataPoints, toClientLocation(toLocation), sorted = true, closePath = false, nullsCounter = counter) + return createPaths(dataPoints, toClientLocation(toLocation), sorted = true, closePath = false) + } + + // Builds a list of PathData splitting by group and null points. + fun createPaths( + dataPoints: Iterable, + pointTransform: ((DataPointAesthetics) -> DoubleVector?), + sorted: Boolean, + closePath: Boolean = false, + ): List { + val groups = createGroups(dataPoints, sorted).let { groups -> + if (closePath) { + groups.mapValues { (_, group) -> group + group.first() } + } else { + groups + } + } + + val result = groups.values + .map { aesthetics -> toPathPoints(aesthetics, pointTransform) } + .flatMap { pathPoints -> pathPoints.splitByNull() } + .mapNotNull { + if (it.size == 1) myDroppedPointsIds.add(it[0].aes.index()) + PathData.create(it) + } + + return result + } + + private fun createGroups( + dataPoints: Iterable, + sorted: Boolean = false + ): Map> { + val map = dataPoints.groupBy { it.group()!! } + return when { + sorted -> map.toList().sortedBy { (g, _) -> g }.toMap() + else -> map + } + } + + private fun toPathPoints( + dataPoints: Iterable, + pointTransform: ((DataPointAesthetics) -> DoubleVector?) + ): List { + return dataPoints.map { aes -> + val p = pointTransform(aes) + if (p == null) { + myDroppedPointsIds.add(aes.index()) + null + } else { + PathPoint(aes, p) + } + } } fun createSteps(paths: Collection, horizontalThenVertical: Boolean): List { @@ -228,8 +284,8 @@ open class LinesHelper( simplifyBorders: Boolean, closePath: Boolean ): List { - val domainUpperPathData = createPaths(dataPoints, toLocationUpper, sorted = true, closePath, nullsCounter = counter) - val domainLowerPathData = createPaths(dataPoints, toLocationLower, sorted = true, closePath, nullsCounter = counter) + val domainUpperPathData = createPaths(dataPoints, toLocationUpper, sorted = true, closePath) + val domainLowerPathData = createPaths(dataPoints, toLocationLower, sorted = true, closePath) if (domainUpperPathData.isEmpty() || domainLowerPathData.isEmpty()) { return emptyList() diff --git a/plot-base/src/jvmTest/kotlin/org/jetbrains/letsPlot/core/plot/base/tooltip/loc/PolygonEdgeCasesTest.kt b/plot-base/src/jvmTest/kotlin/org/jetbrains/letsPlot/core/plot/base/tooltip/loc/PolygonEdgeCasesTest.kt index a07521d1bdc..cbd2820a20f 100644 --- a/plot-base/src/jvmTest/kotlin/org/jetbrains/letsPlot/core/plot/base/tooltip/loc/PolygonEdgeCasesTest.kt +++ b/plot-base/src/jvmTest/kotlin/org/jetbrains/letsPlot/core/plot/base/tooltip/loc/PolygonEdgeCasesTest.kt @@ -11,9 +11,13 @@ import org.jetbrains.letsPlot.commons.intern.typedGeometry.Vec import org.jetbrains.letsPlot.commons.intern.typedGeometry.algorithms.splitRings import org.jetbrains.letsPlot.commons.intern.typedGeometry.createMultiPolygon import org.jetbrains.letsPlot.commons.intern.typedGeometry.explicitVec +import org.jetbrains.letsPlot.core.plot.base.BogusContext +import org.jetbrains.letsPlot.core.plot.base.BogusCoordinateSystem import org.jetbrains.letsPlot.core.plot.base.aes.AestheticsBuilder import org.jetbrains.letsPlot.core.plot.base.aes.AestheticsBuilder.Companion.list import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil +import org.jetbrains.letsPlot.core.plot.base.geom.util.LinesHelper +import org.jetbrains.letsPlot.core.plot.base.pos.PositionAdjustments import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetLocator import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetLocator.LookupSpace import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetLocator.LookupStrategy @@ -163,7 +167,9 @@ class PolygonEdgeCasesTest { .y(list(polygon.map(DoubleVector::y))) .build() - val pathData = GeomUtil.createPaths(aes.dataPoints(), GeomUtil.TO_LOCATION_X_Y, sorted = true) {} + val linesHelper = LinesHelper(PositionAdjustments.identity(), BogusCoordinateSystem, BogusContext) + + val pathData = linesHelper.createPaths(aes.dataPoints(), GeomUtil.TO_LOCATION_X_Y, sorted = true) val rings = splitRings(pathData[0].coordinates) assertEquals(3, rings.size) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/GeomLayer.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/GeomLayer.kt index a4d732ea2ad..9b728c3ea21 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/GeomLayer.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/GeomLayer.kt @@ -61,6 +61,8 @@ interface GeomLayer { val defaultFormatters: Map String> + val naRm: Boolean + fun renderedAes(considerOrientation: Boolean = false): List> fun hasBinding(aes: Aes<*>): Boolean diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/GeomContextBuilder.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/GeomContextBuilder.kt index 77a61619a1b..63768da2640 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/GeomContextBuilder.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/GeomContextBuilder.kt @@ -32,6 +32,7 @@ class GeomContextBuilder : ImmutableGeomContext.Builder { private var contentBounds: DoubleRectangle? = null private var scaleFactor: Double = 1.0 private var geomKind: GeomKind? = null + private var naRm: Boolean = false private var messageConsumer: (String) -> Unit = {} constructor() @@ -101,6 +102,11 @@ class GeomContextBuilder : ImmutableGeomContext.Builder { return this } + override fun naRm(naRm: Boolean): ImmutableGeomContext.Builder { + this.naRm = naRm + return this + } + override fun messageConsumer(messageConsumer: (String) -> Unit): ImmutableGeomContext.Builder { this.messageConsumer = messageConsumer return this @@ -124,6 +130,7 @@ class GeomContextBuilder : ImmutableGeomContext.Builder { val _coordinateSystem = b.coordinateSystem val _contentBounds = b.contentBounds val _scaleFactor = b.scaleFactor + val _naRm = b.naRm val _messageConsumer = b.messageConsumer val _geomKind = b.geomKind @@ -197,6 +204,10 @@ class GeomContextBuilder : ImmutableGeomContext.Builder { return _scaleFactor } + override fun removeNaMessages(): Boolean { + return _naRm + } + override fun consumeMessages(messages: List) { messages.forEach { _messageConsumer(it) } } diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/GeomLayerBuilder.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/GeomLayerBuilder.kt index c16c65ce201..f253e7cb811 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/GeomLayerBuilder.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/GeomLayerBuilder.kt @@ -178,6 +178,7 @@ class GeomLayerBuilder( data: DataFrame, scaleMap: Map, Scale>, scaleMapppersNP: Map, ScaleMapper<*>>, + naRm: Boolean = false, ): GeomLayer { val transformByAes: Map, Transform> = scaleMap.keys.associateWith { scaleMap.getValue(it).transform @@ -252,6 +253,7 @@ class GeomLayerBuilder( fillByAes = fillByAes, annotationProvider = myAnnotationProvider, defaultFormatters = myDefaultFormatters, + naRm = naRm, ) } @@ -282,7 +284,8 @@ class GeomLayerBuilder( override val colorByAes: Aes, override val fillByAes: Aes, private val annotationProvider: ((MappedDataAccess, DataFrame) -> Annotation?)?, - override val defaultFormatters: Map String> + override val defaultFormatters: Map String>, + override val naRm: Boolean ) : GeomLayer { override val geom: Geom = geomProvider.createGeom( diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/ImmutableGeomContext.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/ImmutableGeomContext.kt index 6642dcb79c8..2ffc0a25790 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/ImmutableGeomContext.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/ImmutableGeomContext.kt @@ -41,6 +41,8 @@ interface ImmutableGeomContext : GeomContext { fun scaleFactor(scaleFactor: Double): Builder + fun naRm(naRm: Boolean): Builder + fun messageConsumer(messageConsumer: (String) -> Unit): Builder fun geomKind(geomKind: GeomKind): Builder diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/frame/FrameOfReferenceBase.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/frame/FrameOfReferenceBase.kt index 29d81b0c00e..aa9aa511829 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/frame/FrameOfReferenceBase.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/frame/FrameOfReferenceBase.kt @@ -211,6 +211,7 @@ internal abstract class FrameOfReferenceBase( .coordinateSystem(coord) .contentBounds(bounds) .scaleFactor(plotContext.getScaleFactor()) + .naRm(layer.naRm) .geomKind(layer.geomKind) .messageConsumer(plotContext.getMessageConsumer()) .build() diff --git a/plot-livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/livemap/DataPointsConverter.kt b/plot-livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/livemap/DataPointsConverter.kt index c88e575c134..1a7c980ab76 100644 --- a/plot-livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/livemap/DataPointsConverter.kt +++ b/plot-livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/livemap/DataPointsConverter.kt @@ -12,18 +12,15 @@ import org.jetbrains.letsPlot.commons.intern.typedGeometry.Vec import org.jetbrains.letsPlot.commons.intern.typedGeometry.explicitVec import org.jetbrains.letsPlot.commons.values.Color import org.jetbrains.letsPlot.core.commons.data.SeriesUtil -import org.jetbrains.letsPlot.core.plot.base.Aes -import org.jetbrains.letsPlot.core.plot.base.Aesthetics -import org.jetbrains.letsPlot.core.plot.base.DataPointAesthetics -import org.jetbrains.letsPlot.core.plot.base.Geom +import org.jetbrains.letsPlot.core.plot.base.* import org.jetbrains.letsPlot.core.plot.base.geom.* import org.jetbrains.letsPlot.core.plot.base.geom.util.* import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.TO_LOCATION_X_Y import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.TO_RECTANGLE -import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.createPaths import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.toLocation import org.jetbrains.letsPlot.core.plot.base.geom.util.LinesHelper.Companion.midPointsPathInterpolator import org.jetbrains.letsPlot.core.plot.base.geom.util.LinesHelper.Companion.splitByStyle +import org.jetbrains.letsPlot.core.plot.base.pos.PositionAdjustments import org.jetbrains.letsPlot.core.plot.builder.scale.DefaultNaValue import org.jetbrains.letsPlot.livemap.Client import org.jetbrains.letsPlot.livemap.Client.Companion.px @@ -150,10 +147,12 @@ internal class DataPointsConverter( } } - private inner class MultiPathFeatureConverter( + private class MultiPathFeatureConverter( aes: Aesthetics ) : PathFeatureConverterBase(aes) { + private val linesHelper = LinesHelper(PositionAdjustments.identity(), BogusCoordinateSystem, BogusContext) + fun path(geom: Geom): List { if (geom is PathGeom) { setAnimation(geom.animation) @@ -161,7 +160,7 @@ internal class DataPointsConverter( setGeodesic(geom.geodesic) } - val paths = createPaths(aesthetics.dataPoints(), TO_LOCATION_X_Y, sorted = true) {} + val paths = linesHelper.createPaths(aesthetics.dataPoints(), TO_LOCATION_X_Y, sorted = true) val interpolatedPathData = paths.flatMap { splitByStyle(it).let(::midPointsPathInterpolator) @@ -171,7 +170,7 @@ internal class DataPointsConverter( } fun polygon(): List { - val paths = createPaths(aesthetics.dataPoints(), TO_LOCATION_X_Y, sorted = true) {} + val paths = linesHelper.createPaths(aesthetics.dataPoints(), TO_LOCATION_X_Y, sorted = true) return process(paths = paths, isClosed = true) } @@ -185,7 +184,7 @@ internal class DataPointsConverter( } } - private inner class SinglePathFeatureConverter( + private class SinglePathFeatureConverter( aesthetics: Aesthetics ) : PathFeatureConverterBase(aesthetics) { fun tile(): List { diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/GeomProto.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/GeomProto.kt index 01be6c956c7..34a816c7a76 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/GeomProto.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/GeomProto.kt @@ -191,45 +191,47 @@ class GeomProto(val geomKind: GeomKind) { private companion object { private val DEFAULTS = HashMap>() - private val COMMON = commonDefaults() init { + val defaultsByGeom = HashMap>() + defaultsByGeom[SMOOTH] = smoothDefaults() + defaultsByGeom[BAR] = barDefaults() + defaultsByGeom[HISTOGRAM] = histogramDefaults() + defaultsByGeom[DOT_PLOT] = dotplotDefaults() + defaultsByGeom[CONTOUR] = contourDefaults() + defaultsByGeom[CONTOURF] = contourfDefaults() + defaultsByGeom[CROSS_BAR] = crossBarDefaults() + defaultsByGeom[BOX_PLOT] = boxplotDefaults() + defaultsByGeom[AREA_RIDGES] = areaRidgesDefaults() + defaultsByGeom[VIOLIN] = violinDefaults() + defaultsByGeom[SINA] = sinaDefaults() + defaultsByGeom[Y_DOT_PLOT] = yDotplotDefaults() + defaultsByGeom[AREA] = areaDefaults() + defaultsByGeom[DENSITY] = densityDefaults() + defaultsByGeom[DENSITY2D] = density2dDefaults() + defaultsByGeom[DENSITY2DF] = density2dfDefaults() + defaultsByGeom[POINT_DENSITY] = pointDensityDefaults() + defaultsByGeom[Q_Q] = qqDefaults() + defaultsByGeom[Q_Q_2] = qq2Defaults() + defaultsByGeom[Q_Q_LINE] = qqLineDefaults() + defaultsByGeom[Q_Q_2_LINE] = qq2LineDefaults() + defaultsByGeom[FREQPOLY] = freqpolyDefaults() + defaultsByGeom[BIN_2D] = bin2dDefaults() + defaultsByGeom[HEX] = hexDefaults() + defaultsByGeom[PIE] = pieDefaults() + defaultsByGeom[BRACKET] = bracketDefaults() + defaultsByGeom[BRACKET_DODGE] = bracketDefaults() + + val commonDefaults = commonDefaults() for (geomKind in GeomKind.entries) { - DEFAULTS[geomKind] = COMMON + DEFAULTS[geomKind] = commonDefaults + (defaultsByGeom[geomKind] ?: emptyMap()) } - - DEFAULTS[SMOOTH] = smoothDefaults() - DEFAULTS[BAR] = barDefaults() - DEFAULTS[HISTOGRAM] = histogramDefaults() - DEFAULTS[DOT_PLOT] = dotplotDefaults() - DEFAULTS[CONTOUR] = contourDefaults() - DEFAULTS[CONTOURF] = contourfDefaults() - DEFAULTS[CROSS_BAR] = crossBarDefaults() - DEFAULTS[BOX_PLOT] = boxplotDefaults() - DEFAULTS[AREA_RIDGES] = areaRidgesDefaults() - DEFAULTS[VIOLIN] = violinDefaults() - DEFAULTS[SINA] = sinaDefaults() - DEFAULTS[Y_DOT_PLOT] = yDotplotDefaults() - DEFAULTS[AREA] = areaDefaults() - DEFAULTS[DENSITY] = densityDefaults() - DEFAULTS[DENSITY2D] = density2dDefaults() - DEFAULTS[DENSITY2DF] = density2dfDefaults() - DEFAULTS[POINT_DENSITY] = pointDensityDefaults() - DEFAULTS[Q_Q] = qqDefaults() - DEFAULTS[Q_Q_2] = qq2Defaults() - DEFAULTS[Q_Q_LINE] = qqLineDefaults() - DEFAULTS[Q_Q_2_LINE] = qq2LineDefaults() - DEFAULTS[FREQPOLY] = freqpolyDefaults() - DEFAULTS[BIN_2D] = bin2dDefaults() - DEFAULTS[HEX] = hexDefaults() - DEFAULTS[PIE] = pieDefaults() - DEFAULTS[BRACKET] = bracketDefaults() - DEFAULTS[BRACKET_DODGE] = bracketDefaults() } private fun commonDefaults(): Map { val defaults = HashMap() defaults[Layer.STAT] = "identity" + defaults[Layer.NA_RM] = true // hide NA messages by default return defaults } diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/Option.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/Option.kt index 78329be3787..4adcd1c75d3 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/Option.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/Option.kt @@ -230,7 +230,7 @@ object Option { const val COLOR_BY = "color_by" const val FILL_BY = "fill_by" - const val SCALE_FACTOR = "scale_factor" + const val NA_RM = "na_rm" object Marginal { const val SIZE = "margin_size" diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/tiles/PlotTilesConfig.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/tiles/PlotTilesConfig.kt index 5cad4009010..d93f133527c 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/tiles/PlotTilesConfig.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/tiles/PlotTilesConfig.kt @@ -15,6 +15,7 @@ import org.jetbrains.letsPlot.core.plot.builder.assemble.PlotGeomTiles import org.jetbrains.letsPlot.core.plot.builder.assemble.tiles.FacetedPlotGeomTiles import org.jetbrains.letsPlot.core.plot.builder.assemble.tiles.SimplePlotGeomTiles import org.jetbrains.letsPlot.core.plot.builder.coord.CoordProvider +import org.jetbrains.letsPlot.core.spec.Option.Layer.NA_RM import org.jetbrains.letsPlot.core.spec.PlotConfigUtil import org.jetbrains.letsPlot.core.spec.config.LayerConfig import org.jetbrains.letsPlot.core.spec.config.PlotConfigTransforms @@ -104,7 +105,8 @@ internal object PlotTilesConfig { layerBuilder.build( layerConfigs[layerIndex].combinedData, scalesByLayerBeforeFacets[layerIndex], - mappersNP + mappersNP, + layerConfigs[layerIndex].getBoolean(NA_RM) ) } @@ -184,6 +186,7 @@ internal object PlotTilesConfig { layerData, tileLayerScales, mappersByAesNP, + layerConfigs[layerIndex].getBoolean(NA_RM) ) } diff --git a/python-package/lets_plot/plot/geom.py b/python-package/lets_plot/plot/geom.py index df01aadcb3a..19458463072 100644 --- a/python-package/lets_plot/plot/geom.py +++ b/python-package/lets_plot/plot/geom.py @@ -3955,7 +3955,8 @@ def geom_boxplot(mapping=None, *, data=None, stat=None, position=None, show_lege shape=outlier_param('shape', outlier_shape), size=size, stroke=outlier_param('stroke', outlier_stroke), - color_by=color_by, fill_by=fill_by) + color_by=color_by, fill_by=fill_by, + na_rm=True) return boxplot_layer