Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'mode' parameter to stackable position adjustments #698

Merged
merged 2 commits into from
Feb 22, 2023
Merged
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
2 changes: 1 addition & 1 deletion future_changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@

- Parameter `flat=True` turns off lines re-projection, keeping the original number of points.

- Position adjustments `'stack'` and `'fill'` shifts objects only between different groups.
- Parameter `mode` added to position adjustments `'stack'` and `'fill'`. When `mode='groups'` (default) the position adjustment shifts objects only if their groups are distinct.

See: [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-23a/position_stack.ipynb).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import jetbrains.datalore.plot.base.Aesthetics
import jetbrains.datalore.plot.base.DataPointAesthetics
import jetbrains.datalore.plot.base.GeomContext

internal class FillPos(aes: Aesthetics, vjust: Double?) : StackablePos() {
internal class FillPos(aes: Aesthetics, vjust: Double?, stackingMode: StackingMode) : StackablePos() {

private val myOffsetByIndex: Map<Int, StackOffset> = mapIndexToOffset(aes, vjust ?: 1.0)
private val myOffsetByIndex: Map<Int, StackOffset> = mapIndexToOffset(aes, vjust ?: DEF_VJUST, stackingMode)

override fun translate(v: DoubleVector, p: DataPointAesthetics, ctx: GeomContext): DoubleVector {
val scale = 1.0 / myOffsetByIndex.getValue(p.index()).max
Expand All @@ -22,4 +22,8 @@ internal class FillPos(aes: Aesthetics, vjust: Double?) : StackablePos() {
override fun handlesGroups(): Boolean {
return PositionAdjustments.Meta.FILL.handlesGroups()
}

companion object {
private const val DEF_VJUST = 1.0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ object PositionAdjustments {
return DodgePos(aesthetics, groupCount, width)
}

fun stack(aes: Aesthetics, vjust: Double?): PositionAdjustment {
return StackPos(aes, vjust)
fun stack(aes: Aesthetics, vjust: Double?, stackingMode: StackingMode): PositionAdjustment {
return StackPos(aes, vjust, stackingMode)
}

fun fill(aesthetics: Aesthetics, vjust: Double?): PositionAdjustment {
return FillPos(aesthetics, vjust)
fun fill(aesthetics: Aesthetics, vjust: Double?, stackingMode: StackingMode): PositionAdjustment {
return FillPos(aesthetics, vjust, stackingMode)
}

fun jitter(width: Double?, height: Double?): PositionAdjustment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import jetbrains.datalore.plot.base.Aesthetics
import jetbrains.datalore.plot.base.DataPointAesthetics
import jetbrains.datalore.plot.base.GeomContext

internal class StackPos(aes: Aesthetics, vjust: Double?) : StackablePos() {
internal class StackPos(aes: Aesthetics, vjust: Double?, stackingMode: StackingMode) : StackablePos() {

private val myOffsetByIndex: Map<Int, StackOffset> = mapIndexToOffset(aes, vjust ?: DEF_VJUST)
private val myOffsetByIndex: Map<Int, StackOffset> = mapIndexToOffset(aes, vjust ?: DEF_VJUST, stackingMode)

override fun translate(v: DoubleVector, p: DataPointAesthetics, ctx: GeomContext): DoubleVector {
return v.add(DoubleVector(0.0, myOffsetByIndex.getValue(p.index()).value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,34 @@

package jetbrains.datalore.plot.base.pos

import jetbrains.datalore.base.enums.EnumInfoFactory
import jetbrains.datalore.plot.base.Aesthetics
import jetbrains.datalore.plot.base.PositionAdjustment
import jetbrains.datalore.plot.common.data.SeriesUtil
import kotlin.math.*

enum class StackingMode {
GROUPS, ALL;

companion object {

private val ENUM_INFO = EnumInfoFactory.createEnumInfo<StackingMode>()

fun safeValueOf(v: String): StackingMode {
return ENUM_INFO.safeValueOf(v) ?: throw IllegalArgumentException(
"Unsupported stacking mode: '$v'\n" +
"Use one of: groups, all."
)
}
}
}

abstract class StackablePos : PositionAdjustment {
internal fun mapIndexToOffset(aes: Aesthetics, vjust: Double): Map<Int, StackOffset> {
val stackingContext = StackingContext()
internal fun mapIndexToOffset(aes: Aesthetics, vjust: Double, stackingMode: StackingMode): Map<Int, StackOffset> {
val stackingContext = when (stackingMode) {
StackingMode.GROUPS -> StackingContext(false)
StackingMode.ALL -> StackingContext(true)
}
val offsetByIndex = HashMap<Int, StackOffset>()
val indexedDataPoints = aes.dataPoints().asSequence()
.mapIndexed { i, p -> Pair(i, p) }
Expand Down Expand Up @@ -41,7 +61,7 @@ abstract class StackablePos : PositionAdjustment {

private data class GroupOffset(val value: Double, val stack: Double)

private class StackingContext(private val stackInsideGroups: Boolean = false) {
private class StackingContext(private val stackInsideGroups: Boolean) {
private val positiveOffset = HashMap<Double, GroupOffset>()
private val negativeOffset = HashMap<Double, GroupOffset>()

Expand All @@ -56,7 +76,8 @@ abstract class StackablePos : PositionAdjustment {
fun getTotalOffset(stackId: Double, offsetValue: Double): Double {
return if (offsetValue >= 0) {
val currentOffset = positiveOffset.getOrPut(stackId) { GroupOffset(0.0, 0.0) }
positiveOffset[stackId] = GroupOffset(getGroupOffset(currentOffset.value, offsetValue), currentOffset.stack)
positiveOffset[stackId] =
GroupOffset(getGroupOffset(currentOffset.value, offsetValue), currentOffset.stack)
getCurrentTotalOffset(currentOffset.stack, currentOffset.value)
} else {
val currentOffset = negativeOffset.getOrPut(stackId) { GroupOffset(0.0, 0.0) }
Expand Down Expand Up @@ -89,4 +110,8 @@ abstract class StackablePos : PositionAdjustment {
stackOffset
}
}

companion object {
val DEF_STACKING_MODE = StackingMode.GROUPS
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,47 @@ class FillPosTest : PosTest() {
}

@Test
fun testWithoutGrouping() {
fun testWithoutGroupingWithStackingModeAll() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(6.0, 4.0, 2.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
expectedYOffsets = listOf(0.5, 5.0/6.0, 1.0, 0.5, -0.5, -2.0/3.0, -1.0, 2.0/3.0, 1.0, 0.5, 5.0/6.0, 1.0),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.ALL),
messageBeginning = "Should work without grouping"
)
}

@Test
fun testWithoutGroupingWithStackingModeGroups() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(6.0, 4.0, 2.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
expectedYOffsets = listOf(1.0, 2.0/3.0, 1.0/3.0, 1.0, -1.0, -1.0/3.0, -2.0/3.0, 1.0/3.0, 2.0/3.0, 1.0, 2.0/3.0, 1.0/3.0),
posConstructor = getPositionAdjustmentConstructor(),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.GROUPS),
messageBeginning = "Should work without grouping"
)
}

@Test
fun testWithGrouping() {
fun testWithGroupingAndStackingModeAll() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(6.0, 4.0, 2.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 0, 0, 1, 2, 1, 1, 0, 0),
expectedYOffsets = listOf(0.5, 5.0/6.0, 1.0, 0.5, -0.5, -2.0/3.0, -1.0, 1.0, 5.0/6.0, 1.0, 1.0/3.0, 0.5),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.ALL),
messageBeginning = "Should work with grouping"
)
}

@Test
fun testWithGroupingAndStackingModeGroups() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(6.0, 4.0, 2.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 0, 0, 1, 2, 1, 1, 0, 0),
expectedYOffsets = listOf(0.5, 5.0/6.0, 1.0, 0.5, -0.6, -0.2, -1.0, 1.0, 5.0/6.0, 1.0, 0.4, 0.2),
posConstructor = getPositionAdjustmentConstructor(),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.GROUPS),
messageBeginning = "Should work with grouping"
)
}
Expand All @@ -49,9 +72,8 @@ class FillPosTest : PosTest() {
compareWithExpectedOffsets(
xValues = listOf(null, 0.0, 0.0, null, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(null, 4.0, 2.0, 3.0, 1.0, 2.0, null, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 1, 2, 0, 1, 2),
expectedYOffsets = listOf(null, 2.0/3.0, 1.0, null, 1.0/3.0, 1.0, null, 2.0/3.0, 1.0),
posConstructor = getPositionAdjustmentConstructor(),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.ALL),
messageBeginning = "Should work with NaN values in data"
)
}
Expand All @@ -61,26 +83,25 @@ class FillPosTest : PosTest() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(6.0, 4.0, 2.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 0, 1, 2, 1, 2, 0, 1, 2),
expectedYOffsets = listOf(0.5 * 0.5, 0.5 + 1.0/3.0 * 0.5, 5.0/6.0 + 1.0/6.0 * 0.5,
0.5 * 0.5, -0.5 * 0.5, -0.5 - 1.0/6.0 * 0.5, -2.0/3.0 - 1.0/3.0 * 0.5, 0.5 + 1.0/6.0 * 0.5, 2.0/3.0 + 1.0/3.0 * 0.5,
0.5 * 0.5, 0.5 + 1.0/3.0 * 0.5, 5.0/6.0 + 1.0/6.0 * 0.5),
posConstructor = getPositionAdjustmentConstructor(vjust = 0.5),
posConstructor = getPositionAdjustmentConstructor(vjust = 0.5, stackingMode = StackingMode.ALL),
messageBeginning = "Should work with vjust = 0.5"
)
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(6.0, 4.0, 2.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 0, 1, 2, 1, 2, 0, 1, 2),
expectedYOffsets = listOf(0.0, 0.5, 5.0/6.0, 0.0, 0.0, -0.5, -2.0/3.0, 0.5, 2.0/3.0, 0.0, 0.5, 5.0/6.0),
posConstructor = getPositionAdjustmentConstructor(vjust = 0.0),
posConstructor = getPositionAdjustmentConstructor(vjust = 0.0, stackingMode = StackingMode.ALL),
messageBeginning = "Should work with vjust = 0.0"
)
}

private fun getPositionAdjustmentConstructor(
vjust: Double? = null
vjust: Double? = null,
stackingMode: StackingMode = StackablePos.DEF_STACKING_MODE
): (Aesthetics) -> PositionAdjustment {
return { aes -> FillPos(aes, vjust = vjust) }
return { aes -> FillPos(aes, vjust = vjust, stackingMode = stackingMode) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,47 @@ class StackPosTest : PosTest() {
}

@Test
fun testWithoutGrouping() {
fun testWithoutGroupingWithStackingModeAll() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(3.0, 2.0, 1.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
expectedYOffsets = listOf(3.0, 5.0, 6.0, 3.0, -3.0, -4.0, -6.0, 4.0, 6.0, 3.0, 5.0, 6.0),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.ALL),
messageBeginning = "Should work without grouping"
)
}

@Test
fun testWithoutGroupingWithStackingModeGroups() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(3.0, 2.0, 1.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
expectedYOffsets = listOf(3.0, 2.0, 1.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
posConstructor = getPositionAdjustmentConstructor(),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.GROUPS),
messageBeginning = "Should work without grouping"
)
}

@Test
fun testWithGrouping() {
fun testWithGroupingAndStackingModeAll() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(3.0, 2.0, 1.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 0, 0, 1, 2, 1, 1, 0, 0),
expectedYOffsets = listOf(3.0, 5.0, 6.0, 3.0, -3.0, -4.0, -6.0, 6.0, 5.0, 6.0, 2.0, 3.0),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.ALL),
messageBeginning = "Should work with grouping"
)
}

@Test
fun testWithGroupingAndStackingModeGroups() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(3.0, 2.0, 1.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 0, 0, 1, 2, 1, 1, 0, 0),
expectedYOffsets = listOf(3.0, 5.0, 6.0, 3.0, -3.0, -1.0, -5.0, 6.0, 5.0, 5.0, 2.0, 1.0),
posConstructor = getPositionAdjustmentConstructor(),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.GROUPS),
messageBeginning = "Should work with grouping"
)
}
Expand All @@ -49,9 +72,8 @@ class StackPosTest : PosTest() {
compareWithExpectedOffsets(
xValues = listOf(null, 0.0, 0.0, null, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(null, 2.0, 1.0, 3.0, 1.0, 2.0, null, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 1, 2, 0, 1, 2),
expectedYOffsets = listOf(null, 2.0, 3.0, null, 1.0, 3.0, null, 2.0, 3.0),
posConstructor = getPositionAdjustmentConstructor(),
posConstructor = getPositionAdjustmentConstructor(stackingMode = StackingMode.ALL),
messageBeginning = "Should work with NaN values in data"
)
}
Expand All @@ -61,26 +83,25 @@ class StackPosTest : PosTest() {
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(3.0, 2.0, 1.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 0, 1, 2, 1, 2, 0, 1, 2),
expectedYOffsets = listOf(3.0 * 0.5, 3.0 + 2.0 * 0.5, 5.0 + 1.0 * 0.5,
3.0 * 0.5, -3.0 * 0.5, -3.0 - 1.0 * 0.5, -4.0 - 2.0 * 0.5, 3.0 + 1.0 * 0.5, 4.0 + 2.0 * 0.5,
3.0 * 0.5, 3.0 + 2.0 * 0.5, 5.0 + 1.0 * 0.5),
posConstructor = getPositionAdjustmentConstructor(vjust = 0.5),
posConstructor = getPositionAdjustmentConstructor(vjust = 0.5, stackingMode = StackingMode.ALL),
messageBeginning = "Should work with vjust = 0.5"
)
compareWithExpectedOffsets(
xValues = listOf(0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0),
yValues = listOf(3.0, 2.0, 1.0, 3.0, -3.0, -1.0, -2.0, 1.0, 2.0, 3.0, 2.0, 1.0),
groupValues = listOf(0, 1, 2, 0, 0, 1, 2, 1, 2, 0, 1, 2),
expectedYOffsets = listOf(0.0, 3.0, 5.0, 0.0, 0.0, -3.0, -4.0, 3.0, 4.0, 0.0, 3.0, 5.0),
posConstructor = getPositionAdjustmentConstructor(vjust = 0.0),
posConstructor = getPositionAdjustmentConstructor(vjust = 0.0, stackingMode = StackingMode.ALL),
messageBeginning = "Should work with vjust = 0.0"
)
}

private fun getPositionAdjustmentConstructor(
vjust: Double? = null
vjust: Double? = null,
stackingMode: StackingMode = StackablePos.DEF_STACKING_MODE
): (Aesthetics) -> PositionAdjustment {
return { aes -> StackPos(aes, vjust = vjust) }
return { aes -> StackPos(aes, vjust = vjust, stackingMode = stackingMode) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package jetbrains.datalore.plot.builder.assemble

import jetbrains.datalore.plot.base.PositionAdjustment
import jetbrains.datalore.plot.base.pos.PositionAdjustments
import jetbrains.datalore.plot.base.pos.StackablePos
import jetbrains.datalore.plot.base.pos.StackingMode
import jetbrains.datalore.plot.builder.PosProviderContext
import kotlin.jvm.JvmOverloads

Expand All @@ -30,10 +32,10 @@ abstract class PosProvider {
}
}

fun barStack(vjust: Double? = null): PosProvider {
fun barStack(vjust: Double? = null, stackingMode: StackingMode = StackablePos.DEF_STACKING_MODE): PosProvider {
return object : PosProvider() {
override fun createPos(ctx: PosProviderContext): PositionAdjustment {
return PositionAdjustments.stack(ctx.aesthetics, vjust)
return PositionAdjustments.stack(ctx.aesthetics, vjust, stackingMode)
}

override fun handlesGroups(): Boolean {
Expand All @@ -57,10 +59,10 @@ abstract class PosProvider {
}
}

fun fill(vjust: Double? = null): PosProvider {
fun fill(vjust: Double? = null, stackingMode: StackingMode = StackablePos.DEF_STACKING_MODE): PosProvider {
return object : PosProvider() {
override fun createPos(ctx: PosProviderContext): PositionAdjustment {
return PositionAdjustments.fill(ctx.aesthetics, vjust)
return PositionAdjustments.fill(ctx.aesthetics, vjust, stackingMode)
}

override fun handlesGroups(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,10 +448,12 @@ object Option {

object Stack {
const val VJUST = "vjust"
const val MODE = "mode"
}

object Fill {
const val VJUST = "vjust"
const val MODE = "mode"
}
}

Expand Down
Loading