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..0ca7aaf9781 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 @@ -10,6 +10,7 @@ import org.jetbrains.letsPlot.commons.geometry.DoubleVector import org.jetbrains.letsPlot.commons.values.Color import org.jetbrains.letsPlot.commons.values.Font import org.jetbrains.letsPlot.core.plot.base.* +import org.jetbrains.letsPlot.core.plot.base.geom.DroppedPointsReporter import org.jetbrains.letsPlot.core.plot.base.geom.annotation.Annotation import org.jetbrains.letsPlot.core.plot.base.theme.DefaultFontFamilyRegistry import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetCollector @@ -54,9 +55,7 @@ class EmptyGeomContext : GeomContext { return 1.0 } - override fun consumeMessages(messages: List) { - throw IllegalStateException("Not available in an empty geom context") - } + override fun droppedPointsReporter() = DroppedPointsReporter.NONE override fun geomKind(): GeomKind { throw IllegalStateException("Not available in an empty geom context") @@ -81,4 +80,4 @@ class EmptyGeomContext : GeomContext { ), ).dimensions(text) } -} \ No newline at end of file +} 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 92% 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..6daccbee2f2 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. */ @@ -8,6 +8,7 @@ package org.jetbrains.letsPlot.core.plot.base import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle import org.jetbrains.letsPlot.commons.geometry.DoubleVector import org.jetbrains.letsPlot.commons.values.Color +import org.jetbrains.letsPlot.core.plot.base.geom.DroppedPointsReporter import org.jetbrains.letsPlot.core.plot.base.geom.annotation.Annotation import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetCollector @@ -55,9 +56,7 @@ object BogusContext : GeomContext { return 1.0 } - override fun consumeMessages(messages: List) { - // do nothing - } + override fun droppedPointsReporter() = DroppedPointsReporter.NONE override fun geomKind(): GeomKind { error("Not available in a bogus geom context") @@ -72,4 +71,4 @@ object BogusContext : GeomContext { ): DoubleVector { error("Not available in a bogus geom context") } -} \ No newline at end of file +} 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..0a1306da453 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 @@ -8,6 +8,7 @@ package org.jetbrains.letsPlot.core.plot.base import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle import org.jetbrains.letsPlot.commons.geometry.DoubleVector import org.jetbrains.letsPlot.commons.values.Color +import org.jetbrains.letsPlot.core.plot.base.geom.DroppedPointsReporter import org.jetbrains.letsPlot.core.plot.base.geom.annotation.Annotation import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetCollector @@ -50,7 +51,7 @@ interface GeomContext { fun getScaleFactor(): Double - fun consumeMessages(messages: List) + fun droppedPointsReporter(): DroppedPointsReporter 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..466029d3080 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,9 +26,9 @@ open class AreaGeom : GeomBase() { override fun rangeIncludesZero(aes: Aes<*>): Boolean = (aes == Aes.Y) - override fun prepareDataPoints(dataPoints: Iterable): Iterable { - val data = GeomUtil.with_X(dataPoints) - return GeomUtil.ordered_X(data) + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + val (data, invalid) = GeomUtil.with_X(dataPoints) + return GeomUtil.ordered_X(data) to invalid } override fun buildIntern( @@ -38,21 +38,22 @@ 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 (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.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 +62,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 +73,10 @@ open class AreaGeom : GeomBase() { createQuantileLines(groupDataPoints, quantilesHelper).forEach(root::add) } } + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + val droppedPointsIds = linesHelper.getDroppedPointsIds().asSequence() + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) } private fun createQuantileLines( diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/AreaRidgesGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/AreaRidgesGeom.kt index e9ba62fa265..6a1d21e5d5d 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/AreaRidgesGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/AreaRidgesGeom.kt @@ -22,6 +22,10 @@ class AreaRidgesGeom : GeomBase(), WithHeight { var quantiles: List = DensityRidgesStat.DEF_QUANTILES var quantileLines: Boolean = DEF_QUANTILE_LINES + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + return GeomUtil.with_X_Y(dataPoints) + } + override fun buildIntern( root: SvgRoot, aesthetics: Aesthetics, @@ -29,15 +33,22 @@ class AreaRidgesGeom : GeomBase(), WithHeight { coord: CoordinateSystem, ctx: GeomContext ) { - val definedDataPoints = GeomUtil.with_X_Y(aesthetics.dataPoints()) - if (!definedDataPoints.any()) return - definedDataPoints + val linesHelper = LinesHelper(pos, coord, ctx) + val quantilesHelper = QuantilesHelper(pos, coord, ctx, quantiles, Aes.Y) + + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) + if (!dataPoints.any()) return + dataPoints .sortedByDescending(DataPointAesthetics::y) .groupBy(DataPointAesthetics::y) .map { (y, nonOrderedPoints) -> y to GeomUtil.ordered_X(nonOrderedPoints) } .forEach { (_, dataPoints) -> - splitDataPoints(dataPoints).forEach { buildRidge(root, it, pos, coord, ctx) } + splitDataPoints(dataPoints).forEach { buildRidge(root, it, linesHelper, quantilesHelper, ctx) } } + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + val droppedPointsIds = linesHelper.getDroppedPointsIds().asSequence() + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) } private fun splitDataPoints(dataPoints: Iterable): List> { @@ -60,18 +71,17 @@ class AreaRidgesGeom : GeomBase(), WithHeight { private fun buildRidge( root: SvgRoot, dataPoints: Iterable, - pos: PositionAdjustment, - coord: CoordinateSystem, + linesHelper: LinesHelper, + quantilesHelper: QuantilesHelper, ctx: GeomContext ) { - val helper = LinesHelper(pos, coord, ctx) - val quantilesHelper = QuantilesHelper(pos, coord, ctx, quantiles, Aes.Y) + val boundTransform = toLocationBound(ctx) val targetCollectorHelper = TargetCollectorHelper(ctx) quantilesHelper.splitByQuantiles(dataPoints, Aes.X).forEach { points -> - val paths = helper.createBands( + val paths = linesHelper.createBands( points, boundTransform, GeomUtil.TO_LOCATION_X_Y, @@ -79,10 +89,10 @@ class AreaRidgesGeom : GeomBase(), WithHeight { ) root.appendNodes(paths) - helper.setAlphaEnabled(false) - root.appendNodes(helper.createLines(points, boundTransform)) + linesHelper.setAlphaEnabled(false) + root.appendNodes(linesHelper.createLines(points, boundTransform)) - val pathDataList = helper.createPaths(points, boundTransform) + val pathDataList = linesHelper.createPaths(points, boundTransform) targetCollectorHelper.addPaths(pathDataList) } diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/DotplotGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/DotplotGeom.kt index a61f1d02e26..9f7f8513ff4 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/DotplotGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/DotplotGeom.kt @@ -47,6 +47,10 @@ open class DotplotGeom : GeomBase(), WithWidth { super.preferableNullDomain(aes) } + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + return GeomUtil.withDefined(dataPoints, Aes.BINWIDTH, Aes.STACKSIZE, Aes.X, Aes.Y) + } + override fun buildIntern( root: SvgRoot, aesthetics: Aesthetics, @@ -54,13 +58,11 @@ open class DotplotGeom : GeomBase(), WithWidth { coord: CoordinateSystem, ctx: GeomContext ) { - val pointsWithBinWidth = GeomUtil.withDefined( - aesthetics.dataPoints(), - Aes.BINWIDTH, Aes.X, Aes.Y - ) - if (!pointsWithBinWidth.any()) return + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) - val binWidthPx = pointsWithBinWidth.first().let { + if (!dataPoints.any()) return + + val binWidthPx = dataPoints.first().let { val x = it.x()!! val y = it.y()!! val bw = it.binwidth()!! @@ -72,11 +74,14 @@ open class DotplotGeom : GeomBase(), WithWidth { } } - GeomUtil.withDefined(pointsWithBinWidth, Aes.X, Aes.STACKSIZE) + dataPoints .groupBy(DataPointAesthetics::x) .forEach { (_, dataPointStack) -> buildStack(root, dataPointStack, pos, coord, ctx, binWidthPx) } + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() }.toSet() + ctx.droppedPointsReporter().report(filteredPointsIds) } private fun buildStack( diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/DroppedPointsReporter.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/DroppedPointsReporter.kt new file mode 100644 index 00000000000..dea7bb9e606 --- /dev/null +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/DroppedPointsReporter.kt @@ -0,0 +1,16 @@ +/* + * 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.geom + +interface DroppedPointsReporter { + fun report(droppedIndices: Set) + + companion object { + val NONE = object : DroppedPointsReporter { + override fun report(droppedIndices: Set) {} + } + } +} 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..ef591a8c4ac 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,8 @@ abstract class GeomBase : Geom { return ctx.targetCollector } - open fun prepareDataPoints(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() + open fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + return dataPoints to emptyList() } protected abstract fun buildIntern( @@ -75,8 +54,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 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..a16ce784bea 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,11 +9,10 @@ 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 { - val data = GeomUtil.with_X(dataPoints) - return GeomUtil.ordered_X(data) + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + val (data, invalid) = GeomUtil.with_X(dataPoints) + return GeomUtil.ordered_X(data) to invalid } companion object { 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..d6f37ad4a3e 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,9 @@ open class PathGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - val dataPoints = dataPoints(aesthetics) - val linesHelper = LinesHelper(pos, coord, ctx, ::addNulls) + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) + + val linesHelper = LinesHelper(pos, coord, ctx) linesHelper.setResamplingEnabled(!coord.isLinear && !flat) val closePath = linesHelper.meetsRadarPlotReq() @@ -45,6 +44,10 @@ open class PathGeom : GeomBase() { val svgPath = linesHelper.renderPaths(pathData, filled = false) root.appendNodes(svgPath) + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + val droppedPointsIds = linesHelper.getDroppedPointsIds().asSequence() + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) } companion object { diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PieGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PieGeom.kt index d5ee7161041..79f293c56cc 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PieGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/PieGeom.kt @@ -50,6 +50,10 @@ class PieGeom : GeomBase(), WithWidth, WithHeight { get() = this == INNER || this == BOTH } + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + return GeomUtil.withDefined(dataPoints, Aes.X, Aes.Y, Aes.SLICE) + } + override val legendKeyElementFactory: LegendKeyElementFactory get() = PieLegendKeyElementFactory() @@ -61,7 +65,8 @@ class PieGeom : GeomBase(), WithWidth, WithHeight { ctx: GeomContext ) { val geomHelper = GeomHelper(pos, coord, ctx) - GeomUtil.withDefined(aesthetics.dataPoints(), Aes.X, Aes.Y, Aes.SLICE) + val (validDataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) + validDataPoints .groupBy { p -> DoubleVector(p.x()!!, p.y()!!) } .forEach { (point, dataPoints) -> val sizeUnitRatio = AesScaling.sizeUnitRatio(point, coord, sizeUnit, AesScaling.PIE_UNIT_SIZE) @@ -81,6 +86,9 @@ class PieGeom : GeomBase(), WithWidth, WithHeight { ctx.annotation?.let { PieAnnotation.build(root, pieSectors, ctx) } } + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() }.toSet() + ctx.droppedPointsReporter().report(filteredPointsIds) } private fun SvgPathDataBuilder.svgOuterArc(sector: Sector) { 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..608ccf05630 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 @@ -8,6 +8,7 @@ package org.jetbrains.letsPlot.core.plot.base.geom import org.jetbrains.letsPlot.core.plot.base.* import org.jetbrains.letsPlot.core.plot.base.aes.AesScaling import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomHelper +import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil import org.jetbrains.letsPlot.core.plot.base.geom.util.HintColorUtil import org.jetbrains.letsPlot.core.plot.base.render.LegendKeyElementFactory import org.jetbrains.letsPlot.core.plot.base.render.SvgRoot @@ -19,11 +20,14 @@ open class PointGeom : GeomBase() { var animation: Any? = null var sizeUnit: String? = null - override val geomName: String = "point" override val legendKeyElementFactory: LegendKeyElementFactory get() = PointLegendKeyElementFactory() + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + return GeomUtil.withDefined(dataPoints, Aes.X, Aes.Y, Aes.SIZE) + } + public override fun buildIntern( root: SvgRoot, aesthetics: Aesthetics, @@ -35,15 +39,20 @@ open class PointGeom : GeomBase() { val targetCollector = getGeomTargetCollector(ctx) val colorsByDataPoint = HintColorUtil.createColorMarkerMapper(ctx) - val count = aesthetics.dataPointCount() - val slimGroup = SvgSlimElements.g(count) - var goodPointsCount = 0 + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) + + val slimGroup = SvgSlimElements.g(dataPoints.count()) + val droppedPointsIds = mutableSetOf() + + for (p in dataPoints) { + val point = p.finiteVectorOrNull(Aes.X, Aes.Y)!! + val location = helper.toClient(point, p) + + if (location == null) { + droppedPointsIds.add(p.index()) + continue + } - for (i in 0 until count) { - val p = aesthetics.dataPointAt(i) - if (p.finiteOrNull(Aes.SIZE) == null) continue - val point = p.finiteVectorOrNull(Aes.X, Aes.Y) ?: continue - val location = helper.toClient(point, p) ?: continue val shape = p.shape()!! // Adapt point size to plot 'grid step' if necessary (i.e. in correlation matrix). @@ -57,16 +66,16 @@ open class PointGeom : GeomBase() { } targetCollector.addPoint( - i, location, (shape.size(p, scaleFactor) + shape.strokeWidth(p)) / 2, - GeomTargetCollector.TooltipParams( - markerColors = colorsByDataPoint(p) - ) + p.index(), + location, + (shape.size(p, scaleFactor) + shape.strokeWidth(p)) / 2, + GeomTargetCollector.TooltipParams(markerColors = colorsByDataPoint(p)) ) - val o = PointShapeSvg.create(shape, location, p, scaleFactor) - o.appendTo(slimGroup) - goodPointsCount += 1 + PointShapeSvg.create(shape, location, p, scaleFactor) + .appendTo(slimGroup) } - addNulls(count - goodPointsCount) + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) root.add(wrap(slimGroup)) } @@ -74,4 +83,3 @@ open class PointGeom : GeomBase() { const val HANDLES_GROUPS = false } } - 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..be3055ef992 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): Pair, Iterable> { return GeomUtil.with_X_Y(dataPoints) } @@ -24,7 +24,8 @@ open class PolygonGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - val dataPoints = dataPoints(aesthetics) + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) + val linesHelper = LinesHelper(pos, coord, ctx) linesHelper.setResamplingEnabled(coord.isPolar) @@ -34,6 +35,10 @@ open class PolygonGeom : GeomBase() { targetCollectorHelper.addPolygons(polygonData) root.add(svg) } + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + val droppedPointsIds = linesHelper.getDroppedPointsIds().asSequence() + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) } companion object { diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/RasterGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/RasterGeom.kt index f620ea50dca..b297103a888 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/RasterGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/RasterGeom.kt @@ -11,7 +11,7 @@ import org.jetbrains.letsPlot.commons.values.Color import org.jetbrains.letsPlot.core.commons.data.SeriesUtil import org.jetbrains.letsPlot.core.plot.base.* import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomHelper -import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.with_X_Y +import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil 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.Label @@ -28,6 +28,10 @@ class RasterGeom : GeomBase() { override val legendKeyElementFactory: LegendKeyElementFactory get() = FilledSquareLegendKeyElementFactory() + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + return GeomUtil.with_X_Y(dataPoints) + } + override fun buildIntern( root: SvgRoot, aesthetics: Aesthetics, @@ -35,11 +39,10 @@ class RasterGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - val iter = with_X_Y(aesthetics.dataPoints()).iterator() - if (!iter.hasNext()) { - return - } - val randomP = iter.next() + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) + + val randomP = dataPoints.firstOrNull() ?: return + val helper = GeomHelper(pos, coord, ctx) // Find size of image (row x col) @@ -85,7 +88,7 @@ class RasterGeom : GeomBase() { val y0 = boundsXY.origin.y val argbValues = IntArray(cols * rows) - for (p in with_X_Y(aesthetics.dataPoints())) { + for (p in dataPoints) { val x = p.x() val y = p.y() val alpha = p.alpha() @@ -112,6 +115,9 @@ class RasterGeom : GeomBase() { bitmap ) root.add(svgImageElement) + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() }.toSet() + ctx.droppedPointsReporter().report(filteredPointsIds) } 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..fdb0d2acb99 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,9 +21,9 @@ import org.jetbrains.letsPlot.core.plot.base.tooltip.TipLayoutHint.Kind.VERTICAL class RibbonGeom : GeomBase() { - override fun prepareDataPoints(dataPoints: Iterable): Iterable { - val data = GeomUtil.with_X(dataPoints) - return GeomUtil.ordered_X(data) + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + val (data, invalid) = GeomUtil.with_X(dataPoints) + return GeomUtil.ordered_X(data) to invalid } override fun buildIntern( @@ -33,20 +33,25 @@ class RibbonGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - val dataPoints = dataPoints(aesthetics) - val helper = LinesHelper(pos, coord, ctx) + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.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) + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + val droppedPointsIds = linesHelper.getDroppedPointsIds().asSequence() + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) } 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/SinaGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/SinaGeom.kt index 050e985605b..9af604dfdc5 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/SinaGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/SinaGeom.kt @@ -23,10 +23,15 @@ class SinaGeom : GeomBase() { var seed: Long? = null var quantiles: List = BaseYDensityStat.DEF_QUANTILES var showHalf: Double = DEF_SHOW_HALF + val droppedPointsIds = mutableSetOf() override val legendKeyElementFactory: LegendKeyElementFactory get() = PointLegendKeyElementFactory() + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + return GeomUtil.withDefined(dataPoints, Aes.X, Aes.Y, Aes.SIZE, Aes.VIOLINWIDTH, Aes.WIDTH) + } + override fun buildIntern( root: SvgRoot, aesthetics: Aesthetics, @@ -35,11 +40,14 @@ class SinaGeom : GeomBase() { ctx: GeomContext ) { val rand = seed?.let { Random(seed!!) } ?: Random.Default - val dataPoints = GeomUtil.withDefined(aesthetics.dataPoints(), Aes.X, Aes.Y) + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) dataPoints .groupBy(DataPointAesthetics::x) .map { (x, nonOrderedPoints) -> x to GeomUtil.ordered_Y(nonOrderedPoints, false) } .forEach { (_, dataPoints) -> buildGroup(root, dataPoints, pos, coord, ctx, rand) } + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) } private fun buildGroup( @@ -59,10 +67,17 @@ class SinaGeom : GeomBase() { quantilesHelper.splitByQuantiles(dataPoints, Aes.Y).forEach { points -> val slimGroup = SvgSlimElements.g(points.size) for (p in points) { - p.size() ?: continue - val shape = p.shape() ?: continue - val point = jitterTransform(p) ?: continue - val location = helper.toClient(point, p) ?: continue + p.size()!! + val shape = p.shape()!! + val point = jitterTransform(p)!! + + val location = helper.toClient(point, p) + + if (location == null) { + droppedPointsIds.add(p.index()) + continue + } + targetCollector.addPoint( p.index(), location, (shape.size(p) + shape.strokeWidth(p)) / 2, GeomTargetCollector.TooltipParams( diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/SmoothGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/SmoothGeom.kt index 12076e8ceb6..04aa36abf40 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/SmoothGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/SmoothGeom.kt @@ -7,8 +7,6 @@ package org.jetbrains.letsPlot.core.plot.base.geom import org.jetbrains.letsPlot.core.plot.base.* import org.jetbrains.letsPlot.core.plot.base.geom.util.* -import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.ordered_X -import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.with_X_Y import org.jetbrains.letsPlot.core.plot.base.geom.util.HintsCollection.HintConfigFactory import org.jetbrains.letsPlot.core.plot.base.render.LegendKeyElementFactory import org.jetbrains.letsPlot.core.plot.base.render.SvgRoot @@ -21,6 +19,11 @@ class SmoothGeom : GeomBase() { override val legendKeyElementFactory: LegendKeyElementFactory get() = HLineGeom.LEGEND_KEY_ELEMENT_FACTORY + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + val (data, invalid) = GeomUtil.with_X_Y(dataPoints) + return GeomUtil.ordered_X(data) to invalid + } + override fun buildIntern( root: SvgRoot, aesthetics: Aesthetics, @@ -28,20 +31,24 @@ class SmoothGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - val dataPoints = ordered_X(with_X_Y(aesthetics.dataPoints())) - val helper = LinesHelper(pos, coord, ctx) + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) + val linesHelper = LinesHelper(pos, coord, ctx) - helper.setAlphaEnabled(false) + linesHelper.setAlphaEnabled(false) // Confidence interval - val bands = helper.createBands(dataPoints, GeomUtil.TO_LOCATION_X_YMAX, GeomUtil.TO_LOCATION_X_YMIN) + val bands = linesHelper.createBands(dataPoints, GeomUtil.TO_LOCATION_X_YMAX, GeomUtil.TO_LOCATION_X_YMIN) root.appendNodes(bands) // Regression line - val regressionLines = helper.createLines(dataPoints, GeomUtil.TO_LOCATION_X_Y) + val regressionLines = linesHelper.createLines(dataPoints, GeomUtil.TO_LOCATION_X_Y) root.appendNodes(regressionLines) buildHints(dataPoints, pos, coord, ctx) + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + val droppedPointsIds = linesHelper.getDroppedPointsIds().asSequence() + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) } private fun buildHints( 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..5f75510ea4a 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 @@ -21,15 +21,15 @@ class StepGeom : LineGeom() { fun setDirection(dir: String) { myDirection = Direction.toDirection(dir) } - // commit name: - override fun prepareDataPoints(dataPoints: Iterable): Iterable { + + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { // filter out points with NaN x-values but keep +/-Infinity (for 'padded' mode) - val data = dataPoints.filter { p: DataPointAesthetics -> + val (data, invalid) = dataPoints.partition { p: DataPointAesthetics -> val x = p.x() x != null && (x.isFinite() || x.isInfinite()) } - return GeomUtil.ordered_X(data) + return GeomUtil.ordered_X(data) to invalid } override fun buildIntern( @@ -39,7 +39,8 @@ class StepGeom : LineGeom() { coord: CoordinateSystem, ctx: GeomContext ) { - val dataPoints = dataPoints(aesthetics) + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) + val linesHelper = LinesHelper(pos, coord, ctx) val pathDataList = linesHelper.createPaths(dataPoints, toLocationFor(overallAesBounds(ctx))) @@ -54,6 +55,10 @@ class StepGeom : LineGeom() { val targetCollectorHelper = TargetCollectorHelper(ctx) targetCollectorHelper.addPaths(pathDataList) + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + val droppedPointsIds = linesHelper.getDroppedPointsIds().asSequence() + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) } private fun toLocationFor(viewPort: DoubleRectangle): (DataPointAesthetics) -> DoubleVector? { diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/ViolinGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/ViolinGeom.kt index af5a8f4e9a1..968069944f4 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/ViolinGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/ViolinGeom.kt @@ -24,6 +24,10 @@ class ViolinGeom : GeomBase() { private val positiveSign: Double get() = if (showHalf < 0.0) 0.0 else 1.0 + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + return GeomUtil.withDefined(dataPoints, Aes.X, Aes.Y, Aes.VIOLINWIDTH, Aes.WIDTH) + } + override fun buildIntern( root: SvgRoot, aesthetics: Aesthetics, @@ -41,37 +45,45 @@ class ViolinGeom : GeomBase() { coord: CoordinateSystem, ctx: GeomContext ) { - GeomUtil.withDefined(aesthetics.dataPoints(), Aes.X, Aes.Y, Aes.VIOLINWIDTH, Aes.WIDTH) + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) + + val linesHelper = LinesHelper(pos, coord, ctx) + val quantilesHelper = QuantilesHelper(pos, coord, ctx, quantiles, Aes.X) + + dataPoints .groupBy(DataPointAesthetics::x) .map { (x, nonOrderedPoints) -> x to GeomUtil.ordered_Y(nonOrderedPoints, false) } - .forEach { (_, dataPoints) -> buildViolin(root, dataPoints, pos, coord, ctx) } + .forEach { (_, dataPoints) -> buildViolin(root, dataPoints, linesHelper, quantilesHelper, ctx) } + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() } + val droppedPointsIds = linesHelper.getDroppedPointsIds().asSequence() + ctx.droppedPointsReporter().report((filteredPointsIds + droppedPointsIds).toSet()) } private fun buildViolin( root: SvgRoot, dataPoints: Iterable, - pos: PositionAdjustment, - coord: CoordinateSystem, + linesHelper: LinesHelper, + quantilesHelper: QuantilesHelper, ctx: GeomContext ) { - val helper = LinesHelper(pos, coord, ctx) - val quantilesHelper = QuantilesHelper(pos, coord, ctx, quantiles, Aes.X) + val leftBoundTransform = toLocationBound(negativeSign, ctx) val rightBoundTransform = toLocationBound(positiveSign, ctx) quantilesHelper.splitByQuantiles(dataPoints, Aes.Y).forEach { points -> - val paths = helper.createBands(points, leftBoundTransform, rightBoundTransform) + val paths = linesHelper.createBands(points, leftBoundTransform, rightBoundTransform) root.appendNodes(paths) - helper.setAlphaEnabled(false) - root.appendNodes(helper.createLines(points, leftBoundTransform)) - root.appendNodes(helper.createLines(points, rightBoundTransform)) + linesHelper.setAlphaEnabled(false) + root.appendNodes(linesHelper.createLines(points, leftBoundTransform)) + root.appendNodes(linesHelper.createLines(points, rightBoundTransform)) if (showHalf <= 0.0) { - buildHints(points, ctx, helper, leftBoundTransform) + buildHints(points, ctx, linesHelper, leftBoundTransform) } if (showHalf >= 0.0) { - buildHints(points, ctx, helper, rightBoundTransform) + buildHints(points, ctx, linesHelper, rightBoundTransform) } } diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/YDotplotGeom.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/YDotplotGeom.kt index 865328a6b22..21a391f7ad9 100644 --- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/YDotplotGeom.kt +++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/YDotplotGeom.kt @@ -24,6 +24,10 @@ class YDotplotGeom : DotplotGeom(), WithHeight { override val legendKeyElementFactory: LegendKeyElementFactory get() = FilledCircleLegendKeyElementFactory() + override fun filterDataPoints(dataPoints: Iterable): Pair, Iterable> { + return GeomUtil.withDefined(dataPoints, Aes.BINWIDTH, Aes.STACKSIZE, Aes.X, Aes.Y) + } + override fun buildIntern( root: SvgRoot, aesthetics: Aesthetics, @@ -31,13 +35,11 @@ class YDotplotGeom : DotplotGeom(), WithHeight { coord: CoordinateSystem, ctx: GeomContext ) { - val pointsWithBinWidth = GeomUtil.withDefined( - aesthetics.dataPoints(), - Aes.BINWIDTH, Aes.X, Aes.Y - ) - if (!pointsWithBinWidth.any()) return + val (dataPoints, invalidDataPoints) = filterDataPoints(aesthetics.dataPoints()) - val binWidthPx = pointsWithBinWidth.first().let { + if (!dataPoints.any()) return + + val binWidthPx = dataPoints.first().let { val x = it.x()!! val y = it.y()!! val bw = it.binwidth()!! @@ -48,7 +50,8 @@ class YDotplotGeom : DotplotGeom(), WithHeight { true -> abs(p0.x - p1.x) } } - GeomUtil.withDefined(pointsWithBinWidth, Aes.X, Aes.Y, Aes.STACKSIZE) + + dataPoints .groupBy(DataPointAesthetics::x) .forEach { (_, dataPointGroup) -> dataPointGroup @@ -57,6 +60,9 @@ class YDotplotGeom : DotplotGeom(), WithHeight { buildStack(root, dataPointStack, pos, coord, ctx, binWidthPx) } } + + val filteredPointsIds = invalidDataPoints.asSequence().map { it.index() }.toSet() + ctx.droppedPointsReporter().report(filteredPointsIds) } private fun buildStack( 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..5359345e803 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 @@ -117,18 +116,18 @@ object GeomUtil { } @Suppress("FunctionName") - fun with_X_Y(dataPoints: Iterable): List { - return dataPoints.filter(WITH_X_Y::invoke) + fun with_X_Y(dataPoints: Iterable): Pair, Iterable> { + return dataPoints.partition(WITH_X_Y::invoke) } @Suppress("FunctionName") - fun with_X(dataPoints: Iterable): List { - return dataPoints.filter(WITH_X::invoke) + fun with_X(dataPoints: Iterable): Pair, Iterable> { + return dataPoints.partition(WITH_X::invoke) } @Suppress("FunctionName") - fun with_Y(dataPoints: Iterable): List { - return dataPoints.filter(WITH_Y::invoke) + fun with_Y(dataPoints: Iterable): Pair, Iterable> { + return dataPoints.partition(WITH_Y::invoke) } @Suppress("FunctionName") @@ -152,8 +151,8 @@ object GeomUtil { dataPoints: Iterable, aes0: Aes<*>, aes1: Aes<*> - ): Iterable { - return dataPoints.filter { p -> p.defined(aes0) && p.defined(aes1) } + ): Pair, Iterable> { + return dataPoints.partition { p -> p.defined(aes0) && p.defined(aes1) } } fun withDefined( @@ -161,8 +160,8 @@ object GeomUtil { aes0: Aes<*>, aes1: Aes<*>, aes2: Aes<*> - ): Iterable { - return dataPoints.filter { p -> p.defined(aes0) && p.defined(aes1) && p.defined(aes2) } + ): Pair, Iterable> { + return dataPoints.partition { p -> p.defined(aes0) && p.defined(aes1) && p.defined(aes2) } } fun withDefined( @@ -171,19 +170,19 @@ object GeomUtil { aes1: Aes<*>, aes2: Aes<*>, aes3: Aes<*> - ): Iterable { - return dataPoints.filter { p -> p.defined(aes0) && p.defined(aes1) && p.defined(aes2) && p.defined(aes3) } + ): Pair, Iterable> { + return dataPoints.partition { p -> p.defined(aes0) && p.defined(aes1) && p.defined(aes2) && p.defined(aes3) } } - private fun createGroups( + fun withDefined( dataPoints: Iterable, - sorted: Boolean = false - ): Map> { - val map = dataPoints.groupBy { it.group()!! } - return when { - sorted -> map.toList().sortedBy { (g, _) -> g }.toMap() - else -> map - } + aes0: Aes<*>, + aes1: Aes<*>, + aes2: Aes<*>, + aes3: Aes<*>, + aes4: Aes<*> + ): Pair, Iterable> { + return dataPoints.partition { p -> p.defined(aes0) && p.defined(aes1) && p.defined(aes2) && p.defined(aes3) && p.defined(aes4) } } fun createPathDataFromRectangle( @@ -205,50 +204,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/commonTest/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/LinesHelperResamplingTest.kt b/plot-base/src/commonTest/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/LinesHelperResamplingTest.kt index c49e3bd755d..52293c029c1 100644 --- a/plot-base/src/commonTest/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/LinesHelperResamplingTest.kt +++ b/plot-base/src/commonTest/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/LinesHelperResamplingTest.kt @@ -111,7 +111,7 @@ class LinesHelperResamplingTest { BogusContext, DensityStat.DEF_QUANTILES ) - val dataPoints = GeomUtil.withDefined(GeomUtil.ordered_X(aes.dataPoints()), Aes.X, Aes.Y) + val (dataPoints, _) = GeomUtil.withDefined(GeomUtil.ordered_X(aes.dataPoints()), Aes.X, Aes.Y) val actual = HashMap>() diff --git a/plot-base/src/jvmTest/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/ErrorBarGeomTest.kt b/plot-base/src/jvmTest/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/ErrorBarGeomTest.kt index 9932a575c72..0ded5ed9791 100644 --- a/plot-base/src/jvmTest/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/ErrorBarGeomTest.kt +++ b/plot-base/src/jvmTest/kotlin/org/jetbrains/letsPlot/core/plot/base/geom/ErrorBarGeomTest.kt @@ -90,7 +90,6 @@ class ErrorBarGeomTest { override val targetCollector: GeomTargetCollector = NullGeomTargetCollector override fun getResolution(aes: Aes): Double = 10.0 override fun isMappedAes(aes: Aes<*>) = aes == Aes.X || aes == Aes.YMIN || aes == Aes.YMAX - override fun consumeMessages(messages: List) {} override fun geomKind() = GeomKind.ERROR_BAR }, pos = PositionAdjustments.identity(), 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..158bad57edc 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 @@ -25,6 +25,8 @@ interface GeomLayer { val geomKind: GeomKind + val statName: String + val geom: Geom val posProvider: PosProvider @@ -61,6 +63,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..9dad3255f71 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 @@ -11,6 +11,7 @@ import org.jetbrains.letsPlot.commons.values.Color import org.jetbrains.letsPlot.commons.values.Font import org.jetbrains.letsPlot.core.commons.data.SeriesUtil import org.jetbrains.letsPlot.core.plot.base.* +import org.jetbrains.letsPlot.core.plot.base.geom.DroppedPointsReporter import org.jetbrains.letsPlot.core.plot.base.geom.annotation.Annotation import org.jetbrains.letsPlot.core.plot.base.theme.FontFamilyRegistry import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetCollector @@ -31,8 +32,8 @@ class GeomContextBuilder : ImmutableGeomContext.Builder { private var coordinateSystem: CoordinateSystem? = null private var contentBounds: DoubleRectangle? = null private var scaleFactor: Double = 1.0 + private var droppedPointsReporter: DroppedPointsReporter = DroppedPointsReporter.NONE private var geomKind: GeomKind? = null - private var messageConsumer: (String) -> Unit = {} constructor() @@ -101,8 +102,8 @@ class GeomContextBuilder : ImmutableGeomContext.Builder { return this } - override fun messageConsumer(messageConsumer: (String) -> Unit): ImmutableGeomContext.Builder { - this.messageConsumer = messageConsumer + override fun droppedPointsReporter(reporter: DroppedPointsReporter): ImmutableGeomContext.Builder { + this.droppedPointsReporter = reporter return this } @@ -124,7 +125,7 @@ class GeomContextBuilder : ImmutableGeomContext.Builder { val _coordinateSystem = b.coordinateSystem val _contentBounds = b.contentBounds val _scaleFactor = b.scaleFactor - val _messageConsumer = b.messageConsumer + val _droppedPointsReporter = b.droppedPointsReporter val _geomKind = b.geomKind override val flipped: Boolean = b.flipped @@ -197,8 +198,8 @@ class GeomContextBuilder : ImmutableGeomContext.Builder { return _scaleFactor } - override fun consumeMessages(messages: List) { - messages.forEach { _messageConsumer(it) } + override fun droppedPointsReporter(): DroppedPointsReporter { + return _droppedPointsReporter } override fun geomKind(): GeomKind { 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..a95e46003e6 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 @@ -232,6 +233,7 @@ class GeomLayerBuilder( return MyGeomLayer( data, geomProvider, + getStatName(stat), myGeomTheme, posProvider, groupingContext.groupMapper, @@ -252,6 +254,7 @@ class GeomLayerBuilder( fillByAes = fillByAes, annotationProvider = myAnnotationProvider, defaultFormatters = myDefaultFormatters, + naRm = naRm, ) } @@ -263,6 +266,7 @@ class GeomLayerBuilder( private class MyGeomLayer( override val dataFrame: DataFrame, geomProvider: GeomProvider, + override val statName: String, geomTheme: GeomTheme, override val posProvider: PosProvider, override val group: (Int) -> Int, @@ -282,7 +286,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( @@ -382,6 +387,13 @@ class GeomLayerBuilder( companion object { + fun getStatName(stat: Stat): String { + return stat::class.simpleName!! + .replace("Stat", "") + .replace("([a-z])([A-Z]+)".toRegex(), "$1_$2") + .lowercase() + } + fun demoAndTest( geomProvider: GeomProvider, stat: Stat, 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..ca0aaf297de 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 @@ -8,6 +8,7 @@ package org.jetbrains.letsPlot.core.plot.builder.assemble import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle import org.jetbrains.letsPlot.commons.values.Color import org.jetbrains.letsPlot.core.plot.base.* +import org.jetbrains.letsPlot.core.plot.base.geom.DroppedPointsReporter import org.jetbrains.letsPlot.core.plot.base.geom.annotation.Annotation import org.jetbrains.letsPlot.core.plot.base.theme.FontFamilyRegistry import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetCollector @@ -41,7 +42,7 @@ interface ImmutableGeomContext : GeomContext { fun scaleFactor(scaleFactor: Double): Builder - fun messageConsumer(messageConsumer: (String) -> Unit): Builder + fun droppedPointsReporter(reporter: DroppedPointsReporter): 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..b9482583356 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 @@ -12,6 +12,7 @@ import org.jetbrains.letsPlot.commons.values.Color import org.jetbrains.letsPlot.core.interact.UnsupportedInteractionException import org.jetbrains.letsPlot.core.plot.base.CoordinateSystem import org.jetbrains.letsPlot.core.plot.base.PlotContext +import org.jetbrains.letsPlot.core.plot.base.geom.DroppedPointsReporter import org.jetbrains.letsPlot.core.plot.base.render.svg.SvgComponent import org.jetbrains.letsPlot.core.plot.base.theme.PanelGridTheme import org.jetbrains.letsPlot.core.plot.base.theme.Theme @@ -211,8 +212,10 @@ internal abstract class FrameOfReferenceBase( .coordinateSystem(coord) .contentBounds(bounds) .scaleFactor(plotContext.getScaleFactor()) + .droppedPointsReporter( + createDroppedPointsReporter(layer, plotContext) + ) .geomKind(layer.geomKind) - .messageConsumer(plotContext.getMessageConsumer()) .build() val pos = rendererData.pos @@ -220,5 +223,25 @@ internal abstract class FrameOfReferenceBase( return SvgLayerRenderer(aesthetics, geom, pos, coord, ctx) } + + private fun createDroppedPointsReporter( + layer: GeomLayer, + plotContext: PlotContext + ): DroppedPointsReporter { + if (layer.naRm) { + return DroppedPointsReporter.NONE + } + val messageConsumer = plotContext.getMessageConsumer() + val prefix = "[${layer.geomKind.name.lowercase()}/${layer.statName}]" + + return object : DroppedPointsReporter { + override fun report(droppedIndices: Set) { + if (droppedIndices.isEmpty()) return + val n = droppedIndices.size + val points = if (n == 1) "data point" else "data points" + messageConsumer("$prefix Removed $n $points: missing or outside the scale limits.") + } + } + } } -} \ No newline at end of file +} 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..d3663400f99 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 @@ -61,7 +58,7 @@ internal class DataPointsConverter( clockwise = geom.clockwise ) - val definedDataPoints = GeomUtil.withDefined(aesthetics.dataPoints(), Aes.X, Aes.Y, Aes.SLICE) + val (definedDataPoints, _) = GeomUtil.withDefined(aesthetics.dataPoints(), Aes.X, Aes.Y, Aes.SLICE) return MultiDataPointHelper.getPoints(definedDataPoints) .map { DataPointLiveMapAesthetics(it, MapLayerKind.PIE).apply { @@ -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 c518a96ecdb..621f2837444 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 @@ -245,7 +245,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/back/PlotConfigBackend.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/PlotConfigBackend.kt index 49ecf252658..a7337de9a41 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/PlotConfigBackend.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/PlotConfigBackend.kt @@ -98,11 +98,12 @@ open class PlotConfigBackend( val layerIndexWhereSamplingOccurred = HashSet() - val dataByLayerAfterStat = dataByLayerAfterStat() { layerIndex, message -> + val dataByLayerAfterStat = dataByLayerAfterStat { layerIndex, message -> layerIndexWhereSamplingOccurred.add(layerIndex) - if (theme.plot().showMessage()) { - PlotConfigUtil.addComputationMessage(this, message) + val layerConfig = layerConfigs[layerIndex] + val fullMessage = BackendDataProcUtil.createLayerMessage(message, layerConfig) + PlotConfigUtil.addComputationMessage(this, fullMessage) } } @@ -266,7 +267,7 @@ open class PlotConfigBackend( tileDataAfterStat, layerConfig.samplings, groupMapperAfterStat - ) { message -> messageHandler(BackendDataProcUtil.createSamplingMessage(message, layerConfig)) } + ) { message -> messageHandler(message) } } // merge tiles diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/data/BackendDataProcUtil.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/data/BackendDataProcUtil.kt index b21104bae7d..4942c0d2ad8 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/data/BackendDataProcUtil.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/data/BackendDataProcUtil.kt @@ -77,18 +77,16 @@ internal object BackendDataProcUtil { } private fun getStatName(layerConfig: LayerConfig): String { - var stat: String = layerConfig.stat::class.simpleName!! - stat = stat.replace("Stat", " stat") - stat = stat.replace("([a-z])([A-Z]+)".toRegex(), "$1_$2").lowercase() - - return stat + return layerConfig.stat::class.simpleName!! + .replace("Stat", "") + .replace("([a-z])([A-Z]+)".toRegex(), "$1_$2") + .lowercase() } - internal fun createSamplingMessage(samplingExpression: String, layerConfig: LayerConfig): String { + internal fun createLayerMessage(message: String, layerConfig: LayerConfig): String { val geomKind = layerConfig.geomProto.geomKind.name.lowercase() val stat = getStatName(layerConfig) - - return "$samplingExpression was applied to [$geomKind/$stat] layer" + return "[$geomKind/$stat] $message" } private fun createStatMessage(statInfo: String, layerConfig: LayerConfig): String { diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/data/PlotSampling.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/data/PlotSampling.kt index 6371b169e6e..268626534a6 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/data/PlotSampling.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/back/data/PlotSampling.kt @@ -22,6 +22,7 @@ internal object PlotSampling { @Suppress("NAME_SHADOWING") var data = data + val originalRowCount = data.rowCount() val applied = ArrayList() for (sampling in samplings) { @@ -43,9 +44,16 @@ internal object PlotSampling { } } - if (!applied.isEmpty()) { - val expressionText = applied.map { it.expressionText }.joinToString("+") - samplingExpressionConsumer(expressionText) + if (applied.isNotEmpty()) { + val expressionText = applied.map { it.expressionText }.let { texts -> + when (texts.size) { + 1 -> texts[0] + else -> texts.dropLast(1).joinToString(", ") + " and " + texts.last() + } + } + val droppedCount = originalRowCount - data.rowCount() + val message = "Removed $droppedCount data points out of $originalRowCount by $expressionText." + samplingExpressionConsumer(message) } return data 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