Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class EmptyGeomContext : GeomContext {
return 1.0
}

override fun removeNaMessages() = true

override fun consumeMessages(messages: List<String>) {
throw IllegalStateException("Not available in an empty geom context")
}
Expand Down
2,178 changes: 2,178 additions & 0 deletions docs/dev/notebooks/na_messages.ipynb

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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.
*/

Expand Down Expand Up @@ -55,6 +55,8 @@ object BogusContext : GeomContext {
return 1.0
}

override fun removeNaMessages() = true

override fun consumeMessages(messages: List<String>) {
// do nothing
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ interface GeomContext {

fun getScaleFactor(): Double

fun removeNaMessages(): Boolean

fun consumeMessages(messages: List<String>)

fun geomKind(): GeomKind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ open class AreaGeom : GeomBase() {

override fun rangeIncludesZero(aes: Aes<*>): Boolean = (aes == Aes.Y)

override fun prepareDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
override fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
val data = GeomUtil.with_X(dataPoints)
return GeomUtil.ordered_X(data)
}
Expand All @@ -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,
Expand All @@ -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)
}
Expand All @@ -72,6 +75,7 @@ open class AreaGeom : GeomBase() {
createQuantileLines(groupDataPoints, quantilesHelper).forEach(root::add)
}
}
reportDroppedPoints((filteredPointsIds + linesHelper.getDroppedPointsIds()).size, ctx)
}

private fun createQuantileLines(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -47,23 +41,14 @@ abstract class GeomBase : Geom {
return ctx.targetCollector
}

open fun prepareDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
open fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
return dataPoints
}

protected fun dataPoints(aesthetics: Aesthetics): Iterable<DataPointAesthetics> {
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<String> {
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(
Expand All @@ -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
Expand Down Expand Up @@ -110,5 +93,9 @@ abstract class GeomBase : Geom {
add(path.rootGroup)
}
}

fun Iterable<DataPointAesthetics>.excludedIndicesComparedTo(other: Iterable<DataPointAesthetics>): Set<Int> {
return mapTo(HashSet()) { it.index() } - other.mapTo(HashSet()) { it.index() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataPointAesthetics>): Iterable<DataPointAesthetics> {
override fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
val data = GeomUtil.with_X(dataPoints)
return GeomUtil.ordered_X(data)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -66,7 +65,7 @@ open class PointGeom : GeomBase() {
o.appendTo(slimGroup)
goodPointsCount += 1
}
addNulls(count - goodPointsCount)
reportDroppedPoints(count - goodPointsCount, ctx)
root.add(wrap(slimGroup))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import org.jetbrains.letsPlot.core.plot.base.render.SvgRoot

open class PolygonGeom : GeomBase() {

override fun prepareDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
override fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
return GeomUtil.with_X_Y(dataPoints)
}

Expand All @@ -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)

Expand All @@ -34,6 +37,7 @@ open class PolygonGeom : GeomBase() {
targetCollectorHelper.addPolygons(polygonData)
root.add(svg)
}
reportDroppedPoints((filteredPointsIds + linesHelper.getDroppedPointsIds()).size, ctx)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.jetbrains.letsPlot.core.plot.base.tooltip.TipLayoutHint.Kind.VERTICAL

class RibbonGeom : GeomBase() {

override fun prepareDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
override fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
val data = GeomUtil.with_X(dataPoints)
return GeomUtil.ordered_X(data)
}
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class StepGeom : LineGeom() {
myDirection = Direction.toDirection(dir)
}
// commit name:
override fun prepareDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
override fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
// filter out points with NaN x-values but keep +/-Infinity (for 'padded' mode)
val data = dataPoints.filter { p: DataPointAesthetics ->
val x = p.x()
Expand All @@ -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)))
Expand All @@ -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? {
Expand Down
Loading