Skip to content

Commit c043b2d

Browse files
authored
Merge pull request #1374 from Kotlin/format-colgroups
Format colgroups
2 parents a24e5a5 + 7e12d7a commit c043b2d

File tree

12 files changed

+177
-95
lines changed

12 files changed

+177
-95
lines changed

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

Lines changed: 24 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
@@ -15,7 +14,10 @@ import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear
1514
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
17+
import org.jetbrains.kotlinx.dataframe.columns.ColumnGroup
1818
import org.jetbrains.kotlinx.dataframe.columns.ColumnReference
19+
import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath
20+
import org.jetbrains.kotlinx.dataframe.columns.FrameColumn
1921
import org.jetbrains.kotlinx.dataframe.columns.toColumnSet
2022
import org.jetbrains.kotlinx.dataframe.dataTypes.IFRAME
2123
import org.jetbrains.kotlinx.dataframe.dataTypes.IMG
@@ -66,6 +68,21 @@ import kotlin.reflect.KProperty
6668
*
6769
* You can continue formatting the [FormattedFrame] by calling [format][FormattedFrame.format] on it again.
6870
*
71+
* Specifying a [column group][ColumnGroup] makes all of its inner columns be formatted in the same way unless
72+
* overridden.
73+
*
74+
* Formatting is done additively, meaning you can add more formatting to a cell that's already formatted or
75+
* override certain attributes inherited from its outer group.
76+
*
77+
* Specifying a [frame column][FrameColumn] at the moment does nothing
78+
* ([Issue #1375](https://github.com/Kotlin/dataframe/issues/1375)),
79+
* convert each nested [DataFrame] to a [FormattedFrame] instead:
80+
* ```kt
81+
* df.convert { myFrameCol }.with {
82+
* it.format { someCol }.with { background(green) }
83+
* }.toStandaloneHtml()
84+
* ```
85+
*
6986
* Check out the [Grammar].
7087
*
7188
* For more information: {@include [DocumentationUrls.Format]}
@@ -169,7 +186,7 @@ internal interface FormatDocs {
169186
interface CellFormatterDef
170187

171188
/**
172-
* `rowColFormatter: `{@include [FormattingDslGrammarRef]}`.(row: `[DataRow][DataRow]`<T>, col: `[DataColumn][DataColumn]`<C>) -> `[CellAttributes][CellAttributes]`?`
189+
* `rowColFormatter: `{@include [FormattingDslGrammarRef]}`.(row: `[DataRow][DataRow]`<T>, col: `[ColumnWithPath][ColumnWithPath]`<C>) -> `[CellAttributes][CellAttributes]`?`
173190
*/
174191
interface RowColFormatterDef
175192

@@ -348,7 +365,7 @@ public fun <T> FormattedFrame<T>.format(vararg columns: String): FormatClause<T,
348365
* .toStandaloneHtml().openInBrowser()
349366
* ```
350367
*/
351-
public fun <T> FormattedFrame<T>.format(): FormatClause<T, Any?> = FormatClause(df, null, formatter)
368+
public fun <T> FormattedFrame<T>.format(): FormatClause<T, Any?> = FormatClause(df = df, oldFormatter = formatter)
352369

353370
// endregion
354371

@@ -470,7 +487,7 @@ public fun <T, C> FormatClause<T, C>.perRowCol(formatter: RowColFormatter<T, C>)
470487
*/
471488
@Suppress("UNCHECKED_CAST")
472489
public fun <T, C> FormatClause<T, C>.with(formatter: CellFormatter<C>): FormattedFrame<T> =
473-
formatImpl { row, col -> formatter(row[col.name] as C) }
490+
formatImpl { row, col -> formatter(col[row] as C) }
474491

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

735752
/**
736753
* A lambda function expecting a [CellAttributes] or `null` given an instance of
737-
* [DataRow][DataRow]`<`[T][T]`>` and [DataColumn][DataColumn]`<`[C][C]`>`.
754+
* [DataRow][DataRow]`<`[T][T]`>` and [ColumnWithPath][ColumnWithPath]`<`[C][C]`>`.
738755
*
739756
* This is similar to a [RowColumnExpression], except that you also have access
740757
* to the [FormattingDsl] in the context.
741758
*
742759
* @include [FormattingDsl]
743760
*/
744-
public typealias RowColFormatter<T, C> = FormattingDsl.(row: DataRow<T>, col: DataColumn<C>) -> CellAttributes?
761+
public typealias RowColFormatter<T, C> = FormattingDsl.(row: DataRow<T>, col: ColumnWithPath<C>) -> CellAttributes?
745762

746763
/**
747764
* A lambda function expecting a [CellAttributes] or `null` given an instance of a cell: [C] of the dataframe.
@@ -838,7 +855,7 @@ public class FormattedFrame<T>(internal val df: DataFrame<T>, internal val forma
838855
*/
839856
public class FormatClause<T, C>(
840857
internal val df: DataFrame<T>,
841-
internal val columns: ColumnsSelector<T, C>? = null,
858+
internal val columns: ColumnsSelector<T, C> = { all().cast() },
842859
internal val oldFormatter: RowColFormatter<T, C>? = null,
843860
internal val filter: RowValueFilter<T, C> = { true },
844861
) {

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
}

0 commit comments

Comments
 (0)