Skip to content

Commit 4f8d983

Browse files
committed
migrated the format operation to use ColumnWithPath, instead of just columns by name, so column groups can now also be targeted and used. I turned the gathered attributes into a map, such that duplicate keys are removed. Only the latest ones stay, meaning you can overwrite previous attributes for a cell.
1 parent 0875efd commit 4f8d983

File tree

4 files changed

+69
-42
lines changed

4 files changed

+69
-42
lines changed

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.jetbrains.kotlinx.dataframe.api
22

33
import org.jetbrains.kotlinx.dataframe.ColumnsSelector
4-
import org.jetbrains.kotlinx.dataframe.DataColumn
54
import org.jetbrains.kotlinx.dataframe.DataFrame
65
import org.jetbrains.kotlinx.dataframe.DataRow
76
import org.jetbrains.kotlinx.dataframe.RowColumnExpression
@@ -16,6 +15,7 @@ import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linearBg
1615
import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb
1716
import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor
1817
import org.jetbrains.kotlinx.dataframe.columns.ColumnReference
18+
import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath
1919
import org.jetbrains.kotlinx.dataframe.columns.toColumnSet
2020
import org.jetbrains.kotlinx.dataframe.dataTypes.IFRAME
2121
import org.jetbrains.kotlinx.dataframe.dataTypes.IMG
@@ -66,6 +66,9 @@ import kotlin.reflect.KProperty
6666
*
6767
* You can continue formatting the [FormattedFrame] by calling [format][FormattedFrame.format] on it again.
6868
*
69+
* Formatting is done additively, meaning you can add more formatting to a cell that's already formatted or
70+
* override certain attributes inherited from its outer group.
71+
*
6972
* Check out the [Grammar].
7073
*
7174
* For more information: {@include [DocumentationUrls.Format]}
@@ -169,7 +172,7 @@ internal interface FormatDocs {
169172
interface CellFormatterDef
170173

171174
/**
172-
* `rowColFormatter: `{@include [FormattingDslGrammarRef]}`.(row: `[DataRow][DataRow]`<T>, col: `[DataColumn][DataColumn]`<C>) -> `[CellAttributes][CellAttributes]`?`
175+
* `rowColFormatter: `{@include [FormattingDslGrammarRef]}`.(row: `[DataRow][DataRow]`<T>, col: `[ColumnWithPath][ColumnWithPath]`<C>) -> `[CellAttributes][CellAttributes]`?`
173176
*/
174177
interface RowColFormatterDef
175178

@@ -348,7 +351,7 @@ public fun <T> FormattedFrame<T>.format(vararg columns: String): FormatClause<T,
348351
* .toStandaloneHtml().openInBrowser()
349352
* ```
350353
*/
351-
public fun <T> FormattedFrame<T>.format(): FormatClause<T, Any?> = FormatClause(df, null, formatter)
354+
public fun <T> FormattedFrame<T>.format(): FormatClause<T, Any?> = FormatClause(df = df, oldFormatter = formatter)
352355

353356
// endregion
354357

@@ -470,7 +473,7 @@ public fun <T, C> FormatClause<T, C>.perRowCol(formatter: RowColFormatter<T, C>)
470473
*/
471474
@Suppress("UNCHECKED_CAST")
472475
public fun <T, C> FormatClause<T, C>.with(formatter: CellFormatter<C>): FormattedFrame<T> =
473-
formatImpl { row, col -> formatter(row[col.name] as C) }
476+
formatImpl { row, col -> formatter(col[row] as C) }
474477

475478
/**
476479
* Creates a new [FormattedFrame] that uses the specified [CellFormatter] to format selected non-null cells of the dataframe.
@@ -734,14 +737,14 @@ public object FormattingDsl {
734737

735738
/**
736739
* A lambda function expecting a [CellAttributes] or `null` given an instance of
737-
* [DataRow][DataRow]`<`[T][T]`>` and [DataColumn][DataColumn]`<`[C][C]`>`.
740+
* [DataRow][DataRow]`<`[T][T]`>` and [ColumnWithPath][ColumnWithPath]`<`[C][C]`>`.
738741
*
739742
* This is similar to a [RowColumnExpression], except that you also have access
740743
* to the [FormattingDsl] in the context.
741744
*
742745
* @include [FormattingDsl]
743746
*/
744-
public typealias RowColFormatter<T, C> = FormattingDsl.(row: DataRow<T>, col: DataColumn<C>) -> CellAttributes?
747+
public typealias RowColFormatter<T, C> = FormattingDsl.(row: DataRow<T>, col: ColumnWithPath<C>) -> CellAttributes?
745748

746749
/**
747750
* A lambda function expecting a [CellAttributes] or `null` given an instance of a cell: [C] of the dataframe.
@@ -838,7 +841,7 @@ public class FormattedFrame<T>(internal val df: DataFrame<T>, internal val forma
838841
*/
839842
public class FormatClause<T, C>(
840843
internal val df: DataFrame<T>,
841-
internal val columns: ColumnsSelector<T, C>? = null,
844+
internal val columns: ColumnsSelector<T, C> = { all().cast() },
842845
internal val oldFormatter: RowColFormatter<T, C>? = null,
843846
internal val filter: RowValueFilter<T, C> = { true },
844847
) {

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import org.jetbrains.kotlinx.dataframe.api.RgbColor
88
import org.jetbrains.kotlinx.dataframe.api.RowColFormatter
99
import org.jetbrains.kotlinx.dataframe.api.and
1010
import org.jetbrains.kotlinx.dataframe.api.cast
11-
import org.jetbrains.kotlinx.dataframe.api.getColumnsWithPaths
12-
import org.jetbrains.kotlinx.dataframe.api.name
13-
import org.jetbrains.kotlinx.dataframe.columns.depth
11+
import org.jetbrains.kotlinx.dataframe.api.getColumnPaths
12+
import org.jetbrains.kotlinx.dataframe.columns.UnresolvedColumnsPolicy
13+
import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths
14+
import org.jetbrains.kotlinx.dataframe.path
1415

1516
internal class SingleAttribute(val key: String, val value: String) : CellAttributes {
1617
override fun attributes() = listOf(key to value)
@@ -53,25 +54,17 @@ internal inline fun <T, C> FormatClause<T, C>.formatImpl(
5354
crossinline formatter: RowColFormatter<T, C>,
5455
): FormattedFrame<T> {
5556
val clause = this
56-
val columns =
57-
if (clause.columns != null) {
58-
clause.df.getColumnsWithPaths(clause.columns)
59-
.mapNotNull { if (it.depth == 0) it.name else null } // TODO Causes #1356
60-
.toSet()
61-
} else {
62-
null
63-
}
57+
val columns = clause.df.getColumnPaths(UnresolvedColumnsPolicy.Skip, clause.columns).toSet()
58+
6459
return FormattedFrame(clause.df) { row, col ->
6560
val oldAttributes = clause.oldFormatter?.invoke(FormattingDsl, row, col.cast())
66-
if (columns == null || columns.contains(col.name())) {
67-
val value = row[col.name] as C
61+
if (col.path in columns) {
62+
val value = col[row] as C
6863
if (clause.filter(row, value)) {
69-
oldAttributes and formatter(FormattingDsl, row.cast(), col.cast())
70-
} else {
71-
oldAttributes
64+
return@FormattedFrame oldAttributes and formatter(FormattingDsl, row.cast(), col.cast())
7265
}
73-
} else {
74-
oldAttributes
7566
}
67+
68+
oldAttributes
7669
}
7770
}

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import org.jetbrains.kotlinx.dataframe.AnyCol
55
import org.jetbrains.kotlinx.dataframe.AnyFrame
66
import org.jetbrains.kotlinx.dataframe.AnyRow
77
import org.jetbrains.kotlinx.dataframe.DataFrame
8+
import org.jetbrains.kotlinx.dataframe.api.CellAttributes
89
import org.jetbrains.kotlinx.dataframe.api.FormattedFrame
910
import org.jetbrains.kotlinx.dataframe.api.FormattingDsl
1011
import org.jetbrains.kotlinx.dataframe.api.RowColFormatter
12+
import org.jetbrains.kotlinx.dataframe.api.and
1113
import org.jetbrains.kotlinx.dataframe.api.asColumnGroup
1214
import org.jetbrains.kotlinx.dataframe.api.asNumbers
1315
import org.jetbrains.kotlinx.dataframe.api.format
@@ -24,6 +26,7 @@ import org.jetbrains.kotlinx.dataframe.columns.FrameColumn
2426
import org.jetbrains.kotlinx.dataframe.dataTypes.IFRAME
2527
import org.jetbrains.kotlinx.dataframe.dataTypes.IMG
2628
import org.jetbrains.kotlinx.dataframe.impl.DataFrameSize
29+
import org.jetbrains.kotlinx.dataframe.impl.columns.addParentPath
2730
import org.jetbrains.kotlinx.dataframe.impl.columns.addPath
2831
import org.jetbrains.kotlinx.dataframe.impl.io.resizeKeepingAspectRatio
2932
import org.jetbrains.kotlinx.dataframe.impl.renderType
@@ -161,7 +164,11 @@ internal fun AnyFrame.toHtmlData(
161164
val scripts = mutableListOf<String>()
162165
val queue = LinkedList<RenderingQueueItem>()
163166

164-
fun AnyFrame.columnToJs(col: AnyCol, rowsLimit: Int?, configuration: DisplayConfiguration): ColumnDataForJs {
167+
fun AnyFrame.columnToJs(
168+
col: ColumnWithPath<*>,
169+
rowsLimit: Int?,
170+
configuration: DisplayConfiguration,
171+
): ColumnDataForJs {
165172
val values = if (rowsLimit != null) rows().take(rowsLimit) else rows()
166173
val scale = if (col.isNumber()) col.asNumbers().scale() else 1
167174
val format = if (scale > 0) {
@@ -170,23 +177,40 @@ internal fun AnyFrame.toHtmlData(
170177
RendererDecimalFormat.of("%e")
171178
}
172179
val renderConfig = configuration.copy(decimalFormat = format)
173-
val contents = values.map {
174-
val value = col[it]
175-
val content = value.toDataFrameLikeOrNull()
176-
if (content != null) {
177-
val df = content.df()
180+
val contents = values.map { row ->
181+
val value = col[row]
182+
val dfLikeContent = value.toDataFrameLikeOrNull()
183+
if (dfLikeContent != null) {
184+
val df = dfLikeContent.df()
178185
if (df.isEmpty()) {
179186
HtmlContent("", null)
180187
} else {
181188
val id = nextTableId()
182-
queue += RenderingQueueItem(df, id, content.configuration(defaultConfiguration))
189+
queue += RenderingQueueItem(df, id, dfLikeContent.configuration(defaultConfiguration))
183190
DataFrameReference(id, df.size)
184191
}
185192
} else {
186-
val html =
187-
formatter.format(downsizeBufferedImageIfNeeded(value, renderConfig), cellRenderer, renderConfig)
188-
val style = renderConfig.cellFormatter
189-
?.invoke(FormattingDsl, it, col)
193+
val html = formatter.format(
194+
value = downsizeBufferedImageIfNeeded(value, renderConfig),
195+
renderer = cellRenderer,
196+
configuration = renderConfig,
197+
)
198+
199+
val formatter = renderConfig.cellFormatter
200+
?: return@map HtmlContent(html, null)
201+
202+
// ask formatter for all attributes defined for this cell or any of its parents (outer column groups)
203+
val parentCols = col.path.indices
204+
.map { i -> col.path.take(i + 1) }
205+
.dropLast(1)
206+
.map { ColumnWithPath(this@toHtmlData[it], it) }
207+
val parentAttributes = parentCols
208+
.map { formatter(FormattingDsl, row, it) }
209+
.reduceOrNull(CellAttributes?::and)
210+
211+
val cellAttributes = formatter(FormattingDsl, row, col)
212+
213+
val style = (parentAttributes and cellAttributes)
190214
?.attributes()
191215
?.ifEmpty { null }
192216
?.flatMap {
@@ -204,12 +228,16 @@ internal fun AnyFrame.toHtmlData(
204228
listOf(it)
205229
}
206230
}
207-
?.joinToString(";") { "${it.first}:${it.second}" }
231+
?.toMap() // removing duplicate keys, allowing only the final one to be applied
232+
?.entries
233+
?.joinToString(";") { "${it.key}:${it.value}" }
208234
HtmlContent(html, style)
209235
}
210236
}
211237
val nested = if (col is ColumnGroup<*>) {
212-
col.columns().map { col.columnToJs(it, rowsLimit, configuration) }
238+
col.columns().map {
239+
col.columnToJs(it.addParentPath(col.path), rowsLimit, configuration)
240+
}
213241
} else {
214242
emptyList()
215243
}
@@ -226,7 +254,9 @@ internal fun AnyFrame.toHtmlData(
226254
while (!queue.isEmpty()) {
227255
val (nextDf, nextId, configuration) = queue.pop()
228256
val rowsLimit = if (nextId == rootId) configuration.rowsLimit else configuration.nestedRowsLimit
229-
val preparedColumns = nextDf.columns().map { nextDf.columnToJs(it, rowsLimit, configuration) }
257+
val preparedColumns = nextDf.columns().map {
258+
nextDf.columnToJs(it.addPath(), rowsLimit, configuration)
259+
}
230260
val js = tableJs(preparedColumns, nextId, rootId, nextDf.nrow)
231261
scripts.add(js)
232262
}

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/FormattingTests.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import org.jetbrains.kotlinx.dataframe.api.where
1414
import org.jetbrains.kotlinx.dataframe.api.with
1515
import org.jetbrains.kotlinx.dataframe.impl.api.encode
1616
import org.jetbrains.kotlinx.dataframe.impl.api.linearGradient
17+
import org.jetbrains.kotlinx.dataframe.impl.columns.addPath
1718
import org.jetbrains.kotlinx.dataframe.index
1819
import org.jetbrains.kotlinx.dataframe.io.DisplayConfiguration
1920
import org.jetbrains.kotlinx.dataframe.nrow
@@ -33,7 +34,7 @@ class FormattingTests : BaseTest() {
3334

3435
val formatter = formattedFrame.formatter!!
3536
for (row in 0 until typed.nrow) {
36-
FormattingDsl.formatter(typed[row], typed.age)!!.attributes().size shouldBe
37+
FormattingDsl.formatter(typed[row], typed.age.addPath())!!.attributes().size shouldBe
3738
if (typed[row].age > 10) 3 else 2
3839
}
3940

@@ -48,12 +49,12 @@ class FormattingTests : BaseTest() {
4849
.formatter!!
4950

5051
for (row in 0 until typed.nrow step 2) {
51-
FormattingDsl.formatter(typed[row], typed.age)!!.attributes() shouldBe
52+
FormattingDsl.formatter(typed[row], typed.age.addPath())!!.attributes() shouldBe
5253
listOf("background-color" to gray.encode())
5354
}
5455

5556
for (row in 1 until typed.nrow step 2) {
56-
FormattingDsl.formatter(typed[row], typed.age)!!.attributes() shouldBe
57+
FormattingDsl.formatter(typed[row], typed.age.addPath())!!.attributes() shouldBe
5758
listOf("background-color" to linearGradient(typed[row].age.toDouble(), 20.0, green, 80.0, red).encode())
5859
}
5960
}

0 commit comments

Comments
 (0)