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

Exponent format #964

Merged
merged 4 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add option to switch exponent format between e-notation and superscri…
…pted power
  • Loading branch information
IKupriyanov-HORIS committed Dec 13, 2023
commit 1a3e1784bcea6755ab21cf3af6b93d96aa129d90
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import org.jetbrains.letsPlot.commons.intern.datetime.tz.TimeZone

class StringFormat private constructor(
private val pattern: String,
private val formatType: FormatType
private val formatType: FormatType,
superscriptExponent: Boolean?
) {
enum class FormatType {
NUMBER_FORMAT,
Expand All @@ -26,16 +27,16 @@ class StringFormat private constructor(

init {
myFormatters = when (formatType) {
NUMBER_FORMAT, DATETIME_FORMAT -> listOf(initFormatter(pattern, formatType))
NUMBER_FORMAT, DATETIME_FORMAT -> listOf(initFormatter(pattern, formatType, superscriptExponent))
STRING_FORMAT -> {
BRACES_REGEX.findAll(pattern)
.map { it.groupValues[TEXT_IN_BRACES] }
.map { format ->
val formatType = detectFormatType(format)
.map { pattern ->
val formatType = detectFormatType(pattern)
require(formatType == NUMBER_FORMAT || formatType == DATETIME_FORMAT) {
error("Can't detect type of pattern '$format' used in string pattern '$pattern'")
error("Can't detect type of pattern '$pattern' used in string pattern '${this.pattern}'")
}
initFormatter(format, formatType)
initFormatter(pattern, formatType, superscriptExponent)
}
.toList()
}
Expand Down Expand Up @@ -68,14 +69,22 @@ class StringFormat private constructor(
}
}

private fun initFormatter(formatPattern: String, formatType: FormatType): ((Any) -> String) {
private fun initFormatter(formatPattern: String, formatType: FormatType, superscriptExponent: Boolean?): ((Any) -> String) {
if (formatPattern.isEmpty()) {
return Any::toString
}
when (formatType) {
NUMBER_FORMAT -> {
val formatSpec = NumberFormat.parseSpec(formatPattern).copy(richOutput = true) // force use of superscript exponent
val numberFormatter = NumberFormat(formatSpec)
val formatSpec = NumberFormat.parseSpec(formatPattern)

// override richOutput if superscriptExponent is set
val spec = if (superscriptExponent != null) {
formatSpec.copy(richOutput = superscriptExponent)
} else {
formatSpec
}

val numberFormatter = NumberFormat(spec)
return { value: Any ->
when (value) {
is Number -> numberFormatter.apply(value)
Expand Down Expand Up @@ -118,17 +127,19 @@ class StringFormat private constructor(
fun forOneArg(
pattern: String,
type: FormatType? = null,
formatFor: String? = null
formatFor: String? = null,
superscriptExponent: Boolean = true,
): StringFormat {
return create(pattern, type, formatFor, expectedArgs = 1)
return create(pattern, type, formatFor, expectedArgs = 1, superscriptExponent)
}

fun forNArgs(
pattern: String,
argCount: Int,
formatFor: String? = null
formatFor: String? = null,
superscriptExponent: Boolean = true,
): StringFormat {
return create(pattern, STRING_FORMAT, formatFor, argCount)
return create(pattern, STRING_FORMAT, formatFor, argCount, superscriptExponent)
}

private fun detectFormatType(pattern: String): FormatType {
Expand All @@ -143,10 +154,11 @@ class StringFormat private constructor(
pattern: String,
type: FormatType? = null,
formatFor: String? = null,
expectedArgs: Int = -1
expectedArgs: Int = -1,
superscriptExponent: Boolean? = null
): StringFormat {
val formatType = type ?: detectFormatType(pattern)
return StringFormat(pattern, formatType).also {
return StringFormat(pattern, formatType, superscriptExponent = superscriptExponent).also {
if (expectedArgs > 0) {
require(it.argsNumber == expectedArgs) {
@Suppress("NAME_SHADOWING")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ open class ScatterDemo : SimpleDemoBase() {
)
}

private fun classicTheme() = ThemeUtil.buildTheme(ThemeOption.Name.R_CLASSIC)
private fun classicTheme() = ThemeUtil.buildTheme(ThemeOption.Name.R_CLASSIC,)

private fun gauss(): GroupComponent {
val count = 200
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.jetbrains.letsPlot.core.plot.base.aes.AestheticsDefaults
import org.jetbrains.letsPlot.core.plot.base.render.LegendKeyElementFactory

interface PlotContext {
val superscriptExponent: Boolean
val layers: List<Layer>

fun hasScale(aes: Aes<*>): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ interface Scale {

fun labelFormatter(v: (Any) -> String): Builder

fun superscriptExponent(v: Boolean): Builder

fun multiplicativeExpand(v: Double): Builder

fun additiveExpand(v: Double): Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal abstract class AbstractScale<DomainT> : Scale {
private val definedBreaks: List<DomainT>?
private val definedLabels: List<String>?
private val labelLengthLimit: Int
protected val superscriptExponent: Boolean

final override val name: String

Expand All @@ -27,6 +28,7 @@ internal abstract class AbstractScale<DomainT> : Scale {
labelLengthLimit = 0
definedLabels = null
labelFormatter = null
superscriptExponent = false
}

protected constructor(b: AbstractBuilder<DomainT>) {
Expand All @@ -35,6 +37,7 @@ internal abstract class AbstractScale<DomainT> : Scale {
definedLabels = b.myLabels
labelLengthLimit = b.myLabelLengthLimit
labelFormatter = b.myLabelFormatter
superscriptExponent = b.mySuperscriptExponent

multiplicativeExpand = b.myMultiplicativeExpand
additiveExpand = b.myAdditiveExpand
Expand Down Expand Up @@ -118,6 +121,7 @@ internal abstract class AbstractScale<DomainT> : Scale {
internal var myLabels: List<String>? = scale.definedLabels
internal var myLabelLengthLimit: Int = scale.labelLengthLimit
internal var myLabelFormatter: ((Any) -> String)? = scale.labelFormatter
internal var mySuperscriptExponent: Boolean = scale.superscriptExponent

internal var myMultiplicativeExpand: Double = scale.multiplicativeExpand
internal var myAdditiveExpand: Double = scale.additiveExpand
Expand Down Expand Up @@ -150,6 +154,11 @@ internal abstract class AbstractScale<DomainT> : Scale {
return this
}

override fun superscriptExponent(v: Boolean): Scale.Builder {
mySuperscriptExponent = v
return this
}

override fun multiplicativeExpand(v: Double): Scale.Builder {
myMultiplicativeExpand = v
return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal class ContinuousScale : AbstractScale<Double> {
return if (customBreaksGenerator != null) {
Transforms.BreaksGeneratorForTransformedDomain(continuousTransform, customBreaksGenerator)
} else {
createBreaksGeneratorForTransformedDomain(continuousTransform, labelFormatter)
createBreaksGeneratorForTransformedDomain(continuousTransform, labelFormatter, superscriptExponent)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.log10

class NumericBreakFormatter(value: Double, step: Double, allowMetricPrefix: Boolean) {
class NumericBreakFormatter(
value: Double,
step: Double,
allowMetricPrefix: Boolean,
superscriptExponent: Boolean
) {
private var formatter: NumberFormat

init {
Expand Down Expand Up @@ -65,7 +70,7 @@ class NumericBreakFormatter(value: Double, step: Double, allowMetricPrefix: Bool
delimiter = ","
}

val richOutput = if (type == "e") "&" else ""
val richOutput = if (type == "e" && superscriptExponent) "&" else ""
formatter = NumberFormat("$delimiter.${precision.toInt()}$type$richOutput")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import kotlin.math.abs
import kotlin.math.max

internal class LinearBreaksGen(
private val formatter: ((Any) -> String)? = null
private val formatter: ((Any) -> String)? = null,
private val superscriptExponent: Boolean,
) : BreaksGenerator {

override fun generateBreaks(domain: DoubleSpan, targetCount: Int): ScaleBreaks {
Expand All @@ -32,6 +33,28 @@ internal class LinearBreaksGen(
return createFormatter(generateBreakValues(domain, targetCount))
}

private fun createFormatter(breakValues: List<Double>): (Any) -> String {
val (referenceValue, step) = when {
breakValues.isEmpty() -> Pair(0.0, 0.5)
else -> {
val v = max(abs(breakValues.first()), abs(breakValues.last()))
val s = when {
breakValues.size == 1 -> v / 10
else -> abs(breakValues[1] - breakValues[0])
}
Pair(v, s)
}
}

val formatter = NumericBreakFormatter(
referenceValue,
step,
allowMetricPrefix = true,
superscriptExponent = superscriptExponent
)
return formatter::apply
}

companion object {
internal fun generateBreakValues(domain: DoubleSpan, targetCount: Int): List<Double> {
val helper = LinearBreaksHelper(
Expand All @@ -41,26 +64,5 @@ internal class LinearBreaksGen(
)
return helper.breaks
}

private fun createFormatter(breakValues: List<Double>): (Any) -> String {
val (referenceValue, step) = when {
breakValues.isEmpty() -> Pair(0.0, 0.5)
else -> {
val v = max(abs(breakValues.first()), abs(breakValues.last()))
val s = when {
breakValues.size == 1 -> v / 10
else -> abs(breakValues[1] - breakValues[0])
}
Pair(v, s)
}
}

val formatter = NumericBreakFormatter(
referenceValue,
step,
allowMetricPrefix = true
)
return formatter::apply
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import kotlin.math.*

internal class NonlinearBreaksGen(
private val transform: ContinuousTransform,
private val formatter: ((Any) -> String)? = null
private val formatter: ((Any) -> String)? = null,
private val superscriptExponent: Boolean,
) : BreaksGenerator {

override fun generateBreaks(domain: DoubleSpan, targetCount: Int): ScaleBreaks {
Expand All @@ -38,6 +39,41 @@ internal class NonlinearBreaksGen(
return createMultiFormatter(generateBreakValues(domain, targetCount, transform))
}

private fun createMultiFormatter(breakValues: List<Double>): (Any) -> String {
val breakFormatters = createFormatters(breakValues)
return MultiFormatter(breakValues, breakFormatters)::apply
}

private fun createFormatters(breakValues: List<Double>): List<(Any) -> String> {
if (breakValues.isEmpty()) return emptyList()
if (breakValues.size == 1) {
val domainValue = breakValues[0]
val step = domainValue / 10
return listOf(createFormatter(domainValue, step))
}

// format each tick with its own formatter
val formatters: List<(Any) -> String> = breakValues.mapIndexed { i, currValue ->
val step = abs(
when (i) {
0 -> currValue - breakValues[i + 1]
else -> currValue - breakValues[i - 1]
}
)
createFormatter(currValue, step)
}
return formatters
}

private fun createFormatter(domainValue: Double, step: Double): (Any) -> String {
return NumericBreakFormatter(
domainValue,
step,
true,
superscriptExponent = superscriptExponent
)::apply
}

companion object {
private const val MIN_BREAKS_COUNT = 3

Expand Down Expand Up @@ -69,41 +105,6 @@ internal class NonlinearBreaksGen(
else -> breaksCount
}
}

private fun createMultiFormatter(breakValues: List<Double>): (Any) -> String {
val breakFormatters = createFormatters(breakValues)
return MultiFormatter(breakValues, breakFormatters)::apply
}

private fun createFormatters(breakValues: List<Double>): List<(Any) -> String> {
if (breakValues.isEmpty()) return emptyList()
if (breakValues.size == 1) {
val domainValue = breakValues[0]
val step = domainValue / 10
return listOf(createFormatter(domainValue, step))
}

// format each tick with its own formatter
@Suppress("UnnecessaryVariable")
val formatters: List<(Any) -> String> = breakValues.mapIndexed { i, currValue ->
val step = abs(
when (i) {
0 -> currValue - breakValues[i + 1]
else -> currValue - breakValues[i - 1]
}
)
createFormatter(currValue, step)
}
return formatters
}

private fun createFormatter(domainValue: Double, step: Double): (Any) -> String {
return NumericBreakFormatter(
domainValue,
step,
true
)::apply
}
}

private class MultiFormatter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
package org.jetbrains.letsPlot.core.plot.base.scale.transform

import org.jetbrains.letsPlot.commons.interval.DoubleSpan
import org.jetbrains.letsPlot.core.commons.data.SeriesUtil.isBeyondPrecision
import org.jetbrains.letsPlot.core.plot.base.ContinuousTransform
import org.jetbrains.letsPlot.core.plot.base.scale.BreaksGenerator
import org.jetbrains.letsPlot.core.plot.base.scale.ScaleBreaks
import org.jetbrains.letsPlot.core.plot.base.scale.ScaleUtil
import org.jetbrains.letsPlot.core.commons.data.SeriesUtil.isBeyondPrecision

object Transforms {
val IDENTITY: ContinuousTransform = IdentityTransform()
Expand All @@ -26,15 +26,16 @@ object Transforms {

fun createBreaksGeneratorForTransformedDomain(
transform: ContinuousTransform,
labelFormatter: ((Any) -> String)? = null
labelFormatter: ((Any) -> String)? = null,
superscriptExponent: Boolean
): BreaksGenerator {
val breaksGenerator: BreaksGenerator = when (transform.unwrap()) {
IDENTITY -> LinearBreaksGen(labelFormatter)
REVERSE -> LinearBreaksGen(labelFormatter)
SQRT -> NonlinearBreaksGen(SQRT, labelFormatter)
LOG10 -> NonlinearBreaksGen(LOG10, labelFormatter)
LOG2 -> NonlinearBreaksGen(LOG2, labelFormatter)
SYMLOG -> NonlinearBreaksGen(SYMLOG, labelFormatter)
IDENTITY -> LinearBreaksGen(labelFormatter, superscriptExponent)
REVERSE -> LinearBreaksGen(labelFormatter, superscriptExponent)
SQRT -> NonlinearBreaksGen(SQRT, labelFormatter, superscriptExponent)
LOG10 -> NonlinearBreaksGen(LOG10, labelFormatter, superscriptExponent)
LOG2 -> NonlinearBreaksGen(LOG2, labelFormatter, superscriptExponent)
SYMLOG -> NonlinearBreaksGen(SYMLOG, labelFormatter, superscriptExponent)
else -> throw IllegalStateException("Unexpected 'transform' type: ${transform::class.simpleName}")
}

Expand Down
Loading