From cac1b7f08b692174ba99e18c979e5ee79a5e8fa5 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Fri, 19 Sep 2025 14:15:12 +0200 Subject: [PATCH 01/18] (Junie) add formatHeader --- .../jetbrains/kotlinx/dataframe/api/format.kt | 13 +- .../kotlinx/dataframe/api/formatHeader.kt | 119 ++++++++++++++++++ .../kotlinx/dataframe/impl/api/format.kt | 23 ++-- .../jetbrains/kotlinx/dataframe/io/html.kt | 30 ++++- .../kotlinx/dataframe/api/formatHeader.kt | 78 ++++++++++++ 5 files changed, 248 insertions(+), 15 deletions(-) create mode 100644 core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt create mode 100644 core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt index 2d7e6c6110..d3c084f261 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt @@ -325,7 +325,7 @@ public fun DataFrame.format(vararg columns: KProperty): FormatClaus * If unspecified, all columns will be formatted. */ public fun FormattedFrame.format(columns: ColumnsSelector): FormatClause = - FormatClause(df, columns, formatter) + FormatClause(df, columns, formatter, oldHeaderFormatter = headerFormatter) /** * @include [CommonFormatDocs] @@ -390,7 +390,7 @@ public fun FormattedFrame.format(): FormatClause = FormatClause( * Check out the full [Grammar][FormatDocs.Grammar]. */ public fun FormatClause.where(filter: RowValueFilter): FormatClause = - FormatClause(filter = this.filter and filter, df = df, columns = columns, oldFormatter = oldFormatter) + FormatClause(filter = this.filter and filter, df = df, columns = columns, oldFormatter = oldFormatter, oldHeaderFormatter = oldHeaderFormatter) /** * Only format the selected columns at given row indices. @@ -780,7 +780,11 @@ public typealias CellFormatter = FormattingDsl.(cell: C) -> CellAttributes? * * You can apply further formatting to this [FormattedFrame] by calling [format()][FormattedFrame.format] once again. */ -public class FormattedFrame(internal val df: DataFrame, internal val formatter: RowColFormatter? = null) { +public class FormattedFrame( + internal val df: DataFrame, + internal val formatter: RowColFormatter? = null, + internal val headerFormatter: HeaderColFormatter<*>? = null, +) { /** * Returns a [DataFrameHtmlData] without additional definitions. @@ -826,7 +830,7 @@ public class FormattedFrame(internal val df: DataFrame, internal val forma /** Applies this formatter to the given [configuration] and returns a new instance. */ @Suppress("UNCHECKED_CAST") public fun getDisplayConfiguration(configuration: DisplayConfiguration): DisplayConfiguration = - configuration.copy(cellFormatter = formatter as RowColFormatter<*, *>?) + configuration.copy(cellFormatter = formatter as RowColFormatter<*, *>?, headerFormatter = headerFormatter as HeaderColFormatter<*>?) } /** @@ -858,6 +862,7 @@ public class FormatClause( internal val columns: ColumnsSelector = { all().cast() }, internal val oldFormatter: RowColFormatter? = null, internal val filter: RowValueFilter = { true }, + internal val oldHeaderFormatter: HeaderColFormatter<*>? = null, ) { override fun toString(): String = "FormatClause(df=$df, columns=$columns, oldFormatter=$oldFormatter, filter=$filter)" diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt new file mode 100644 index 0000000000..314f21bbf8 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -0,0 +1,119 @@ +package org.jetbrains.kotlinx.dataframe.api + +import org.jetbrains.kotlinx.dataframe.ColumnsSelector +import org.jetbrains.kotlinx.dataframe.DataFrame +import org.jetbrains.kotlinx.dataframe.annotations.AccessApiOverload +import org.jetbrains.kotlinx.dataframe.columns.ColumnReference +import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath +import org.jetbrains.kotlinx.dataframe.columns.toColumnSet +import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths +import org.jetbrains.kotlinx.dataframe.columns.UnresolvedColumnsPolicy +import kotlin.reflect.KProperty +import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API + +/** + * Formats the headers (column names) of the selected columns similarly to how [format] formats cell values. + * + * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] which must be + * finalized using [HeaderFormatClause.with]. + * + * Header formatting is additive and supports nested column groups: styles specified for a parent [ColumnGroup] + * are inherited by its child columns unless overridden for the child. + * + * Examples: + * ```kt + * // center a single column header + * df.formatHeader { age }.with { attr("text-align", "center") } + * + * // style a whole group header and override one child + * df.formatHeader { name }.with { bold } + * .formatHeader { name.firstName }.with { textColor(green) } + * .toStandaloneHtml() + * ``` + */ +public typealias HeaderColFormatter = FormattingDsl.(col: ColumnWithPath) -> CellAttributes? + +/** + * Intermediate clause for header formatting, analogous to [FormatClause] but without rows. + * + * Use [with] to specify how to format the selected column headers, producing a [FormattedFrame]. + */ +public class HeaderFormatClause( + internal val df: DataFrame, + internal val columns: ColumnsSelector = { all().cast() }, + internal val oldHeaderFormatter: HeaderColFormatter? = null, + internal val oldCellFormatter: RowColFormatter? = null, +) { + override fun toString(): String = + "HeaderFormatClause(df=$df, columns=$columns, oldHeaderFormatter=$oldHeaderFormatter, oldCellFormatter=$oldCellFormatter)" +} + +// region DataFrame.formatHeader + +/** + * Selects [columns] whose headers should be formatted; finalize with [HeaderFormatClause.with]. + */ +public fun DataFrame.formatHeader(columns: ColumnsSelector): HeaderFormatClause = + HeaderFormatClause(this, columns) + +/** Selects columns by [columns] names for header formatting. */ +public fun DataFrame.formatHeader(vararg columns: String): HeaderFormatClause = + formatHeader { columns.toColumnSet() } + +/** Selects all columns for header formatting. */ +public fun DataFrame.formatHeader(): HeaderFormatClause = HeaderFormatClause(this) + +@Deprecated(DEPRECATED_ACCESS_API) +@AccessApiOverload +public fun DataFrame.formatHeader(vararg columns: ColumnReference): HeaderFormatClause = + formatHeader { columns.toColumnSet() } + +@Deprecated(DEPRECATED_ACCESS_API) +@AccessApiOverload +public fun DataFrame.formatHeader(vararg columns: KProperty): HeaderFormatClause = + formatHeader { columns.toColumnSet() } + +// endregion + +// region FormattedFrame.formatHeader + +public fun FormattedFrame.formatHeader(columns: ColumnsSelector): HeaderFormatClause = + HeaderFormatClause(df = df, columns = columns, oldHeaderFormatter = headerFormatter as HeaderColFormatter?, oldCellFormatter = formatter) + +public fun FormattedFrame.formatHeader(vararg columns: String): HeaderFormatClause = + formatHeader { columns.toColumnSet() } + +public fun FormattedFrame.formatHeader(): HeaderFormatClause = + HeaderFormatClause(df = df, oldHeaderFormatter = headerFormatter as HeaderColFormatter?, oldCellFormatter = formatter) + +// endregion + +// region terminal operations + +@Suppress("UNCHECKED_CAST") +public fun HeaderFormatClause.with(formatter: HeaderColFormatter): FormattedFrame { + val paths = df.getColumnPaths(UnresolvedColumnsPolicy.Skip, columns).toSet() + val oldHeader = oldHeaderFormatter + val composedHeader: HeaderColFormatter = { col -> + val parentCols = col.path.indices + .map { i -> col.path.take(i + 1) } + .dropLast(0) // include self and parents handled below + // Merge attributes from parents that are selected + val parentAttributes = parentCols + .dropLast(1) + .map { path -> ColumnWithPath(df[path], path) } + .map { parentCol -> if (parentCol.path in paths) (oldHeader?.invoke(FormattingDsl, parentCol as ColumnWithPath)) else null } + .reduceOrNull(CellAttributes?::and) + val selfAttr = if (col.path in paths) { + val oldAttr = oldHeader?.invoke(FormattingDsl, col as ColumnWithPath) + oldAttr and formatter(FormattingDsl, col as ColumnWithPath) + } else { + oldHeader?.invoke(FormattingDsl, col as ColumnWithPath) + } + parentAttributes and selfAttr + } + @Suppress("UNCHECKED_CAST") + return FormattedFrame(df, oldCellFormatter, composedHeader) +} + +// endregion diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt index 8b78bdfe5e..79a1d4cb34 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt @@ -56,15 +56,18 @@ internal inline fun FormatClause.formatImpl( val clause = this val columns = clause.df.getColumnPaths(UnresolvedColumnsPolicy.Skip, clause.columns).toSet() - return FormattedFrame(clause.df) { row, col -> - val oldAttributes = clause.oldFormatter?.invoke(FormattingDsl, row, col.cast()) - if (col.path in columns) { - val value = col[row] as C - if (clause.filter(row, value)) { - return@FormattedFrame oldAttributes and formatter(FormattingDsl, row.cast(), col.cast()) + return FormattedFrame( + df = clause.df, + formatter = { row, col -> + val oldAttributes = clause.oldFormatter?.invoke(FormattingDsl, row, col.cast()) + if (col.path in columns) { + val value = col[row] as C + if (clause.filter(row, value)) { + return@FormattedFrame oldAttributes and formatter(FormattingDsl, row.cast(), col.cast()) + } } - } - - oldAttributes - } + oldAttributes + }, + headerFormatter = clause.oldHeaderFormatter + ) } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt index dec55ef272..2a4874243c 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt @@ -9,6 +9,7 @@ import org.jetbrains.kotlinx.dataframe.api.CellAttributes import org.jetbrains.kotlinx.dataframe.api.FormattedFrame import org.jetbrains.kotlinx.dataframe.api.FormattingDsl import org.jetbrains.kotlinx.dataframe.api.RowColFormatter +import org.jetbrains.kotlinx.dataframe.api.HeaderColFormatter import org.jetbrains.kotlinx.dataframe.api.and import org.jetbrains.kotlinx.dataframe.api.asColumnGroup import org.jetbrains.kotlinx.dataframe.api.asNumbers @@ -68,6 +69,7 @@ internal data class ColumnDataForJs( val nested: List, val rightAlign: Boolean, val values: List, + val headerStyle: String?, ) internal val formatter = DataFrameFormatter( @@ -98,7 +100,8 @@ internal fun getResourceText(resource: String, vararg replacement: Pair${column.name()}" + val styleAttr = if (headerStyle != null) " style=\"$headerStyle\"" else "" + return "${column.name()}" } internal fun tableJs( @@ -234,6 +237,27 @@ internal fun AnyFrame.toHtmlData( HtmlContent(html, style) } } + val headerStyle = run { + val hf = configuration.headerFormatter + if (hf == null) null else { + // collect attributes from parents + val parentCols = col.path.indices + .map { i -> col.path.take(i + 1) } + .dropLast(1) + .map { ColumnWithPath(this@toHtmlData[it], it) } + val parentAttributes = parentCols + .map { hf(FormattingDsl, it) } + .reduceOrNull(CellAttributes?::and) + val selfAttributes = hf(FormattingDsl, col) + val attrs = parentAttributes and selfAttributes + attrs + ?.attributes() + ?.ifEmpty { null } + ?.toMap() + ?.entries + ?.joinToString(";") { "${it.key}:${it.value}" } + } + } val nested = if (col is ColumnGroup<*>) { col.columns().map { col.columnToJs(it.addParentPath(col.path), rowsLimit, configuration) @@ -246,6 +270,7 @@ internal fun AnyFrame.toHtmlData( nested = nested, rightAlign = col.isSubtypeOf(), values = contents, + headerStyle = headerStyle, ) } @@ -826,12 +851,15 @@ public class DataFrameHtmlData( * @param cellContentLimit -1 to disable content trimming * @param enableFallbackStaticTables true to add additional pure HTML table that will be visible only if JS is disabled; * For example hosting *.ipynb files with outputs on GitHub + * @param cellFormatter Optional cell formatter applied to data cells during HTML rendering. + * @param headerFormatter Optional header formatter applied to column headers; supports inheritance for nested column groups. */ public data class DisplayConfiguration( var rowsLimit: Int? = 20, var nestedRowsLimit: Int? = 5, var cellContentLimit: Int = 40, var cellFormatter: RowColFormatter<*, *>? = null, + var headerFormatter: HeaderColFormatter<*>? = null, var decimalFormat: RendererDecimalFormat = RendererDecimalFormat.DEFAULT, var isolatedOutputs: Boolean = flagFromEnv("LETS_PLOT_HTML_ISOLATED_FRAME"), internal val localTesting: Boolean = flagFromEnv("KOTLIN_DATAFRAME_LOCAL_TESTING"), diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt new file mode 100644 index 0000000000..76cc80a81a --- /dev/null +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -0,0 +1,78 @@ +package org.jetbrains.kotlinx.dataframe.api + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.blue +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.green +import org.jetbrains.kotlinx.dataframe.samples.api.TestBase +import org.jetbrains.kotlinx.dataframe.samples.api.age +import org.jetbrains.kotlinx.dataframe.samples.api.name +import org.jetbrains.kotlinx.dataframe.samples.api.firstName +import org.junit.Test + +class FormatHeaderTests : TestBase() { + + @Test + fun `formatHeader on single column adds inline style to header`() { + val formatted = df.formatHeader { age }.with { attr("border", "3px solid green") } + val html = formatted.toHtml().toString() + + // header style is rendered inline inside + // count exact style occurrences to avoid interference with CSS + val occurrences = html.split("border:3px solid green").size - 1 + occurrences shouldBe 1 + + } + + @Test + fun `formatHeader by names overload`() { + val formatted = df.formatHeader("age").with { attr("text-align", "center") } + val html = formatted.toHtml().toString() + val occurrences = html.split("text-align:center").size - 1 + occurrences shouldBe 1 + } + + @Test + fun `header style inherited from group to children`() { + // Apply style to the group header only + val formatted = df.formatHeader { name }.with { attr("border", "1px solid red") } + val html = formatted.toHtml().toString() + + // We expect the style on the group header itself and each direct child header + // In the default TestBase dataset, name group has two children + val occurrences = html.split("border:1px solid red").size - 1 + occurrences shouldBe 3 + } + + @Test + fun `child header overrides parent group header style`() { + val formatted = df + .formatHeader { name }.with { attr("border", "1px solid red") } + .formatHeader { name.firstName }.with { attr("border", "2px dashed green") } + val html = formatted.toHtml().toString() + + // Parent style applies to group and lastName, but firstName gets its own style in addition to or replacing + // We check for both occurrences + val parentOcc = html.split("border:1px solid red").size - 1 + val childOcc = html.split("border:2px dashed green").size - 1 + + parentOcc shouldBe 2 // group + lastName + childOcc shouldBe 1 // firstName only + } + + @Test + fun `format and formatHeader can be chained and both persist`() { + val formatted = df + .format { age }.with { background(blue) } + .formatHeader { age }.with { attr("border", "3px solid green") } + + val html = formatted.toHtml().toString() + + // body cell style + (html.split("background-color:#0000ff").size - 1) shouldBe 7 + // header style + (html.split("border:3px solid green").size - 1) shouldBe 1 + + formatted::class.simpleName shouldNotBe null + } +} From 30059fb616eab45fc7423097143f583589a79dc8 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Sat, 20 Sep 2025 12:07:12 +0200 Subject: [PATCH 02/18] formatting --- .../jetbrains/kotlinx/dataframe/api/format.kt | 13 +++++++-- .../kotlinx/dataframe/api/formatHeader.kt | 27 +++++++++++++++---- .../kotlinx/dataframe/impl/api/format.kt | 2 +- .../jetbrains/kotlinx/dataframe/io/html.kt | 6 +++-- .../kotlinx/dataframe/api/formatHeader.kt | 4 +-- .../kotlinx/dataframe/samples/io/Parquet.kt | 9 +++---- 6 files changed, 43 insertions(+), 18 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt index d3c084f261..80bd659083 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt @@ -390,7 +390,13 @@ public fun FormattedFrame.format(): FormatClause = FormatClause( * Check out the full [Grammar][FormatDocs.Grammar]. */ public fun FormatClause.where(filter: RowValueFilter): FormatClause = - FormatClause(filter = this.filter and filter, df = df, columns = columns, oldFormatter = oldFormatter, oldHeaderFormatter = oldHeaderFormatter) + FormatClause( + filter = this.filter and filter, + df = df, + columns = columns, + oldFormatter = oldFormatter, + oldHeaderFormatter = oldHeaderFormatter, + ) /** * Only format the selected columns at given row indices. @@ -830,7 +836,10 @@ public class FormattedFrame( /** Applies this formatter to the given [configuration] and returns a new instance. */ @Suppress("UNCHECKED_CAST") public fun getDisplayConfiguration(configuration: DisplayConfiguration): DisplayConfiguration = - configuration.copy(cellFormatter = formatter as RowColFormatter<*, *>?, headerFormatter = headerFormatter as HeaderColFormatter<*>?) + configuration.copy( + cellFormatter = formatter as RowColFormatter<*, *>?, + headerFormatter = headerFormatter as HeaderColFormatter<*>?, + ) } /** diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt index 314f21bbf8..5f7a5cf4e1 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -5,11 +5,11 @@ import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.annotations.AccessApiOverload import org.jetbrains.kotlinx.dataframe.columns.ColumnReference import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath +import org.jetbrains.kotlinx.dataframe.columns.UnresolvedColumnsPolicy import org.jetbrains.kotlinx.dataframe.columns.toColumnSet import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths -import org.jetbrains.kotlinx.dataframe.columns.UnresolvedColumnsPolicy -import kotlin.reflect.KProperty import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API +import kotlin.reflect.KProperty /** * Formats the headers (column names) of the selected columns similarly to how [format] formats cell values. @@ -78,13 +78,22 @@ public fun DataFrame.formatHeader(vararg columns: KProperty): Heade // region FormattedFrame.formatHeader public fun FormattedFrame.formatHeader(columns: ColumnsSelector): HeaderFormatClause = - HeaderFormatClause(df = df, columns = columns, oldHeaderFormatter = headerFormatter as HeaderColFormatter?, oldCellFormatter = formatter) + HeaderFormatClause( + df = df, + columns = columns, + oldHeaderFormatter = headerFormatter as HeaderColFormatter?, + oldCellFormatter = formatter, + ) public fun FormattedFrame.formatHeader(vararg columns: String): HeaderFormatClause = formatHeader { columns.toColumnSet() } public fun FormattedFrame.formatHeader(): HeaderFormatClause = - HeaderFormatClause(df = df, oldHeaderFormatter = headerFormatter as HeaderColFormatter?, oldCellFormatter = formatter) + HeaderFormatClause( + df = df, + oldHeaderFormatter = headerFormatter as HeaderColFormatter?, + oldCellFormatter = formatter, + ) // endregion @@ -102,7 +111,15 @@ public fun HeaderFormatClause.with(formatter: HeaderColFormatter val parentAttributes = parentCols .dropLast(1) .map { path -> ColumnWithPath(df[path], path) } - .map { parentCol -> if (parentCol.path in paths) (oldHeader?.invoke(FormattingDsl, parentCol as ColumnWithPath)) else null } + .map { parentCol -> + if (parentCol.path in + paths + ) { + (oldHeader?.invoke(FormattingDsl, parentCol as ColumnWithPath)) + } else { + null + } + } .reduceOrNull(CellAttributes?::and) val selfAttr = if (col.path in paths) { val oldAttr = oldHeader?.invoke(FormattingDsl, col as ColumnWithPath) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt index 79a1d4cb34..3f1aec332a 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt @@ -68,6 +68,6 @@ internal inline fun FormatClause.formatImpl( } oldAttributes }, - headerFormatter = clause.oldHeaderFormatter + headerFormatter = clause.oldHeaderFormatter, ) } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt index 2a4874243c..ec4d632ce3 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt @@ -8,8 +8,8 @@ import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.CellAttributes import org.jetbrains.kotlinx.dataframe.api.FormattedFrame import org.jetbrains.kotlinx.dataframe.api.FormattingDsl -import org.jetbrains.kotlinx.dataframe.api.RowColFormatter import org.jetbrains.kotlinx.dataframe.api.HeaderColFormatter +import org.jetbrains.kotlinx.dataframe.api.RowColFormatter import org.jetbrains.kotlinx.dataframe.api.and import org.jetbrains.kotlinx.dataframe.api.asColumnGroup import org.jetbrains.kotlinx.dataframe.api.asNumbers @@ -239,7 +239,9 @@ internal fun AnyFrame.toHtmlData( } val headerStyle = run { val hf = configuration.headerFormatter - if (hf == null) null else { + if (hf == null) { + null + } else { // collect attributes from parents val parentCols = col.path.indices .map { i -> col.path.take(i + 1) } diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt index 76cc80a81a..295c5dc1eb 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -3,11 +3,10 @@ package org.jetbrains.kotlinx.dataframe.api import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.blue -import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.green import org.jetbrains.kotlinx.dataframe.samples.api.TestBase import org.jetbrains.kotlinx.dataframe.samples.api.age -import org.jetbrains.kotlinx.dataframe.samples.api.name import org.jetbrains.kotlinx.dataframe.samples.api.firstName +import org.jetbrains.kotlinx.dataframe.samples.api.name import org.junit.Test class FormatHeaderTests : TestBase() { @@ -21,7 +20,6 @@ class FormatHeaderTests : TestBase() { // count exact style occurrences to avoid interference with CSS val occurrences = html.split("border:3px solid green").size - 1 occurrences shouldBe 1 - } @Test diff --git a/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/io/Parquet.kt b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/io/Parquet.kt index c423cbdfc0..3d235e296e 100644 --- a/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/io/Parquet.kt +++ b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/io/Parquet.kt @@ -1,14 +1,13 @@ package org.jetbrains.kotlinx.dataframe.samples.io import io.kotest.matchers.shouldBe -import java.io.File -import java.nio.file.Path -import java.nio.file.Paths import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.NullabilityOptions -import org.junit.Test import org.jetbrains.kotlinx.dataframe.io.readParquet import org.jetbrains.kotlinx.dataframe.testParquet +import org.junit.Test +import java.io.File +import java.nio.file.Paths class Parquet { @Test @@ -56,7 +55,7 @@ class Parquet { val df = DataFrame.readParquet( file, nullability = NullabilityOptions.Infer, - batchSize = 64L * 1024 + batchSize = 64L * 1024, ) // SampleEnd df.rowsCount() shouldBe 300 From 92eae1ddc1d57d0adb127deb918ada37616e6b31 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Sat, 20 Sep 2025 12:29:38 +0200 Subject: [PATCH 03/18] api dump --- core/api/core.api | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/core/api/core.api b/core/api/core.api index 283e186f54..4674be4b8e 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -2232,11 +2232,23 @@ public final class org/jetbrains/kotlinx/dataframe/api/ForEachKt { } public final class org/jetbrains/kotlinx/dataframe/api/FormatClause { - public fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V + public synthetic fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun toString ()Ljava/lang/String; } +public final class org/jetbrains/kotlinx/dataframe/api/FormatHeaderKt { + public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; + public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; + public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;[Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; + public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;[Lkotlin/reflect/KProperty;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; + public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;[Lorg/jetbrains/kotlinx/dataframe/columns/ColumnReference;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; + public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; + public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; + public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;[Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; + public static final fun with (Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame; +} + public final class org/jetbrains/kotlinx/dataframe/api/FormatKt { public static final fun and (Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes;Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes;)Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; public static final fun at (Lorg/jetbrains/kotlinx/dataframe/api/FormatClause;Ljava/util/Collection;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; @@ -2259,8 +2271,8 @@ public final class org/jetbrains/kotlinx/dataframe/api/FormatKt { } public final class org/jetbrains/kotlinx/dataframe/api/FormattedFrame { - public fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function3;)V - public synthetic fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;)V + public synthetic fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getDisplayConfiguration (Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration;)Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration; public final fun toHtml (Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration;)Lorg/jetbrains/kotlinx/dataframe/io/DataFrameHtmlData; public static synthetic fun toHtml$default (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration;ILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/io/DataFrameHtmlData; @@ -2545,6 +2557,12 @@ public final class org/jetbrains/kotlinx/dataframe/api/HeadKt { public static synthetic fun head$default (Lorg/jetbrains/kotlinx/dataframe/DataFrame;IILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/DataFrame; } +public final class org/jetbrains/kotlinx/dataframe/api/HeaderFormatClause { + public fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)V + public synthetic fun (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun toString ()Ljava/lang/String; +} + public final class org/jetbrains/kotlinx/dataframe/api/ImplodeKt { public static final fun implode (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Z)Lorg/jetbrains/kotlinx/dataframe/DataRow; public static final fun implode (Lorg/jetbrains/kotlinx/dataframe/DataFrame;ZLkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/DataFrame; @@ -6194,25 +6212,27 @@ public final class org/jetbrains/kotlinx/dataframe/io/DataFrameHtmlData$Companio public final class org/jetbrains/kotlinx/dataframe/io/DisplayConfiguration { public static final field Companion Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration$Companion; - public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Ljava/lang/String;ZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Ljava/lang/String;ZZZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Ljava/lang/String;ZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Ljava/lang/String;ZZZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/Integer; public final fun component10 ()Z + public final fun component11 ()Z public final fun component2 ()Ljava/lang/Integer; public final fun component3 ()I public final fun component4 ()Lkotlin/jvm/functions/Function3; - public final fun component5-3Sl7FsM ()Ljava/lang/String; - public final fun component6 ()Z - public final fun component8 ()Z + public final fun component5 ()Lkotlin/jvm/functions/Function2; + public final fun component6-3Sl7FsM ()Ljava/lang/String; + public final fun component7 ()Z public final fun component9 ()Z - public final fun copy-rqXL5tM (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Ljava/lang/String;ZZZZZ)Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration; - public static synthetic fun copy-rqXL5tM$default (Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Ljava/lang/String;ZZZZZILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration; + public final fun copy-bMNacXk (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Ljava/lang/String;ZZZZZ)Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration; + public static synthetic fun copy-bMNacXk$default (Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Ljava/lang/String;ZZZZZILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration; public fun equals (Ljava/lang/Object;)Z public final fun getCellContentLimit ()I public final fun getCellFormatter ()Lkotlin/jvm/functions/Function3; public final fun getDecimalFormat-3Sl7FsM ()Ljava/lang/String; public final fun getDownsizeBufferedImage ()Z public final fun getEnableFallbackStaticTables ()Z + public final fun getHeaderFormatter ()Lkotlin/jvm/functions/Function2; public final fun getIsolatedOutputs ()Z public final fun getNestedRowsLimit ()Ljava/lang/Integer; public final fun getRowsLimit ()Ljava/lang/Integer; @@ -6224,6 +6244,7 @@ public final class org/jetbrains/kotlinx/dataframe/io/DisplayConfiguration { public final fun setDecimalFormat-h5o3lmc (Ljava/lang/String;)V public final fun setDownsizeBufferedImage (Z)V public final fun setEnableFallbackStaticTables (Z)V + public final fun setHeaderFormatter (Lkotlin/jvm/functions/Function2;)V public final fun setIsolatedOutputs (Z)V public final fun setNestedRowsLimit (Ljava/lang/Integer;)V public final fun setRowsLimit (Ljava/lang/Integer;)V From 5b9e1a912989c08e8f3dbe4c78dfe185187bcd77 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Sat, 20 Sep 2025 15:30:50 +0200 Subject: [PATCH 04/18] disable :samples module --- build.gradle.kts | 2 +- settings.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2ab279f51e..fb7d1aae31 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -158,7 +158,7 @@ val modulesUsingJava11 = with(projects) { dataframeGeoJupyter, examples.ideaExamples.titanic, examples.ideaExamples.unsupportedDataSources, - samples, + //samples, plugins.dataframeGradlePlugin, ) }.map { it.path } diff --git a/settings.gradle.kts b/settings.gradle.kts index e227071c9f..26e42f79e6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,7 @@ include("plugins:symbol-processor") include("plugins:expressions-converter") include("plugins:kotlin-dataframe") include("plugins:public-api-modifier") -include("samples") +//include("samples") include("dataframe-json") include("dataframe-arrow") include("dataframe-openapi") From 1f08fbc148dca6ec61210f74b0a307ee864e8f0f Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Sat, 20 Sep 2025 15:33:40 +0200 Subject: [PATCH 05/18] disable :samples module --- build.gradle.kts | 2 +- settings.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fb7d1aae31..bd471e8d12 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -158,7 +158,7 @@ val modulesUsingJava11 = with(projects) { dataframeGeoJupyter, examples.ideaExamples.titanic, examples.ideaExamples.unsupportedDataSources, - //samples, + // samples, plugins.dataframeGradlePlugin, ) }.map { it.path } diff --git a/settings.gradle.kts b/settings.gradle.kts index 26e42f79e6..89386beb15 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,7 @@ include("plugins:symbol-processor") include("plugins:expressions-converter") include("plugins:kotlin-dataframe") include("plugins:public-api-modifier") -//include("samples") +// include("samples") include("dataframe-json") include("dataframe-arrow") include("dataframe-openapi") From b74d091dfead8cd0deedbab757030dcb686e6e8f Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Tue, 30 Sep 2025 13:51:35 +0400 Subject: [PATCH 06/18] remove formatHeader overloads --- .../kotlinx/dataframe/api/formatHeader.kt | 56 ++++++++----------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt index 5f7a5cf4e1..24abd7d056 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -11,30 +11,10 @@ import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API import kotlin.reflect.KProperty -/** - * Formats the headers (column names) of the selected columns similarly to how [format] formats cell values. - * - * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] which must be - * finalized using [HeaderFormatClause.with]. - * - * Header formatting is additive and supports nested column groups: styles specified for a parent [ColumnGroup] - * are inherited by its child columns unless overridden for the child. - * - * Examples: - * ```kt - * // center a single column header - * df.formatHeader { age }.with { attr("text-align", "center") } - * - * // style a whole group header and override one child - * df.formatHeader { name }.with { bold } - * .formatHeader { name.firstName }.with { textColor(green) } - * .toStandaloneHtml() - * ``` - */ public typealias HeaderColFormatter = FormattingDsl.(col: ColumnWithPath) -> CellAttributes? /** - * Intermediate clause for header formatting, analogous to [FormatClause] but without rows. + * Intermediate clause for header formatting. * * Use [with] to specify how to format the selected column headers, producing a [FormattedFrame]. */ @@ -56,23 +36,32 @@ public class HeaderFormatClause( public fun DataFrame.formatHeader(columns: ColumnsSelector): HeaderFormatClause = HeaderFormatClause(this, columns) -/** Selects columns by [columns] names for header formatting. */ +/** + * Formats the headers (column names) of the selected columns similarly to how [format] formats cell values. + * + * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] which must be + * finalized using [HeaderFormatClause.with]. + * + * Header formatting is additive and supports nested column groups: styles specified for a parent [ColumnGroup] + * are inherited by its child columns unless overridden for the child. + * + * Examples: + * ```kt + * // center a single column header + * df.formatHeader { age }.with { attr("text-align", "center") } + * + * // style a whole group header and override one child + * df.formatHeader { name }.with { bold } + * .formatHeader { name.firstName }.with { textColor(green) } + * .toStandaloneHtml() + * ``` + */ public fun DataFrame.formatHeader(vararg columns: String): HeaderFormatClause = formatHeader { columns.toColumnSet() } /** Selects all columns for header formatting. */ public fun DataFrame.formatHeader(): HeaderFormatClause = HeaderFormatClause(this) -@Deprecated(DEPRECATED_ACCESS_API) -@AccessApiOverload -public fun DataFrame.formatHeader(vararg columns: ColumnReference): HeaderFormatClause = - formatHeader { columns.toColumnSet() } - -@Deprecated(DEPRECATED_ACCESS_API) -@AccessApiOverload -public fun DataFrame.formatHeader(vararg columns: KProperty): HeaderFormatClause = - formatHeader { columns.toColumnSet() } - // endregion // region FormattedFrame.formatHeader @@ -91,7 +80,7 @@ public fun FormattedFrame.formatHeader(vararg columns: String): HeaderFor public fun FormattedFrame.formatHeader(): HeaderFormatClause = HeaderFormatClause( df = df, - oldHeaderFormatter = headerFormatter as HeaderColFormatter?, + oldHeaderFormatter = headerFormatter, oldCellFormatter = formatter, ) @@ -129,7 +118,6 @@ public fun HeaderFormatClause.with(formatter: HeaderColFormatter } parentAttributes and selfAttr } - @Suppress("UNCHECKED_CAST") return FormattedFrame(df, oldCellFormatter, composedHeader) } From 029a71c0e72b7c62db2cdef47ee3146c271308ad Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Tue, 30 Sep 2025 13:59:32 +0400 Subject: [PATCH 07/18] remove formatHeader overloads --- .../kotlinx/dataframe/api/formatHeader.kt | 106 +++++++++++------- 1 file changed, 66 insertions(+), 40 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt index 24abd7d056..0a6fcc363a 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -11,12 +11,29 @@ import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API import kotlin.reflect.KProperty +// region docs + +/** + * A lambda used to format a column header (its displayed name) when rendering a dataframe to HTML. + * + * The lambda runs in the context of [FormattingDsl] and receives the [ColumnWithPath] of the header to format. + * Return a [CellAttributes] (or `null`) describing CSS you want to apply to the header cell. + * + * Examples: + * - Center a header: `attr("text-align", "center")` + * - Make it bold: `bold` + * - Set custom color: `textColor(rgb(10, 10, 10))` + */ public typealias HeaderColFormatter = FormattingDsl.(col: ColumnWithPath) -> CellAttributes? /** - * Intermediate clause for header formatting. + * An intermediate class used in the header-format operation [formatHeader]. + * + * This class itself does nothing—it represents a selection of columns whose headers will be formatted. + * Finalize this step by calling [with] to produce a new [FormattedFrame]. * - * Use [with] to specify how to format the selected column headers, producing a [FormattedFrame]. + * Header formatting is additive and supports nested column groups: styles specified for a parent group + * are inherited by its child columns unless overridden for the child. */ public class HeaderFormatClause( internal val df: DataFrame, @@ -28,44 +45,44 @@ public class HeaderFormatClause( "HeaderFormatClause(df=$df, columns=$columns, oldHeaderFormatter=$oldHeaderFormatter, oldCellFormatter=$oldCellFormatter)" } +// endregion + // region DataFrame.formatHeader /** - * Selects [columns] whose headers should be formatted; finalize with [HeaderFormatClause.with]. + * Selects [columns] whose headers should be formatted. + * + * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] + * which must be finalized using [HeaderFormatClause.with]. */ public fun DataFrame.formatHeader(columns: ColumnsSelector): HeaderFormatClause = HeaderFormatClause(this, columns) /** - * Formats the headers (column names) of the selected columns similarly to how [format] formats cell values. - * - * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] which must be - * finalized using [HeaderFormatClause.with]. + * Selects headers by [column names][String]. * - * Header formatting is additive and supports nested column groups: styles specified for a parent [ColumnGroup] - * are inherited by its child columns unless overridden for the child. + * Equivalent to `formatHeader { columns.toColumnSet() }`. * * Examples: * ```kt * // center a single column header - * df.formatHeader { age }.with { attr("text-align", "center") } - * - * // style a whole group header and override one child - * df.formatHeader { name }.with { bold } - * .formatHeader { name.firstName }.with { textColor(green) } - * .toStandaloneHtml() + * df.formatHeader("age").with { attr("text-align", "center") } * ``` */ public fun DataFrame.formatHeader(vararg columns: String): HeaderFormatClause = formatHeader { columns.toColumnSet() } -/** Selects all columns for header formatting. */ +/** Formats all column headers. */ public fun DataFrame.formatHeader(): HeaderFormatClause = HeaderFormatClause(this) + // endregion // region FormattedFrame.formatHeader +/** + * Continue header formatting on an already [FormattedFrame], preserving existing cell- and header formatting. + */ public fun FormattedFrame.formatHeader(columns: ColumnsSelector): HeaderFormatClause = HeaderFormatClause( df = df, @@ -74,9 +91,11 @@ public fun FormattedFrame.formatHeader(columns: ColumnsSelector) oldCellFormatter = formatter, ) +/** Selects headers by [column names][String] on an existing [FormattedFrame]. */ public fun FormattedFrame.formatHeader(vararg columns: String): HeaderFormatClause = formatHeader { columns.toColumnSet() } +/** Selects all headers on an existing [FormattedFrame]. */ public fun FormattedFrame.formatHeader(): HeaderFormatClause = HeaderFormatClause( df = df, @@ -88,36 +107,43 @@ public fun FormattedFrame.formatHeader(): HeaderFormatClause = // region terminal operations +/** + * Creates a new [FormattedFrame] that uses the specified [HeaderColFormatter] to format the selected headers. + * + * Header formatting is additive: attributes from already-applied header formatters are combined with the newly + * returned attributes using [CellAttributes.and]. If a parent column group is selected, its attributes are + * applied to its children unless explicitly overridden. + */ @Suppress("UNCHECKED_CAST") public fun HeaderFormatClause.with(formatter: HeaderColFormatter): FormattedFrame { - val paths = df.getColumnPaths(UnresolvedColumnsPolicy.Skip, columns).toSet() + val selectedPaths = df.getColumnPaths(UnresolvedColumnsPolicy.Skip, columns).toSet() val oldHeader = oldHeaderFormatter + val composedHeader: HeaderColFormatter = { col -> - val parentCols = col.path.indices - .map { i -> col.path.take(i + 1) } - .dropLast(0) // include self and parents handled below - // Merge attributes from parents that are selected - val parentAttributes = parentCols - .dropLast(1) - .map { path -> ColumnWithPath(df[path], path) } - .map { parentCol -> - if (parentCol.path in - paths - ) { - (oldHeader?.invoke(FormattingDsl, parentCol as ColumnWithPath)) - } else { - null + val path = col.path + // Merge attributes from selected parents + val parentAttributes = if (path.size > 1) { + val parentPaths = (0 until path.size - 1).map { i -> path.take(i + 1) } + parentPaths + .map { p -> ColumnWithPath(df[p], p) } + .map { parentCol -> + if (parentCol.path in selectedPaths) { + @Suppress("UNCHECKED_CAST") + oldHeader?.invoke(FormattingDsl, parentCol as ColumnWithPath) + } else null } - } - .reduceOrNull(CellAttributes?::and) - val selfAttr = if (col.path in paths) { - val oldAttr = oldHeader?.invoke(FormattingDsl, col as ColumnWithPath) - oldAttr and formatter(FormattingDsl, col as ColumnWithPath) - } else { - oldHeader?.invoke(FormattingDsl, col as ColumnWithPath) - } - parentAttributes and selfAttr + .reduceOrNull(CellAttributes?::and) + } else null + + @Suppress("UNCHECKED_CAST") + val typedCol = col as ColumnWithPath + + val existingAttr = oldHeader?.invoke(FormattingDsl, typedCol) + val newAttr = if (path in selectedPaths) formatter(FormattingDsl, typedCol) else null + + parentAttributes and (existingAttr and newAttr) } + return FormattedFrame(df, oldCellFormatter, composedHeader) } From 40caf4ecf027326e46181fbcea77af824b85df66 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Tue, 30 Sep 2025 14:07:57 +0400 Subject: [PATCH 08/18] formatHeader kdocs --- .../kotlinx/dataframe/api/formatHeader.kt | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt index 0a6fcc363a..9dae5d46c3 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -50,6 +50,8 @@ public class HeaderFormatClause( // region DataFrame.formatHeader /** + * **Experimental API. It may be changed in the future.** + * * Selects [columns] whose headers should be formatted. * * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] @@ -59,29 +61,37 @@ public fun DataFrame.formatHeader(columns: ColumnsSelector): Hea HeaderFormatClause(this, columns) /** - * Selects headers by [column names][String]. + * **Experimental API. It may be changed in the future.** * - * Equivalent to `formatHeader { columns.toColumnSet() }`. + * Selects [columns] whose headers should be formatted. * - * Examples: - * ```kt - * // center a single column header - * df.formatHeader("age").with { attr("text-align", "center") } - * ``` + * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] + * which must be finalized using [HeaderFormatClause.with]. */ public fun DataFrame.formatHeader(vararg columns: String): HeaderFormatClause = formatHeader { columns.toColumnSet() } -/** Formats all column headers. */ +/** + * **Experimental API. It may be changed in the future.** + * + * Selects all columns for header formatting. + * + * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] + * which must be finalized using [HeaderFormatClause.with]. + */ public fun DataFrame.formatHeader(): HeaderFormatClause = HeaderFormatClause(this) - // endregion // region FormattedFrame.formatHeader /** - * Continue header formatting on an already [FormattedFrame], preserving existing cell- and header formatting. + * **Experimental API. It may be changed in the future.** + * + * Selects [columns] whose headers should be formatted. + * + * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] + * which must be finalized using [HeaderFormatClause.with]. */ public fun FormattedFrame.formatHeader(columns: ColumnsSelector): HeaderFormatClause = HeaderFormatClause( @@ -91,11 +101,25 @@ public fun FormattedFrame.formatHeader(columns: ColumnsSelector) oldCellFormatter = formatter, ) -/** Selects headers by [column names][String] on an existing [FormattedFrame]. */ +/** + * **Experimental API. It may be changed in the future.** + * + * Selects [columns] whose headers should be formatted. + * + * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] + * which must be finalized using [HeaderFormatClause.with]. + */ public fun FormattedFrame.formatHeader(vararg columns: String): HeaderFormatClause = formatHeader { columns.toColumnSet() } -/** Selects all headers on an existing [FormattedFrame]. */ +/** + * **Experimental API. It may be changed in the future.** + * + * Selects all columns for header formatting. + * + * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] + * which must be finalized using [HeaderFormatClause.with]. + */ public fun FormattedFrame.formatHeader(): HeaderFormatClause = HeaderFormatClause( df = df, @@ -108,6 +132,8 @@ public fun FormattedFrame.formatHeader(): HeaderFormatClause = // region terminal operations /** + * **Experimental API. It may be changed in the future.** + * * Creates a new [FormattedFrame] that uses the specified [HeaderColFormatter] to format the selected headers. * * Header formatting is additive: attributes from already-applied header formatters are combined with the newly @@ -128,14 +154,12 @@ public fun HeaderFormatClause.with(formatter: HeaderColFormatter .map { p -> ColumnWithPath(df[p], p) } .map { parentCol -> if (parentCol.path in selectedPaths) { - @Suppress("UNCHECKED_CAST") oldHeader?.invoke(FormattingDsl, parentCol as ColumnWithPath) } else null } .reduceOrNull(CellAttributes?::and) } else null - @Suppress("UNCHECKED_CAST") val typedCol = col as ColumnWithPath val existingAttr = oldHeader?.invoke(FormattingDsl, typedCol) From c32e9b8695b2ba9bd6a0f51e8050913cd0fa0777 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Tue, 30 Sep 2025 14:18:43 +0400 Subject: [PATCH 09/18] formatHeader ktlint format --- .../jetbrains/kotlinx/dataframe/api/formatHeader.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt index 9dae5d46c3..fd740bba29 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -2,14 +2,10 @@ package org.jetbrains.kotlinx.dataframe.api import org.jetbrains.kotlinx.dataframe.ColumnsSelector import org.jetbrains.kotlinx.dataframe.DataFrame -import org.jetbrains.kotlinx.dataframe.annotations.AccessApiOverload -import org.jetbrains.kotlinx.dataframe.columns.ColumnReference import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath import org.jetbrains.kotlinx.dataframe.columns.UnresolvedColumnsPolicy import org.jetbrains.kotlinx.dataframe.columns.toColumnSet import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths -import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API -import kotlin.reflect.KProperty // region docs @@ -155,10 +151,14 @@ public fun HeaderFormatClause.with(formatter: HeaderColFormatter .map { parentCol -> if (parentCol.path in selectedPaths) { oldHeader?.invoke(FormattingDsl, parentCol as ColumnWithPath) - } else null + } else { + null + } } .reduceOrNull(CellAttributes?::and) - } else null + } else { + null + } val typedCol = col as ColumnWithPath From 06586709c801cc3baf5068cd89e07d9031f718a3 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Tue, 30 Sep 2025 14:19:55 +0400 Subject: [PATCH 10/18] api update --- core/api/core.api | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/api/core.api b/core/api/core.api index 4674be4b8e..3684a2e55b 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -2241,8 +2241,6 @@ public final class org/jetbrains/kotlinx/dataframe/api/FormatHeaderKt { public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;[Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; - public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;[Lkotlin/reflect/KProperty;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; - public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/DataFrame;[Lorg/jetbrains/kotlinx/dataframe/columns/ColumnReference;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; public static final fun formatHeader (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;[Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/api/HeaderFormatClause; From 8a76753c4899c908bfd7680bd487709a8bce5a99 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Wed, 1 Oct 2025 13:19:57 +0400 Subject: [PATCH 11/18] enable samples --- build.gradle.kts | 2 +- dataframe-jupyter/build.gradle.kts | 8 ++++++-- gradle/libs.versions.toml | 2 +- settings.gradle.kts | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index bd471e8d12..2ab279f51e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -158,7 +158,7 @@ val modulesUsingJava11 = with(projects) { dataframeGeoJupyter, examples.ideaExamples.titanic, examples.ideaExamples.unsupportedDataSources, - // samples, + samples, plugins.dataframeGradlePlugin, ) }.map { it.path } diff --git a/dataframe-jupyter/build.gradle.kts b/dataframe-jupyter/build.gradle.kts index 1269ac0602..00723fc9c1 100644 --- a/dataframe-jupyter/build.gradle.kts +++ b/dataframe-jupyter/build.gradle.kts @@ -36,8 +36,12 @@ dependencies { testImplementation(projects.dataframeJupyter) testImplementation(projects.dataframeGeoJupyter) - testImplementation(libs.kandy.notebook) - testImplementation(libs.kandy.stats) + testImplementation(libs.kandy.notebook) { + exclude("org.jetbrains.kotlinx", "dataframe") + } + testImplementation(libs.kandy.stats) { + exclude("org.jetbrains.kotlinx", "dataframe") + } testImplementation(libs.kotestAssertions) { exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index edf0a98f12..67515d2cd2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -63,7 +63,7 @@ jai-core = "1.1.3" jts = "1.20.0" # Normal examples Kandy versions -kandy = "0.8.2-dev-87" +kandy = "0.8.1-dev-89" # Example notebooks Kandy versions kandy-notebook = "0.8.1n" diff --git a/settings.gradle.kts b/settings.gradle.kts index 89386beb15..e227071c9f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,7 @@ include("plugins:symbol-processor") include("plugins:expressions-converter") include("plugins:kotlin-dataframe") include("plugins:public-api-modifier") -// include("samples") +include("samples") include("dataframe-json") include("dataframe-arrow") include("dataframe-openapi") From e10316040bc1d385c8f79feb508e38459e9b667a Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Wed, 1 Oct 2025 13:53:26 +0400 Subject: [PATCH 12/18] formatHeader docs --- .../resources/api/format/formatHeader.html | 516 ++++++++++++++++++ docs/StardustDocs/topics/format.md | 15 + .../samples/api/render/FormatHeaderSamples.kt | 45 ++ 3 files changed, 576 insertions(+) create mode 100644 docs/StardustDocs/resources/api/format/formatHeader.html create mode 100644 samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/render/FormatHeaderSamples.kt diff --git a/docs/StardustDocs/resources/api/format/formatHeader.html b/docs/StardustDocs/resources/api/format/formatHeader.html new file mode 100644 index 0000000000..574641079b --- /dev/null +++ b/docs/StardustDocs/resources/api/format/formatHeader.html @@ -0,0 +1,516 @@ + + + + + +
+ +

+ + + diff --git a/docs/StardustDocs/topics/format.md b/docs/StardustDocs/topics/format.md index b996532ca1..f48f4914e9 100644 --- a/docs/StardustDocs/topics/format.md +++ b/docs/StardustDocs/topics/format.md @@ -136,3 +136,18 @@ df2.format().perRowCol { row, col -> + +## formatHeader + +> This method is experimental and may be unstable. +> +> {type="warning"} + +Formats the specified column headers. + + + + + + + diff --git a/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/render/FormatHeaderSamples.kt b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/render/FormatHeaderSamples.kt new file mode 100644 index 0000000000..97ca65c8fa --- /dev/null +++ b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/render/FormatHeaderSamples.kt @@ -0,0 +1,45 @@ +package org.jetbrains.kotlinx.dataframe.samples.api.render + +import org.jetbrains.kotlinx.dataframe.DataRow +import org.jetbrains.kotlinx.dataframe.annotations.DataSchema +import org.jetbrains.kotlinx.dataframe.api.cast +import org.jetbrains.kotlinx.dataframe.api.colsOf +import org.jetbrains.kotlinx.dataframe.api.formatHeader +import org.jetbrains.kotlinx.dataframe.api.with +import org.jetbrains.kotlinx.dataframe.samples.DataFrameSampleHelper +import org.junit.Test + +class FormatHeaderSamples: DataFrameSampleHelper("format", "api") { + val df = peopleDf.cast() + + @DataSchema + interface Name { + val firstName: String + val lastName: String + } + + @DataSchema + interface Person { + val age: Int + val city: String? + val name: DataRow // TODO Requires https://code.jetbrains.team/p/kt/repositories/kotlin/reviews/23694 to be merged + val weight: Int? + val isHappy: Boolean + } + + @Test + fun formatHeader() { + //SampleStart + df + // Format all column headers with bold + .formatHeader().with { bold } + // Format the "name" column (including nested) header with red text + .formatHeader { name }.with { textColor(red) } + // Override "name"/"lastName" column formating header with blue text + .formatHeader { name.lastName }.with { textColor(blue) } + // Format all numeric column headers with underlines + .formatHeader { colsOf() }.with { underline } + //SampleEnd + .saveDfHtmlSample() + } +} From 784304020140f8af74f009b15888b28612e2d99a Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Wed, 1 Oct 2025 13:55:54 +0400 Subject: [PATCH 13/18] formatHeader docs korro and sample --- docs/StardustDocs/topics/_shadow_resources.md | 1 + docs/StardustDocs/topics/format.md | 12 ++++++++++++ samples/build.gradle.kts | 1 + 3 files changed, 14 insertions(+) diff --git a/docs/StardustDocs/topics/_shadow_resources.md b/docs/StardustDocs/topics/_shadow_resources.md index f4c5465027..b9e0d4d08e 100644 --- a/docs/StardustDocs/topics/_shadow_resources.md +++ b/docs/StardustDocs/topics/_shadow_resources.md @@ -179,6 +179,7 @@ + \ No newline at end of file diff --git a/docs/StardustDocs/topics/format.md b/docs/StardustDocs/topics/format.md index f48f4914e9..5088935ffc 100644 --- a/docs/StardustDocs/topics/format.md +++ b/docs/StardustDocs/topics/format.md @@ -149,5 +149,17 @@ Formats the specified column headers. +```kotlin +df + // Format all column headers with bold + .formatHeader().with { bold } + // Format the "name" column (including nested) header with red text + .formatHeader { name }.with { textColor(red) } + // Override "name"/"lastName" column formating header with blue text + .formatHeader { name.lastName }.with { textColor(blue) } + // Format all numeric column headers with underlines + .formatHeader { colsOf() }.with { underline } +``` + diff --git a/samples/build.gradle.kts b/samples/build.gradle.kts index aa48e99d95..6311bffd63 100644 --- a/samples/build.gradle.kts +++ b/samples/build.gradle.kts @@ -118,6 +118,7 @@ korro { include("src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/*.kt") include("src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/*.kt") include("src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/utils/*.kt") + include("src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/render/*.kt") include("src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/collectionsInterop/*.kt") include("src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/column/*.kt") include("src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/info/*.kt") From 967f6119e09f8535cf7e0b87ea458f2bded4eaad Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Wed, 1 Oct 2025 14:00:05 +0400 Subject: [PATCH 14/18] ktlint format --- .../dataframe/samples/api/render/FormatHeaderSamples.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/render/FormatHeaderSamples.kt b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/render/FormatHeaderSamples.kt index 97ca65c8fa..cdd717fe04 100644 --- a/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/render/FormatHeaderSamples.kt +++ b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/render/FormatHeaderSamples.kt @@ -9,7 +9,7 @@ import org.jetbrains.kotlinx.dataframe.api.with import org.jetbrains.kotlinx.dataframe.samples.DataFrameSampleHelper import org.junit.Test -class FormatHeaderSamples: DataFrameSampleHelper("format", "api") { +class FormatHeaderSamples : DataFrameSampleHelper("format", "api") { val df = peopleDf.cast() @DataSchema @@ -22,14 +22,14 @@ class FormatHeaderSamples: DataFrameSampleHelper("format", "api") { interface Person { val age: Int val city: String? - val name: DataRow // TODO Requires https://code.jetbrains.team/p/kt/repositories/kotlin/reviews/23694 to be merged + val name: DataRow val weight: Int? val isHappy: Boolean } @Test fun formatHeader() { - //SampleStart + // SampleStart df // Format all column headers with bold .formatHeader().with { bold } @@ -39,7 +39,7 @@ class FormatHeaderSamples: DataFrameSampleHelper("format", "api") { .formatHeader { name.lastName }.with { textColor(blue) } // Format all numeric column headers with underlines .formatHeader { colsOf() }.with { underline } - //SampleEnd + // SampleEnd .saveDfHtmlSample() } } From 3e9f7a7743d641bce84ed9daa4d1f6e950aea543 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Wed, 1 Oct 2025 15:45:25 +0400 Subject: [PATCH 15/18] formatHeader KDocs fix --- .../org/jetbrains/kotlinx/dataframe/api/formatHeader.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt index fd740bba29..0088c4d5a6 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -13,12 +13,12 @@ import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths * A lambda used to format a column header (its displayed name) when rendering a dataframe to HTML. * * The lambda runs in the context of [FormattingDsl] and receives the [ColumnWithPath] of the header to format. - * Return a [CellAttributes] (or `null`) describing CSS you want to apply to the header cell. + * Return a [CellAttributes] (or `null`) describing the CSS you want to apply to the header cell. * * Examples: - * - Center a header: `attr("text-align", "center")` + * - Center the header: `attr("text-align", "center")` * - Make it bold: `bold` - * - Set custom color: `textColor(rgb(10, 10, 10))` + * - Set a custom color: `textColor(rgb(10, 10, 10))` */ public typealias HeaderColFormatter = FormattingDsl.(col: ColumnWithPath) -> CellAttributes? @@ -133,7 +133,8 @@ public fun FormattedFrame.formatHeader(): HeaderFormatClause = * Creates a new [FormattedFrame] that uses the specified [HeaderColFormatter] to format the selected headers. * * Header formatting is additive: attributes from already-applied header formatters are combined with the newly - * returned attributes using [CellAttributes.and]. If a parent column group is selected, its attributes are + * + * returned attributes using [CellAttributes.and]. If a parent column group is selected, its attributes are * applied to its children unless explicitly overridden. */ @Suppress("UNCHECKED_CAST") From 2c50b7829fd58c7d8a94991118b5f21e3d514c73 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Thu, 2 Oct 2025 15:53:09 +0400 Subject: [PATCH 16/18] formatHeader fixes --- .../jetbrains/kotlinx/dataframe/api/format.kt | 2 +- .../kotlinx/dataframe/api/formatHeader.kt | 38 ++--------- .../dataframe/impl/api/formatHeader.kt | 24 +++++++ .../jetbrains/kotlinx/dataframe/io/html.kt | 63 +++++++++++++++++++ .../dataframe/util/deprecationMessages.kt | 5 ++ 5 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/formatHeader.kt diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt index 80bd659083..fd1390ec2a 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt @@ -838,7 +838,7 @@ public class FormattedFrame( public fun getDisplayConfiguration(configuration: DisplayConfiguration): DisplayConfiguration = configuration.copy( cellFormatter = formatter as RowColFormatter<*, *>?, - headerFormatter = headerFormatter as HeaderColFormatter<*>?, + headerFormatter = headerFormatter, ) } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt index 0088c4d5a6..2c09447b7b 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -5,6 +5,7 @@ import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath import org.jetbrains.kotlinx.dataframe.columns.UnresolvedColumnsPolicy import org.jetbrains.kotlinx.dataframe.columns.toColumnSet +import org.jetbrains.kotlinx.dataframe.impl.api.formatHeaderImpl import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths // region docs @@ -48,7 +49,7 @@ public class HeaderFormatClause( /** * **Experimental API. It may be changed in the future.** * - * Selects [columns] whose headers should be formatted. + * Selects [columns] whose headers should be formatted. If unspecified, all columns will be formatted. * * This does not immediately produce a [FormattedFrame]; instead it returns a [HeaderFormatClause] * which must be finalized using [HeaderFormatClause.with]. @@ -138,38 +139,7 @@ public fun FormattedFrame.formatHeader(): HeaderFormatClause = * applied to its children unless explicitly overridden. */ @Suppress("UNCHECKED_CAST") -public fun HeaderFormatClause.with(formatter: HeaderColFormatter): FormattedFrame { - val selectedPaths = df.getColumnPaths(UnresolvedColumnsPolicy.Skip, columns).toSet() - val oldHeader = oldHeaderFormatter - - val composedHeader: HeaderColFormatter = { col -> - val path = col.path - // Merge attributes from selected parents - val parentAttributes = if (path.size > 1) { - val parentPaths = (0 until path.size - 1).map { i -> path.take(i + 1) } - parentPaths - .map { p -> ColumnWithPath(df[p], p) } - .map { parentCol -> - if (parentCol.path in selectedPaths) { - oldHeader?.invoke(FormattingDsl, parentCol as ColumnWithPath) - } else { - null - } - } - .reduceOrNull(CellAttributes?::and) - } else { - null - } - - val typedCol = col as ColumnWithPath - - val existingAttr = oldHeader?.invoke(FormattingDsl, typedCol) - val newAttr = if (path in selectedPaths) formatter(FormattingDsl, typedCol) else null - - parentAttributes and (existingAttr and newAttr) - } - - return FormattedFrame(df, oldCellFormatter, composedHeader) -} +public fun HeaderFormatClause.with(formatter: HeaderColFormatter): FormattedFrame = + formatHeaderImpl(formatter) // endregion diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/formatHeader.kt new file mode 100644 index 0000000000..fc020c1e59 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/formatHeader.kt @@ -0,0 +1,24 @@ +package org.jetbrains.kotlinx.dataframe.impl.api + +import org.jetbrains.kotlinx.dataframe.api.FormattedFrame +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl +import org.jetbrains.kotlinx.dataframe.api.HeaderColFormatter +import org.jetbrains.kotlinx.dataframe.api.HeaderFormatClause +import org.jetbrains.kotlinx.dataframe.api.and +import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath +import org.jetbrains.kotlinx.dataframe.columns.UnresolvedColumnsPolicy +import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths + +internal fun HeaderFormatClause.formatHeaderImpl(formatter: HeaderColFormatter): FormattedFrame { + val selectedPaths = df.getColumnPaths(UnresolvedColumnsPolicy.Skip, columns).toSet() + val oldHeader = oldHeaderFormatter + + val composedHeader: HeaderColFormatter = { col -> + val typedCol = col as ColumnWithPath + val existingAttr = oldHeader?.invoke(FormattingDsl, typedCol) + val newAttr = if (col.path in selectedPaths) formatter(FormattingDsl, typedCol) else null + existingAttr and newAttr + } + + return FormattedFrame(df, oldCellFormatter, composedHeader) +} diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt index ec4d632ce3..7a48c157c5 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt @@ -39,6 +39,8 @@ import org.jetbrains.kotlinx.dataframe.jupyter.RenderedContent import org.jetbrains.kotlinx.dataframe.name import org.jetbrains.kotlinx.dataframe.nrow import org.jetbrains.kotlinx.dataframe.size +import org.jetbrains.kotlinx.dataframe.util.DISPLAY_CONFIGURATION +import org.jetbrains.kotlinx.dataframe.util.DISPLAY_CONFIGURATION_COPY import org.jetbrains.kotlinx.dataframe.util.TO_HTML import org.jetbrains.kotlinx.dataframe.util.TO_HTML_REPLACE import org.jetbrains.kotlinx.dataframe.util.TO_STANDALONE_HTML @@ -873,6 +875,67 @@ public data class DisplayConfiguration( public val DEFAULT: DisplayConfiguration = DisplayConfiguration() } + /** For binary compatibility. */ + @Deprecated( + message = DISPLAY_CONFIGURATION, + level = DeprecationLevel.HIDDEN, + ) + public constructor( + rowsLimit: Int? = 20, + nestedRowsLimit: Int? = 5, + cellContentLimit: Int = 40, + cellFormatter: RowColFormatter<*, *>? = null, + decimalFormat: RendererDecimalFormat = RendererDecimalFormat.DEFAULT, + isolatedOutputs: Boolean = flagFromEnv("LETS_PLOT_HTML_ISOLATED_FRAME"), + localTesting: Boolean = flagFromEnv("KOTLIN_DATAFRAME_LOCAL_TESTING"), + useDarkColorScheme: Boolean = false, + enableFallbackStaticTables: Boolean = true, + downsizeBufferedImage: Boolean = true, + ): this ( + rowsLimit, + nestedRowsLimit, + cellContentLimit, + cellFormatter, + null, + decimalFormat, + isolatedOutputs, + localTesting, + useDarkColorScheme, + enableFallbackStaticTables, + downsizeBufferedImage + ) + + /** For binary compatibility. */ + @Deprecated( + message = DISPLAY_CONFIGURATION_COPY, + level = DeprecationLevel.HIDDEN, + ) + public fun copy( + rowsLimit: Int? = this.rowsLimit, + nestedRowsLimit: Int? = this.nestedRowsLimit, + cellContentLimit: Int = this.cellContentLimit, + cellFormatter: RowColFormatter<*, *>? = this.cellFormatter, + decimalFormat: RendererDecimalFormat = this.decimalFormat, + isolatedOutputs: Boolean = this.isolatedOutputs, + localTesting: Boolean = this.localTesting, + useDarkColorScheme: Boolean = this.useDarkColorScheme, + enableFallbackStaticTables: Boolean = this.enableFallbackStaticTables, + downsizeBufferedImage: Boolean = this.downsizeBufferedImage, + ): DisplayConfiguration = + DisplayConfiguration( + rowsLimit, + nestedRowsLimit, + cellContentLimit, + cellFormatter, + null, + decimalFormat, + isolatedOutputs, + localTesting, + useDarkColorScheme, + enableFallbackStaticTables, + downsizeBufferedImage + ) + /** DSL accessor. */ public operator fun invoke(block: DisplayConfiguration.() -> Unit): DisplayConfiguration = apply(block) } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/util/deprecationMessages.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/util/deprecationMessages.kt index 933d5081e2..92fd9d9266 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/util/deprecationMessages.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/util/deprecationMessages.kt @@ -143,6 +143,11 @@ internal const val INSERT_AFTER_COL_PATH = "This `after()` overload will be removed in favor of `after { }` with Column Selection DSL. $MESSAGE_1_0" internal const val INSERT_AFTER_COL_PATH_REPLACE = "this.after { columnPath }" +internal const val DISPLAY_CONFIGURATION = "This constructor is only here for binary compatibility. $MESSAGE_1_0" + +internal const val DISPLAY_CONFIGURATION_COPY = "This function is only here for binary compatibility. $MESSAGE_1_0" + + // endregion // region WARNING in 1.0, ERROR in 1.1 From 133850f29d6e51e48dc1076f364928d933ee836d Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Thu, 2 Oct 2025 15:55:27 +0400 Subject: [PATCH 17/18] ktlint format --- .../kotlinx/dataframe/api/formatHeader.kt | 2 -- .../jetbrains/kotlinx/dataframe/io/html.kt | 26 +++++++++---------- .../dataframe/util/deprecationMessages.kt | 1 - 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt index 2c09447b7b..9417e97130 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/formatHeader.kt @@ -3,10 +3,8 @@ package org.jetbrains.kotlinx.dataframe.api import org.jetbrains.kotlinx.dataframe.ColumnsSelector import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath -import org.jetbrains.kotlinx.dataframe.columns.UnresolvedColumnsPolicy import org.jetbrains.kotlinx.dataframe.columns.toColumnSet import org.jetbrains.kotlinx.dataframe.impl.api.formatHeaderImpl -import org.jetbrains.kotlinx.dataframe.impl.getColumnPaths // region docs diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt index 7a48c157c5..978368d7c5 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt @@ -881,17 +881,17 @@ public data class DisplayConfiguration( level = DeprecationLevel.HIDDEN, ) public constructor( - rowsLimit: Int? = 20, - nestedRowsLimit: Int? = 5, - cellContentLimit: Int = 40, - cellFormatter: RowColFormatter<*, *>? = null, - decimalFormat: RendererDecimalFormat = RendererDecimalFormat.DEFAULT, - isolatedOutputs: Boolean = flagFromEnv("LETS_PLOT_HTML_ISOLATED_FRAME"), - localTesting: Boolean = flagFromEnv("KOTLIN_DATAFRAME_LOCAL_TESTING"), - useDarkColorScheme: Boolean = false, - enableFallbackStaticTables: Boolean = true, - downsizeBufferedImage: Boolean = true, - ): this ( + rowsLimit: Int? = 20, + nestedRowsLimit: Int? = 5, + cellContentLimit: Int = 40, + cellFormatter: RowColFormatter<*, *>? = null, + decimalFormat: RendererDecimalFormat = RendererDecimalFormat.DEFAULT, + isolatedOutputs: Boolean = flagFromEnv("LETS_PLOT_HTML_ISOLATED_FRAME"), + localTesting: Boolean = flagFromEnv("KOTLIN_DATAFRAME_LOCAL_TESTING"), + useDarkColorScheme: Boolean = false, + enableFallbackStaticTables: Boolean = true, + downsizeBufferedImage: Boolean = true, + ) : this ( rowsLimit, nestedRowsLimit, cellContentLimit, @@ -902,7 +902,7 @@ public data class DisplayConfiguration( localTesting, useDarkColorScheme, enableFallbackStaticTables, - downsizeBufferedImage + downsizeBufferedImage, ) /** For binary compatibility. */ @@ -933,7 +933,7 @@ public data class DisplayConfiguration( localTesting, useDarkColorScheme, enableFallbackStaticTables, - downsizeBufferedImage + downsizeBufferedImage, ) /** DSL accessor. */ diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/util/deprecationMessages.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/util/deprecationMessages.kt index 92fd9d9266..d94069b8d6 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/util/deprecationMessages.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/util/deprecationMessages.kt @@ -147,7 +147,6 @@ internal const val DISPLAY_CONFIGURATION = "This constructor is only here for bi internal const val DISPLAY_CONFIGURATION_COPY = "This function is only here for binary compatibility. $MESSAGE_1_0" - // endregion // region WARNING in 1.0, ERROR in 1.1 From a533a76ecfec60219558cbf7715cf95d3b10470a Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Thu, 2 Oct 2025 15:57:12 +0400 Subject: [PATCH 18/18] dump api --- core/api/core.api | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/api/core.api b/core/api/core.api index 3684a2e55b..2d41fc3077 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -6210,6 +6210,8 @@ public final class org/jetbrains/kotlinx/dataframe/io/DataFrameHtmlData$Companio public final class org/jetbrains/kotlinx/dataframe/io/DisplayConfiguration { public static final field Companion Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration$Companion; + public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Ljava/lang/String;ZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Ljava/lang/String;ZZZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Ljava/lang/String;ZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Ljava/lang/String;ZZZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/Integer; @@ -6224,6 +6226,8 @@ public final class org/jetbrains/kotlinx/dataframe/io/DisplayConfiguration { public final fun component9 ()Z public final fun copy-bMNacXk (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Ljava/lang/String;ZZZZZ)Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration; public static synthetic fun copy-bMNacXk$default (Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Ljava/lang/String;ZZZZZILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration; + public final synthetic fun copy-rqXL5tM (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Ljava/lang/String;ZZZZZ)Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration; + public static synthetic fun copy-rqXL5tM$default (Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/functions/Function3;Ljava/lang/String;ZZZZZILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration; public fun equals (Ljava/lang/Object;)Z public final fun getCellContentLimit ()I public final fun getCellFormatter ()Lkotlin/jvm/functions/Function3;