Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,9 +55,7 @@ class EmptyGeomContext : GeomContext {
return 1.0
}

override fun consumeMessages(messages: List<String>) {
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")
Expand All @@ -81,4 +80,4 @@ class EmptyGeomContext : GeomContext {
),
).dimensions(text)
}
}
}
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 All @@ -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

Expand Down Expand Up @@ -55,9 +56,7 @@ object BogusContext : GeomContext {
return 1.0
}

override fun consumeMessages(messages: List<String>) {
// do nothing
}
override fun droppedPointsReporter() = DroppedPointsReporter.NONE

override fun geomKind(): GeomKind {
error("Not available in a bogus geom context")
Expand All @@ -72,4 +71,4 @@ object BogusContext : GeomContext {
): DoubleVector {
error("Not available in a bogus geom context")
}
}
}
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 @@ -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

Expand Down Expand Up @@ -50,7 +51,7 @@ interface GeomContext {

fun getScaleFactor(): Double

fun consumeMessages(messages: List<String>)
fun droppedPointsReporter(): DroppedPointsReporter

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

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

override fun prepareDataPoints(dataPoints: Iterable<DataPointAesthetics>): Iterable<DataPointAesthetics> {
val data = GeomUtil.with_X(dataPoints)
return GeomUtil.ordered_X(data)
override fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Pair<Iterable<DataPointAesthetics>, Iterable<DataPointAesthetics>> {
val (data, invalid) = GeomUtil.with_X(dataPoints)
return GeomUtil.ordered_X(data) to invalid
}

override fun buildIntern(
Expand All @@ -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,
Expand All @@ -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)
}
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,33 @@ class AreaRidgesGeom : GeomBase(), WithHeight {
var quantiles: List<Double> = DensityRidgesStat.DEF_QUANTILES
var quantileLines: Boolean = DEF_QUANTILE_LINES

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

override fun buildIntern(
root: SvgRoot,
aesthetics: Aesthetics,
pos: PositionAdjustment,
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<DataPointAesthetics>): List<Iterable<DataPointAesthetics>> {
Expand All @@ -60,29 +71,28 @@ class AreaRidgesGeom : GeomBase(), WithHeight {
private fun buildRidge(
root: SvgRoot,
dataPoints: Iterable<DataPointAesthetics>,
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,
simplifyBorders = true
)
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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,22 @@ open class DotplotGeom : GeomBase(), WithWidth {
super.preferableNullDomain(aes)
}

override fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Pair<Iterable<DataPointAesthetics>, Iterable<DataPointAesthetics>> {
return GeomUtil.withDefined(dataPoints, Aes.BINWIDTH, Aes.STACKSIZE, Aes.X, Aes.Y)
}

override fun buildIntern(
root: SvgRoot,
aesthetics: Aesthetics,
pos: PositionAdjustment,
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()!!
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Int>)

companion object {
val NONE = object : DroppedPointsReporter {
override fun report(droppedIndices: Set<Int>) {}
}
}
}
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,8 @@ abstract class GeomBase : Geom {
return ctx.targetCollector
}

open fun prepareDataPoints(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()
open fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Pair<Iterable<DataPointAesthetics>, Iterable<DataPointAesthetics>> {
return dataPoints to emptyList()
}

protected abstract fun buildIntern(
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataPointAesthetics>): Iterable<DataPointAesthetics> {
val data = GeomUtil.with_X(dataPoints)
return GeomUtil.ordered_X(data)
override fun filterDataPoints(dataPoints: Iterable<DataPointAesthetics>): Pair<Iterable<DataPointAesthetics>, Iterable<DataPointAesthetics>> {
val (data, invalid) = GeomUtil.with_X(dataPoints)
return GeomUtil.ordered_X(data) to invalid
}

companion object {
Expand Down
Loading