Skip to content

Commit

Permalink
Restrict the number of tooltips for a bar plot (#903)
Browse files Browse the repository at this point in the history
* Add LocatedTargetsPicker test: primary filtering of results.

* Add restriction on tooltip count for geom_bar. Add test.

* Improve tests.
  • Loading branch information
OLarionova-HORIS committed Oct 17, 2023
1 parent f366ad5 commit 1543a55
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ class LocatedTargetsPicker(
internal const val CUTOFF_DISTANCE = 30.0
internal const val FAKE_DISTANCE = 15.0

private const val BAR_TOOLTIPS_MAX_COUNT = 5 // allowed number of visible tooltips
private val BAR_GEOMS = setOf(BAR, HISTOGRAM)

// Consider layers with the same geom as a single layer to join their tooltips
private val STACKABLE_GEOMS = setOf(
DENSITY,
Expand All @@ -127,8 +130,7 @@ class LocatedTargetsPicker(
// use XY distance for tooltips with crosshair to avoid giving them priority
locatedTargetList.targets
.filter { it.tipLayoutHint.coord != null }
.map { target -> MathUtil.distance(coord, target.tipLayoutHint.coord!!) }
.minOrNull()
.minOfOrNull { target -> MathUtil.distance(coord, target.tipLayoutHint.coord!!) }
?: FAKE_DISTANCE
}
} else {
Expand All @@ -140,12 +142,32 @@ class LocatedTargetsPicker(
return lft.geomKind === rgt.geomKind && STACKABLE_GEOMS.contains(rgt.geomKind)
}

private fun LookupResult.withTargets(newTargets: List<GeomTarget>) = LookupResult(
targets = newTargets,
distance = distance,
geomKind = geomKind,
contextualMapping = contextualMapping,
isCrosshairEnabled = isCrosshairEnabled
)

private fun filterResults(
lookupResult: LookupResult,
coord: DoubleVector?,
flippedAxis: Boolean
): LookupResult {
if (coord == null || lookupResult.geomKind !in setOf(DENSITY, HISTOGRAM, FREQPOLY, LINE, AREA, SEGMENT, RIBBON)) {
if (coord == null) return lookupResult

val geomTargets = lookupResult.targets.filter { it.tipLayoutHint.coord != null }

// for bar - if the number of targets exceeds the restriction value => use the closest one
if (lookupResult.geomKind in BAR_GEOMS && geomTargets.size > BAR_TOOLTIPS_MAX_COUNT) {
val closestTarget = geomTargets.minBy { target ->
MathUtil.distance(coord, target.tipLayoutHint.coord!!)
}
return lookupResult.withTargets(listOf(closestTarget))
}

if (lookupResult.geomKind !in setOf(DENSITY, HISTOGRAM, FREQPOLY, LINE, AREA, SEGMENT, RIBBON)) {
return lookupResult
}

Expand All @@ -157,8 +179,7 @@ class LocatedTargetsPicker(
}
}

// Get closest targets and remove duplicates
val geomTargets = lookupResult.targets.filter { it.tipLayoutHint.coord != null }
// Get the closest targets and remove duplicates

val minXDistanceToTarget = geomTargets
.map(::xDistanceToCoord)
Expand All @@ -168,13 +189,7 @@ class LocatedTargetsPicker(
.filter { target -> xDistanceToCoord(target) == minXDistanceToTarget }
.distinctBy(GeomTarget::hitIndex)

return LookupResult(
targets = newTargets,
distance = lookupResult.distance,
geomKind = lookupResult.geomKind,
contextualMapping = lookupResult.contextualMapping,
isCrosshairEnabled = lookupResult.isCrosshairEnabled
)
return lookupResult.withTargets(newTargets)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright (c) 2023. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package jetbrains.datalore.org.jetbrains.letsPlot.core.plot.builder.tooltip.loc

import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle
import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.core.plot.base.Aes
import org.jetbrains.letsPlot.core.plot.base.DataFrame
import org.jetbrains.letsPlot.core.plot.base.GeomKind
import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTarget
import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetLocator
import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetLocator.*
import org.jetbrains.letsPlot.core.plot.builder.tooltip.MappedDataAccessMock
import org.jetbrains.letsPlot.core.plot.builder.tooltip.TestUtil
import org.jetbrains.letsPlot.core.plot.builder.tooltip.TestUtil.createLocator
import org.jetbrains.letsPlot.core.plot.builder.tooltip.conf.GeomInteractionBuilder
import org.jetbrains.letsPlot.core.plot.builder.tooltip.loc.LocatedTargetsPicker
import org.jetbrains.letsPlot.core.plot.builder.tooltip.loc.TargetPrototype
import kotlin.test.Test
import kotlin.test.assertEquals

class LocatedTargetsPickerFilterTargetsTest {

@Test
fun `line plot - choose targets closest to cursor by x`() {
val pathKey1 = 1
val pathKey2 = 2
val pathKey3 = 3

val targetPrototypes = listOf(
TestUtil.pathTarget(listOf(DoubleVector(0.0, 0.0), DoubleVector(3.0, 0.0)), indexMapper = { pathKey1 }),
TestUtil.pathTarget(listOf(DoubleVector(1.0, 1.0), DoubleVector(3.0, 1.0)), indexMapper = { pathKey2 }),
TestUtil.pathTarget(listOf(DoubleVector(0.0, 2.0), DoubleVector(2.0, 2.0)), indexMapper = { pathKey3 }),
)
val locator = createLocator(GeomKind.LINE, targetPrototypes)

assertTargets(
findTargets(locator, cursor = DoubleVector(-0.5, 0.0))
// no targets
)
assertTargets(
findTargets(locator, cursor = DoubleVector(0.5, 0.0)),
pathKey1, pathKey3
)
assertTargets(
findTargets(locator, cursor = DoubleVector(1.5, 0.0)),
pathKey2
)
assertTargets(
findTargets(locator, cursor = DoubleVector(2.0, 0.0)),
pathKey3
)
assertTargets(
findTargets(locator, cursor = DoubleVector(2.5, 0.0)),
pathKey1, pathKey2
)
}

@Test
fun `bar plot - check restriction on visible tooltips`() {

val targetPrototypes = run {
val startTargetRect = DoubleRectangle(DoubleVector.ZERO, DoubleVector(1.0, 10.0))
(0..10)
.toList()
.map { startTargetRect.add(DoubleVector(0.0, it.toDouble())) }
.mapIndexed { index, rect -> TestUtil.rectTarget(index, rect) }
}

// restriction for bar tooltips = 5:
// - if more - choose the closest one
// - else - get all targets

run {
val locator = createLocator(GeomKind.BAR, targetPrototypes)
assertTargets(
findTargets(locator, cursor = DoubleVector(0.0, 0.0)),
0
)
assertTargets(
findTargets(locator, cursor = DoubleVector(0.0, 6.0)),
6
)
assertTargets(
findTargets(locator, cursor = DoubleVector(0.0, 10.0)),
10
)
}
run {
// targets no more than the restriction value => use all targets
val locator = createLocator(GeomKind.BAR, targetPrototypes.take(5))
assertTargets(
findTargets(locator, cursor = DoubleVector(0.0, 10.0)),
0, 1, 2, 3, 4
)
}
}

private fun createLocator(geomKind: GeomKind, targetPrototypes: List<TargetPrototype>): GeomTargetLocator {
val contextualMapping = GeomInteractionBuilder.DemoAndTest(supportedAes = Aes.values())
.xUnivariateFunction(LookupStrategy.HOVER)
.build()
.createContextualMapping(
MappedDataAccessMock().mappedDataAccess,
DataFrame.Builder().build()
)
return createLocator(
lookupSpec = LookupSpec(LookupSpace.X, LookupStrategy.HOVER),
contextualMapping = contextualMapping,
targetPrototypes = targetPrototypes,
geomKind = geomKind
)
}

private fun findTargets(
locator: GeomTargetLocator,
cursor: DoubleVector
): List<GeomTarget> {
return LocatedTargetsPicker(flippedAxis = false, cursor)
.apply { locator.search(cursor)?.let(::addLookupResult) }
.picked
.singleOrNull()
?.targets
?: emptyList()
}

private fun assertTargets(targets: List<GeomTarget>, vararg expected: Int) {
assertEquals(expected.size, targets.size)
val actual = targets.map(GeomTarget::hitIndex)
assertEquals(expected.toList(), actual)
}
}

0 comments on commit 1543a55

Please sign in to comment.