Skip to content

Commit

Permalink
Scale aware tooltips (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
IKupriyanov-HORIS authored and VDovidaytis-HORIS committed Dec 9, 2019
1 parent b4986f5 commit 378f20b
Show file tree
Hide file tree
Showing 15 changed files with 134 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ import jetbrains.datalore.base.gcommon.collect.ClosedRange

interface BreaksGenerator {
fun generateBreaks(domainAfterTransform: ClosedRange<Double>, targetCount: Int): ScaleBreaks
fun labelFormatter(domainAfterTransform: ClosedRange<Double>, targetCount: Int): (Any) -> String
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import jetbrains.datalore.base.gcommon.collect.ClosedRange
import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.plot.base.CoordinateSystem
import jetbrains.datalore.plot.base.Scale
import jetbrains.datalore.plot.base.scale.transform.LinearBreaksGen

object ScaleUtil {

Expand Down Expand Up @@ -166,4 +167,9 @@ object ScaleUtil {
}
return result
}

fun getBreaksGenerator(scale: Scale<*>) = when {
scale.hasBreaksGenerator() -> scale.breaksGenerator
else -> LinearBreaksGen()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ package jetbrains.datalore.plot.base.scale.breaks

import jetbrains.datalore.base.gcommon.base.Preconditions.checkArgument

abstract class BreaksHelperBase protected constructor(start: Double, end: Double, targetCount: Int) {
abstract class BreaksHelperBase protected constructor(
start: Double,
end: Double,
targetCount: Int
) {
abstract val breaks: List<Double>
abstract val labelFormatter: (Any) -> String

protected val normalStart: Double
protected val normalEnd: Double
protected val span: Double
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ class DateTimeBreaksHelper(
rangeStart: Double,
rangeEnd: Double,
count: Int,
minInterval: TimeInterval?) :
BreaksHelperBase(rangeStart, rangeEnd, count) {
minInterval: TimeInterval?
) : BreaksHelperBase(rangeStart, rangeEnd, count) {

var breaks: List<Double>
var labelFormatter: (Any) -> String
override val breaks: List<Double>
override val labelFormatter: (Any) -> String

constructor(rangeStart: Double, rangeEnd: Double, count: Int) : this(rangeStart, rangeEnd, count, null)

Expand All @@ -31,8 +31,7 @@ class DateTimeBreaksHelper(
minInterval
).getFormatter(step)
// compute step so that it is multiple of automatic time steps
val helper = LinearBreaksHelper(rangeStart, rangeEnd, count)
breaks = helper.breaks
breaks = LinearBreaksHelper(rangeStart, rangeEnd, count).breaks

} else {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ package jetbrains.datalore.plot.base.scale.breaks
import jetbrains.datalore.base.gcommon.collect.ClosedRange
import kotlin.math.*

class LinearBreaksHelper(rangeStart: Double, rangeEnd: Double, count: Int) : BreaksHelperBase(rangeStart, rangeEnd, count) {
val breaks: List<Double>
val labelFormatter: (Any) -> String
class LinearBreaksHelper(
rangeStart: Double,
rangeEnd: Double,
count: Int
) : BreaksHelperBase(rangeStart, rangeEnd, count) {
override val breaks: List<Double>
override val labelFormatter: (Any) -> String

init {

Expand All @@ -23,12 +27,10 @@ class LinearBreaksHelper(rangeStart: Double, rangeEnd: Double, count: Int) : Bre
val step10Power = floor(log10(step))
step = 10.0.pow(step10Power)
val error = step * count / span
if (error <= 0.15) {
step *= 10.0
} else if (error <= 0.35) {
step *= 5.0
} else if (error <= 0.75) {
step *= 2.0
when {
error <= 0.15 -> step *= 10.0
error <= 0.35 -> step *= 5.0
error <= 0.75 -> step *= 2.0
}

// extend range to allow for FP errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,25 @@ import kotlin.math.abs
import kotlin.math.log10
import kotlin.math.max

/*package*/ internal class LinearScaleTickFormatterFactory
/**
* @param useMetricPrefix see: https://en.wikipedia.org/wiki/Metric_prefix
*/
(private val useMetricPrefix: Boolean) : QuantitativeTickFormatterFactory() {
/*package*/ internal class LinearScaleTickFormatterFactory(
private val useMetricPrefix: Boolean
) : QuantitativeTickFormatterFactory() {

override fun getFormatter(range: ClosedRange<Double>, step: Double): (Any) -> String {
// avoid 0 values because log10(0) = - Infinity
var referenceValue = max(abs(range.lowerEndpoint()), range.upperEndpoint())
if (referenceValue == 0.0) {
referenceValue = 1.0
}

return { it -> NumericBreakFormatter(
val formatter = NumericBreakFormatter(
referenceValue,
step,
useMetricPrefix
).apply(it) }
)
return formatter::apply
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import jetbrains.datalore.plot.common.time.interval.TimeInterval
import jetbrains.datalore.plot.common.time.interval.YearInterval

internal class TimeScaleTickFormatterFactory(
private val myMinInterval: TimeInterval?) :
QuantitativeTickFormatterFactory() {
private val myMinInterval: TimeInterval?
) : QuantitativeTickFormatterFactory() {

override fun getFormatter(range: ClosedRange<Double>, step: Double): (Any) -> String {
return Formatter.time(formatPattern(step))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ import jetbrains.datalore.plot.base.scale.breaks.DateTimeBreaksHelper

class DateTimeBreaksGen : BreaksGenerator {
override fun generateBreaks(domainAfterTransform: ClosedRange<Double>, targetCount: Int): ScaleBreaks {
val helper = DateTimeBreaksHelper(
domainAfterTransform.lowerEndpoint(),
domainAfterTransform.upperEndpoint(),
targetCount
)
val helper = breaksHelper(domainAfterTransform, targetCount)
val ticks = helper.breaks
val labelFormatter = helper.labelFormatter
val labels = ArrayList<String>()
Expand All @@ -25,4 +21,19 @@ class DateTimeBreaksGen : BreaksGenerator {
}
return ScaleBreaks(ticks, ticks, labels)
}

private fun breaksHelper(
domainAfterTransform: ClosedRange<Double>,
targetCount: Int
): DateTimeBreaksHelper {
return DateTimeBreaksHelper(
domainAfterTransform.lowerEndpoint(),
domainAfterTransform.upperEndpoint(),
targetCount
)
}

override fun labelFormatter(domainAfterTransform: ClosedRange<Double>, targetCount: Int): (Any) -> String {
return breaksHelper(domainAfterTransform, targetCount).labelFormatter
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ open class FunTransform(
private val myInverse: (Double?) -> Double?) :
Transform, BreaksGenerator {


private val myLinearBreaksGen = LinearBreaksGen()

override fun labelFormatter(domainAfterTransform: ClosedRange<Double>, targetCount: Int): (Any) -> String {
val domainBeforeTransform = MapperUtil.map(domainAfterTransform) { myInverse(it) }
return myLinearBreaksGen.labelFormatter(domainBeforeTransform, targetCount)
}

override fun apply(rawData: List<*>): List<Double?> {
return rawData.map { myFun(it as Double) }
}
Expand All @@ -24,10 +32,10 @@ open class FunTransform(
return myInverse(v)
}


override fun generateBreaks(domainAfterTransform: ClosedRange<Double>, targetCount: Int): ScaleBreaks {
val domainBeforeTransform = MapperUtil.map(domainAfterTransform) { myInverse(it) }
val originalBreaks = LinearBreaksGen()
.generateBreaks(domainBeforeTransform, targetCount)
val originalBreaks = myLinearBreaksGen.generateBreaks(domainBeforeTransform, targetCount)
val domainValues = originalBreaks.domainValues
val transformValues = ArrayList<Double>()
for (domainValue in domainValues) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ internal class IdentityTransform @JvmOverloads constructor(
private val myBreaksGenerator: BreaksGenerator = LinearBreaksGen()
) :
Transform, BreaksGenerator {
override fun labelFormatter(domainAfterTransform: ClosedRange<Double>, targetCount: Int): (Any) -> String {
return myBreaksGenerator.labelFormatter(domainAfterTransform, targetCount)
}

override fun apply(rawData: List<*>): List<Double?> {
val checkedDoubles = SeriesUtil.checkedDoubles(rawData)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ import jetbrains.datalore.plot.base.scale.breaks.LinearBreaksHelper

class LinearBreaksGen : BreaksGenerator {
override fun generateBreaks(domainAfterTransform: ClosedRange<Double>, targetCount: Int): ScaleBreaks {
val helper = LinearBreaksHelper(
domainAfterTransform.lowerEndpoint(),
domainAfterTransform.upperEndpoint(),
targetCount
)
val helper = breaksHelper(domainAfterTransform, targetCount)
val ticks = helper.breaks
val labelFormatter = helper.labelFormatter
val labels = ArrayList<String>()
Expand All @@ -25,4 +21,20 @@ class LinearBreaksGen : BreaksGenerator {
}
return ScaleBreaks(ticks, ticks, labels)
}

private fun breaksHelper(
domainAfterTransform: ClosedRange<Double>,
targetCount: Int
): LinearBreaksHelper {
val helper = LinearBreaksHelper(
domainAfterTransform.lowerEndpoint(),
domainAfterTransform.upperEndpoint(),
targetCount
)
return helper
}

override fun labelFormatter(domainAfterTransform: ClosedRange<Double>, targetCount: Int): (Any) -> String {
return breaksHelper(domainAfterTransform, targetCount).labelFormatter
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ class ScaleProviderBuilder<T>(private val myAes: Aes<T>) {
{ v -> myMapperProvider!!.createDiscreteMapper(data, variable).apply(v) }
}

@Suppress("UNCHECKED_CAST")
val domainValues = DataFrameUtil.distinctValues(data, variable).filter { it != null } as List<Any>
val domainValues = DataFrameUtil.distinctValues(data, variable).filterNotNull()
scale = Scales.discreteDomain(
name,
domainValues,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,66 +10,59 @@ import jetbrains.datalore.plot.base.Aes
import jetbrains.datalore.plot.base.DataFrame
import jetbrains.datalore.plot.base.Scale
import jetbrains.datalore.plot.base.interact.MappedDataAccess
import jetbrains.datalore.plot.base.scale.breaks.QuantitativeTickFormatterFactory
import jetbrains.datalore.plot.base.scale.ScaleUtil.getBreaksGenerator
import jetbrains.datalore.plot.base.scale.ScaleUtil.labelByBreak
import jetbrains.datalore.plot.builder.VarBinding
import jetbrains.datalore.plot.common.data.SeriesUtil
import jetbrains.datalore.plot.common.data.SeriesUtil.ensureNotZeroRange

internal class PointDataAccess(private val data: DataFrame,
bindings: Map<Aes<*>, VarBinding>) :
MappedDataAccess {
internal class PointDataAccess(
private val data: DataFrame,
bindings: Map<Aes<*>, VarBinding>
) : MappedDataAccess {

override val mappedAes: Set<Aes<*>> = HashSet(bindings.keys)

private val myBindings: Map<Aes<*>, VarBinding> = bindings.toMap()
private val myFormatters = HashMap<Aes<*>, (Any?) -> String>()

private val myFormatters = HashMap<Aes<*>, (Any) -> String>()

override fun isMapped(aes: Aes<*>): Boolean {
return myBindings.containsKey(aes)
}
override fun isMapped(aes: Aes<*>) = myBindings.containsKey(aes)

override fun <T> getMappedData(aes: Aes<T>, index: Int): MappedDataAccess.MappedData<T> {
checkArgument(isMapped(aes), "Not mapped: $aes")

val value = valueAfterTransform(aes, index)!!
@Suppress("UNCHECKED_CAST")
val scale = myBindings[aes]!!.scale as Scale<T>

val original = scale.transform.applyInverse(value)
val s: String
s = if (original is Number) {
formatter(aes)(original)
} else {
original.toString()
}
val binding = myBindings.getValue(aes)
val scale = binding.scale!!

val continuous = scale.isContinuous
val originalValue = binding
.variable
.let { variable -> data.getNumeric(variable)[index] }
.let { value -> scale.transform.applyInverse(value) }

return MappedDataAccess.MappedData(label(aes), s, continuous)
return MappedDataAccess.MappedData(
label = scale.name,
value = formatter(aes).invoke(originalValue),
isContinuous = scale.isContinuous
)
}

private fun label(aes: Aes<*>): String {
return myBindings[aes]!!.scale!!.name
private fun <T> formatter(aes: Aes<T>): (Any?) -> String {
val scale = myBindings.getValue(aes).scale
return myFormatters.getOrPut(aes, defaultValue = { createFormatter(aes, scale!!) })
}

private fun valueAfterTransform(aes: Aes<*>, index: Int): Double? {
val variable = myBindings[aes]!!.variable
return data.getNumeric(variable)[index]
}

private fun formatter(aes: Aes<*>): (Any) -> String {
if (!myFormatters.containsKey(aes)) {
myFormatters[aes] = createFormatter(aes)
private fun createFormatter(aes: Aes<*>, scale: Scale<*>): (Any?) -> String {
if (scale.isContinuousDomain) {
// only 'stat' or 'transform' vars here
val domain = myBindings
.getValue(aes)
.variable
.run(data::range)
.run(::ensureNotZeroRange)

val formatter = getBreaksGenerator(scale).labelFormatter(domain, 100)
return { value -> value?.let { formatter.invoke(it) } ?: "NULL" }
} else {
val labelsMap = labelByBreak(scale)
return { value -> value?.let { labelsMap.getValue(it) } ?: "NULL" }
}
return myFormatters[aes]!!
}

private fun createFormatter(aes: Aes<*>): (Any) -> String {
val varBinding = myBindings[aes]
// only 'stat' or 'transform' vars here
val `var` = varBinding!!.variable
var domain = data.range(`var`)
domain = SeriesUtil.ensureNotZeroRange(domain)
return QuantitativeTickFormatterFactory.forLinearScale().getFormatter(domain, SeriesUtil.span(domain) / 100.0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,13 @@ package jetbrains.datalore.plot.builder.layout.axis

import jetbrains.datalore.base.gcommon.collect.ClosedRange
import jetbrains.datalore.plot.base.Scale
import jetbrains.datalore.plot.base.scale.BreaksGenerator
import jetbrains.datalore.plot.base.scale.ScaleUtil
import jetbrains.datalore.plot.base.scale.transform.LinearBreaksGen
import jetbrains.datalore.plot.base.scale.ScaleUtil.breaksTransformed
import jetbrains.datalore.plot.base.scale.ScaleUtil.getBreaksGenerator
import jetbrains.datalore.plot.base.scale.ScaleUtil.labels

object AxisBreaksUtil {
fun createAxisBreaksProvider(scale: Scale<Double>, axisDomain: ClosedRange<Double>): AxisBreaksProvider {
val breaksProvider: AxisBreaksProvider
if (scale.hasBreaks()) {
breaksProvider = FixedAxisBreaksProvider(scale.breaks, ScaleUtil.breaksTransformed(scale), ScaleUtil.labels(scale))
} else {
val breaksGen: BreaksGenerator
if (scale.hasBreaksGenerator()) {
breaksGen = scale.breaksGenerator
} else {
breaksGen = LinearBreaksGen()
}
breaksProvider = AdaptableAxisBreaksProvider(axisDomain, breaksGen)
}

return breaksProvider
fun createAxisBreaksProvider(scale: Scale<Double>, axisDomain: ClosedRange<Double>): AxisBreaksProvider = when {
scale.hasBreaks() -> FixedAxisBreaksProvider(scale.breaks, breaksTransformed(scale), labels(scale))
else -> AdaptableAxisBreaksProvider(axisDomain, getBreaksGenerator(scale))
}
}
Loading

0 comments on commit 378f20b

Please sign in to comment.