diff --git a/core/README.md b/core/README.md index e476913ccc..0ff572dfa3 100644 --- a/core/README.md +++ b/core/README.md @@ -18,7 +18,7 @@ and [tests/.../samples/api](../tests/src/test/kotlin/org/jetbrains/kotlinx/dataf and they are copied over to Markdown files in [docs/StardustDocs/topics](../docs/StardustDocs/topics) by [Korro](https://github.com/devcrocod/korro). -### Explainer dataframes +### ~~Explainer dataframes~~ NOTE: This is being moved to [tests](../tests) Aside from code samples, `@TransformDataFrameExpressions` annotated test functions also generate sample dataframe HTML files that can be used as iFrames on the documentation website. diff --git a/core/api/core.api b/core/api/core.api index 14a1033edb..e8e22657d1 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -2217,6 +2217,9 @@ public final class org/jetbrains/kotlinx/dataframe/api/FormatClause { 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; + public static final fun at (Lorg/jetbrains/kotlinx/dataframe/api/FormatClause;Lkotlin/ranges/IntRange;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; + public static final fun at (Lorg/jetbrains/kotlinx/dataframe/api/FormatClause;[I)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; public static final fun format (Lorg/jetbrains/kotlinx/dataframe/DataFrame;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; public static final fun format (Lorg/jetbrains/kotlinx/dataframe/DataFrame;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; public static final fun format (Lorg/jetbrains/kotlinx/dataframe/DataFrame;[Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; @@ -2224,7 +2227,10 @@ public final class org/jetbrains/kotlinx/dataframe/api/FormatKt { public static final fun format (Lorg/jetbrains/kotlinx/dataframe/DataFrame;[Lorg/jetbrains/kotlinx/dataframe/columns/ColumnReference;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; public static final fun format (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; public static final fun format (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; + public static final fun format (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;[Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; public static final fun linearBg (Lorg/jetbrains/kotlinx/dataframe/api/FormatClause;Lkotlin/Pair;Lkotlin/Pair;)Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame; + public static final fun notNull (Lorg/jetbrains/kotlinx/dataframe/api/FormatClause;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; + public static final fun notNull (Lorg/jetbrains/kotlinx/dataframe/api/FormatClause;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame; public static final fun perRowCol (Lorg/jetbrains/kotlinx/dataframe/api/FormatClause;Lkotlin/jvm/functions/Function3;)Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame; public static final fun where (Lorg/jetbrains/kotlinx/dataframe/api/FormatClause;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/FormatClause; public static final fun with (Lorg/jetbrains/kotlinx/dataframe/api/FormatClause;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame; @@ -2240,26 +2246,26 @@ public final class org/jetbrains/kotlinx/dataframe/api/FormattedFrame { public static synthetic fun toStandaloneHtml$default (Lorg/jetbrains/kotlinx/dataframe/api/FormattedFrame;Lorg/jetbrains/kotlinx/dataframe/io/DisplayConfiguration;ILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/io/DataFrameHtmlData; } -public final class org/jetbrains/kotlinx/dataframe/api/FormattingDSL { - public static final field INSTANCE Lorg/jetbrains/kotlinx/dataframe/api/FormattingDSL; +public final class org/jetbrains/kotlinx/dataframe/api/FormattingDsl { + public static final field INSTANCE Lorg/jetbrains/kotlinx/dataframe/api/FormattingDsl; public final fun attr (Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; - public final fun background (Lorg/jetbrains/kotlinx/dataframe/api/RGBColor;)Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; + public final fun background (Lorg/jetbrains/kotlinx/dataframe/api/RgbColor;)Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; public final fun background (SSS)Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; - public final fun getBlack ()Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; - public final fun getBlue ()Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; + public final fun getBlack ()Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; + public final fun getBlue ()Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; public final fun getBold ()Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; - public final fun getDarkGray ()Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; - public final fun getGray ()Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; - public final fun getGreen ()Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; + public final fun getDarkGray ()Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; + public final fun getGray ()Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; + public final fun getGreen ()Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; public final fun getItalic ()Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; - public final fun getLightGray ()Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; - public final fun getRed ()Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; + public final fun getLightGray ()Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; + public final fun getRed ()Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; public final fun getUnderline ()Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; - public final fun getWhite ()Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; - public final fun linear (Ljava/lang/Number;Lkotlin/Pair;Lkotlin/Pair;)Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; + public final fun getWhite ()Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; + public final fun linear (Ljava/lang/Number;Lkotlin/Pair;Lkotlin/Pair;)Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; public final fun linearBg (Ljava/lang/Number;Lkotlin/Pair;Lkotlin/Pair;)Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; - public final fun rgb (SSS)Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; - public final fun textColor (Lorg/jetbrains/kotlinx/dataframe/api/RGBColor;)Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; + public final fun rgb (SSS)Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; + public final fun textColor (Lorg/jetbrains/kotlinx/dataframe/api/RgbColor;)Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; public final fun textColor (SSS)Lorg/jetbrains/kotlinx/dataframe/api/CellAttributes; } @@ -3764,21 +3770,6 @@ public final class org/jetbrains/kotlinx/dataframe/api/PrintKt { public static final fun print-XIlnkTk (Ljava/lang/String;)V } -public final class org/jetbrains/kotlinx/dataframe/api/RGBColor { - public fun (SSS)V - public final fun component1 ()S - public final fun component2 ()S - public final fun component3 ()S - public final fun copy (SSS)Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; - public static synthetic fun copy$default (Lorg/jetbrains/kotlinx/dataframe/api/RGBColor;SSSILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/api/RGBColor; - public fun equals (Ljava/lang/Object;)Z - public final fun getB ()S - public final fun getG ()S - public final fun getR ()S - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - public final class org/jetbrains/kotlinx/dataframe/api/ReducedGroupBy { public fun (Lorg/jetbrains/kotlinx/dataframe/api/GroupBy;Lkotlin/jvm/functions/Function2;)V public final fun getGroupBy ()Lorg/jetbrains/kotlinx/dataframe/api/GroupBy; @@ -3922,6 +3913,21 @@ public final class org/jetbrains/kotlinx/dataframe/api/ReverseKt { public static final fun reverse (Lorg/jetbrains/kotlinx/dataframe/columns/ValueColumn;)Lorg/jetbrains/kotlinx/dataframe/columns/ValueColumn; } +public final class org/jetbrains/kotlinx/dataframe/api/RgbColor { + public fun (SSS)V + public final fun component1 ()S + public final fun component2 ()S + public final fun component3 ()S + public final fun copy (SSS)Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; + public static synthetic fun copy$default (Lorg/jetbrains/kotlinx/dataframe/api/RgbColor;SSSILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/api/RgbColor; + public fun equals (Ljava/lang/Object;)Z + public final fun getB ()S + public final fun getG ()S + public final fun getR ()S + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class org/jetbrains/kotlinx/dataframe/api/SchemaKt { public static final fun schema (Lorg/jetbrains/kotlinx/dataframe/DataFrame;)Lorg/jetbrains/kotlinx/dataframe/schema/DataFrameSchema; public static final fun schema (Lorg/jetbrains/kotlinx/dataframe/DataRow;)Lorg/jetbrains/kotlinx/dataframe/schema/DataFrameSchema; diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/Nulls.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/Nulls.kt index ca9325d359..fd7ac1a2a6 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/Nulls.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/Nulls.kt @@ -55,6 +55,9 @@ internal interface FillNulls { * `[ `__`.`__[**`at`**][org.jetbrains.kotlinx.dataframe.api.Update.at]**`(`**[`rowIndices`][org.jetbrains.kotlinx.dataframe.api.CommonUpdateAtFunctionDoc.RowIndicesParam]**`)`**` ]` * *      + * `[ `__`.`__[**`notNull`**][org.jetbrains.kotlinx.dataframe.api.Update.notNull]**`()`**` ]` + * + *      * __`.`__[**`with`**][org.jetbrains.kotlinx.dataframe.api.Update.with]**` { `**[`rowExpression`][org.jetbrains.kotlinx.dataframe.documentation.ExpressionsGivenRow.RowValueExpression.WithExample]**` }`** * *      @@ -389,6 +392,9 @@ internal interface FillNaNs { * `[ `__`.`__[**`at`**][org.jetbrains.kotlinx.dataframe.api.Update.at]**`(`**[`rowIndices`][org.jetbrains.kotlinx.dataframe.api.CommonUpdateAtFunctionDoc.RowIndicesParam]**`)`**` ]` * *      + * `[ `__`.`__[**`notNull`**][org.jetbrains.kotlinx.dataframe.api.Update.notNull]**`()`**` ]` + * + *      * __`.`__[**`with`**][org.jetbrains.kotlinx.dataframe.api.Update.with]**` { `**[`rowExpression`][org.jetbrains.kotlinx.dataframe.documentation.ExpressionsGivenRow.RowValueExpression.WithExample]**` }`** * *      @@ -662,6 +668,9 @@ internal interface FillNA { * `[ `__`.`__[**`at`**][org.jetbrains.kotlinx.dataframe.api.Update.at]**`(`**[`rowIndices`][org.jetbrains.kotlinx.dataframe.api.CommonUpdateAtFunctionDoc.RowIndicesParam]**`)`**` ]` * *      + * `[ `__`.`__[**`notNull`**][org.jetbrains.kotlinx.dataframe.api.Update.notNull]**`()`**` ]` + * + *      * __`.`__[**`with`**][org.jetbrains.kotlinx.dataframe.api.Update.with]**` { `**[`rowExpression`][org.jetbrains.kotlinx.dataframe.documentation.ExpressionsGivenRow.RowValueExpression.WithExample]**` }`** * *      diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt index 7da093d464..49457cba2b 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt @@ -649,7 +649,7 @@ public inline fun Convert.asColumn( * ```kotlin * // Convert values in all columns to `String` and add their column name to the end * df.convert { all() }.perRowCol { row, col -> - * row[col].toString() + col.name() + * col[row].toString() + col.name() * } * ``` * diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt index d21d4d4fa2..d73bb95820 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt @@ -4,30 +4,493 @@ import org.jetbrains.kotlinx.dataframe.ColumnsSelector import org.jetbrains.kotlinx.dataframe.DataColumn import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.DataRow +import org.jetbrains.kotlinx.dataframe.RowColumnExpression import org.jetbrains.kotlinx.dataframe.RowValueFilter import org.jetbrains.kotlinx.dataframe.annotations.AccessApiOverload +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.attr +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.black +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.green +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linearBg +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor import org.jetbrains.kotlinx.dataframe.columns.ColumnReference import org.jetbrains.kotlinx.dataframe.columns.toColumnSet +import org.jetbrains.kotlinx.dataframe.dataTypes.IFRAME +import org.jetbrains.kotlinx.dataframe.dataTypes.IMG +import org.jetbrains.kotlinx.dataframe.documentation.ExportAsHtml +import org.jetbrains.kotlinx.dataframe.documentation.SelectingColumns +import org.jetbrains.kotlinx.dataframe.documentation.SelectingRows import org.jetbrains.kotlinx.dataframe.impl.api.MergedAttributes import org.jetbrains.kotlinx.dataframe.impl.api.SingleAttribute import org.jetbrains.kotlinx.dataframe.impl.api.encode import org.jetbrains.kotlinx.dataframe.impl.api.formatImpl import org.jetbrains.kotlinx.dataframe.impl.api.linearGradient +import org.jetbrains.kotlinx.dataframe.index import org.jetbrains.kotlinx.dataframe.io.DataFrameHtmlData import org.jetbrains.kotlinx.dataframe.io.DisplayConfiguration import org.jetbrains.kotlinx.dataframe.io.toHtml import org.jetbrains.kotlinx.dataframe.io.toStandaloneHtml +import org.jetbrains.kotlinx.dataframe.jupyter.RenderedContent.Companion.media import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API import kotlin.reflect.KProperty -// region DataFrame +// region docs -// region format +/** + * Formats the specified [columns] or cells within this dataframe such that + * they have specific CSS attributes applied to them when rendering the dataframe to HTML. + * + * This function does not immediately produce a [FormattedFrame], but instead it selects the columns to be formatted + * and returns a [FormatClause] which serves as an intermediate step. + * + * This can include [column groups][org.jetbrains.kotlinx.dataframe.columns.ColumnGroup] and nested columns. + * + * See [Selecting Columns][FormatSelectingColumns]. + * + * The [FormatClause] allows to further narrow down the selection to individual cells + * by selecting only certain rows, using [where][FormatClause.where], + * and then finally specify how to format the cells using + * [with][FormatClause.with], [perRowCol][FormatClause.perRowCol], or [linearBg][FormatClause.linearBg]. + * + * You can continue formatting the [FormattedFrame] by calling [format][FormattedFrame.format] on it again. + * + * Check out the [Grammar]. + * + * For more information: [See `format` on the documentation website.](https://kotlin.github.io/dataframe/format.html) + */ +internal interface FormatDocs { + /** + * + * ## Selecting Columns + * Selecting columns for various operations (including but not limited to + * [DataFrame.select][org.jetbrains.kotlinx.dataframe.DataFrame.select], [DataFrame.update][org.jetbrains.kotlinx.dataframe.DataFrame.update], [DataFrame.gather][org.jetbrains.kotlinx.dataframe.DataFrame.gather], and [DataFrame.fillNulls][org.jetbrains.kotlinx.dataframe.DataFrame.fillNulls]) + * can be done in the following ways: + * ### 1. [Columns Selection DSL][org.jetbrains.kotlinx.dataframe.documentation.SelectingColumns.Dsl.WithExample] + * Select or express columns using the [Columns Selection DSL][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl]. + * (Any (combination of) [Access API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi]). + * + * This DSL is initiated by a [Columns Selector][org.jetbrains.kotlinx.dataframe.ColumnsSelector] lambda, + * which operates in the context of the [Columns Selection DSL][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl] and + * expects you to return a [SingleColumn][org.jetbrains.kotlinx.dataframe.columns.SingleColumn] or [ColumnSet][org.jetbrains.kotlinx.dataframe.columns.ColumnSet] (so, a [ColumnsResolver][org.jetbrains.kotlinx.dataframe.columns.ColumnsResolver]). + * This is an entity formed by calling any (combination) of the functions + * in the DSL that is or can be resolved into one or more columns. + * + * #### NOTE: + * While you can use the [String API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.StringApi] and [KProperties API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.KPropertiesApi] + * in this DSL directly with any function, they are NOT valid return types for the + * [Columns Selector][org.jetbrains.kotlinx.dataframe.ColumnsSelector] lambda. You'd need to turn them into a [ColumnReference][org.jetbrains.kotlinx.dataframe.columns.ColumnReference] first, for instance + * with a function like [`col("name")`][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl.col]. + * + * ### Check out: [Columns Selection DSL Grammar][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl.DslGrammar] + * + *      + * + * [See Column Selectors on the documentation website.](https://kotlin.github.io/dataframe/columnselectors.html) + * + * #### For example: + * + * `df.`[format][org.jetbrains.kotlinx.dataframe.api.format]` { length `[and][org.jetbrains.kotlinx.dataframe.api.AndColumnsSelectionDsl.and]` age }` + * + * `df.`[format][org.jetbrains.kotlinx.dataframe.api.format]` { `[cols][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl.cols]`(1..5) }` + * + * `df.`[format][org.jetbrains.kotlinx.dataframe.api.format]` { `[colsOf][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl.colsOf]`<`[Double][Double]`>() }` + * + * + * #### NOTE: There's also a 'single column' variant used sometimes: [Column Selection DSL][org.jetbrains.kotlinx.dataframe.documentation.SelectingColumns.DslSingle.WithExample]. + * ### 2. [Column names][org.jetbrains.kotlinx.dataframe.documentation.SelectingColumns.ColumnNames.WithExample] + * Select columns using their [column names][String] + * ([String API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.StringApi]). + * + * #### For example: + * + * `df.`[format][org.jetbrains.kotlinx.dataframe.api.format]`("length", "age")` + * + * ### 3. [Column references][org.jetbrains.kotlinx.dataframe.documentation.SelectingColumns.ColumnAccessors.WithExample] + * Select columns using [column accessors][org.jetbrains.kotlinx.dataframe.columns.ColumnReference] + * ([Column Accessors API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.ColumnAccessorsApi]). + * + * #### For example: + * + * `val length by `[column][org.jetbrains.kotlinx.dataframe.api.column]`<`[Double][Double]`>()` + * + * `val age by `[column][org.jetbrains.kotlinx.dataframe.api.column]`<`[Double][Double]`>()` + * + * `df.`[format][org.jetbrains.kotlinx.dataframe.api.format]`(length, age)` + * + * ### 4. [KProperties][org.jetbrains.kotlinx.dataframe.documentation.SelectingColumns.KProperties.WithExample] + * Select columns using [KProperties][KProperty] ([KProperties API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.KPropertiesApi]). + * + * #### For example: + * ```kotlin + * data class Person(val length: Double, val age: Double) + * ``` + * + * `df.`[format][org.jetbrains.kotlinx.dataframe.api.format]`(Person::length, Person::age)` + * + */ + interface FormatSelectingColumns + + /** + * ## Format Operation Grammar + * + *      + * + * [(What is this notation?)][org.jetbrains.kotlinx.dataframe.documentation.DslGrammar] + * + * ### Definitions: + * `cellFormatter: `[FormattingDsl][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.FormattingDslGrammarDef]`.(cell: C) -> `[CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes]`?` + * + *      + * + * `rowColFormatter: `[FormattingDsl][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.FormattingDslGrammarDef]`.(row: `[DataRow][org.jetbrains.kotlinx.dataframe.DataRow]`, col: `[DataColumn][org.jetbrains.kotlinx.dataframe.DataColumn]`) -> `[CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes]`?` + * + * ### Notation: + * + * [**format**][org.jetbrains.kotlinx.dataframe.DataFrame.format]**` { `**[`columns`][org.jetbrains.kotlinx.dataframe.documentation.SelectingColumns]**` }`** + * + *      + * `[ `__`.`__[**`where`**][org.jetbrains.kotlinx.dataframe.api.FormatClause.where]**` { `**[`filter`][org.jetbrains.kotlinx.dataframe.documentation.SelectingRows.RowValueCondition]`: `[`RowValueFilter`][org.jetbrains.kotlinx.dataframe.RowValueFilter]**` } `**`]` + * + *      + * `[ `__`.`__[**`at`**][org.jetbrains.kotlinx.dataframe.api.FormatClause.at]**`(`**`rowIndices: `[Collection][Collection]`<`[Int][Int]`> | `[IntRange][IntRange]` | `**`vararg`**` `[Int][Int]**`)`**` ]` + * + *      + * `[ `__`.`__[**`notNull`**][org.jetbrains.kotlinx.dataframe.api.FormatClause.notNull]**`()`**` ]` + * + *      + * __`.`__[**`with`**][org.jetbrains.kotlinx.dataframe.api.FormatClause.with]**` { `**[cellFormatter][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellFormatterDef]**` }`** + * + *      + * `| `__`.`__[**`notNull`**][org.jetbrains.kotlinx.dataframe.api.FormatClause.notNull]**` { `**[cellFormatter][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellFormatterDef]**` }`** + * + *      + * `| `__`.`__[**`perRowCol`**][org.jetbrains.kotlinx.dataframe.api.FormatClause.perRowCol]**` { `**[rowColFormatter][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.RowColFormatterDef]**` }`** + * + *      + * `| `__`.`__[**`linearBg`**][org.jetbrains.kotlinx.dataframe.api.FormatClause.linearBg]**`(`**`from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`)`** + * + * `[ `__`.`__[**format**][org.jetbrains.kotlinx.dataframe.api.FormattedFrame.format]` ↺ ]` + * + *      + * + * ## Formatting DSL Grammar + * + * ### Definitions: + * `cellAttributes: `[CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] + * + *      + * + * `color: `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor] + * + * ### Notation: + * _- Returning [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes]_: + * + * [cellAttributes][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellAttributesDef]` `[**`and`**][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` `[cellAttributes][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellAttributesDef] + * + * `| `[**`italic`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.italic]` | `[**`bold`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.bold]` | `[**`underline`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.underline] + * + * `| `[**`background`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background]**`(`**[color][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.RgbColorDef]**`)`** + * + * `| `[**`background`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`linearBg`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linearBg]**`(`**`value: `[Number][Number]**`,`**` from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`)`** + * + * `| `[**`textColor`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor]**`(`**[color][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.RgbColorDef]**`)`** + * + * `| `[**`textColor`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`attr`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.attr]**`(`**`name: `[String][String]**`,`**` value: `[String][String]**`)`** + * + * _- Returning [RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]:_ + * + * [**`black`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.black]` | `[**`white`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.white]` | `[**`green`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.green]` | `[**`red`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.red]` | `[**`blue`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.blue]` | `[**`gray`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.gray]` | `[**`darkGray`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.darkGray]` | `[**`lightGray`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.lightGray] + * + * `| `[**`rgb`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`linear`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear]**`(`**`value: `[Number][Number]**`,`**` from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`)`** + */ + interface Grammar { + + /** + * ### Definitions: + * `cellFormatter: `[FormattingDsl][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.FormattingDslGrammarDef]`.(cell: C) -> `[CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes]`?` + * + *      + * + * `rowColFormatter: `[FormattingDsl][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.FormattingDslGrammarDef]`.(row: `[DataRow][org.jetbrains.kotlinx.dataframe.DataRow]`, col: `[DataColumn][org.jetbrains.kotlinx.dataframe.DataColumn]`) -> `[CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes]`?` + * + * ### Notation: + * + * [**format**][DataFrame.format]**` { `**[`columns`][SelectingColumns]**` }`** + * + *      + * `[ `__`.`__[**`where`**][FormatClause.where]**` { `**[`filter`][SelectingRows.RowValueCondition]`: `[`RowValueFilter`][RowValueFilter]**` } `**`]` + * + *      + * `[ `__`.`__[**`at`**][FormatClause.at]**`(`**`rowIndices: `[Collection][Collection]`<`[Int][Int]`> | `[IntRange][IntRange]` | `**`vararg`**` `[Int][Int]**`)`**` ]` + * + *      + * `[ `__`.`__[**`notNull`**][FormatClause.notNull]**`()`**` ]` + * + *      + * __`.`__[**`with`**][FormatClause.with]**` { `**[cellFormatter][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellFormatterDef]**` }`** + * + *      + * `| `__`.`__[**`notNull`**][FormatClause.notNull]**` { `**[cellFormatter][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellFormatterDef]**` }`** + * + *      + * `| `__`.`__[**`perRowCol`**][FormatClause.perRowCol]**` { `**[rowColFormatter][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.RowColFormatterDef]**` }`** + * + *      + * `| `__`.`__[**`linearBg`**][FormatClause.linearBg]**`(`**`from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`)`** + * + * `[ `__`.`__[**format**][FormattedFrame.format]` ↺ ]` + * + *      + * + * ## Formatting DSL Grammar + * + * ### Definitions: + * `cellAttributes: `[CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] + * + *      + * + * `color: `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor] + * + * ### Notation: + * _- Returning [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes]_: + * + * [cellAttributes][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellAttributesDef]` `[**`and`**][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` `[cellAttributes][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellAttributesDef] + * + * `| `[**`italic`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.italic]` | `[**`bold`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.bold]` | `[**`underline`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.underline] + * + * `| `[**`background`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background]**`(`**[color][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.RgbColorDef]**`)`** + * + * `| `[**`background`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`linearBg`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linearBg]**`(`**`value: `[Number][Number]**`,`**` from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`)`** + * + * `| `[**`textColor`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor]**`(`**[color][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.RgbColorDef]**`)`** + * + * `| `[**`textColor`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`attr`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.attr]**`(`**`name: `[String][String]**`,`**` value: `[String][String]**`)`** + * + * _- Returning [RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]:_ + * + * [**`black`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.black]` | `[**`white`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.white]` | `[**`green`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.green]` | `[**`red`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.red]` | `[**`blue`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.blue]` | `[**`gray`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.gray]` | `[**`darkGray`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.darkGray]` | `[**`lightGray`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.lightGray] + * + * `| `[**`rgb`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`linear`**][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear]**`(`**`value: `[Number][Number]**`,`**` from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor]`>`**`)`** + */ + @ExportAsHtml + interface ForHtml + + /** + * ## Formatting DSL Grammar + * + * ### Definitions: + * `cellAttributes: `[CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] + * + *      + * + * `color: `[RgbColor][org.jetbrains.kotlinx.dataframe.api.RgbColor] + * + * ### Notation: + * _- Returning [CellAttributes][CellAttributes]_: + * + * [cellAttributes][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellAttributesDef]` `[**`and`**][CellAttributes.and]` `[cellAttributes][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.CellAttributesDef] + * + * `| `[**`italic`**][FormattingDsl.italic]` | `[**`bold`**][FormattingDsl.bold]` | `[**`underline`**][FormattingDsl.underline] + * + * `| `[**`background`**][FormattingDsl.background]**`(`**[color][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.RgbColorDef]**`)`** + * + * `| `[**`background`**][FormattingDsl.background]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`linearBg`**][FormattingDsl.linearBg]**`(`**`value: `[Number][Number]**`,`**` from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`)`** + * + * `| `[**`textColor`**][FormattingDsl.textColor]**`(`**[color][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.RgbColorDef]**`)`** + * + * `| `[**`textColor`**][FormattingDsl.textColor]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`attr`**][attr]**`(`**`name: `[String][String]**`,`**` value: `[String][String]**`)`** + * + * _- Returning [RgbColor][RgbColor]:_ + * + * [**`black`**][FormattingDsl.black]` | `[**`white`**][FormattingDsl.white]` | `[**`green`**][FormattingDsl.green]` | `[**`red`**][FormattingDsl.red]` | `[**`blue`**][FormattingDsl.blue]` | `[**`gray`**][FormattingDsl.gray]` | `[**`darkGray`**][FormattingDsl.darkGray]` | `[**`lightGray`**][FormattingDsl.lightGray] + * + * `| `[**`rgb`**][FormattingDsl.rgb]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`linear`**][FormattingDsl.linear]**`(`**`value: `[Number][Number]**`,`**` from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`)`** + */ + interface FormattingDslGrammarDef + + /** + * `cellFormatter: `[FormattingDsl][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.FormattingDslGrammarDef]`.(cell: C) -> `[CellAttributes][CellAttributes]`?` + */ + interface CellFormatterDef + + /** + * `rowColFormatter: `[FormattingDsl][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.FormattingDslGrammarDef]`.(row: `[DataRow][DataRow]`, col: `[DataColumn][DataColumn]`) -> `[CellAttributes][CellAttributes]`?` + */ + interface RowColFormatterDef + + /** + * `cellAttributes: `[CellAttributes][CellAttributes] + */ + interface CellAttributesDef + + /** + * `color: `[RgbColor][RgbColor] + */ + interface RgbColorDef + } +} + +// endregion + +// region DataFrame format + +/** + * Formats the specified [columns] or cells within this dataframe such that + * they have specific CSS attributes applied to them when rendering the dataframe to HTML. + * + * This function does not immediately produce a [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame], but instead it selects the columns to be formatted + * and returns a [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] which serves as an intermediate step. + * + * This can include [column groups][org.jetbrains.kotlinx.dataframe.columns.ColumnGroup] and nested columns. + * + * See [Selecting Columns][org.jetbrains.kotlinx.dataframe.api.FormatDocs.FormatSelectingColumns]. + * + * The [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] allows to further narrow down the selection to individual cells + * by selecting only certain rows, using [where][org.jetbrains.kotlinx.dataframe.api.FormatClause.where], + * and then finally specify how to format the cells using + * [with][org.jetbrains.kotlinx.dataframe.api.FormatClause.with], [perRowCol][org.jetbrains.kotlinx.dataframe.api.FormatClause.perRowCol], or [linearBg][org.jetbrains.kotlinx.dataframe.api.FormatClause.linearBg]. + * + * You can continue formatting the [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame] by calling [format][org.jetbrains.kotlinx.dataframe.api.FormattedFrame.format] on it again. + * + * Check out the [Grammar][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar]. + * + * For more information: [See `format` on the documentation website.](https://kotlin.github.io/dataframe/format.html) + * ### This Format Overload + * Select or express columns using the [Columns Selection DSL][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl]. + * (Any (combination of) [Access API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi]). + * + * This DSL is initiated by a [Columns Selector][org.jetbrains.kotlinx.dataframe.ColumnsSelector] lambda, + * which operates in the context of the [Columns Selection DSL][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl] and + * expects you to return a [SingleColumn][org.jetbrains.kotlinx.dataframe.columns.SingleColumn] or [ColumnSet][org.jetbrains.kotlinx.dataframe.columns.ColumnSet] (so, a [ColumnsResolver][org.jetbrains.kotlinx.dataframe.columns.ColumnsResolver]). + * This is an entity formed by calling any (combination) of the functions + * in the DSL that is or can be resolved into one or more columns. + * + * #### NOTE: + * While you can use the [String API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.StringApi] and [KProperties API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.KPropertiesApi] + * in this DSL directly with any function, they are NOT valid return types for the + * [Columns Selector][org.jetbrains.kotlinx.dataframe.ColumnsSelector] lambda. You'd need to turn them into a [ColumnReference][org.jetbrains.kotlinx.dataframe.columns.ColumnReference] first, for instance + * with a function like [`col("name")`][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl.col]. + * + * ### Check out: [Columns Selection DSL Grammar][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl.DslGrammar] + * + *      + * + * [See Column Selectors on the documentation website.](https://kotlin.github.io/dataframe/columnselectors.html) + * ### Examples: + * ```kt + * df.format { temperature }.linearBg(-20 to FormattingDsl.blue, 50 to FormattingDsl.red) + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * @param [columns] The [columns-selector][ColumnsSelector] used to select the columns to be formatted. + * If unspecified, all columns will be formatted. + */ public fun DataFrame.format(columns: ColumnsSelector): FormatClause = FormatClause(this, columns) +/** + * Formats the specified [columns] or cells within this dataframe such that + * they have specific CSS attributes applied to them when rendering the dataframe to HTML. + * + * This function does not immediately produce a [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame], but instead it selects the columns to be formatted + * and returns a [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] which serves as an intermediate step. + * + * This can include [column groups][org.jetbrains.kotlinx.dataframe.columns.ColumnGroup] and nested columns. + * + * See [Selecting Columns][org.jetbrains.kotlinx.dataframe.api.FormatDocs.FormatSelectingColumns]. + * + * The [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] allows to further narrow down the selection to individual cells + * by selecting only certain rows, using [where][org.jetbrains.kotlinx.dataframe.api.FormatClause.where], + * and then finally specify how to format the cells using + * [with][org.jetbrains.kotlinx.dataframe.api.FormatClause.with], [perRowCol][org.jetbrains.kotlinx.dataframe.api.FormatClause.perRowCol], or [linearBg][org.jetbrains.kotlinx.dataframe.api.FormatClause.linearBg]. + * + * You can continue formatting the [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame] by calling [format][org.jetbrains.kotlinx.dataframe.api.FormattedFrame.format] on it again. + * + * Check out the [Grammar][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar]. + * + * For more information: [See `format` on the documentation website.](https://kotlin.github.io/dataframe/format.html) + * ### This Format Overload + * Select columns using their [column names][String] + * ([String API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.StringApi]). + * ### Examples: + * ```kt + * df.format("temperature").with { linearBg(it as Number, -20 to blue, 50 to red) } + * .format("age").notNull().perRowCol { row, col -> + * col as DataColumn + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * @param [columns] The names of the columns to be formatted. + * If unspecified, all columns will be formatted. + */ public fun DataFrame.format(vararg columns: String): FormatClause = format { columns.toColumnSet() } +/** + * Formats the specified [columns] or cells within this dataframe such that + * they have specific CSS attributes applied to them when rendering the dataframe to HTML. + * + * This function does not immediately produce a [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame], but instead it selects the columns to be formatted + * and returns a [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] which serves as an intermediate step. + * + * This can include [column groups][org.jetbrains.kotlinx.dataframe.columns.ColumnGroup] and nested columns. + * + * See [Selecting Columns][org.jetbrains.kotlinx.dataframe.api.FormatDocs.FormatSelectingColumns]. + * + * The [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] allows to further narrow down the selection to individual cells + * by selecting only certain rows, using [where][org.jetbrains.kotlinx.dataframe.api.FormatClause.where], + * and then finally specify how to format the cells using + * [with][org.jetbrains.kotlinx.dataframe.api.FormatClause.with], [perRowCol][org.jetbrains.kotlinx.dataframe.api.FormatClause.perRowCol], or [linearBg][org.jetbrains.kotlinx.dataframe.api.FormatClause.linearBg]. + * + * You can continue formatting the [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame] by calling [format][org.jetbrains.kotlinx.dataframe.api.FormattedFrame.format] on it again. + * + * Check out the [Grammar][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar]. + * + * For more information: [See `format` on the documentation website.](https://kotlin.github.io/dataframe/format.html) + * ### This Format Overload + * + * This simply formats all columns. Optionally, you can specify which columns to format using a + * [columns-selector][ColumnsSelector] or by [column names][String]. + * + * ### Examples: + * ```kt + * df.format().with { background(white) and textColor(black) and bold } + * .format { temperature }.linearBg(-20 to FormattingDsl.blue, 50 to FormattingDsl.red) + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + */ +public fun DataFrame.format(): FormatClause = FormatClause(this) + @Deprecated(DEPRECATED_ACCESS_API) @AccessApiOverload public fun DataFrame.format(vararg columns: ColumnReference): FormatClause = @@ -38,30 +501,432 @@ public fun DataFrame.format(vararg columns: ColumnReference): Forma public fun DataFrame.format(vararg columns: KProperty): FormatClause = format { columns.toColumnSet() } -public fun DataFrame.format(): FormatClause = FormatClause(this) +// endregion + +// region FormattedFrame format + +/** + * Formats the specified [columns] or cells within this dataframe such that + * they have specific CSS attributes applied to them when rendering the dataframe to HTML. + * + * This function does not immediately produce a [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame], but instead it selects the columns to be formatted + * and returns a [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] which serves as an intermediate step. + * + * This can include [column groups][org.jetbrains.kotlinx.dataframe.columns.ColumnGroup] and nested columns. + * + * See [Selecting Columns][org.jetbrains.kotlinx.dataframe.api.FormatDocs.FormatSelectingColumns]. + * + * The [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] allows to further narrow down the selection to individual cells + * by selecting only certain rows, using [where][org.jetbrains.kotlinx.dataframe.api.FormatClause.where], + * and then finally specify how to format the cells using + * [with][org.jetbrains.kotlinx.dataframe.api.FormatClause.with], [perRowCol][org.jetbrains.kotlinx.dataframe.api.FormatClause.perRowCol], or [linearBg][org.jetbrains.kotlinx.dataframe.api.FormatClause.linearBg]. + * + * You can continue formatting the [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame] by calling [format][org.jetbrains.kotlinx.dataframe.api.FormattedFrame.format] on it again. + * + * Check out the [Grammar][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar]. + * + * For more information: [See `format` on the documentation website.](https://kotlin.github.io/dataframe/format.html) + * ### This Format Overload + * Select or express columns using the [Columns Selection DSL][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl]. + * (Any (combination of) [Access API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi]). + * + * This DSL is initiated by a [Columns Selector][org.jetbrains.kotlinx.dataframe.ColumnsSelector] lambda, + * which operates in the context of the [Columns Selection DSL][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl] and + * expects you to return a [SingleColumn][org.jetbrains.kotlinx.dataframe.columns.SingleColumn] or [ColumnSet][org.jetbrains.kotlinx.dataframe.columns.ColumnSet] (so, a [ColumnsResolver][org.jetbrains.kotlinx.dataframe.columns.ColumnsResolver]). + * This is an entity formed by calling any (combination) of the functions + * in the DSL that is or can be resolved into one or more columns. + * + * #### NOTE: + * While you can use the [String API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.StringApi] and [KProperties API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.KPropertiesApi] + * in this DSL directly with any function, they are NOT valid return types for the + * [Columns Selector][org.jetbrains.kotlinx.dataframe.ColumnsSelector] lambda. You'd need to turn them into a [ColumnReference][org.jetbrains.kotlinx.dataframe.columns.ColumnReference] first, for instance + * with a function like [`col("name")`][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl.col]. + * + * ### Check out: [Columns Selection DSL Grammar][org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl.DslGrammar] + * + *      + * + * [See Column Selectors on the documentation website.](https://kotlin.github.io/dataframe/columnselectors.html) + * ### Examples: + * ```kt + * df.format().with { background(white) and textColor(black) and bold } + * .format { temperature }.linearBg(-20 to FormattingDsl.blue, 50 to FormattingDsl.red) + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * @param [columns] The [columns-selector][ColumnsSelector] used to select the columns to be formatted. + * If unspecified, all columns will be formatted. + */ +public fun FormattedFrame.format(columns: ColumnsSelector): FormatClause = + FormatClause(df, columns, formatter) + +/** + * Formats the specified [columns] or cells within this dataframe such that + * they have specific CSS attributes applied to them when rendering the dataframe to HTML. + * + * This function does not immediately produce a [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame], but instead it selects the columns to be formatted + * and returns a [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] which serves as an intermediate step. + * + * This can include [column groups][org.jetbrains.kotlinx.dataframe.columns.ColumnGroup] and nested columns. + * + * See [Selecting Columns][org.jetbrains.kotlinx.dataframe.api.FormatDocs.FormatSelectingColumns]. + * + * The [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] allows to further narrow down the selection to individual cells + * by selecting only certain rows, using [where][org.jetbrains.kotlinx.dataframe.api.FormatClause.where], + * and then finally specify how to format the cells using + * [with][org.jetbrains.kotlinx.dataframe.api.FormatClause.with], [perRowCol][org.jetbrains.kotlinx.dataframe.api.FormatClause.perRowCol], or [linearBg][org.jetbrains.kotlinx.dataframe.api.FormatClause.linearBg]. + * + * You can continue formatting the [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame] by calling [format][org.jetbrains.kotlinx.dataframe.api.FormattedFrame.format] on it again. + * + * Check out the [Grammar][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar]. + * + * For more information: [See `format` on the documentation website.](https://kotlin.github.io/dataframe/format.html) + * ### This Format Overload + * Select columns using their [column names][String] + * ([String API][org.jetbrains.kotlinx.dataframe.documentation.AccessApi.StringApi]). + * ### Examples: + * ```kt + * df.format("temperature").with { linearBg(it as Number, -20 to blue, 50 to red) } + * .format("age").notNull().perRowCol { row, col -> + * col as DataColumn + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * @param [columns] The names of the columns to be formatted. + * If unspecified, all columns will be formatted. + */ +public fun FormattedFrame.format(vararg columns: String): FormatClause = + format { columns.toColumnSet() } + +/** + * Formats the specified [columns] or cells within this dataframe such that + * they have specific CSS attributes applied to them when rendering the dataframe to HTML. + * + * This function does not immediately produce a [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame], but instead it selects the columns to be formatted + * and returns a [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] which serves as an intermediate step. + * + * This can include [column groups][org.jetbrains.kotlinx.dataframe.columns.ColumnGroup] and nested columns. + * + * See [Selecting Columns][org.jetbrains.kotlinx.dataframe.api.FormatDocs.FormatSelectingColumns]. + * + * The [FormatClause][org.jetbrains.kotlinx.dataframe.api.FormatClause] allows to further narrow down the selection to individual cells + * by selecting only certain rows, using [where][org.jetbrains.kotlinx.dataframe.api.FormatClause.where], + * and then finally specify how to format the cells using + * [with][org.jetbrains.kotlinx.dataframe.api.FormatClause.with], [perRowCol][org.jetbrains.kotlinx.dataframe.api.FormatClause.perRowCol], or [linearBg][org.jetbrains.kotlinx.dataframe.api.FormatClause.linearBg]. + * + * You can continue formatting the [FormattedFrame][org.jetbrains.kotlinx.dataframe.api.FormattedFrame] by calling [format][org.jetbrains.kotlinx.dataframe.api.FormattedFrame.format] on it again. + * + * Check out the [Grammar][org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar]. + * + * For more information: [See `format` on the documentation website.](https://kotlin.github.io/dataframe/format.html) + * ### This Format Overload + * + * This simply formats all columns. Optionally, you can specify which columns to format using a + * [columns-selector][ColumnsSelector] or by [column names][String]. + * + * ### Examples: + * ```kt + * df.format { temperature }.with { textColor(linear(-20 to FormattingDsl.blue, 50 to FormattingDsl.red)) } + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * } + * .format().with { background(white) and bold } + * .toStandaloneHtml().openInBrowser() + * ``` + */ +public fun FormattedFrame.format(): FormatClause = FormatClause(df, null, formatter) + +// endregion + +// region intermediate operations + +/** + * Filters the rows to format using a [RowValueFilter]. + * + * See [Row Condition][org.jetbrains.kotlinx.dataframe.documentation.SelectingRows]. + * + * You need to specify [filter]: A lambda function expecting a `true` result for each + * cell that should be included in the formatting selection. + * Both the cell value (`it: `[C][C]) and its row (`this: `[DataRow][DataRow]`<`[T][T]`>`) are available. + * + * ### Examples using [where]: + * ```kt + * df.format { temperature } + * .where { it !in -10..40 } + * .with { background(red) } + * ``` + * + * 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) + +/** + * Only format the selected columns at given row indices. + * + * Accepts either a [Collection]<[Int]>, an [IntRange], or just `vararg `[Int] indices. + * + * ### Examples using [at][org.jetbrains.kotlinx.dataframe.api.at] + * ```kt + * df.format() + * .at(df.indices().step(2).toList()) + * .with { background(lightGray) } + * ``` + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +public fun FormatClause.at(rowIndices: Collection): FormatClause = where { index in rowIndices } + +/** + * Only format the selected columns at given row indices. + * + * Accepts either a [Collection]<[Int]>, an [IntRange], or just `vararg `[Int] indices. + * + * ### Examples using [at][org.jetbrains.kotlinx.dataframe.api.at] + * ```kt + * df.format { colsOf() } + * .at(0, 3, 4) + * .with { background(lightGray) } + * ``` + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +public fun FormatClause.at(vararg rowIndices: Int): FormatClause = at(rowIndices.toSet()) + +/** + * Only format the selected columns at given row indices. + * + * Accepts either a [Collection]<[Int]>, an [IntRange], or just `vararg `[Int] indices. + * + * ### Examples using [at][org.jetbrains.kotlinx.dataframe.api.at] + * ```kt + * df.format { cols(2..7) } + * .at(2..7) + * .with { background(lightGray) } + * ``` + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +public fun FormatClause.at(rowRange: IntRange): FormatClause = where { index in rowRange } + +/** + * Filters the format-selection to only include cells where the value is not null. + * + * This is shorthand for `.`[where][FormatClause.where]` { it != null }`. + * + * ### Examples using [notNull]: + * ```kt + * df.format { colsOf() }.notNull().perRowCol { row, col -> + * linearBg(col[row], col.min() to red, col.max() to green) + * } + * ``` + */ +@Suppress("UNCHECKED_CAST") +public fun FormatClause.notNull(): FormatClause = where { it != null } as FormatClause // endregion +// region terminal operations + +/** + * Creates a new [FormattedFrame] that uses the specified [RowColFormatter] to format the selected cells of the dataframe. + * + * You need to specify [formatter]: A lambda function expecting a [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] or `null` given an instance of + * [DataRow][org.jetbrains.kotlinx.dataframe.DataRow]`<`[T][T]`>` and [DataColumn][org.jetbrains.kotlinx.dataframe.DataColumn]`<`[C][C]`>`. + * + * This is similar to a [RowColumnExpression][org.jetbrains.kotlinx.dataframe.RowColumnExpression], except that you also have access + * to the [FormattingDsl][org.jetbrains.kotlinx.dataframe.api.FormattingDsl] in the context. + * + * The formatting DSL allows you to create and combine [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] to apply to one + * or multiple cells of a dataframe such that they have specific CSS attributes applied to them + * when rendered to HTML. + * + * For instance, to specify black, bold text on a white background, you could write: + * + * [background][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background]`(`[white][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.white]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [textColor][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor]`(`[black][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.black]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [bold][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.bold] + * + * It's also possible to define your own colors using [rgb][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb] or interpolate + * colors using [linear][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear]. + * + * Use [attr][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.attr] if you want to specify a custom CSS attribute. + * + * ### Examples using [perRowCol]: + * ```kt + * df.format { colsOf() }.perRowCol { row, col -> + * linearBg(col[row], col.min() to red, col.max() to green) + * } + * ``` + * + * Check out the full [Grammar][FormatDocs.Grammar]. + */ public fun FormatClause.perRowCol(formatter: RowColFormatter): FormattedFrame = formatImpl(formatter) +/** + * Creates a new [FormattedFrame] that uses the specified [CellFormatter] to format the selected cells of the dataframe. + * + * You need to specify [formatter]: A lambda function expecting a [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] or `null` given an instance of a cell: [C] of the dataframe. + * + * You have access to the [FormattingDsl][org.jetbrains.kotlinx.dataframe.api.FormattingDsl] in the context. + * + * The formatting DSL allows you to create and combine [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] to apply to one + * or multiple cells of a dataframe such that they have specific CSS attributes applied to them + * when rendered to HTML. + * + * For instance, to specify black, bold text on a white background, you could write: + * + * [background][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background]`(`[white][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.white]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [textColor][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor]`(`[black][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.black]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [bold][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.bold] + * + * It's also possible to define your own colors using [rgb][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb] or interpolate + * colors using [linear][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear]. + * + * Use [attr][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.attr] if you want to specify a custom CSS attribute. + * + * ### Examples using [with]: + * ```kt + * df.format() + * .at(df.indices().step(2).toList()) + * .with { background(lightGray) and bold and textColor(black) } + * ``` + * + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +@Suppress("UNCHECKED_CAST") public fun FormatClause.with(formatter: CellFormatter): FormattedFrame = - formatImpl { row, col -> formatter(row[col]) } + formatImpl { row, col -> formatter(row[col.name] as C) } -public fun FormatClause.where(filter: RowValueFilter): FormatClause = - FormatClause(filter = filter, df = df, columns = columns, oldFormatter = oldFormatter) +/** + * Creates a new [FormattedFrame] that uses the specified [CellFormatter] to format selected non-null cells of the dataframe. + * + * This function is shorthand for `.`[notNull()][FormatClause.notNull]`.`[with { }][FormatClause.with]. + * + * You need to specify [formatter]: A lambda function expecting a [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] or `null` given an instance of a cell: [C] of the dataframe. + * + * You have access to the [FormattingDsl][org.jetbrains.kotlinx.dataframe.api.FormattingDsl] in the context. + * + * The formatting DSL allows you to create and combine [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] to apply to one + * or multiple cells of a dataframe such that they have specific CSS attributes applied to them + * when rendered to HTML. + * + * For instance, to specify black, bold text on a white background, you could write: + * + * [background][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background]`(`[white][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.white]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [textColor][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor]`(`[black][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.black]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [bold][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.bold] + * + * It's also possible to define your own colors using [rgb][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb] or interpolate + * colors using [linear][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear]. + * + * Use [attr][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.attr] if you want to specify a custom CSS attribute. + * + * ### Examples using [notNull]: + * ```kt + * df.format().notNull { bold and textColor(black) } + * ``` + * + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +public fun FormatClause.notNull(formatter: CellFormatter): FormattedFrame = + notNull().with(formatter) -public fun FormattedFrame.format(): FormatClause = FormatClause(df, null, formatter) +/** + * Creates a new [FormattedFrame] by just changing the background colors of the selected cells. + * + * The background color of each selected cell is calculated by interpolating between [from] and [to], + * given the numeric value of that cell. + * The interpolation is linear. + * + * If the numeric cell value falls outside the range [from]..[to], the colors at the bounds will be used. + * + * This function is shorthand for: + * + * `.`[with][FormatClause.with]` { `[background][FormattingDsl.background]`(`[linear][FormattingDsl.linear]`(it, `[from][from]`, `[to][to]`)) }` + * + * See also [with][FormatClause.with], [background][FormattingDsl.background], and [linear][FormattingDsl.linear]. + * + * ### Examples using [linearBg]: + * ```kt + * df.format { temperature }.linearBg(-20 to FormattingDsl.blue, 50 to FormattingDsl.red) + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * Check out the full [Grammar][FormatDocs.Grammar]. + * + * @param [from] The lower bound of the interpolation range and the color that will be returned when the cell value touches this bound. + * @param [to] The upper bound of the interpolation range and the color that will be returned when the cell value touches this bound. + */ +public fun FormatClause.linearBg( + from: Pair, + to: Pair, +): FormattedFrame = + with { + if (it != null) { + background(linear(it, from, to)) + } else { + null + } + } // endregion -public data class RGBColor(val r: Short, val g: Short, val b: Short) +// region Formatting DSL + +/** + * Represents a color in the RGB color space. + * To be used in the [DataFrame.format]; [FormattingDsl]. + * + * Any color can be represented in terms of [r] (red), [g] (green), and [b] (blue) values from `0..255`. + * + * Inside [FormattingDsl], there are shortcuts for common colors, like [white][FormattingDsl.white], + * [green][FormattingDsl.green], and [gray][FormattingDsl.gray]. + */ +public data class RgbColor(val r: Short, val g: Short, val b: Short) { + /** Encodes the color as a [String] such that it can be used as the value of an attribute in CSS. */ + override fun toString(): String = encode() +} + +/** + * This represents a collection of CSS cell attributes that can be applied to a cell in an HTML-rendered dataframe. + * + * [Cell attributes][CellAttributes] are created inside the [FormattingDsl] by calling + * [FormatClause.with] or [FormatClause.perRowCol]. + * + * Multiple attributes can be combined using the [and] operator. + * + * For instance: + * + * `df.`[format()][DataFrame.format]`.`[`with {`][FormatClause.with]` `[background][FormattingDsl.background]`(`[white][FormattingDsl.white]`) `[and][CellAttributes.and]` `[textColor][FormattingDsl.textColor]`(`[black][FormattingDsl.black]`) `[`}`][FormatClause.with] + * + * @see [CellAttributes.and] + */ public interface CellAttributes { + /** Retrieves all CSS cell attributes as a list of name-value pairs. */ public fun attributes(): List> } +/** + * Combines two [CellAttributes] instances into a new one that combines their attributes. + * + * For instance: + * + * `df.`[format()][DataFrame.format]`.`[`with {`][FormatClause.with]` `[background][FormattingDsl.background]`(`[white][FormattingDsl.white]`) `[and][CellAttributes.and]` `[textColor][FormattingDsl.textColor]`(`[black][FormattingDsl.black]`) `[`}`][FormatClause.with] + */ public infix fun CellAttributes?.and(other: CellAttributes?): CellAttributes? = when { other == null -> this @@ -69,73 +934,274 @@ public infix fun CellAttributes?.and(other: CellAttributes?): CellAttributes? = else -> MergedAttributes(listOf(this, other)) } -public object FormattingDSL { - public fun rgb(r: Short, g: Short, b: Short): RGBColor = RGBColor(r, g, b) +/** + * The formatting DSL allows you to create and combine [CellAttributes] to apply to one + * or multiple cells of a dataframe such that they have specific CSS attributes applied to them + * when rendered to HTML. + * + * For instance, to specify black, bold text on a white background, you could write: + * + * [background][FormattingDsl.background]`(`[white][FormattingDsl.white]`) `[and][CellAttributes.and]` ` + * [textColor][FormattingDsl.textColor]`(`[black][FormattingDsl.black]`) `[and][CellAttributes.and]` ` + * [bold][FormattingDsl.bold] + * + * It's also possible to define your own colors using [rgb][FormattingDsl.rgb] or interpolate + * colors using [linear][FormattingDsl.linear]. + * + * Use [attr] if you want to specify a custom CSS attribute. + */ +public object FormattingDsl { + + /** Creates a new [RgbColor] instance with [r] (red), [g] (green), and [b] (blue) values from `0..255`. */ + public fun rgb(r: Short, g: Short, b: Short): RgbColor = RgbColor(r, g, b) - public val black: RGBColor = rgb(0, 0, 0) + public val black: RgbColor = rgb(0, 0, 0) - public val white: RGBColor = rgb(255, 255, 255) + public val white: RgbColor = rgb(255, 255, 255) - public val green: RGBColor = rgb(0, 255, 0) + public val green: RgbColor = rgb(0, 255, 0) - public val red: RGBColor = rgb(255, 0, 0) + public val red: RgbColor = rgb(255, 0, 0) - public val blue: RGBColor = rgb(0, 0, 255) + public val blue: RgbColor = rgb(0, 0, 255) - public val gray: RGBColor = rgb(128, 128, 128) + public val gray: RgbColor = rgb(128, 128, 128) - public val darkGray: RGBColor = rgb(169, 169, 169) + public val darkGray: RgbColor = rgb(169, 169, 169) - public val lightGray: RGBColor = rgb(211, 211, 211) + public val lightGray: RgbColor = rgb(211, 211, 211) + /** + * A custom [cell attribute][CellAttributes] + * that allows you to specify any custom CSS attribute by [name] and [value]. + * + * For example: + * ```kt + * attr("text-align", "center") + * attr("border", "3px solid green") + * ``` + */ public fun attr(name: String, value: String): CellAttributes = SingleAttribute(name, value) - public fun background(color: RGBColor): CellAttributes = attr("background-color", color.encode()) + /** + * A [cell attribute][CellAttributes] that sets the background color of a cell. + * @param color Either one of the predefined colors, like [black], or [green], or a custom color using [rgb()][rgb]. + */ + public fun background(color: RgbColor): CellAttributes = attr("background-color", color.toString()) - public fun background(r: Short, g: Short, b: Short): CellAttributes = background(RGBColor(r, g, b)) + /** + * A [cell attribute][CellAttributes] that sets the background color of a cell. + * A shortcut for [background][background]`(`[rgb(...)][rgb]`)`. + * @see [rgb] + */ + public fun background(r: Short, g: Short, b: Short): CellAttributes = background(RgbColor(r, g, b)) - public fun textColor(color: RGBColor): CellAttributes = attr("color", color.encode()) + /** + * A [cell attribute][CellAttributes] that sets the text color of a cell. + * @param color Either one of the predefined colors, like [black], or [green], or a custom color using [rgb()][rgb]. + */ + public fun textColor(color: RgbColor): CellAttributes = attr("color", color.toString()) - public fun textColor(r: Short, g: Short, b: Short): CellAttributes = textColor(RGBColor(r, g, b)) + /** + * A [cell attribute][CellAttributes] that sets the text color of a cell. + * A shortcut for [textColor][textColor]`(`[rgb(...)][rgb]`)`. + * @see [rgb] + */ + public fun textColor(r: Short, g: Short, b: Short): CellAttributes = textColor(RgbColor(r, g, b)) + /** A [cell attribute][CellAttributes] that makes the text inside the cell *italic*. */ public val italic: CellAttributes = attr("font-style", "italic") + /** A [cell attribute][CellAttributes] that makes the text inside the cell **bold**. */ public val bold: CellAttributes = attr("font-weight", "bold") + /** A [cell attribute][CellAttributes] that u͟n͟d͟e͟r͟l͟i͟n͟e͟s͟ the text inside the cell. */ public val underline: CellAttributes = attr("text-decoration", "underline") - public fun linearBg(value: Number, from: Pair, to: Pair): CellAttributes = + /** + * Shorthand for [background][background]`(`[linear][linear]`(...))` + * + * Creates a [cell attribute][CellAttributes] that applies a background color calculated + * by interpolating between [from] and [to], given [value]. + * + * See [linear] for more information. + * + * @see linear + * @see background + */ + public fun linearBg(value: Number, from: Pair, to: Pair): CellAttributes = background( linear(value, from, to), ) - public fun linear(value: Number, from: Pair, to: Pair): RGBColor { + /** + * Calculates an [RgbColor] by interpolating between [from] and [to], given [value]. + * The interpolation is linear. + * If [value] falls outside the range [from]..[to], the colors at the bounds will be used. + * + * Very useful if you want the text-, or background color to correspond to the value of a cell, for instance. + * + * For example: + * ```kt + * df.format { temperature }.with { value -> + * background(linear(value, -20 to blue, 40 to red)) and + * textColor(black) + * } + * ``` + * + * @param [value] The value to interpolate the color for. + * @param [from] The lower bound of the interpolation range and the color that will be returned when [value] touches this bound. + * @param [to] The upper bound of the interpolation range and the color that will be returned when [value] touches this bound. + * @return An [RgbColor] that corresponds to the interpolation. + * @see linearBg + */ + public fun linear(value: Number, from: Pair, to: Pair): RgbColor { val a = from.first.toDouble() val b = to.first.toDouble() - if (a < b) return linearGradient(value.toDouble(), a, from.second, b, to.second) - return linearGradient(value.toDouble(), b, to.second, a, from.second) + return if (a < b) { + linearGradient( + x = value.toDouble(), + minValue = a, + minColor = from.second, + maxValue = b, + maxColor = to.second, + ) + } else { + linearGradient( + x = value.toDouble(), + minValue = b, + minColor = to.second, + maxValue = a, + maxColor = from.second, + ) + } } } -public typealias RowColFormatter = FormattingDSL.(DataRow, DataColumn) -> CellAttributes? +// endregion + +// region types and classes + +/** + * A lambda function expecting a [CellAttributes] or `null` given an instance of + * [DataRow][DataRow]`<`[T][T]`>` and [DataColumn][DataColumn]`<`[C][C]`>`. + * + * This is similar to a [RowColumnExpression], except that you also have access + * to the [FormattingDsl] in the context. + * + * The formatting DSL allows you to create and combine [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] to apply to one + * or multiple cells of a dataframe such that they have specific CSS attributes applied to them + * when rendered to HTML. + * + * For instance, to specify black, bold text on a white background, you could write: + * + * [background][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background]`(`[white][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.white]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [textColor][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor]`(`[black][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.black]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [bold][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.bold] + * + * It's also possible to define your own colors using [rgb][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb] or interpolate + * colors using [linear][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear]. + * + * Use [attr][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.attr] if you want to specify a custom CSS attribute. + */ +public typealias RowColFormatter = FormattingDsl.(row: DataRow, col: DataColumn) -> CellAttributes? +/** + * A lambda function expecting a [CellAttributes] or `null` given an instance of a cell: [C] of the dataframe. + * + * You have access to the [FormattingDsl] in the context. + * + * The formatting DSL allows you to create and combine [CellAttributes][org.jetbrains.kotlinx.dataframe.api.CellAttributes] to apply to one + * or multiple cells of a dataframe such that they have specific CSS attributes applied to them + * when rendered to HTML. + * + * For instance, to specify black, bold text on a white background, you could write: + * + * [background][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background]`(`[white][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.white]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [textColor][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor]`(`[black][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.black]`) `[and][org.jetbrains.kotlinx.dataframe.api.CellAttributes.and]` ` + * [bold][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.bold] + * + * It's also possible to define your own colors using [rgb][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb] or interpolate + * colors using [linear][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear]. + * + * Use [attr][org.jetbrains.kotlinx.dataframe.api.FormattingDsl.attr] if you want to specify a custom CSS attribute. + */ +public typealias CellFormatter = FormattingDsl.(cell: C) -> CellAttributes? + +/** + * A wrapper around a [DataFrame][df] with CSS attributes that can be + * converted to a formatted HTML table in the form of [DataFrameHtmlData]. + * + * Call [toHtml] or [toStandaloneHtml] to get the HTML representation of the [DataFrame]. + * + * 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) { + /** - * @return DataFrameHtmlData without additional definitions. Can be rendered in Jupyter kernel environments + * Returns a [DataFrameHtmlData] without additional definitions. + * Can be rendered in Jupyter kernel (Notebook) environments or other environments that already have + * CSS- and script definitions for DataFrame. + * Use [toStandaloneHtml] if you need the [DataFrameHtmlData] to include CSS- and script definitions. + * + * By default, cell content is formatted as text + * Use [RenderedContent.media][media] or [IMG], [IFRAME] if you need custom HTML inside a cell. + * + * @param [configuration] The [DisplayConfiguration] to use as a base for this [FormattedFrame]. + * Default: [DisplayConfiguration.DEFAULT]. + * @see toStandaloneHtml */ public fun toHtml(configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT): DataFrameHtmlData = df.toHtml(getDisplayConfiguration(configuration)) /** - * @return DataFrameHtmlData with table script and css definitions. Can be saved as an *.html file and displayed in the browser + * Returns a [DataFrameHtmlData] with CSS- and script definitions for DataFrame. + * + * The [DataFrameHtmlData] can be saved as an *.html file and displayed in the browser. + * If you save it as a file and find it in the project tree, + * the ["Open in browser"](https://www.jetbrains.com/help/idea/editing-html-files.html#ws_html_preview_output_procedure) + * feature of IntelliJ IDEA will automatically reload the file content when it's updated. + * + * By default, cell content is formatted as text + * Use [RenderedContent.media][media] or [IMG], [IFRAME] if you need custom HTML inside a cell. + * + * @param [configuration] The [DisplayConfiguration] to use as a base for this [FormattedFrame]. + * Default: [DisplayConfiguration.DEFAULT]. + * @see toHtml */ public fun toStandaloneHtml(configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT): DataFrameHtmlData = df.toStandaloneHtml(getDisplayConfiguration(configuration)) + /** 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<*, *>?) } +/** + * An intermediate class used in the [format] operation. + * + * This class itself does nothing—it is just a transitional step before specifying + * how to format the selected columns. + * It must be followed by one of the positioning methods + * to produce a new [FormattedFrame]; a [DataFrame] with HTML formatting data. + * + * Use the following function to filter the rows to format: + * - [where][FormatClause.where] – filters the rows to format using a [RowValueFilter]. + * - [at][FormatClause.at] – Only format in rows with certain indices. + * - [notNull][FormatClause.notNull] – Only format cells that have non-null values. + * + * Use the following functions to finalize this formatting round: + * - [with][FormatClause.with] – Specifies how to format the cells using a [CellFormatter]. + * - [perRowCol][FormatClause.perRowCol] – Specifies how to format each cell individually using a [RowColFormatter]. + * - [linearBg][FormatClause.linearBg] – + * Interpolates between two colors to set the background color of each numeric cell based on its value. + * Shorthand for `.`[with][FormatClause.with]` { `[background][FormattingDsl.background]`(`[linear][FormattingDsl.linear]`(it, from, to)) }` + * - [notNull][FormatClause.notNull] – Specifies how to format non-null cells using a [CellFormatter]. + * Shorthand for `.`[notNull()][FormatClause.notNull]`.`[with { }][FormatClause.with]. + * + * See [Grammar][FormatDocs.Grammar] for more details. + */ public class FormatClause( internal val df: DataFrame, internal val columns: ColumnsSelector? = null, @@ -146,19 +1212,4 @@ public class FormatClause( "FormatClause(df=$df, columns=$columns, oldFormatter=$oldFormatter, filter=$filter)" } -public fun FormattedFrame.format(columns: ColumnsSelector): FormatClause = - FormatClause(df, columns, formatter) - -public typealias CellFormatter = FormattingDSL.(V) -> CellAttributes? - -public fun FormatClause.linearBg( - from: Pair, - to: Pair, -): FormattedFrame = - with { - if (it != null) { - background(linear(it, from, to)) - } else { - null - } - } +// endregion diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/update.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/update.kt index f5631b148b..4ccd715c1b 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/update.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/update.kt @@ -3,6 +3,7 @@ package org.jetbrains.kotlinx.dataframe.api import org.jetbrains.kotlinx.dataframe.AnyRow import org.jetbrains.kotlinx.dataframe.ColumnExpression import org.jetbrains.kotlinx.dataframe.ColumnsSelector +import org.jetbrains.kotlinx.dataframe.DataColumn import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.DataFrameExpression import org.jetbrains.kotlinx.dataframe.DataRow @@ -11,6 +12,7 @@ import org.jetbrains.kotlinx.dataframe.RowValueFilter import org.jetbrains.kotlinx.dataframe.annotations.AccessApiOverload import org.jetbrains.kotlinx.dataframe.annotations.Interpretable import org.jetbrains.kotlinx.dataframe.annotations.Refine +import org.jetbrains.kotlinx.dataframe.api.mean import org.jetbrains.kotlinx.dataframe.columns.ColumnGroup import org.jetbrains.kotlinx.dataframe.columns.ColumnReference import org.jetbrains.kotlinx.dataframe.columns.toColumnSet @@ -69,6 +71,9 @@ public class Update( * `[ `__`.`__[**`at`**][Update.at]**`(`**[`rowIndices`][CommonUpdateAtFunctionDoc.RowIndicesParam]**`)`**` ]` * *      + * `[ `__`.`__[**`notNull`**][Update.notNull]**`()`**` ]` + * + *      * __`.`__[**`with`**][Update.with]**` { `**[`rowExpression`][ExpressionsGivenRow.RowValueExpression.WithExample]**` }`** * *      @@ -611,25 +616,21 @@ internal infix fun RowValueFilter?.and(other: RowValueFilter) return { thisExp(this, it) && other(this, it) } } -/** ## Not Null - * - * Selects only the rows where the values in the selected columns are not null. +/** + * ## Not Null + * Filters the update-selection to only include cells where the value is not null. * - * Shorthand for: [update][org.jetbrains.kotlinx.dataframe.api.update]` { ... }.`[where][org.jetbrains.kotlinx.dataframe.api.Update.where]` { it != null }` + * This is shorthand for `.`[where][Update.where]` { it != null }`. * * For example: * - * `df.`[update][org.jetbrains.kotlinx.dataframe.api.update]` { `[colsOf][org.jetbrains.kotlinx.dataframe.api.colsOf]`<`[Number][Number]`?>() }.`[notNull][org.jetbrains.kotlinx.dataframe.api.notNull]`().`[perCol][org.jetbrains.kotlinx.dataframe.api.Update.perCol]` { `[mean][org.jetbrains.kotlinx.dataframe.api.mean]`() }` + * `df.`[update][update]` { `[colsOf][colsOf]`<`[Int][Int]`?>() }.`[notNull][notNull]`().`[perRowCol][Update.perRowCol]` { row, col ->` * - * ### Optional - * Provide an [expression] to update the rows with. - * This combines [with][org.jetbrains.kotlinx.dataframe.api.Update.with] with [notNull][org.jetbrains.kotlinx.dataframe.api.notNull]. - * - * For example: + *     `row[col] / col.`[mean][DataColumn.mean]`(skipNA = true)` * - * `df.`[update][org.jetbrains.kotlinx.dataframe.api.update]` { city }.`[notNull][org.jetbrains.kotlinx.dataframe.api.Update.notNull]` { it.`[toUpperCase][String.toUpperCase]`() }` - * - * @param expression Optional [Row Expression][org.jetbrains.kotlinx.dataframe.documentation.ExpressionsGivenRow.RowExpression.WithExample] to update the rows with. */ + * `}` + */ +@Suppress("UNCHECKED_CAST") @Interpretable("UpdateNotNullDefault") public fun Update.notNull(): Update = where { it != null } as Update diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DslGrammar.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DslGrammar.kt index 85f62a95c0..a389a88648 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DslGrammar.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DslGrammar.kt @@ -3,7 +3,7 @@ package org.jetbrains.kotlinx.dataframe.documentation /** * ## DSL Grammar * - * If you've come across notations like **`a(`** `(`**`b`**` | [`**`c, .. `**`] )` **`)`** + * If you've come across notations like **`a(`**` (`**`b`**` | [`**`c, .. `**`] ) `**`)`** * either in the KDocs or on the website and would like some further explanation * for what it means, you've come to the right place. * diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt index a1c0fce0c3..579c48213f 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/format.kt @@ -3,8 +3,8 @@ package org.jetbrains.kotlinx.dataframe.impl.api import org.jetbrains.kotlinx.dataframe.api.CellAttributes import org.jetbrains.kotlinx.dataframe.api.FormatClause import org.jetbrains.kotlinx.dataframe.api.FormattedFrame -import org.jetbrains.kotlinx.dataframe.api.FormattingDSL -import org.jetbrains.kotlinx.dataframe.api.RGBColor +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl +import org.jetbrains.kotlinx.dataframe.api.RgbColor import org.jetbrains.kotlinx.dataframe.api.RowColFormatter import org.jetbrains.kotlinx.dataframe.api.and import org.jetbrains.kotlinx.dataframe.api.cast @@ -24,10 +24,10 @@ internal fun encRgb(r: Short, g: Short, b: Short): String = "#${encHex(r)}${encH internal fun encHex(v: Short): String = "${(v / 16).toString(16)}${(v % 16).toString(16)}" -internal fun RGBColor.encode() = encRgb(r, g, b) +internal fun RgbColor.encode() = encRgb(r, g, b) -internal fun componentWise(color1: RGBColor, color2: RGBColor, f: (Short, Short) -> Short) = - RGBColor( +internal fun componentWise(color1: RgbColor, color2: RgbColor, f: (Short, Short) -> Short) = + RgbColor( f(color1.r, color2.r), f(color1.g, color2.g), f(color1.b, color2.b), @@ -36,35 +36,37 @@ internal fun componentWise(color1: RGBColor, color2: RGBColor, f: (Short, Short) internal fun linearGradient( x: Double, minValue: Double, - minColor: RGBColor, + minColor: RgbColor, maxValue: Double, - maxColor: RGBColor, -): RGBColor { + maxColor: RgbColor, +): RgbColor { if (x < minValue) return minColor if (x > maxValue) return maxColor val t = (x - minValue) / (maxValue - minValue) - return componentWise(minColor, maxColor) { cmin, cmax -> - (cmin + t * (cmax - cmin)).toInt().toShort() + return componentWise(minColor, maxColor) { cMin, cMax -> + (cMin + t * (cMax - cMin)).toInt().toShort() } } +@Suppress("UNCHECKED_CAST") internal inline fun FormatClause.formatImpl( crossinline formatter: RowColFormatter, ): FormattedFrame { + val clause = this val columns = - if (columns != null) { - df.getColumnsWithPaths(columns) - .mapNotNull { if (it.depth == 0) it.name else null } + if (clause.columns != null) { + clause.df.getColumnsWithPaths(clause.columns) + .mapNotNull { if (it.depth == 0) it.name else null } // TODO Causes #1356 .toSet() } else { null } - return FormattedFrame(df) { row, col -> - val oldAttributes = oldFormatter?.invoke(FormattingDSL, row, col.cast()) + return FormattedFrame(clause.df) { row, col -> + val oldAttributes = clause.oldFormatter?.invoke(FormattingDsl, row, col.cast()) if (columns == null || columns.contains(col.name())) { - val value = row[col] as C - if (filter == null || filter(row, value)) { - oldAttributes and formatter(FormattingDSL, row.cast(), col.cast()) + val value = row[col.name] as C + if (clause.filter(row, value)) { + oldAttributes and formatter(FormattingDsl, row.cast(), col.cast()) } else { oldAttributes } diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt index 6994580bab..33ec189259 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt @@ -6,10 +6,11 @@ import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.AnyRow import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.FormattedFrame -import org.jetbrains.kotlinx.dataframe.api.FormattingDSL +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl import org.jetbrains.kotlinx.dataframe.api.RowColFormatter import org.jetbrains.kotlinx.dataframe.api.asColumnGroup import org.jetbrains.kotlinx.dataframe.api.asNumbers +import org.jetbrains.kotlinx.dataframe.api.format import org.jetbrains.kotlinx.dataframe.api.getColumnsWithPaths import org.jetbrains.kotlinx.dataframe.api.isColumnGroup import org.jetbrains.kotlinx.dataframe.api.isEmpty @@ -170,7 +171,7 @@ internal fun AnyFrame.toHtmlData( } val renderConfig = configuration.copy(decimalFormat = format) val contents = values.map { - val value = it[col] + val value = col[it] val content = value.toDataFrameLikeOrNull() if (content != null) { val df = content.df() @@ -178,16 +179,31 @@ internal fun AnyFrame.toHtmlData( HtmlContent("", null) } else { val id = nextTableId() - queue.add(RenderingQueueItem(df, id, content.configuration(defaultConfiguration))) + queue += RenderingQueueItem(df, id, content.configuration(defaultConfiguration)) DataFrameReference(id, df.size) } } else { val html = formatter.format(downsizeBufferedImageIfNeeded(value, renderConfig), cellRenderer, renderConfig) val style = renderConfig.cellFormatter - ?.invoke(FormattingDSL, it, col) + ?.invoke(FormattingDsl, it, col) ?.attributes() ?.ifEmpty { null } + ?.flatMap { + if (it.first == "color") { + // override all --text-color* variables that + // are used to color text of .numbers, .null, etc., inside DataFrame + listOf( + it, + "--text-color" to "${it.second} !important", + "--text-color-dark" to "${it.second} !important", + "--text-color-pale" to "${it.second} !important", + "--text-color-medium" to "${it.second} !important", + ) + } else { + listOf(it) + } + } ?.joinToString(";") { "${it.first}:${it.second}" } HtmlContent(html, style) } @@ -206,7 +222,7 @@ internal fun AnyFrame.toHtmlData( } val rootId = nextTableId() - queue.add(RenderingQueueItem(this, rootId, defaultConfiguration)) + queue += RenderingQueueItem(this, rootId, defaultConfiguration) while (!queue.isEmpty()) { val (nextDf, nextId, configuration) = queue.pop() val rowsLimit = if (nextId == rootId) configuration.rowsLimit else configuration.nestedRowsLimit @@ -546,9 +562,13 @@ public fun DataFrame.toStandaloneHTML( * By default, cell content is formatted as text * Use [RenderedContent.media] or [IMG], [IFRAME] if you need custom HTML inside a cell. * - * The [DataFrameHtmlData] be saved as an *.html file and displayed in the browser. + * To change the formatting of certain cells or columns in the dataframe, + * use [DataFrame.format]. + * + * The [DataFrameHtmlData] can be saved as an *.html file and displayed in the browser. * If you save it as a file and find it in the project tree, - * the ["Open in browser"](https://www.jetbrains.com/help/idea/editing-html-files.html#ws_html_preview_output_procedure) feature of IntelliJ IDEA will automatically reload the file content when it's updated + * the ["Open in browser"](https://www.jetbrains.com/help/idea/editing-html-files.html#ws_html_preview_output_procedure) + * feature of IntelliJ IDEA will automatically reload the file content when it's updated. * @return DataFrameHtmlData with table script and css definitions */ public fun DataFrame.toStandaloneHtml( @@ -560,6 +580,10 @@ public fun DataFrame.toStandaloneHtml( /** * By default, cell content is formatted as text * Use [RenderedContent.media] or [IMG], [IFRAME] if you need custom HTML inside a cell. + * + * To change the formatting of certain cells or columns in the dataframe, + * use [DataFrame.format]. + * * @return DataFrameHtmlData without additional definitions. Can be rendered in Jupyter kernel environments */ public fun DataFrame.toHtml( @@ -597,8 +621,10 @@ public fun DataFrame.toHtml( } /** - * Container for HTML page data in the form of a String - * Can be used to compose rendered dataframe tables with additional HTML elements + * Container for HTML data, often containing a dataframe table. + * + * It can be used to compose rendered dataframe tables with additional HTML elements, + * or to simply print the HTML or write it to file. */ public class DataFrameHtmlData( @Language("css") public val style: String = "", @@ -735,6 +761,9 @@ public class DataFrameHtmlData( } /** + * A collection of settings for rendering dataframes as HTML tables or native + * Kotlin Notebook table output. + * * @param rowsLimit null to disable rows limit * @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; diff --git a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/JoinWith.kt b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/JoinWith.kt index 104d427176..e105fd2b63 100644 --- a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/JoinWith.kt +++ b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/JoinWith.kt @@ -8,10 +8,10 @@ import kotlinx.datetime.toJavaLocalDate import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.annotations.DataSchema -import org.jetbrains.kotlinx.dataframe.api.FormattingDSL +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl import org.jetbrains.kotlinx.dataframe.api.Infer import org.jetbrains.kotlinx.dataframe.api.JoinedDataRow -import org.jetbrains.kotlinx.dataframe.api.RGBColor +import org.jetbrains.kotlinx.dataframe.api.RgbColor import org.jetbrains.kotlinx.dataframe.api.and import org.jetbrains.kotlinx.dataframe.api.cast import org.jetbrains.kotlinx.dataframe.api.colsOf @@ -73,7 +73,7 @@ class JoinWith : TestBase() { LocalDate(2023, 7, 10), 2, ).cast() - class ColoredValue(val value: T, val backgroundColor: RGBColor, val textColor: RGBColor) { + class ColoredValue(val value: T, val backgroundColor: RgbColor, val textColor: RgbColor) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -108,18 +108,18 @@ class JoinWith : TestBase() { colsAtAnyDepth().colsOf?>() }.with(Infer.Type) { it?.value } - private fun T.colored(background: RGBColor, text: RGBColor) = ColoredValue(this, background, text) + private fun T.colored(background: RgbColor, text: RgbColor) = ColoredValue(this, background, text) - private fun T.winter(background: RGBColor = RGBColor(179, 205, 224), text: RGBColor = RGBColor(0, 0, 51)) = + private fun T.winter(background: RgbColor = RgbColor(179, 205, 224), text: RgbColor = RgbColor(0, 0, 51)) = ColoredValue(this, background, text) - private fun T.spring(background: RGBColor = RGBColor(204, 235, 197), text: RGBColor = RGBColor(0, 51, 0)) = + private fun T.spring(background: RgbColor = RgbColor(204, 235, 197), text: RgbColor = RgbColor(0, 51, 0)) = ColoredValue(this, background, text) - private fun T.summer(background: RGBColor = RGBColor(176, 224, 230), text: RGBColor = RGBColor(25, 25, 112)) = + private fun T.summer(background: RgbColor = RgbColor(176, 224, 230), text: RgbColor = RgbColor(25, 25, 112)) = ColoredValue(this, background, text) - private fun T.autumn(background: RGBColor = RGBColor(221, 160, 221), text: RGBColor = RGBColor(85, 26, 139)) = + private fun T.autumn(background: RgbColor = RgbColor(221, 160, 221), text: RgbColor = RgbColor(85, 26, 139)) = ColoredValue(this, background, text) private val coloredCampaigns = dataFrameOf("name", "startDate", "endDate")( @@ -134,7 +134,7 @@ class JoinWith : TestBase() { LocalDate(2023, 1, 10).winter(), 1.winter(), LocalDate(2023, 1, 20).winter(), 2.winter(), LocalDate(2023, 4, 15).spring(), 1.spring(), - LocalDate(2023, 5, 1).colored(FormattingDSL.white, FormattingDSL.black), 3.colored(FormattingDSL.white, FormattingDSL.black), + LocalDate(2023, 5, 1).colored(FormattingDsl.white, FormattingDsl.black), 3.colored(FormattingDsl.white, FormattingDsl.black), LocalDate(2023, 7, 10).summer(), 2.summer(), ) diff --git a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Modify.kt b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Modify.kt index 4fd781d5cb..96301e636a 100644 --- a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Modify.kt +++ b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Modify.kt @@ -3,13 +3,16 @@ package org.jetbrains.kotlinx.dataframe.samples.api import io.kotest.matchers.shouldBe +import org.jetbrains.kotlinx.dataframe.DataColumn import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.DataRow import org.jetbrains.kotlinx.dataframe.alsoDebug import org.jetbrains.kotlinx.dataframe.annotations.DataSchema +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl import org.jetbrains.kotlinx.dataframe.api.ParserOptions import org.jetbrains.kotlinx.dataframe.api.add import org.jetbrains.kotlinx.dataframe.api.after +import org.jetbrains.kotlinx.dataframe.api.and import org.jetbrains.kotlinx.dataframe.api.asColumn import org.jetbrains.kotlinx.dataframe.api.to import org.jetbrains.kotlinx.dataframe.api.asFrame @@ -36,6 +39,7 @@ import org.jetbrains.kotlinx.dataframe.api.fillNaNs import org.jetbrains.kotlinx.dataframe.api.fillNulls import org.jetbrains.kotlinx.dataframe.api.filter import org.jetbrains.kotlinx.dataframe.api.flatten +import org.jetbrains.kotlinx.dataframe.api.format import org.jetbrains.kotlinx.dataframe.api.gather import org.jetbrains.kotlinx.dataframe.api.getRows import org.jetbrains.kotlinx.dataframe.api.group @@ -51,6 +55,7 @@ import org.jetbrains.kotlinx.dataframe.api.intoRows import org.jetbrains.kotlinx.dataframe.api.inward import org.jetbrains.kotlinx.dataframe.api.keysInto import org.jetbrains.kotlinx.dataframe.api.length +import org.jetbrains.kotlinx.dataframe.api.linearBg import org.jetbrains.kotlinx.dataframe.api.lowercase import org.jetbrains.kotlinx.dataframe.api.map import org.jetbrains.kotlinx.dataframe.api.mapKeys @@ -62,6 +67,7 @@ import org.jetbrains.kotlinx.dataframe.api.max import org.jetbrains.kotlinx.dataframe.api.mean import org.jetbrains.kotlinx.dataframe.api.meanFor import org.jetbrains.kotlinx.dataframe.api.merge +import org.jetbrains.kotlinx.dataframe.api.min import org.jetbrains.kotlinx.dataframe.api.minus import org.jetbrains.kotlinx.dataframe.api.move import org.jetbrains.kotlinx.dataframe.api.named @@ -1303,4 +1309,38 @@ class Modify : TestBase() { } // SampleEnd } + + @Test + fun formatExample_properties() { + // SampleStart + df + .format().with { bold and textColor(black) } + .format { isHappy }.with { background(if (it) green else red) } + .format { weight }.notNull().linearBg(50 to FormattingDsl.blue, 90 to FormattingDsl.red) + .format { age }.perRowCol { row, col -> + textColor( + linear(value = col[row], from = col.min() to blue, to = col.max() to green) + ) + } + .toStandaloneHtml() + // SampleEnd + } + + @Suppress("UNCHECKED_CAST") + @Test + fun formatExample_strings() { + // SampleStart + df + .format().with { bold and textColor(black) } + .format("isHappy").with { background(if (it as Boolean) green else red) } + .format("weight").notNull().with { linearBg(it as Int, 50 to blue, 90 to red) } + .format("age").perRowCol { row, col -> + col as DataColumn + textColor( + linear(value = col[row], from = col.min() to blue, to = col.max() to green) + ) + } + .toStandaloneHtml() + // SampleEnd + } } diff --git a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/OtherSamples.kt b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/OtherSamples.kt index c875bd9fb5..0690bbf091 100644 --- a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/OtherSamples.kt +++ b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/OtherSamples.kt @@ -2,9 +2,12 @@ package org.jetbrains.kotlinx.dataframe.samples.api import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.DataFrame +import org.jetbrains.kotlinx.dataframe.api.* +import org.jetbrains.kotlinx.dataframe.api.FormattedFrame import org.jetbrains.kotlinx.dataframe.api.take import org.jetbrains.kotlinx.dataframe.explainer.WritersideFooter import org.jetbrains.kotlinx.dataframe.explainer.WritersideStyle +import org.jetbrains.kotlinx.dataframe.io.DisplayConfiguration import org.jetbrains.kotlinx.dataframe.io.read import org.jetbrains.kotlinx.dataframe.io.toStandaloneHtml import org.junit.Test @@ -12,7 +15,7 @@ import java.io.File // To display code together with a table, we can use TransformDataFrameExpressions annotation together with korro // This class provides an ability to save only a table that can be embedded anywhere in the documentation -class OtherSamples { +class OtherSamples : TestBase() { @Test fun example() { @@ -20,9 +23,33 @@ class OtherSamples { // writeTable(df, "exampleName") } + @Test + fun formatExample() { + val formattedDf = df + .format().with { bold and textColor(black) } + .format { isHappy }.with { background(if (it) green else red) } + .format { weight }.notNull().linearBg(50 to FormattingDsl.blue, 90 to FormattingDsl.red) + .format { age }.perRowCol { row, col -> + textColor( + linear(value = col[row], from = col.min() to blue, to = col.max() to green), + ) + } + + writeTable(formattedDf, "formatExample") + } + private fun writeTable(df: AnyFrame, name: String) { val dir = File("../docs/StardustDocs/resources/snippets/manual").also { it.mkdirs() } val html = df.toStandaloneHtml(getFooter = WritersideFooter) + WritersideStyle html.writeHtml(File(dir, "$name.html")) } + + private fun writeTable(formattedDf: FormattedFrame<*>, name: String) { + val dir = File("../docs/StardustDocs/resources/snippets/manual").also { it.mkdirs() } + val html = formattedDf.df.toStandaloneHtml( + configuration = formattedDf.getDisplayConfiguration(DisplayConfiguration.DEFAULT), + getFooter = WritersideFooter, + ) + WritersideStyle + html.writeHtml(File(dir, "$name.html")) + } } diff --git a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/FormattingTests.kt b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/FormattingTests.kt index 324e5cedd8..0bb1476577 100644 --- a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/FormattingTests.kt +++ b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/FormattingTests.kt @@ -2,10 +2,10 @@ package org.jetbrains.kotlinx.dataframe.testSets.person import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain -import org.jetbrains.kotlinx.dataframe.api.FormattingDSL -import org.jetbrains.kotlinx.dataframe.api.FormattingDSL.gray -import org.jetbrains.kotlinx.dataframe.api.FormattingDSL.green -import org.jetbrains.kotlinx.dataframe.api.FormattingDSL.red +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.gray +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.green +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.red import org.jetbrains.kotlinx.dataframe.api.and import org.jetbrains.kotlinx.dataframe.api.colsOf import org.jetbrains.kotlinx.dataframe.api.format @@ -33,7 +33,7 @@ class FormattingTests : BaseTest() { val formatter = formattedFrame.formatter!! for (row in 0 until typed.nrow) { - FormattingDSL.formatter(typed[row], typed.age)!!.attributes().size shouldBe + FormattingDsl.formatter(typed[row], typed.age)!!.attributes().size shouldBe if (typed[row].age > 10) 3 else 2 } @@ -48,12 +48,12 @@ class FormattingTests : BaseTest() { .formatter!! for (row in 0 until typed.nrow step 2) { - FormattingDSL.formatter(typed[row], typed.age)!!.attributes() shouldBe + FormattingDsl.formatter(typed[row], typed.age)!!.attributes() shouldBe listOf("background-color" to gray.encode()) } for (row in 1 until typed.nrow step 2) { - FormattingDSL.formatter(typed[row], typed.age)!!.attributes() shouldBe + FormattingDsl.formatter(typed[row], typed.age)!!.attributes() shouldBe listOf("background-color" to linearGradient(typed[row].age.toDouble(), 20.0, green, 80.0, red).encode()) } } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt index db0b97e986..00738d42e3 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt @@ -516,7 +516,7 @@ private interface SeeAlsoConvertWith * ```kotlin * // Convert values in all columns to `String` and add their column name to the end * df.convert { all() }.perRowCol { row, col -> - * row[col].toString() + col.name() + * col[row].toString() + col.name() * } * ``` * 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 d21d4d4fa2..b2962defa0 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 @@ -4,30 +4,278 @@ import org.jetbrains.kotlinx.dataframe.ColumnsSelector import org.jetbrains.kotlinx.dataframe.DataColumn import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.DataRow +import org.jetbrains.kotlinx.dataframe.RowColumnExpression import org.jetbrains.kotlinx.dataframe.RowValueFilter import org.jetbrains.kotlinx.dataframe.annotations.AccessApiOverload +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.attr +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.background +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.black +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.green +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linear +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.linearBg +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.textColor import org.jetbrains.kotlinx.dataframe.columns.ColumnReference import org.jetbrains.kotlinx.dataframe.columns.toColumnSet +import org.jetbrains.kotlinx.dataframe.dataTypes.IFRAME +import org.jetbrains.kotlinx.dataframe.dataTypes.IMG +import org.jetbrains.kotlinx.dataframe.documentation.DocumentationUrls +import org.jetbrains.kotlinx.dataframe.documentation.DslGrammarLink +import org.jetbrains.kotlinx.dataframe.documentation.ExcludeFromSources +import org.jetbrains.kotlinx.dataframe.documentation.ExportAsHtml +import org.jetbrains.kotlinx.dataframe.documentation.Indent +import org.jetbrains.kotlinx.dataframe.documentation.LineBreak +import org.jetbrains.kotlinx.dataframe.documentation.RowConditionLink +import org.jetbrains.kotlinx.dataframe.documentation.SelectingColumns +import org.jetbrains.kotlinx.dataframe.documentation.SelectingRows import org.jetbrains.kotlinx.dataframe.impl.api.MergedAttributes import org.jetbrains.kotlinx.dataframe.impl.api.SingleAttribute import org.jetbrains.kotlinx.dataframe.impl.api.encode import org.jetbrains.kotlinx.dataframe.impl.api.formatImpl import org.jetbrains.kotlinx.dataframe.impl.api.linearGradient +import org.jetbrains.kotlinx.dataframe.index import org.jetbrains.kotlinx.dataframe.io.DataFrameHtmlData import org.jetbrains.kotlinx.dataframe.io.DisplayConfiguration import org.jetbrains.kotlinx.dataframe.io.toHtml import org.jetbrains.kotlinx.dataframe.io.toStandaloneHtml +import org.jetbrains.kotlinx.dataframe.jupyter.RenderedContent.Companion.media import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API +import org.jetbrains.kotlinx.dataframe.util.FORMATTING_DSL +import org.jetbrains.kotlinx.dataframe.util.FORMATTING_DSL_REPLACE +import org.jetbrains.kotlinx.dataframe.util.RGB_COLOR +import org.jetbrains.kotlinx.dataframe.util.RGB_COLOR_REPLACE import kotlin.reflect.KProperty -// region DataFrame +// region docs + +/** + * Formats the specified [columns\] or cells within this dataframe such that + * they have specific CSS attributes applied to them when rendering the dataframe to HTML. + * + * This function does not immediately produce a [FormattedFrame], but instead it selects the columns to be formatted + * and returns a [FormatClause] which serves as an intermediate step. + * + * @include [SelectingColumns.ColumnGroupsAndNestedColumnsMention] + * + * See [Selecting Columns][FormatSelectingColumns]. + * + * The [FormatClause] allows to further narrow down the selection to individual cells + * by selecting only certain rows, using [where][FormatClause.where], + * and then finally specify how to format the cells using + * [with][FormatClause.with], [perRowCol][FormatClause.perRowCol], or [linearBg][FormatClause.linearBg]. + * + * You can continue formatting the [FormattedFrame] by calling [format][FormattedFrame.format] on it again. + * + * Check out the [Grammar]. + * + * For more information: {@include [DocumentationUrls.Format]} + */ +internal interface FormatDocs { -// region format + /** + * {@comment Version of [SelectingColumns] with correctly filled in examples} + * @include [SelectingColumns] {@include [SetFormatOperationArg]} + */ + interface FormatSelectingColumns + + /** + * ## Format Operation Grammar + * {@include [LineBreak]} + * {@include [DslGrammarLink]} + * + * @include [ForHtml] + */ + interface Grammar { + + /** + * ### Definitions: + * {@include [CellFormatterDef]} + * {@include [LineBreak]} + * {@include [RowColFormatterDef]} + * + * ### Notation: + * + * [**format**][DataFrame.format]**` { `**[`columns`][SelectingColumns]**` }`** + * + * {@include [Indent]} + * `\[ `__`.`__[**`where`**][FormatClause.where]**` { `**[`filter`][SelectingRows.RowValueCondition]`: `[`RowValueFilter`][RowValueFilter]**` } `**`]` + * + * {@include [Indent]} + * `\[ `__`.`__[**`at`**][FormatClause.at]**`(`**`rowIndices: `[Collection][Collection]`<`[Int][Int]`> | `[IntRange][IntRange]` | `**`vararg`**` `[Int][Int]**`)`**` ]` + * + * {@include [Indent]} + * `\[ `__`.`__[**`notNull`**][FormatClause.notNull]**`()`**` ]` + * + * {@include [Indent]} + * __`.`__[**`with`**][FormatClause.with]**` { `**{@include [CellFormatterRef]}**` }`** + * + * {@include [Indent]} + * `| `__`.`__[**`notNull`**][FormatClause.notNull]**` { `**{@include [CellFormatterRef]}**` }`** + * + * {@include [Indent]} + * `| `__`.`__[**`perRowCol`**][FormatClause.perRowCol]**` { `**{@include [RowColFormatterRef]}**` }`** + * + * {@include [Indent]} + * `| `__`.`__[**`linearBg`**][FormatClause.linearBg]**`(`**`from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`)`** + * + * `\[ `__`.`__[**format**][FormattedFrame.format]` ↺ \]` + * {@include [LineBreak]} + * {@include [FormattingDslGrammarDef]} + */ + @ExportAsHtml + @ExcludeFromSources + interface ForHtml + + /** + * ## Formatting DSL Grammar + * + * ### Definitions: + * {@include [CellAttributesDef]} + * {@include [LineBreak]} + * {@include [RgbColorDef]} + * + * ### Notation: + * _- Returning [CellAttributes][CellAttributes]_: + * + * {@include [CellAttributesRef]}` `[**`and`**][CellAttributes.and]` `{@include [CellAttributesRef]} + * + * `| `[**`italic`**][FormattingDsl.italic]` | `[**`bold`**][FormattingDsl.bold]` | `[**`underline`**][FormattingDsl.underline] + * + * `| `[**`background`**][FormattingDsl.background]**`(`**{@include [RgbColorRef]}**`)`** + * + * `| `[**`background`**][FormattingDsl.background]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`linearBg`**][FormattingDsl.linearBg]**`(`**`value: `[Number][Number]**`,`**` from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`)`** + * + * `| `[**`textColor`**][FormattingDsl.textColor]**`(`**{@include [RgbColorRef]}**`)`** + * + * `| `[**`textColor`**][FormattingDsl.textColor]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`attr`**][attr]**`(`**`name: `[String][String]**`,`**` value: `[String][String]**`)`** + * + * _- Returning [RgbColor][RgbColor]:_ + * + * [**`black`**][FormattingDsl.black]` | `[**`white`**][FormattingDsl.white]` | `[**`green`**][FormattingDsl.green]` | `[**`red`**][FormattingDsl.red]` | `[**`blue`**][FormattingDsl.blue]` | `[**`gray`**][FormattingDsl.gray]` | `[**`darkGray`**][FormattingDsl.darkGray]` | `[**`lightGray`**][FormattingDsl.lightGray] + * + * `| `[**`rgb`**][FormattingDsl.rgb]**`(`**`r: `[Short][Short]**`,`**` g: `[Short][Short]**`,`**` b: `[Short][Short]**`)`** + * + * `| `[**`linear`**][FormattingDsl.linear]**`(`**`value: `[Number][Number]**`,`**` from: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`,`**` to: `[Pair][Pair]`<`[Number][Number]`, `[RgbColor][RgbColor]`>`**`)`** + */ + interface FormattingDslGrammarDef + + /** + * `cellFormatter: `{@include [FormattingDslGrammarRef]}`.(cell: C) -> `[CellAttributes][CellAttributes]`?` + */ + interface CellFormatterDef + + /** + * `rowColFormatter: `{@include [FormattingDslGrammarRef]}`.(row: `[DataRow][DataRow]`, col: `[DataColumn][DataColumn]`) -> `[CellAttributes][CellAttributes]`?` + */ + interface RowColFormatterDef + + /** + * `cellAttributes: `[CellAttributes][CellAttributes] + */ + interface CellAttributesDef + + /** + * `color: `[RgbColor][RgbColor] + */ + interface RgbColorDef + + /** [cellFormatter][CellFormatterDef] */ + @ExcludeFromSources + interface CellFormatterRef + + /** [rowColFormatter][RowColFormatterDef] */ + @ExcludeFromSources + interface RowColFormatterRef + + /** [FormattingDsl][FormattingDslGrammarDef] */ + @ExcludeFromSources + interface FormattingDslGrammarRef + + /** [cellAttributes][CellAttributesDef] */ + @ExcludeFromSources + interface CellAttributesRef + + /** [color][RgbColorDef] */ + @ExcludeFromSources + interface RgbColorRef + } +} + +/** {@set [SelectingColumns.OPERATION] [format][format]} */ +@ExcludeFromSources +private interface SetFormatOperationArg +/** + * @include [FormatDocs] + * ### This Format Overload + */ +@ExcludeFromSources +private interface CommonFormatDocs + +// endregion + +// region DataFrame format + +/** + * @include [CommonFormatDocs] + * @include [SelectingColumns.Dsl] {@include [SetFormatOperationArg]} + * ### Examples: + * ```kt + * df.format { temperature }.linearBg(-20 to FormattingDsl.blue, 50 to FormattingDsl.red) + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * @param [columns\] The [columns-selector][ColumnsSelector] used to select the columns to be formatted. + * If unspecified, all columns will be formatted. + */ public fun DataFrame.format(columns: ColumnsSelector): FormatClause = FormatClause(this, columns) +/** + * @include [CommonFormatDocs] + * @include [SelectingColumns.ColumnNames] {@include [SetFormatOperationArg]} + * ### Examples: + * ```kt + * df.format("temperature").with { linearBg(it as Number, -20 to blue, 50 to red) } + * .format("age").notNull().perRowCol { row, col -> + * col as DataColumn + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * @param [columns\] The names of the columns to be formatted. + * If unspecified, all columns will be formatted. + */ public fun DataFrame.format(vararg columns: String): FormatClause = format { columns.toColumnSet() } +/** + * @include [CommonFormatDocs] + * + * This simply formats all columns. Optionally, you can specify which columns to format using a + * [columns-selector][ColumnsSelector] or by [column names][String]. + * + * ### Examples: + * ```kt + * df.format().with { background(white) and textColor(black) and bold } + * .format { temperature }.linearBg(-20 to FormattingDsl.blue, 50 to FormattingDsl.red) + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + */ +public fun DataFrame.format(): FormatClause = FormatClause(this) + @Deprecated(DEPRECATED_ACCESS_API) @AccessApiOverload public fun DataFrame.format(vararg columns: ColumnReference): FormatClause = @@ -38,30 +286,297 @@ public fun DataFrame.format(vararg columns: ColumnReference): Forma public fun DataFrame.format(vararg columns: KProperty): FormatClause = format { columns.toColumnSet() } -public fun DataFrame.format(): FormatClause = FormatClause(this) +// endregion + +// region FormattedFrame format + +/** + * @include [CommonFormatDocs] + * @include [SelectingColumns.Dsl] {@include [SetFormatOperationArg]} + * ### Examples: + * ```kt + * df.format().with { background(white) and textColor(black) and bold } + * .format { temperature }.linearBg(-20 to FormattingDsl.blue, 50 to FormattingDsl.red) + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * @param [columns\] The [columns-selector][ColumnsSelector] used to select the columns to be formatted. + * If unspecified, all columns will be formatted. + */ +public fun FormattedFrame.format(columns: ColumnsSelector): FormatClause = + FormatClause(df, columns, formatter) + +/** + * @include [CommonFormatDocs] + * @include [SelectingColumns.ColumnNames] {@include [SetFormatOperationArg]} + * ### Examples: + * ```kt + * df.format("temperature").with { linearBg(it as Number, -20 to blue, 50 to red) } + * .format("age").notNull().perRowCol { row, col -> + * col as DataColumn + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * @param [columns\] The names of the columns to be formatted. + * If unspecified, all columns will be formatted. + */ +public fun FormattedFrame.format(vararg columns: String): FormatClause = + format { columns.toColumnSet() } + +/** + * @include [CommonFormatDocs] + * + * This simply formats all columns. Optionally, you can specify which columns to format using a + * [columns-selector][ColumnsSelector] or by [column names][String]. + * + * ### Examples: + * ```kt + * df.format { temperature }.with { textColor(linear(-20 to FormattingDsl.blue, 50 to FormattingDsl.red)) } + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * } + * .format().with { background(white) and bold } + * .toStandaloneHtml().openInBrowser() + * ``` + */ +public fun FormattedFrame.format(): FormatClause = FormatClause(df, null, formatter) // endregion +// region intermediate operations + +/** + * Filters the rows to format using a [RowValueFilter]. + * + * See {@include [RowConditionLink]}. + * + * You need to specify [filter]: A lambda function expecting a `true` result for each + * cell that should be included in the formatting selection. + * Both the cell value (`it: `[C][C]) and its row (`this: `[DataRow][DataRow]`<`[T][T]`>`) are available. + * + * ### Examples using [where]: + * ```kt + * df.format { temperature } + * .where { it !in -10..40 } + * .with { background(red) } + * ``` + * + * 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) + +/** + * Only format the selected columns at given row indices. + * + * Accepts either a [Collection]<[Int]>, an [IntRange], or just `vararg `[Int] indices. + * + * ### Examples using [at] + */ +@ExcludeFromSources +private interface CommonFormatAtDocs + +/** + * @include [CommonFormatAtDocs] + * ```kt + * df.format() + * .at(df.indices().step(2).toList()) + * .with { background(lightGray) } + * ``` + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +public fun FormatClause.at(rowIndices: Collection): FormatClause = where { index in rowIndices } + +/** + * @include [CommonFormatAtDocs] + * ```kt + * df.format { colsOf() } + * .at(0, 3, 4) + * .with { background(lightGray) } + * ``` + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +public fun FormatClause.at(vararg rowIndices: Int): FormatClause = at(rowIndices.toSet()) + +/** + * @include [CommonFormatAtDocs] + * ```kt + * df.format { cols(2..7) } + * .at(2..7) + * .with { background(lightGray) } + * ``` + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +public fun FormatClause.at(rowRange: IntRange): FormatClause = where { index in rowRange } + +/** + * Filters the format-selection to only include cells where the value is not null. + * + * This is shorthand for `.`[where][FormatClause.where]` { it != null }`. + * + * ### Examples using [notNull]: + * ```kt + * df.format { colsOf() }.notNull().perRowCol { row, col -> + * linearBg(col[row], col.min() to red, col.max() to green) + * } + * ``` + */ +@Suppress("UNCHECKED_CAST") +public fun FormatClause.notNull(): FormatClause = where { it != null } as FormatClause + +// endregion + +// region terminal operations + +/** + * Creates a new [FormattedFrame] that uses the specified [RowColFormatter] to format the selected cells of the dataframe. + * + * You need to specify [formatter]: {@include [RowColFormatter]} + * + * ### Examples using [perRowCol]: + * ```kt + * df.format { colsOf() }.perRowCol { row, col -> + * linearBg(col[row], col.min() to red, col.max() to green) + * } + * ``` + * + * Check out the full [Grammar][FormatDocs.Grammar]. + */ public fun FormatClause.perRowCol(formatter: RowColFormatter): FormattedFrame = formatImpl(formatter) +/** + * Creates a new [FormattedFrame] that uses the specified [CellFormatter] to format the selected cells of the dataframe. + * + * You need to specify [formatter]: {@include [CellFormatter]} + * + * ### Examples using [with]: + * ```kt + * df.format() + * .at(df.indices().step(2).toList()) + * .with { background(lightGray) and bold and textColor(black) } + * ``` + * + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +@Suppress("UNCHECKED_CAST") public fun FormatClause.with(formatter: CellFormatter): FormattedFrame = - formatImpl { row, col -> formatter(row[col]) } - -public fun FormatClause.where(filter: RowValueFilter): FormatClause = - FormatClause(filter = filter, df = df, columns = columns, oldFormatter = oldFormatter) - -public fun FormattedFrame.format(): FormatClause = FormatClause(df, null, formatter) + formatImpl { row, col -> formatter(row[col.name] as C) } + +/** + * Creates a new [FormattedFrame] that uses the specified [CellFormatter] to format selected non-null cells of the dataframe. + * + * This function is shorthand for `.`[notNull()][FormatClause.notNull]`.`[with { }][FormatClause.with]. + * + * You need to specify [formatter]: {@include [CellFormatter]} + * + * ### Examples using [notNull]: + * ```kt + * df.format().notNull { bold and textColor(black) } + * ``` + * + * Check out the full [Grammar][FormatDocs.Grammar]. + */ +public fun FormatClause.notNull(formatter: CellFormatter): FormattedFrame = + notNull().with(formatter) + +/** + * Creates a new [FormattedFrame] by just changing the background colors of the selected cells. + * + * The background color of each selected cell is calculated by interpolating between [from] and [to], + * given the numeric value of that cell. + * The interpolation is linear. + * + * If the numeric cell value falls outside the range [from]..[to], the colors at the bounds will be used. + * + * This function is shorthand for: + * + * `.`[with][FormatClause.with]` { `[background][FormattingDsl.background]`(`[linear][FormattingDsl.linear]`(it, `[from][from]`, `[to][to]`)) }` + * + * See also [with][FormatClause.with], [background][FormattingDsl.background], and [linear][FormattingDsl.linear]. + * + * ### Examples using [linearBg]: + * ```kt + * df.format { temperature }.linearBg(-20 to FormattingDsl.blue, 50 to FormattingDsl.red) + * .format { age }.notNull().perRowCol { row, col -> + * textColor( + * linear(col[row], col.min() to green, col.max() to red) + * ) + * }.toStandaloneHtml().openInBrowser() + * ``` + * + * Check out the full [Grammar][FormatDocs.Grammar]. + * + * @param [from] The lower bound of the interpolation range and the color that will be returned when the cell value touches this bound. + * @param [to] The upper bound of the interpolation range and the color that will be returned when the cell value touches this bound. + */ +public fun FormatClause.linearBg( + from: Pair, + to: Pair, +): FormattedFrame = + with { + if (it != null) { + background(linear(it, from, to)) + } else { + null + } + } // endregion -public data class RGBColor(val r: Short, val g: Short, val b: Short) +// region Formatting DSL + +/** + * Represents a color in the RGB color space. + * To be used in the [DataFrame.format]; [FormattingDsl]. + * + * Any color can be represented in terms of [r] (red), [g] (green), and [b] (blue) values from `0..255`. + * + * Inside [FormattingDsl], there are shortcuts for common colors, like [white][FormattingDsl.white], + * [green][FormattingDsl.green], and [gray][FormattingDsl.gray]. + */ +public data class RgbColor(val r: Short, val g: Short, val b: Short) { + + /** Encodes the color as a [String] such that it can be used as the value of an attribute in CSS. */ + override fun toString(): String = encode() +} +/** + * This represents a collection of CSS cell attributes that can be applied to a cell in an HTML-rendered dataframe. + * + * [Cell attributes][CellAttributes] are created inside the [FormattingDsl] by calling + * [FormatClause.with] or [FormatClause.perRowCol]. + * + * Multiple attributes can be combined using the [and] operator. + * + * For instance: + * + * `df.`[format()][DataFrame.format]`.`[`with {`][FormatClause.with]` `[background][FormattingDsl.background]`(`[white][FormattingDsl.white]`) `[and][CellAttributes.and]` `[textColor][FormattingDsl.textColor]`(`[black][FormattingDsl.black]`) `[`}`][FormatClause.with] + * + * @see [CellAttributes.and] + */ public interface CellAttributes { + /** Retrieves all CSS cell attributes as a list of name-value pairs. */ public fun attributes(): List> } +/** + * Combines two [CellAttributes] instances into a new one that combines their attributes. + * + * For instance: + * + * `df.`[format()][DataFrame.format]`.`[`with {`][FormatClause.with]` `[background][FormattingDsl.background]`(`[white][FormattingDsl.white]`) `[and][CellAttributes.and]` `[textColor][FormattingDsl.textColor]`(`[black][FormattingDsl.black]`) `[`}`][FormatClause.with] + */ public infix fun CellAttributes?.and(other: CellAttributes?): CellAttributes? = when { other == null -> this @@ -69,73 +584,258 @@ public infix fun CellAttributes?.and(other: CellAttributes?): CellAttributes? = else -> MergedAttributes(listOf(this, other)) } -public object FormattingDSL { - public fun rgb(r: Short, g: Short, b: Short): RGBColor = RGBColor(r, g, b) +/** + * The formatting DSL allows you to create and combine [CellAttributes] to apply to one + * or multiple cells of a dataframe such that they have specific CSS attributes applied to them + * when rendered to HTML. + * + * For instance, to specify black, bold text on a white background, you could write: + * + * [background][FormattingDsl.background]`(`[white][FormattingDsl.white]`) `[and][CellAttributes.and]` ` + * [textColor][FormattingDsl.textColor]`(`[black][FormattingDsl.black]`) `[and][CellAttributes.and]` ` + * [bold][FormattingDsl.bold] + * + * It's also possible to define your own colors using [rgb][FormattingDsl.rgb] or interpolate + * colors using [linear][FormattingDsl.linear]. + * + * Use [attr] if you want to specify a custom CSS attribute. + */ +public object FormattingDsl { - public val black: RGBColor = rgb(0, 0, 0) + /** Creates a new [RgbColor] instance with [r] (red), [g] (green), and [b] (blue) values from `0..255`. */ + public fun rgb(r: Short, g: Short, b: Short): RgbColor = RgbColor(r, g, b) - public val white: RGBColor = rgb(255, 255, 255) + public val black: RgbColor = rgb(0, 0, 0) - public val green: RGBColor = rgb(0, 255, 0) + public val white: RgbColor = rgb(255, 255, 255) - public val red: RGBColor = rgb(255, 0, 0) + public val green: RgbColor = rgb(0, 255, 0) - public val blue: RGBColor = rgb(0, 0, 255) + public val red: RgbColor = rgb(255, 0, 0) - public val gray: RGBColor = rgb(128, 128, 128) + public val blue: RgbColor = rgb(0, 0, 255) - public val darkGray: RGBColor = rgb(169, 169, 169) + public val gray: RgbColor = rgb(128, 128, 128) - public val lightGray: RGBColor = rgb(211, 211, 211) + public val darkGray: RgbColor = rgb(169, 169, 169) + public val lightGray: RgbColor = rgb(211, 211, 211) + + /** + * A custom [cell attribute][CellAttributes] + * that allows you to specify any custom CSS attribute by [name] and [value]. + * + * For example: + * ```kt + * attr("text-align", "center") + * attr("border", "3px solid green") + * ``` + */ public fun attr(name: String, value: String): CellAttributes = SingleAttribute(name, value) - public fun background(color: RGBColor): CellAttributes = attr("background-color", color.encode()) + /** + * A [cell attribute][CellAttributes] that sets the background color of a cell. + * @param color Either one of the predefined colors, like [black], or [green], or a custom color using [rgb()][rgb]. + */ + public fun background(color: RgbColor): CellAttributes = attr("background-color", color.toString()) - public fun background(r: Short, g: Short, b: Short): CellAttributes = background(RGBColor(r, g, b)) + /** + * A [cell attribute][CellAttributes] that sets the background color of a cell. + * A shortcut for [background][background]`(`[rgb(...)][rgb]`)`. + * @see [rgb] + */ + public fun background(r: Short, g: Short, b: Short): CellAttributes = background(RgbColor(r, g, b)) - public fun textColor(color: RGBColor): CellAttributes = attr("color", color.encode()) + /** + * A [cell attribute][CellAttributes] that sets the text color of a cell. + * @param color Either one of the predefined colors, like [black], or [green], or a custom color using [rgb()][rgb]. + */ + public fun textColor(color: RgbColor): CellAttributes = attr("color", color.toString()) - public fun textColor(r: Short, g: Short, b: Short): CellAttributes = textColor(RGBColor(r, g, b)) + /** + * A [cell attribute][CellAttributes] that sets the text color of a cell. + * A shortcut for [textColor][textColor]`(`[rgb(...)][rgb]`)`. + * @see [rgb] + */ + public fun textColor(r: Short, g: Short, b: Short): CellAttributes = textColor(RgbColor(r, g, b)) + /** A [cell attribute][CellAttributes] that makes the text inside the cell *italic*. */ public val italic: CellAttributes = attr("font-style", "italic") + /** A [cell attribute][CellAttributes] that makes the text inside the cell **bold**. */ public val bold: CellAttributes = attr("font-weight", "bold") + /** A [cell attribute][CellAttributes] that u͟n͟d͟e͟r͟l͟i͟n͟e͟s͟ the text inside the cell. */ public val underline: CellAttributes = attr("text-decoration", "underline") - public fun linearBg(value: Number, from: Pair, to: Pair): CellAttributes = + /** + * Shorthand for [background][background]`(`[linear][linear]`(...))` + * + * Creates a [cell attribute][CellAttributes] that applies a background color calculated + * by interpolating between [from] and [to], given [value]. + * + * See [linear] for more information. + * + * @see linear + * @see background + */ + public fun linearBg(value: Number, from: Pair, to: Pair): CellAttributes = background( linear(value, from, to), ) - public fun linear(value: Number, from: Pair, to: Pair): RGBColor { + /** + * Calculates an [RgbColor] by interpolating between [from] and [to], given [value]. + * The interpolation is linear. + * If [value] falls outside the range [from]..[to], the colors at the bounds will be used. + * + * Very useful if you want the text-, or background color to correspond to the value of a cell, for instance. + * + * For example: + * ```kt + * df.format { temperature }.with { value -> + * background(linear(value, -20 to blue, 40 to red)) and + * textColor(black) + * } + * ``` + * + * @param [value] The value to interpolate the color for. + * @param [from] The lower bound of the interpolation range and the color that will be returned when [value] touches this bound. + * @param [to] The upper bound of the interpolation range and the color that will be returned when [value] touches this bound. + * @return An [RgbColor] that corresponds to the interpolation. + * @see linearBg + */ + public fun linear(value: Number, from: Pair, to: Pair): RgbColor { val a = from.first.toDouble() val b = to.first.toDouble() - if (a < b) return linearGradient(value.toDouble(), a, from.second, b, to.second) - return linearGradient(value.toDouble(), b, to.second, a, from.second) + return if (a < b) { + linearGradient( + x = value.toDouble(), + minValue = a, + minColor = from.second, + maxValue = b, + maxColor = to.second, + ) + } else { + linearGradient( + x = value.toDouble(), + minValue = b, + minColor = to.second, + maxValue = a, + maxColor = from.second, + ) + } } } -public typealias RowColFormatter = FormattingDSL.(DataRow, DataColumn) -> CellAttributes? +// endregion +// region types and classes + +/** + * A lambda function expecting a [CellAttributes] or `null` given an instance of + * [DataRow][DataRow]`<`[T][T]`>` and [DataColumn][DataColumn]`<`[C][C]`>`. + * + * This is similar to a [RowColumnExpression], except that you also have access + * to the [FormattingDsl] in the context. + * + * @include [FormattingDsl] + */ +public typealias RowColFormatter = FormattingDsl.(row: DataRow, col: DataColumn) -> CellAttributes? + +/** + * A lambda function expecting a [CellAttributes] or `null` given an instance of a cell: [C] of the dataframe. + * + * You have access to the [FormattingDsl] in the context. + * + * @include [FormattingDsl] + */ +public typealias CellFormatter = FormattingDsl.(cell: C) -> CellAttributes? + +/** + * A wrapper around a [DataFrame][df] with CSS attributes that can be + * converted to a formatted HTML table in the form of [DataFrameHtmlData]. + * + * Call [toHtml] or [toStandaloneHtml] to get the HTML representation of the [DataFrame]. + * + * In Jupyter kernel (Kotlin Notebook) environments, you can often output this class directly. + * Use [toHtml] or [toStandaloneHtml] when this produces unexpected results. + * + * 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) { + /** - * @return DataFrameHtmlData without additional definitions. Can be rendered in Jupyter kernel environments + * Returns a [DataFrameHtmlData] without additional definitions. + * Can be rendered in Jupyter kernel (Kotlin Notebook) environments or other environments that already have + * CSS- and script definitions for DataFrame. + * + * Use [toStandaloneHtml] if you need the [DataFrameHtmlData] to include CSS- and script definitions. + * + * By default, cell content is formatted as text + * Use [RenderedContent.media][media] or [IMG], [IFRAME] if you need custom HTML inside a cell. + * + * @param [configuration] The [DisplayConfiguration] to use as a base for this [FormattedFrame]. + * Default: [DisplayConfiguration.DEFAULT]. + * @see toStandaloneHtml */ public fun toHtml(configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT): DataFrameHtmlData = df.toHtml(getDisplayConfiguration(configuration)) /** - * @return DataFrameHtmlData with table script and css definitions. Can be saved as an *.html file and displayed in the browser + * Returns a [DataFrameHtmlData] with CSS- and script definitions for DataFrame. + * + * Use [toHtml] if you don't need the [DataFrameHtmlData] to include CSS- and script definitions. + * + * The [DataFrameHtmlData] can be saved as an *.html file and displayed in the browser. + * If you save it as a file and find it in the project tree, + * the ["Open in browser"](https://www.jetbrains.com/help/idea/editing-html-files.html#ws_html_preview_output_procedure) + * feature of IntelliJ IDEA will automatically reload the file content when it's updated. + * + * By default, cell content is formatted as text + * Use [RenderedContent.media][media] or [IMG], [IFRAME] if you need custom HTML inside a cell. + * + * __NOTE:__ In Kotlin Notebook, output [FormattedFrame] directly, or use [toHtml], + * as that environment already has CSS- and script definitions for DataFrame. + * Using [toStandaloneHtml] might produce unexpected results. + * + * @param [configuration] The [DisplayConfiguration] to use as a base for this [FormattedFrame]. + * Default: [DisplayConfiguration.DEFAULT]. + * @see toHtml */ public fun toStandaloneHtml(configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT): DataFrameHtmlData = df.toStandaloneHtml(getDisplayConfiguration(configuration)) + /** 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<*, *>?) } +/** + * An intermediate class used in the [format] operation. + * + * This class itself does nothing—it is just a transitional step before specifying + * how to format the selected columns. + * It must be followed by one of the positioning methods + * to produce a new [FormattedFrame]; a [DataFrame] with HTML formatting data. + * + * Use the following function to filter the rows to format: + * - [where][FormatClause.where] – filters the rows to format using a [RowValueFilter]. + * - [at][FormatClause.at] – Only format in rows with certain indices. + * - [notNull][FormatClause.notNull] – Only format cells that have non-null values. + * + * Use the following functions to finalize this formatting round: + * - [with][FormatClause.with] – Specifies how to format the cells using a [CellFormatter]. + * - [perRowCol][FormatClause.perRowCol] – Specifies how to format each cell individually using a [RowColFormatter]. + * - [linearBg][FormatClause.linearBg] – + * Interpolates between two colors to set the background color of each numeric cell based on its value. + * Shorthand for `.`[with][FormatClause.with]` { `[background][FormattingDsl.background]`(`[linear][FormattingDsl.linear]`(it, from, to)) }` + * - [notNull][FormatClause.notNull] – Specifies how to format non-null cells using a [CellFormatter]. + * Shorthand for `.`[notNull()][FormatClause.notNull]`.`[with { }][FormatClause.with]. + * + * See [Grammar][FormatDocs.Grammar] for more details. + */ public class FormatClause( internal val df: DataFrame, internal val columns: ColumnsSelector? = null, @@ -146,19 +846,22 @@ public class FormatClause( "FormatClause(df=$df, columns=$columns, oldFormatter=$oldFormatter, filter=$filter)" } -public fun FormattedFrame.format(columns: ColumnsSelector): FormatClause = - FormatClause(df, columns, formatter) +// endregion -public typealias CellFormatter = FormattingDSL.(V) -> CellAttributes? +// region Deprecated -public fun FormatClause.linearBg( - from: Pair, - to: Pair, -): FormattedFrame = - with { - if (it != null) { - background(linear(it, from, to)) - } else { - null - } - } +@Deprecated( + message = FORMATTING_DSL, + replaceWith = ReplaceWith(FORMATTING_DSL_REPLACE), + level = DeprecationLevel.ERROR, +) +public typealias FormattingDSL = FormattingDsl + +@Deprecated( + message = RGB_COLOR, + replaceWith = ReplaceWith(RGB_COLOR_REPLACE), + level = DeprecationLevel.ERROR, +) +public typealias RGBColor = RgbColor + +// endregion diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/update.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/update.kt index 312ed95b23..ecbced9ec9 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/update.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/update.kt @@ -3,6 +3,7 @@ package org.jetbrains.kotlinx.dataframe.api import org.jetbrains.kotlinx.dataframe.AnyRow import org.jetbrains.kotlinx.dataframe.ColumnExpression import org.jetbrains.kotlinx.dataframe.ColumnsSelector +import org.jetbrains.kotlinx.dataframe.DataColumn import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.DataFrameExpression import org.jetbrains.kotlinx.dataframe.DataRow @@ -11,6 +12,7 @@ import org.jetbrains.kotlinx.dataframe.RowValueFilter import org.jetbrains.kotlinx.dataframe.annotations.AccessApiOverload import org.jetbrains.kotlinx.dataframe.annotations.Interpretable import org.jetbrains.kotlinx.dataframe.annotations.Refine +import org.jetbrains.kotlinx.dataframe.api.mean import org.jetbrains.kotlinx.dataframe.columns.ColumnGroup import org.jetbrains.kotlinx.dataframe.columns.ColumnReference import org.jetbrains.kotlinx.dataframe.columns.toColumnSet @@ -78,6 +80,9 @@ public class Update( * `\[ `__`.`__[**`at`**][Update.at]**`(`**[`rowIndices`][CommonUpdateAtFunctionDoc.RowIndicesParam]**`)`**` ]` * * {@include [Indent]} + * `\[ `__`.`__[**`notNull`**][Update.notNull]**`()`**` ]` + * + * {@include [Indent]} * __`.`__[**`with`**][Update.with]**` { `**[`rowExpression`][ExpressionsGivenRow.RowValueExpression.WithExample]**` }`** * * {@include [Indent]} @@ -403,7 +408,21 @@ internal infix fun RowValueFilter?.and(other: RowValueFilter) return { thisExp(this, it) && other(this, it) } } -/** @include [Update.notNull] */ +/** + * ## Not Null + * Filters the update-selection to only include cells where the value is not null. + * + * This is shorthand for `.`[where][Update.where]` { it != null }`. + * + * For example: + * + * `df.`[update][update]` { `[colsOf][colsOf]`<`[Int][Int]`?>() }.`[notNull][notNull]`().`[perRowCol][Update.perRowCol]` { row, col ->` + * + * {@include [Indent]}`row\[col\] / col.`[mean][DataColumn.mean]`(skipNA = true)` + * + * `}` + */ +@Suppress("UNCHECKED_CAST") @Interpretable("UpdateNotNullDefault") public fun Update.notNull(): Update = where { it != null } as Update diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DocumentationUrls.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DocumentationUrls.kt index 772b647fe9..4e99b0ffc8 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DocumentationUrls.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DocumentationUrls.kt @@ -62,6 +62,9 @@ internal interface DocumentationUrls { interface FillNA } + /** [See `format` on the documentation website.]({@include [Url]}/format.html) */ + interface Format + /** [See `NaN` and `NA` on the documentation website.]({@include [Url]}/nanAndNa.html) */ interface NanAndNa { diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DslGrammar.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DslGrammar.kt index 9b0182cd6a..da2328f4a8 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DslGrammar.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DslGrammar.kt @@ -3,7 +3,7 @@ package org.jetbrains.kotlinx.dataframe.documentation /** * ## DSL Grammar * - * If you've come across notations like **`a(`** `(`**`b`**` | [`**`c, .. `**`] )` **`)`** + * If you've come across notations like **`a(`**` (`**`b`**` | [`**`c, .. `**`] ) `**`)`** * either in the KDocs or on the website and would like some further explanation * for what it means, you've come to the right place. * 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 a1c0fce0c3..579c48213f 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 @@ -3,8 +3,8 @@ package org.jetbrains.kotlinx.dataframe.impl.api import org.jetbrains.kotlinx.dataframe.api.CellAttributes import org.jetbrains.kotlinx.dataframe.api.FormatClause import org.jetbrains.kotlinx.dataframe.api.FormattedFrame -import org.jetbrains.kotlinx.dataframe.api.FormattingDSL -import org.jetbrains.kotlinx.dataframe.api.RGBColor +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl +import org.jetbrains.kotlinx.dataframe.api.RgbColor import org.jetbrains.kotlinx.dataframe.api.RowColFormatter import org.jetbrains.kotlinx.dataframe.api.and import org.jetbrains.kotlinx.dataframe.api.cast @@ -24,10 +24,10 @@ internal fun encRgb(r: Short, g: Short, b: Short): String = "#${encHex(r)}${encH internal fun encHex(v: Short): String = "${(v / 16).toString(16)}${(v % 16).toString(16)}" -internal fun RGBColor.encode() = encRgb(r, g, b) +internal fun RgbColor.encode() = encRgb(r, g, b) -internal fun componentWise(color1: RGBColor, color2: RGBColor, f: (Short, Short) -> Short) = - RGBColor( +internal fun componentWise(color1: RgbColor, color2: RgbColor, f: (Short, Short) -> Short) = + RgbColor( f(color1.r, color2.r), f(color1.g, color2.g), f(color1.b, color2.b), @@ -36,35 +36,37 @@ internal fun componentWise(color1: RGBColor, color2: RGBColor, f: (Short, Short) internal fun linearGradient( x: Double, minValue: Double, - minColor: RGBColor, + minColor: RgbColor, maxValue: Double, - maxColor: RGBColor, -): RGBColor { + maxColor: RgbColor, +): RgbColor { if (x < minValue) return minColor if (x > maxValue) return maxColor val t = (x - minValue) / (maxValue - minValue) - return componentWise(minColor, maxColor) { cmin, cmax -> - (cmin + t * (cmax - cmin)).toInt().toShort() + return componentWise(minColor, maxColor) { cMin, cMax -> + (cMin + t * (cMax - cMin)).toInt().toShort() } } +@Suppress("UNCHECKED_CAST") internal inline fun FormatClause.formatImpl( crossinline formatter: RowColFormatter, ): FormattedFrame { + val clause = this val columns = - if (columns != null) { - df.getColumnsWithPaths(columns) - .mapNotNull { if (it.depth == 0) it.name else null } + if (clause.columns != null) { + clause.df.getColumnsWithPaths(clause.columns) + .mapNotNull { if (it.depth == 0) it.name else null } // TODO Causes #1356 .toSet() } else { null } - return FormattedFrame(df) { row, col -> - val oldAttributes = oldFormatter?.invoke(FormattingDSL, row, col.cast()) + return FormattedFrame(clause.df) { row, col -> + val oldAttributes = clause.oldFormatter?.invoke(FormattingDsl, row, col.cast()) if (columns == null || columns.contains(col.name())) { - val value = row[col] as C - if (filter == null || filter(row, value)) { - oldAttributes and formatter(FormattingDSL, row.cast(), col.cast()) + val value = row[col.name] as C + if (clause.filter(row, value)) { + oldAttributes and formatter(FormattingDsl, row.cast(), col.cast()) } else { oldAttributes } 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 6994580bab..bd1ab19e60 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 @@ -6,10 +6,11 @@ import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.AnyRow import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.FormattedFrame -import org.jetbrains.kotlinx.dataframe.api.FormattingDSL +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl import org.jetbrains.kotlinx.dataframe.api.RowColFormatter import org.jetbrains.kotlinx.dataframe.api.asColumnGroup import org.jetbrains.kotlinx.dataframe.api.asNumbers +import org.jetbrains.kotlinx.dataframe.api.format import org.jetbrains.kotlinx.dataframe.api.getColumnsWithPaths import org.jetbrains.kotlinx.dataframe.api.isColumnGroup import org.jetbrains.kotlinx.dataframe.api.isEmpty @@ -170,7 +171,7 @@ internal fun AnyFrame.toHtmlData( } val renderConfig = configuration.copy(decimalFormat = format) val contents = values.map { - val value = it[col] + val value = col[it] val content = value.toDataFrameLikeOrNull() if (content != null) { val df = content.df() @@ -178,16 +179,31 @@ internal fun AnyFrame.toHtmlData( HtmlContent("", null) } else { val id = nextTableId() - queue.add(RenderingQueueItem(df, id, content.configuration(defaultConfiguration))) + queue += RenderingQueueItem(df, id, content.configuration(defaultConfiguration)) DataFrameReference(id, df.size) } } else { val html = formatter.format(downsizeBufferedImageIfNeeded(value, renderConfig), cellRenderer, renderConfig) val style = renderConfig.cellFormatter - ?.invoke(FormattingDSL, it, col) + ?.invoke(FormattingDsl, it, col) ?.attributes() ?.ifEmpty { null } + ?.flatMap { + if (it.first == "color") { + // override all --text-color* variables that + // are used to color text of .numbers, .null, etc., inside DataFrame + listOf( + it, + "--text-color" to "${it.second} !important", + "--text-color-dark" to "${it.second} !important", + "--text-color-pale" to "${it.second} !important", + "--text-color-medium" to "${it.second} !important", + ) + } else { + listOf(it) + } + } ?.joinToString(";") { "${it.first}:${it.second}" } HtmlContent(html, style) } @@ -206,7 +222,7 @@ internal fun AnyFrame.toHtmlData( } val rootId = nextTableId() - queue.add(RenderingQueueItem(this, rootId, defaultConfiguration)) + queue += RenderingQueueItem(this, rootId, defaultConfiguration) while (!queue.isEmpty()) { val (nextDf, nextId, configuration) = queue.pop() val rowsLimit = if (nextId == rootId) configuration.rowsLimit else configuration.nestedRowsLimit @@ -543,13 +559,30 @@ public fun DataFrame.toStandaloneHTML( ): DataFrameHtmlData = toStandaloneHtml(configuration, cellRenderer, getFooter) /** + * Returns a [DataFrameHtmlData] with CSS- and script definitions for DataFrame. + * + * To change the formatting of certain cells or columns in the dataframe, + * use [DataFrame.format]. + * + * Use [toHtml] if you don't need the [DataFrameHtmlData] to include CSS- and script definitions. + * + * The [DataFrameHtmlData] can be saved as an *.html file and displayed in the browser. + * If you save it as a file and find it in the project tree, + * the ["Open in browser"](https://www.jetbrains.com/help/idea/editing-html-files.html#ws_html_preview_output_procedure) + * feature of IntelliJ IDEA will automatically reload the file content when it's updated. + * * By default, cell content is formatted as text * Use [RenderedContent.media] or [IMG], [IFRAME] if you need custom HTML inside a cell. * - * The [DataFrameHtmlData] be saved as an *.html file and displayed in the browser. - * If you save it as a file and find it in the project tree, - * the ["Open in browser"](https://www.jetbrains.com/help/idea/editing-html-files.html#ws_html_preview_output_procedure) feature of IntelliJ IDEA will automatically reload the file content when it's updated - * @return DataFrameHtmlData with table script and css definitions + * __NOTE:__ In Kotlin Notebook, output [DataFrame] directly, or use [toHtml], + * as that environment already has CSS- and script definitions for DataFrame. + * Using [toStandaloneHtml] might produce unexpected results. + * + * @param [configuration] The [DisplayConfiguration] to use. Default: [DisplayConfiguration.DEFAULT]. + * @param [cellRenderer] Mostly for internal usage, use [DefaultCellRenderer] if unsure. + * @param [getFooter] Allows you to specify how to render the footer text beneath the dataframe. + * Default: `"DataFrame [rows x cols]"` + * @see toHtml */ public fun DataFrame.toStandaloneHtml( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, @@ -558,9 +591,23 @@ public fun DataFrame.toStandaloneHtml( ): DataFrameHtmlData = toHtml(configuration, cellRenderer, getFooter).withTableDefinitions() /** + * Returns a [DataFrameHtmlData] without additional definitions. + * Can be rendered in Jupyter kernel (Kotlin Notebook) environments or other environments that already have + * CSS- and script definitions for DataFrame. + * + * To change the formatting of certain cells or columns in the dataframe, + * use [DataFrame.format]. + * + * Use [toStandaloneHtml] if you need the [DataFrameHtmlData] to include CSS- and script definitions. + * * By default, cell content is formatted as text * Use [RenderedContent.media] or [IMG], [IFRAME] if you need custom HTML inside a cell. - * @return DataFrameHtmlData without additional definitions. Can be rendered in Jupyter kernel environments + * + * @param [configuration] The [DisplayConfiguration] to use. Default: [DisplayConfiguration.DEFAULT]. + * @param [cellRenderer] Mostly for internal usage, use [DefaultCellRenderer] if unsure. + * @param [getFooter] Allows you to specify how to render the footer text beneath the dataframe. + * Default: `"DataFrame [rows x cols]"` + * @see toStandaloneHtml */ public fun DataFrame.toHtml( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, @@ -597,8 +644,10 @@ public fun DataFrame.toHtml( } /** - * Container for HTML page data in the form of a String - * Can be used to compose rendered dataframe tables with additional HTML elements + * Container for HTML data, often containing a dataframe table. + * + * It can be used to compose rendered dataframe tables with additional HTML elements, + * or to simply print the HTML or write it to file. */ public class DataFrameHtmlData( @Language("css") public val style: String = "", @@ -735,6 +784,9 @@ public class DataFrameHtmlData( } /** + * A collection of settings for rendering dataframes as HTML tables or native + * Kotlin Notebook table output. + * * @param rowsLimit null to disable rows limit * @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; 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 c24f934432..5d068864ae 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 @@ -113,6 +113,12 @@ internal const val TO_URL_REPLACE = "toUrl()" internal const val FILTER_BY = "This function is deprecated in favor of `filter { }`. $MESSAGE_1_0" internal const val FILTER_BY_REPLACE = "filter { column }" +internal const val FORMATTING_DSL = "Replaced by `FormattingDsl`. $MESSAGE_1_0" +internal const val FORMATTING_DSL_REPLACE = "FormattingDsl" + +internal const val RGB_COLOR = "Replaced by `RgbColor`. $MESSAGE_1_0" +internal const val RGB_COLOR_REPLACE = "RgbColor" + // endregion // region WARNING in 1.0, ERROR in 1.1 diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt new file mode 100644 index 0000000000..055b4e01ca --- /dev/null +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt @@ -0,0 +1,298 @@ +package org.jetbrains.kotlinx.dataframe.api + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.blue +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.red +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.rgb +import org.jetbrains.kotlinx.dataframe.io.DisplayConfiguration +import org.jetbrains.kotlinx.dataframe.samples.api.TestBase +import org.jetbrains.kotlinx.dataframe.samples.api.age +import org.jetbrains.kotlinx.dataframe.samples.api.firstName +import org.jetbrains.kotlinx.dataframe.samples.api.isHappy +import org.jetbrains.kotlinx.dataframe.samples.api.name +import org.jetbrains.kotlinx.dataframe.samples.api.weight +import org.junit.Ignore +import org.junit.Test + +class FormatTests : TestBase() { + + @Test + fun `basic format with background color`() { + val formatted = df.format { age }.with { background(red) } + val html = formatted.toHtml().toString() + + // Should contain CSS background-color styling + html shouldContain "background-color:#ff0000" + // Format operation should produce a FormattedFrame + formatted::class.simpleName shouldBe "FormattedFrame" + } + + @Test + fun `format with text color`() { + val formatted = df.format { age }.with { textColor(blue) } + val html = formatted.toHtml().toString() + + html shouldContain "color:#0000ff" + formatted::class.simpleName shouldBe "FormattedFrame" + } + + @Test + fun `format with multiple attributes using and`() { + val formatted = df.format { age }.with { background(white) and textColor(black) and bold } + val html = formatted.toHtml().toString() + + html shouldContain "background-color:#ffffff" + html shouldContain "color" + html shouldContain "font-weight" + html shouldContain "bold" + } + + @Test + fun `format with italic and underline`() { + val formatted = df.format { age }.with { italic and underline } + val html = formatted.toHtml().toString() + + html shouldContain "font-style" + html shouldContain "italic" + html shouldContain "text-decoration" + html shouldContain "underline" + } + + // TODO #1356 + @Ignore + @Test + fun `format with italic and underline in nested group`() { + val formatted = df.format { name.firstName }.with { italic and underline } + val html = formatted.toHtml().toString() + + html shouldContain "font-style" + html shouldContain "italic" + html shouldContain "text-decoration" + html shouldContain "underline" + } + + @Test + fun `format with custom rgb color`() { + val customColor = rgb(128, 64, 192) + val formatted = df.format { age }.with { background(customColor) } + val html = formatted.toHtml().toString() + + // Custom color should be applied + html shouldContain "background-color:#8040c0" + } + + @Test + fun `format with custom attribute`() { + val formatted = df.format { age }.with { attr("text-align", "center") } + val html = formatted.toHtml().toString() + + val occurrences = html.split("text-align:center").size - 1 + occurrences shouldBe 7 + } + + @Test + fun `format with where clause`() { + val formatted = df.format { age }.where { it > 30 }.with { background(red) } + val html = formatted.toHtml().toString() + + // Should contain styling but only for cells where age > 30 + val occurrences = html.split("background-color:#ff0000").size - 1 + occurrences shouldBe 2 // Two cells where age > 30 + } + + @Test + fun `format with at specific rows`() { + val formatted = df.format { age }.at(0, 2, 4, 9999).with { background(green) } + val html = formatted.toHtml().toString() + + val occurrences = html.split("background-color:#00ff00").size - 1 + occurrences shouldBe 3 + } + + @Test + fun `format with at range`() { + val formatted = df.format { age }.at(1..3).with { background(blue) } + val html = formatted.toHtml().toString() + + val occurrences = html.split("background-color:#0000ff").size - 1 + occurrences shouldBe 3 + } + + @Test + fun `format with notNull filter`() { + val formatted = df.format { weight }.notNull().with { background(green) } + val html = formatted.toHtml().toString() + + // Should only format non-null weight values + val occurrences = html.split("background-color:#00ff00").size - 1 + occurrences shouldBe 5 + } + + @Test + fun `format with notNull shorthand`() { + val formatted = df.format { weight }.notNull { background(red) and bold } + val html = formatted.toHtml().toString() + + html.split("background-color:#ff0000").size - 1 shouldBe 5 + html.split("font-weight:bold").size - 1 shouldBe 5 + } + + @Test + fun `format with perRowCol`() { + val formatted = df.format { age }.perRowCol { row, col -> + if (col[row] > 25) background(red) else background(green) + } + val html = formatted.toHtml().toString() + + // Should contain formatting based on age values + val occurrences = html.split("background-color:#00ff00").size - 1 + occurrences shouldBe 3 + + formatted::class.simpleName shouldBe "FormattedFrame" + } + + @Test + fun `format with linearBg`() { + val formatted = df.format { age }.linearBg(15 to blue, 45 to red) + val html = formatted.toHtml().toString() + + html shouldContain "background-color:#0000ff" + html shouldContain "background-color:#2a00d4" + html shouldContain "background-color:#d4002a" + html shouldContain "background-color:#7f007f" + html shouldContain "background-color:#2a00d4" + html shouldContain "background-color:#7f007f" + html shouldContain "background-color:#ff0000" + formatted::class.simpleName shouldBe "FormattedFrame" + } + + @Test + fun `format with linear color interpolation`() { + val formatted = df.format { age }.with { value -> + textColor(linear(value, 15 to blue, 45 to red)) + } + val html = formatted.toHtml().toString() + + html shouldContain "color:#0000ff" + html shouldContain "color:#2a00d4" + html shouldContain "color:#d4002a" + html shouldContain "color:#7f007f" + html shouldContain "color:#2a00d4" + html shouldContain "color:#7f007f" + html shouldContain "color:#ff0000" + formatted::class.simpleName shouldBe "FormattedFrame" + } + + @Test + fun `chained format operations`() { + val formatted = df + .format().with { background(white) and textColor(black) } + .format { age }.with { background(red) } + .format { isHappy }.with { background(if (it) green else red) } + + val html = formatted.toHtml().toString() + + // Should contain all applied styles + html.split("background-color:#ffffff").size - 1 shouldBe 35 + html.split("background-color:#ff0000").size - 1 shouldBe 9 + html.split("background-color:#00ff00").size - 1 shouldBe 5 + html.split("color:#000000").size - 1 shouldBe 98 // includes attributes outside cells + formatted::class.simpleName shouldBe "FormattedFrame" + } + + @Test + fun `format all columns`() { + val formatted = df.format().with { bold and textColor(black) } + val html = formatted.toHtml().toString() + + html.split("font-weight:bold").size - 1 shouldBe 49 // All cells formatted + html.split("color:#000000").size - 1 shouldBe 98 // includes attributes outside cells + } + + @Test + fun `format by column names`() { + val formatted = df.format("age", "weight").with { background(blue) } + val html = formatted.toHtml().toString() + + html.split("background-color:#0000ff").size - 1 shouldBe 14 // 7 rows * 2 columns (age, weight) + } + + @Test + fun `format with complex perRowCol logic`() { + val formatted = df.format { age }.perRowCol { row, col -> + val value = col[row] + when { + value < 20 -> textColor(blue) + value < 30 -> textColor(green) + else -> textColor(red) + } + } + val html = formatted.toHtml().toString() + + // Ages: 15(blue), 45(red), 20(green), 40(red), 30(green), 20(green), 30(green) + html.split("color:#0000ff").size - 1 shouldBe 2 // blue: age < 20 + html.split("color:#00ff00").size - 1 shouldBe 4 // green: 20 <= age < 30 + html.split("color:#ff0000").size - 1 shouldBe 8 // red: age >= 30 + formatted::class.simpleName shouldBe "FormattedFrame" + } + + @Test + fun `toStandaloneHtml includes CSS definitions`() { + val formatted = df.format { age }.with { background(red) } + val standaloneHtml = formatted.toStandaloneHtml().toString() + val regularHtml = formatted.toHtml().toString() + + // Standalone should be longer and contain more CSS/script definitions + standaloneHtml.length shouldNotBe regularHtml.length + standaloneHtml.split("").size - 1 shouldBe 0 + standaloneHtml.split("").size - 1 shouldBe 1 + standaloneHtml.split("").size - 1 shouldBe 1 + standaloneHtml.split(" + + +
+ +

+ + + diff --git a/docs/StardustDocs/resources/modify/operations/formatExample_properties.html b/docs/StardustDocs/resources/modify/operations/formatExample_properties.html new file mode 100644 index 0000000000..1cd67c82d1 --- /dev/null +++ b/docs/StardustDocs/resources/modify/operations/formatExample_properties.html @@ -0,0 +1,516 @@ + + + + + +
+ +

+ + + diff --git a/docs/StardustDocs/resources/modify/operations/formatExample_strings.html b/docs/StardustDocs/resources/modify/operations/formatExample_strings.html new file mode 100644 index 0000000000..4edefab106 --- /dev/null +++ b/docs/StardustDocs/resources/modify/operations/formatExample_strings.html @@ -0,0 +1,516 @@ + + + + + +
+ +

+ + + diff --git a/docs/StardustDocs/resources/snippets/kdocs/org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.ForHtml.html b/docs/StardustDocs/resources/snippets/kdocs/org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.ForHtml.html new file mode 100644 index 0000000000..a8ecf17deb --- /dev/null +++ b/docs/StardustDocs/resources/snippets/kdocs/org.jetbrains.kotlinx.dataframe.api.FormatDocs.Grammar.ForHtml.html @@ -0,0 +1,96 @@ + + + + + +

Definitions:

cellFormatter: FormattingDsl.(cell: C) -> CellAttributes?

    

rowColFormatter: FormattingDsl.(row: DataRow<T>, col: DataColumn<C>) -> CellAttributes?

Notation:

format { columns }

     +[ .where { filter: RowValueFilter } ]

     +[ .at(rowIndices: Collection<Int> | IntRange|vararg Int) ]

     +[ .notNull() ]

     +.with { cellFormatter }

     +| .notNull { cellFormatter }

     +| .perRowCol { rowColFormatter }

     +| .linearBg(from: Pair<Number, RgbColor>,to:Pair<Number, RgbColor>)

[ .format ↺ ]

    

Formatting DSL Grammar

Definitions:

cellAttributes: CellAttributes

    

color: RgbColor

Notation:

- Returning CellAttributes:

cellAttributes and cellAttributes

| italic | bold | underline

| background(color)

| background(r: Short,g:Short,b:Short)

| linearBg(value: Number,from:Pair<Number, RgbColor>,to:Pair<Number, RgbColor>)

| textColor(color)

| textColor(r: Short,g:Short,b:Short)

| attr(name: String,value:String)

- Returning RgbColor:

black | white | green | red | blue | gray | darkGray | lightGray

| rgb(r: Short,g:Short,b:Short)

| linear(value: Number,from:Pair<Number, RgbColor>,to:Pair<Number, RgbColor>)

+ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareInnerColumns.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareInnerColumns.html index e4ca55bde7..8ebfd6f97d 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareInnerColumns.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareInnerColumns.html @@ -478,28 +478,28 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareInnerValues.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareInnerValues.html index 6ff02f4989..a7a8bceb7e 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareInnerValues.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareInnerValues.html @@ -478,30 +478,30 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareLeft.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareLeft.html index 8f0b495c69..f245392bb7 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareLeft.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareLeft.html @@ -493,58 +493,58 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ call_DataFrame(function() { DataFrame.renderTable(2) }); /**/ call_DataFrame(function() { DataFrame.renderTable(3) }); /**/ call_DataFrame(function() { DataFrame.renderTable(4) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareRight.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareRight.html index 42b6cb6b07..2bb61d79df 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareRight.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.compareRight.html @@ -493,58 +493,58 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ call_DataFrame(function() { DataFrame.renderTable(2) }); /**/ call_DataFrame(function() { DataFrame.renderTable(3) }); /**/ call_DataFrame(function() { DataFrame.renderTable(4) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.crossProduct.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.crossProduct.html index c5d2711640..17e0c1e2d5 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.crossProduct.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.crossProduct.html @@ -477,28 +477,28 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.excludeJoinWith.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.excludeJoinWith.html index 168cb2bed6..34be0c7e3a 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.excludeJoinWith.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.excludeJoinWith.html @@ -477,26 +477,26 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.filterJoinWith.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.filterJoinWith.html index 4305270ff8..6f9ab0de88 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.filterJoinWith.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.filterJoinWith.html @@ -477,26 +477,26 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(2) }); /**/ call_DataFrame(function() { DataFrame.renderTable(3) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.fullJoinWith.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.fullJoinWith.html index c8cca41ea7..79900e68ee 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.fullJoinWith.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.fullJoinWith.html @@ -477,28 +477,28 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.joinWith.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.joinWith.html index 7e8b476bc6..f5b1e0e8e0 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.joinWith.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.joinWith.html @@ -477,28 +477,28 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.leftJoinWith.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.leftJoinWith.html index 3ff3d9f327..d19bce8604 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.leftJoinWith.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.leftJoinWith.html @@ -477,28 +477,28 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.rightJoinWith.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.rightJoinWith.html index 9b901183ec..ccd8f9d6ee 100644 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.rightJoinWith.html +++ b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.JoinWith.rightJoinWith.html @@ -477,28 +477,28 @@ })() /**/ call_DataFrame(function() { DataFrame.renderTable(0) }); /**/ call_DataFrame(function() { DataFrame.renderTable(1) }); /**/ diff --git a/docs/StardustDocs/topics/_shadow_resources.md b/docs/StardustDocs/topics/_shadow_resources.md index 954797634e..6292554580 100644 --- a/docs/StardustDocs/topics/_shadow_resources.md +++ b/docs/StardustDocs/topics/_shadow_resources.md @@ -148,7 +148,11 @@ + + + + diff --git a/docs/StardustDocs/topics/format.md b/docs/StardustDocs/topics/format.md index 8414612caf..9ca9872343 100644 --- a/docs/StardustDocs/topics/format.md +++ b/docs/StardustDocs/topics/format.md @@ -1,3 +1,118 @@ [//]: # (title: format) -// TODO + + + +DataFrame Format Operation: Apply CSS formatting for rendering a dataframe to HTML. + + + +DataFrame Format Operation: Apply CSS formatting for rendering a dataframe to HTML. + + + +DataFrame Format Operation: Apply CSS formatting for rendering a dataframe to HTML. + + +Formats the specified columns or cells within the dataframe such that +they have specific CSS attributes applied to them when rendering the dataframe to HTML. + +See [column selectors](ColumnSelectors.md) for how to select the columns for this operation. + +The selection of columns and rows to apply formatting to follows the [`update` operation](update.md). +This means you can `format { }` some columns `where {}` some predicate holds true `at()` a certain range of rows +`with {}` some cell attributes, just to name an example. + +`.perRowCol { row, col -> }` is also available as an alternative to `.with {}`, if you want to format cells based on +their relative context. See the example below for a use-case for this operation. + +There are also a handful of shortcuts for common operations within `format`, such as `.linearBg(-20 to blue, 50 to red)` +which is a shortcut for `.with { background(linear(it, -20 to blue, 50 to red)) }`, and `.notNull {}` which is a +shortcut +for `.notNull().with {}`, filtering cells to only include non-null ones. + +Finally, you can decide which attributes the selected cells get. +You can combine as many as you like by chaining +them with the `and` infix inside the Formatting DSL. +Some common examples include `background(white)`, which sets the background to `white` for a cell, +`italic`, which makes the cell text _italic_, `textColor(linear(it, 0 to green, 100 to rgb(255, 255, 0)))`, which +interpolates the text color between green and yellow based on where the value of the cell lies in between 0 and 100, and +finally `attr("text-align", "center")`, a custom attribute which centers the text inside the cell. +See [](#grammar) for everything that's available. + +The `format` function can be repeated as many times as needed and, to view the result, you can call +[`toHtml()`/`toStandaloneHtml()`](toHTML.md). + +#### Grammar {collapsible="true"} + + + +#### Examples + +The formatting DSL allows you to create all sorts of formatted tables. +The formatting can depend on the data; for instance, to highlight how the value of +a column corresponds to values of other columns: + + + + + +```kotlin +val ageMin = df.age.min() +val ageMax = df.age.max() + +df + .format().with { bold and textColor(black) and background(white) } + .format { isHappy }.with { background(if (it) green else red) } + .format { weight }.notNull().linearBg(50 to FormattingDsl.blue, 90 to FormattingDsl.red) + .format { age }.perRowCol { row, col -> + textColor( + linear(value = col[row], from = ageMin to blue, to = ageMax to green), + ) + } +``` + + + + +```kotlin +val ageMin = df.min { "age"() } +val ageMax = df.max { "age"() } + +df + .format().with { bold and textColor(black) and background(white) } + .format("isHappy").with { + background(if (it as Boolean) green else red) + } + .format("weight").notNull().with { linearBg(it as Int, 50 to blue, 90 to red) } + .format("age").perRowCol { row, col -> + col as DataColumn + textColor( + linear(value = col[row], from = ageMin to blue, to = ageMax to green), + ) + } +``` + + + + + + +Alternatively, you could also customize the dataframe in a data-independent manner: + + + +```kotlin +df2.format().perRowCol { row, col -> + val rowIndex = row.index() + val colIndex = row.df().getColumnIndex(col) + if ((rowIndex - colIndex) % 3 == 0) { + background(darkGray) and textColor(white) + } else { + background(white) and textColor(black) + } +} +``` + + + diff --git a/docs/StardustDocs/topics/rendering.md b/docs/StardustDocs/topics/rendering.md index 4c536081b3..32f307069d 100644 --- a/docs/StardustDocs/topics/rendering.md +++ b/docs/StardustDocs/topics/rendering.md @@ -1,6 +1,7 @@ [//]: # (title: Rendering) -This section describes APIs that you can use to render DataFrame types and configure display. +This section describes APIs that you can use to render DataFrame types and configure how they are displayed. -* [`toHTML`](toHTML.md) — operation for rendering DataFrame object to an HTML table +* [`toHtml`](toHTML.md) — an operation for rendering DataFrame objects to an HTML table +* [`format`](format.md) — an operation to apply visual attributes to cells before rendering to HTML * [`Jupyter Notebooks`](jupyterRendering.md) — configuration specific to notebook environments diff --git a/docs/StardustDocs/topics/toHTML.md b/docs/StardustDocs/topics/toHTML.md index 2ab1d314c8..89baa91269 100644 --- a/docs/StardustDocs/topics/toHTML.md +++ b/docs/StardustDocs/topics/toHTML.md @@ -2,17 +2,21 @@ -DataFrame can be rendered to HTML. +`DataFrame` instances can be rendered to HTML. Rendering of hierarchical tables in HTML is supported by JS and CSS definitions that can be found in project resources. -Depending on your environment there can be different ways to use result of `toHtml` functions +Dataframes can also be formatted before being converted to HTML. +See [](format.md) for how to do this. + +Depending on your environment, there can be different ways to use the result of `toHtml` functions. ## IntelliJ IDEA -### Working with result +### Working with the result -The following function produces HTML that includes JS and CSS definitions. It can be displayed in the browser and has parameters for customization. +The following function produces HTML that includes JS and CSS definitions. +It can be displayed in the browser and has parameters for customization. @@ -26,7 +30,9 @@ df.toStandaloneHtml(DisplayConfiguration(rowsLimit = null)).writeHtml(Path("/pat ### Composing multiple tables -`toHtml` and `toStandaloneHtml` return composable `DataFrameHtmlData`. You can use it to include additional scripts, elements, styles on final page or just merge together multiple tables. +`toHtml` and `toStandaloneHtml` return composable `DataFrameHtmlData`, +which you can use to include additional scripts, elements, +or styles at the end of the page or just to merge multiple tables into one HTML snippet. @@ -44,7 +50,8 @@ listOf(df1, df2, df3).fold(DataFrameHtmlData.tableDefinitions()) { acc, df -> ac ### Configuring display for individual output -`toHtml` is useful if you want to configure how a single cell is displayed. To configure the display for the entire notebook, please refer to [Jupyter Notebooks](jupyterRendering.md) section. +`toHtml` is useful if you want to configure how a single cell is displayed. +To configure the display for the entire notebook, please refer to the [](jupyterRendering.md) section. diff --git a/examples/notebooks/dev/wine/WineNetWIthKotlinDL.ipynb b/examples/notebooks/dev/wine/WineNetWIthKotlinDL.ipynb index 175a8ac1f3..8bab36888d 100644 --- a/examples/notebooks/dev/wine/WineNetWIthKotlinDL.ipynb +++ b/examples/notebooks/dev/wine/WineNetWIthKotlinDL.ipynb @@ -19,8 +19,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:56:40.349956Z", - "start_time": "2025-05-28T10:56:40.324636Z" + "end_time": "2025-08-04T19:10:46.465849Z", + "start_time": "2025-08-04T19:10:46.437989Z" } }, "cell_type": "code", @@ -31,14 +31,14 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:56:46.678907Z", - "start_time": "2025-05-28T10:56:40.353708Z" + "end_time": "2025-08-04T19:10:54.630902Z", + "start_time": "2025-08-04T19:10:47.236780Z" } }, "cell_type": "code", "source": "%use dataframe", "outputs": [], - "execution_count": 2 + "execution_count": 3 }, { "cell_type": "code", @@ -47,13 +47,13 @@ "is_executing": true }, "ExecuteTime": { - "end_time": "2025-05-28T10:56:49.158217Z", - "start_time": "2025-05-28T10:56:46.694225Z" + "end_time": "2025-08-04T19:10:58.474041Z", + "start_time": "2025-08-04T19:10:54.658015Z" } }, "source": "%use kotlin-dl", "outputs": [], - "execution_count": 3 + "execution_count": 4 }, { "cell_type": "markdown", @@ -66,8 +66,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:56:53.207927Z", - "start_time": "2025-05-28T10:56:49.171422Z" + "end_time": "2025-08-04T19:11:02.418907Z", + "start_time": "2025-08-04T19:10:58.768380Z" } }, "source": [ @@ -257,7 +257,7 @@ " </style>\n", " </head>\n", " <body>\n", - " <table class="dataframe" id="df_-1358954496"></table>\n", + " <table class="dataframe" id="df_687865856"></table>\n", "\n", "<p class="dataframe_description">DataFrame: rowsCount = 5, columnsCount = 12</p>\n", "\n", @@ -550,10 +550,10 @@ "{ name: "<span title=\"sulphates: Double\">sulphates</span>", children: [], rightAlign: true, values: ["<span class=\"formatted\" title=\"\"><span class=\"numbers\">0.56</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">0.68</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">0.65</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">0.58</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">0.56</span></span>"] }, \n", "{ name: "<span title=\"alcohol: Double\">alcohol</span>", children: [], rightAlign: true, values: ["<span class=\"formatted\" title=\"\"><span class=\"numbers\">9.4</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">9.8</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">9.8</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">9.8</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">9.4</span></span>"] }, \n", "{ name: "<span title=\"quality: Int\">quality</span>", children: [], rightAlign: true, values: ["<span class=\"formatted\" title=\"\"><span class=\"numbers\">5</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">5</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">5</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">6</span></span>","<span class=\"formatted\" title=\"\"><span class=\"numbers\">5</span></span>"] }, \n", - "], id: -1358954496, rootId: -1358954496, totalRows: 5 } ) });\n", + "], id: 687865856, rootId: 687865856, totalRows: 5 } ) });\n", "/*-->*/\n", "\n", - "call_DataFrame(function() { DataFrame.renderTable(-1358954496) });\n", + "call_DataFrame(function() { DataFrame.renderTable(687865856) });\n", "\n", "\n", " </script>\n", @@ -726,21 +726,21 @@ " \n", " \n", " \n", - "
fixed acidityvolatile aciditycitric acidresidual sugarchloridesfree sulfur dioxidetotal sulfur dioxidedensitypHsulphatesalcoholquality
7.4000000.7000000.0000001.9000000.07600011.00000034.0000000.9978003.5100000.5600009.4000005
7.8000000.8800000.0000002.6000000.09800025.00000067.0000000.9968003.2000000.6800009.8000005
7.8000000.7600000.0400002.3000000.09200015.00000054.0000000.9970003.2600000.6500009.8000005
11.2000000.2800000.5600001.9000000.07500017.00000060.0000000.9980003.1600000.5800009.8000006
7.4000000.7000000.0000001.9000000.07600011.00000034.0000000.9978003.5100000.5600009.4000005
\n", + "
fixed acidityvolatile aciditycitric acidresidual sugarchloridesfree sulfur dioxidetotal sulfur dioxidedensitypHsulphatesalcoholquality
7.4000000.7000000.0000001.9000000.07600011.00000034.0000000.9978003.5100000.5600009.4000005
7.8000000.8800000.0000002.6000000.09800025.00000067.0000000.9968003.2000000.6800009.8000005
7.8000000.7600000.0400002.3000000.09200015.00000054.0000000.9970003.2600000.6500009.8000005
11.2000000.2800000.5600001.9000000.07500017.00000060.0000000.9980003.1600000.5800009.8000006
7.4000000.7000000.0000001.9000000.07600011.00000034.0000000.9978003.5100000.5600009.4000005
\n", " \n", " \n", " " ], "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"fixed acidity\",\"volatile acidity\",\"citric acid\",\"residual sugar\",\"chlorides\",\"free sulfur dioxide\",\"total sulfur dioxide\",\"density\",\"pH\",\"sulphates\",\"alcohol\",\"quality\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"}],\"nrow\":5,\"ncol\":12},\"kotlin_dataframe\":[{\"fixed acidity\":7.4,\"volatile acidity\":0.7,\"citric acid\":0.0,\"residual sugar\":1.9,\"chlorides\":0.076,\"free sulfur dioxide\":11.0,\"total sulfur dioxide\":34.0,\"density\":0.9978,\"pH\":3.51,\"sulphates\":0.56,\"alcohol\":9.4,\"quality\":5},{\"fixed acidity\":7.8,\"volatile acidity\":0.88,\"citric acid\":0.0,\"residual sugar\":2.6,\"chlorides\":0.098,\"free sulfur dioxide\":25.0,\"total sulfur dioxide\":67.0,\"density\":0.9968,\"pH\":3.2,\"sulphates\":0.68,\"alcohol\":9.8,\"quality\":5},{\"fixed acidity\":7.8,\"volatile acidity\":0.76,\"citric acid\":0.04,\"residual sugar\":2.3,\"chlorides\":0.092,\"free sulfur dioxide\":15.0,\"total sulfur dioxide\":54.0,\"density\":0.997,\"pH\":3.26,\"sulphates\":0.65,\"alcohol\":9.8,\"quality\":5},{\"fixed acidity\":11.2,\"volatile acidity\":0.28,\"citric acid\":0.56,\"residual sugar\":1.9,\"chlorides\":0.075,\"free sulfur dioxide\":17.0,\"total sulfur dioxide\":60.0,\"density\":0.998,\"pH\":3.16,\"sulphates\":0.58,\"alcohol\":9.8,\"quality\":6},{\"fixed acidity\":7.4,\"volatile acidity\":0.7,\"citric acid\":0.0,\"residual sugar\":1.9,\"chlorides\":0.076,\"free sulfur dioxide\":11.0,\"total sulfur dioxide\":34.0,\"density\":0.9978,\"pH\":3.51,\"sulphates\":0.56,\"alcohol\":9.4,\"quality\":5}]}" }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 4 + "execution_count": 5 }, { "metadata": {}, @@ -755,20 +755,23 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:58:34.197613Z", - "start_time": "2025-05-28T10:58:34.019830Z" + "end_time": "2025-08-04T19:11:03.284594Z", + "start_time": "2025-08-04T19:11:02.584533Z" } }, "source": [ + "import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.red\n", + "import org.jetbrains.kotlinx.dataframe.api.FormattingDsl.green\n", + "\n", "rawDf.corr()\n", - " .format { colsOf() }.with { linearBg(value = it, from = -1.0 to red, to = 1.0 to green) }\n", + " .format { colsOf() }.linearBg(from = -1 to red, to = +1 to green)\n", " .toHtml()" ], "outputs": [ { "data": { "text/html": [ - " \n", " \n", " " ], @@ -2115,8 +2118,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:58:56.621291Z", - "start_time": "2025-05-28T10:58:56.116886Z" + "end_time": "2025-08-04T19:11:07.051759Z", + "start_time": "2025-08-04T19:11:06.171066Z" } }, "source": [ @@ -2152,8 +2155,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:59:04.592184Z", - "start_time": "2025-05-28T10:59:04.415519Z" + "end_time": "2025-08-04T19:11:08.239873Z", + "start_time": "2025-08-04T19:11:07.966027Z" } }, "source": [ @@ -2175,8 +2178,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:59:17.233420Z", - "start_time": "2025-05-28T10:59:15.925180Z" + "end_time": "2025-08-04T19:11:11.182470Z", + "start_time": "2025-08-04T19:11:08.910459Z" } }, "source": [ @@ -2213,8 +2216,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:59:19.520847Z", - "start_time": "2025-05-28T10:59:19.323480Z" + "end_time": "2025-08-04T19:11:11.464569Z", + "start_time": "2025-08-04T19:11:11.208143Z" } }, "source": [ @@ -2227,8 +2230,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:59:21.999158Z", - "start_time": "2025-05-28T10:59:21.845761Z" + "end_time": "2025-08-04T19:11:11.605662Z", + "start_time": "2025-08-04T19:11:11.479856Z" } }, "source": [ @@ -2273,8 +2276,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:59:34.097925Z", - "start_time": "2025-05-28T10:59:28.769627Z" + "end_time": "2025-08-04T19:11:29.937339Z", + "start_time": "2025-08-04T19:11:11.887298Z" } }, "source": [ @@ -2287,8 +2290,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:59:34.289579Z", - "start_time": "2025-05-28T10:59:34.101283Z" + "end_time": "2025-08-04T19:11:30.297752Z", + "start_time": "2025-08-04T19:11:29.971573Z" } }, "source": [ @@ -2298,7 +2301,7 @@ { "data": { "text/html": [ - " \n", " \n", " " ], - "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"epochIndex\",\"lossValue\",\"metricValues\",\"valLossValue\",\"valMetricValues\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.collections.List\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.collections.List\"}],\"nrow\":5,\"ncol\":5},\"kotlin_dataframe\":[{\"epochIndex\":1996,\"lossValue\":0.334850937128067,\"metricValues\":[0.45112717151641846],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1997,\"lossValue\":0.3348143994808197,\"metricValues\":[0.45109668374061584],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1998,\"lossValue\":0.33477771282196045,\"metricValues\":[0.45106613636016846],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1999,\"lossValue\":0.3347410261631012,\"metricValues\":[0.45103588700294495],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":2000,\"lossValue\":0.33470451831817627,\"metricValues\":[0.45100536942481995],\"valLossValue\":NaN,\"valMetricValues\":[NaN]}]}" + "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"epochIndex\",\"lossValue\",\"metricValues\",\"valLossValue\",\"valMetricValues\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.collections.List\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.collections.List\"}],\"nrow\":5,\"ncol\":5},\"kotlin_dataframe\":[{\"epochIndex\":1996,\"lossValue\":0.3344540596008301,\"metricValues\":[0.45078158378601074],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1997,\"lossValue\":0.3344161808490753,\"metricValues\":[0.4507482051849365],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1998,\"lossValue\":0.3343783915042877,\"metricValues\":[0.4507148265838623],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1999,\"lossValue\":0.33434057235717773,\"metricValues\":[0.45068153738975525],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":2000,\"lossValue\":0.33430275321006775,\"metricValues\":[0.45064806938171387],\"valLossValue\":NaN,\"valMetricValues\":[NaN]}]}" }, "execution_count": 14, "metadata": {}, @@ -2966,8 +2969,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:59:41.417539Z", - "start_time": "2025-05-28T10:59:41.312689Z" + "end_time": "2025-08-04T19:11:30.766336Z", + "start_time": "2025-08-04T19:11:30.617645Z" } }, "source": [ @@ -2977,7 +2980,7 @@ { "data": { "text/plain": [ - "5.24972" + "5.2470083" ] }, "execution_count": 15, @@ -2991,8 +2994,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:59:42.274260Z", - "start_time": "2025-05-28T10:59:42.202425Z" + "end_time": "2025-08-04T19:11:31.573009Z", + "start_time": "2025-08-04T19:11:31.454306Z" } }, "source": [ @@ -3023,8 +3026,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T10:59:44.410731Z", - "start_time": "2025-05-28T10:59:44.301119Z" + "end_time": "2025-08-04T19:11:33.029286Z", + "start_time": "2025-08-04T19:11:32.904035Z" } }, "source": [ @@ -3044,8 +3047,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:00:40.005511Z", - "start_time": "2025-05-28T11:00:39.811245Z" + "end_time": "2025-08-04T19:11:34.342143Z", + "start_time": "2025-08-04T19:11:33.501033Z" } }, "source": [ @@ -3081,7 +3084,7 @@ "}" ], "outputs": [], - "execution_count": 19 + "execution_count": 18 }, { "cell_type": "markdown", @@ -3094,8 +3097,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:00:44.289678Z", - "start_time": "2025-05-28T11:00:43.442641Z" + "end_time": "2025-08-04T19:11:36.294575Z", + "start_time": "2025-08-04T19:11:34.865Z" } }, "source": [ @@ -3103,14 +3106,14 @@ " trainTestSplit(df, \"quality\", 0.8)" ], "outputs": [], - "execution_count": 20 + "execution_count": 19 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:01:31.190997Z", - "start_time": "2025-05-28T11:01:31.053044Z" + "end_time": "2025-08-04T19:11:37.272642Z", + "start_time": "2025-08-04T19:11:36.852357Z" } }, "source": [ @@ -3121,14 +3124,14 @@ " .toTypedArray()" ], "outputs": [], - "execution_count": 22 + "execution_count": 20 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:01:38.146951Z", - "start_time": "2025-05-28T11:01:38.051890Z" + "end_time": "2025-08-04T19:11:38.217463Z", + "start_time": "2025-08-04T19:11:37.974931Z" } }, "source": [ @@ -3139,14 +3142,14 @@ " .toFloatArray()" ], "outputs": [], - "execution_count": 23 + "execution_count": 21 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:01:42.822059Z", - "start_time": "2025-05-28T11:01:42.710503Z" + "end_time": "2025-08-04T19:11:39.039353Z", + "start_time": "2025-08-04T19:11:38.774209Z" } }, "source": [ @@ -3156,14 +3159,14 @@ "val testYDL = testY.toY()" ], "outputs": [], - "execution_count": 24 + "execution_count": 22 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:01:44.236813Z", - "start_time": "2025-05-28T11:01:44.058822Z" + "end_time": "2025-08-04T19:11:39.697144Z", + "start_time": "2025-08-04T19:11:39.554841Z" } }, "source": [ @@ -3171,14 +3174,14 @@ "val testKotlinDLDataset = OnHeapDataset.create({ testXDL }, { testYDL })" ], "outputs": [], - "execution_count": 25 + "execution_count": 23 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:01:48.178391Z", - "start_time": "2025-05-28T11:01:47.942204Z" + "end_time": "2025-08-04T19:11:40.708347Z", + "start_time": "2025-08-04T19:11:40.017912Z" } }, "source": [ @@ -3236,14 +3239,14 @@ ] } ], - "execution_count": 26 + "execution_count": 24 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:01:56.732805Z", - "start_time": "2025-05-28T11:01:50.419220Z" + "end_time": "2025-08-04T19:11:54.863249Z", + "start_time": "2025-08-04T19:11:41.167286Z" } }, "source": [ @@ -3254,7 +3257,7 @@ { "data": { "text/html": [ - " \n", " \n", " " ], - "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"epochIndex\",\"lossValue\",\"metricValues\",\"valLossValue\",\"valMetricValues\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.collections.List\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.collections.List\"}],\"nrow\":5,\"ncol\":5},\"kotlin_dataframe\":[{\"epochIndex\":1996,\"lossValue\":0.3345320522785187,\"metricValues\":[0.4508610963821411],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1997,\"lossValue\":0.33449509739875793,\"metricValues\":[0.45082950592041016],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1998,\"lossValue\":0.3344581127166748,\"metricValues\":[0.45079800486564636],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1999,\"lossValue\":0.33442115783691406,\"metricValues\":[0.4507667124271393],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":2000,\"lossValue\":0.33438411355018616,\"metricValues\":[0.45073509216308594],\"valLossValue\":NaN,\"valMetricValues\":[NaN]}]}" + "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"epochIndex\",\"lossValue\",\"metricValues\",\"valLossValue\",\"valMetricValues\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.collections.List\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.collections.List\"}],\"nrow\":5,\"ncol\":5},\"kotlin_dataframe\":[{\"epochIndex\":1996,\"lossValue\":0.33471378684043884,\"metricValues\":[0.45107153058052063],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1997,\"lossValue\":0.3346775472164154,\"metricValues\":[0.45104148983955383],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1998,\"lossValue\":0.3346412479877472,\"metricValues\":[0.4510118067264557],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":1999,\"lossValue\":0.334604948759079,\"metricValues\":[0.4509819746017456],\"valLossValue\":NaN,\"valMetricValues\":[NaN]},{\"epochIndex\":2000,\"lossValue\":0.334568589925766,\"metricValues\":[0.4509516954421997],\"valLossValue\":NaN,\"valMetricValues\":[NaN]}]}" }, - "execution_count": 27, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 27 + "execution_count": 25 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:01:56.818019Z", - "start_time": "2025-05-28T11:01:56.735440Z" + "end_time": "2025-08-04T19:11:55.328992Z", + "start_time": "2025-08-04T19:11:55.231901Z" } }, "source": [ @@ -3926,22 +3929,22 @@ { "data": { "text/plain": [ - "5.8768764" + "5.4023023" ] }, - "execution_count": 28, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 28 + "execution_count": 26 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:02:06.727475Z", - "start_time": "2025-05-28T11:02:06.684985Z" + "end_time": "2025-08-04T19:11:56.007498Z", + "start_time": "2025-08-04T19:11:55.935914Z" } }, "source": [ @@ -3951,15 +3954,15 @@ { "data": { "text/plain": [ - "5.0" + "6.0" ] }, - "execution_count": 29, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 29 + "execution_count": 27 }, { "cell_type": "markdown", @@ -3972,8 +3975,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:02:58.701570Z", - "start_time": "2025-05-28T11:02:58.378637Z" + "end_time": "2025-08-04T19:11:57.516188Z", + "start_time": "2025-08-04T19:11:56.816467Z" } }, "source": [ @@ -3988,14 +3991,14 @@ "val predDf = dataFrameOf(predicted, ground_truth)" ], "outputs": [], - "execution_count": 30 + "execution_count": 28 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:02:59.233583Z", - "start_time": "2025-05-28T11:02:59.172927Z" + "end_time": "2025-08-04T19:11:58.274177Z", + "start_time": "2025-08-04T19:11:58.184350Z" } }, "source": [ @@ -4005,7 +4008,7 @@ { "data": { "text/html": [ - " \n", " \n", " " ], - "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"predicted\",\"ground_truth\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"}],\"nrow\":5,\"ncol\":2},\"kotlin_dataframe\":[{\"predicted\":6,\"ground_truth\":6},{\"predicted\":5,\"ground_truth\":4},{\"predicted\":6,\"ground_truth\":6},{\"predicted\":5,\"ground_truth\":5},{\"predicted\":5,\"ground_truth\":5}]}" + "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"predicted\",\"ground_truth\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"}],\"nrow\":5,\"ncol\":2},\"kotlin_dataframe\":[{\"predicted\":7,\"ground_truth\":6},{\"predicted\":5,\"ground_truth\":5},{\"predicted\":7,\"ground_truth\":7},{\"predicted\":6,\"ground_truth\":6},{\"predicted\":6,\"ground_truth\":6}]}" }, - "execution_count": 31, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 31 + "execution_count": 29 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:03:39.596244Z", - "start_time": "2025-05-28T11:03:39.225342Z" + "end_time": "2025-08-04T19:11:59.946126Z", + "start_time": "2025-08-04T19:11:58.837265Z" } }, "source": [ @@ -4677,14 +4680,14 @@ " val y = col.name().toInt()\n", " val x = row.ground_truth\n", " val k = 1.0 - abs(x - y) / 10.0\n", - " background(RGBColor(50, (50 + k * 200).toInt().toShort(), 50))\n", + " background(RgbColor(50, (50 + k * 200).toInt().toShort(), 50))\n", "}.toHtml()" ], "outputs": [ { "data": { "text/html": [ - " \n", " " ] }, - "execution_count": 35, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 35 + "execution_count": 30 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:03:56.515424Z", - "start_time": "2025-05-28T11:03:56.317184Z" + "end_time": "2025-08-04T19:12:01.141646Z", + "start_time": "2025-08-04T19:12:00.677606Z" } }, "source": [ "val predDf2 = predDf.add(\"avg_dev\") { abs(predicted - ground_truth) }" ], "outputs": [], - "execution_count": 36 + "execution_count": 31 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:03:57.220978Z", - "start_time": "2025-05-28T11:03:57.020582Z" + "end_time": "2025-08-04T19:12:02.093517Z", + "start_time": "2025-08-04T19:12:01.821333Z" } }, "source": [ @@ -5360,7 +5363,7 @@ { "data": { "text/html": [ - " \n", " \n", " " ], - "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"name\",\"type\",\"count\",\"unique\",\"nulls\",\"top\",\"freq\",\"mean\",\"std\",\"min\",\"p25\",\"median\",\"p75\",\"max\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.String\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.String\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"}],\"nrow\":1,\"ncol\":14},\"kotlin_dataframe\":[{\"name\":\"avg_dev\",\"type\":\"Int\",\"count\":319,\"unique\":3,\"nulls\":0,\"top\":0,\"freq\":196,\"mean\":0.40752351097178685,\"std\":0.5350070344969485,\"min\":0,\"p25\":0.0,\"median\":0.0,\"p75\":1.0,\"max\":2}]}" + "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"name\",\"type\",\"count\",\"unique\",\"nulls\",\"top\",\"freq\",\"mean\",\"std\",\"min\",\"p25\",\"median\",\"p75\",\"max\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.String\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.String\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Double\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"}],\"nrow\":1,\"ncol\":14},\"kotlin_dataframe\":[{\"name\":\"avg_dev\",\"type\":\"Int\",\"count\":319,\"unique\":3,\"nulls\":0,\"top\":0,\"freq\":199,\"mean\":0.3824451410658307,\"std\":0.4995019305115917,\"min\":0,\"p25\":0.0,\"median\":0.0,\"p75\":1.0,\"max\":2}]}" }, - "execution_count": 37, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 37 + "execution_count": 32 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:03:58.933406Z", - "start_time": "2025-05-28T11:03:58.800454Z" + "end_time": "2025-08-04T19:12:02.676139Z", + "start_time": "2025-08-04T19:12:02.520194Z" } }, "source": [ @@ -6041,7 +6044,7 @@ { "data": { "text/html": [ - " \n", " \n", " " ], - "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"predicted\",\"ground_truth\",\"avg_dev\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"}],\"nrow\":1,\"ncol\":3},\"kotlin_dataframe\":[{\"predicted\":6,\"ground_truth\":5,\"avg_dev\":1}]}" + "application/kotlindataframe+json": "{\"$version\":\"2.1.1\",\"metadata\":{\"columns\":[\"predicted\",\"ground_truth\",\"avg_dev\"],\"types\":[{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"},{\"kind\":\"ValueColumn\",\"type\":\"kotlin.Int\"}],\"nrow\":1,\"ncol\":3},\"kotlin_dataframe\":[{\"predicted\":5,\"ground_truth\":6,\"avg_dev\":1}]}" }, - "execution_count": 38, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 38 + "execution_count": 33 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-05-28T11:04:00.646210Z", - "start_time": "2025-05-28T11:04:00.585028Z" + "end_time": "2025-08-04T19:12:04.096796Z", + "start_time": "2025-08-04T19:12:04.033449Z" } }, "source": [ "model2.close()" ], "outputs": [], - "execution_count": 39 + "execution_count": 34 }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-04T19:12:04.609727Z", + "start_time": "2025-08-04T19:12:04.602716Z" + } + }, "cell_type": "code", + "source": "", "outputs": [], - "execution_count": null, - "source": "" + "execution_count": null } ], "metadata": { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1dac5c8d48..a2bca39a92 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,7 +62,7 @@ geotools = "32.1" jai-core = "1.1.3" jts = "1.20.0" -kandy = "0.8.1-dev-66" +kandy = "0.8.1-dev-73" exposed = "1.0.0-beta-2" # check the versions down in the [libraries] section too! diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index f39386869b..62997a28d2 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -42,8 +42,14 @@ dependencies { testImplementation(libs.kotestAssertions) { exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") } - testImplementation(libs.kandy) - testImplementation(libs.kandy.samples.utils) + testImplementation(libs.kandy) { + // TODO remove when kandy uses version of DF with `FormatDsl` + exclude("org.jetbrains.kotlinx", "dataframe") + } + testImplementation(libs.kandy.samples.utils) { + // TODO remove when kandy uses version of DF with `FormatDsl` + exclude("org.jetbrains.kotlinx", "dataframe") + } testImplementation(libs.kotlin.datetimeJvm) testImplementation(libs.poi) testImplementation(libs.arrow.vector) @@ -65,6 +71,7 @@ korro { include("docs/StardustDocs/topics/read.md") include("docs/StardustDocs/topics/write.md") include("docs/StardustDocs/topics/rename.md") + include("docs/StardustDocs/topics/format.md") include("docs/StardustDocs/topics/guides/*.md") } diff --git a/tests/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Modify.kt b/tests/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Modify.kt new file mode 100644 index 0000000000..dc9c73f07d --- /dev/null +++ b/tests/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Modify.kt @@ -0,0 +1,106 @@ +package org.jetbrains.kotlinx.dataframe.samples.api + +import org.jetbrains.kotlinx.dataframe.DataColumn +import org.jetbrains.kotlinx.dataframe.api.FormattingDsl +import org.jetbrains.kotlinx.dataframe.api.and +import org.jetbrains.kotlinx.dataframe.api.cast +import org.jetbrains.kotlinx.dataframe.api.dataFrameOf +import org.jetbrains.kotlinx.dataframe.api.format +import org.jetbrains.kotlinx.dataframe.api.getColumnIndex +import org.jetbrains.kotlinx.dataframe.api.group +import org.jetbrains.kotlinx.dataframe.api.into +import org.jetbrains.kotlinx.dataframe.api.linearBg +import org.jetbrains.kotlinx.dataframe.api.max +import org.jetbrains.kotlinx.dataframe.api.min +import org.jetbrains.kotlinx.dataframe.api.notNull +import org.jetbrains.kotlinx.dataframe.api.perRowCol +import org.jetbrains.kotlinx.dataframe.api.with +import org.jetbrains.kotlinx.dataframe.samples.DataFrameSampleHelper +import org.junit.Test + +@Suppress("ktlint:standard:argument-list-wrapping") +class Modify : DataFrameSampleHelper("operations", "modify") { + + val df = dataFrameOf("firstName", "lastName", "age", "city", "weight", "isHappy")( + "Alice", "Cooper", 15, "London", 54, true, + "Bob", "Dylan", 45, "Dubai", 87, true, + "Charlie", "Daniels", 20, "Moscow", null, false, + "Charlie", "Chaplin", 40, "Milan", null, true, + "Bob", "Marley", 30, "Tokyo", 68, true, + "Alice", "Wolf", 20, null, 55, false, + "Charlie", "Byrd", 30, "Moscow", 90, true, + ).group("firstName", "lastName").into("name").cast() + + val df2 = dataFrameOf( + "col1", "col2", "col3", "col4", "col5", "col6", "col7", "col8", "col9", "col10", + )( + 45, 12, 78, 34, 90, 23, 67, 89, 56, 43, + 87, 34, 56, 78, 12, 45, 90, 23, 67, 89, + 23, 67, 89, 45, 78, 90, 12, 56, 34, 78, + 90, 45, 23, 67, 34, 78, 89, 12, 56, 23, + 12, 89, 45, 90, 56, 34, 78, 67, 23, 90, + 78, 56, 12, 23, 89, 67, 34, 90, 45, 12, + 34, 90, 67, 12, 45, 23, 56, 78, 89, 67, + 56, 23, 34, 89, 67, 12, 45, 34, 78, 90, + 89, 78, 90, 56, 23, 89, 67, 45, 12, 34, + 67, 45, 78, 12, 90, 56, 23, 89, 34, 78, + ) + + @Suppress("UNCHECKED_CAST") + @Test + fun formatExample_strings() { + // SampleStart + val ageMin = df.min { "age"() } + val ageMax = df.max { "age"() } + + df + .format().with { bold and textColor(black) and background(white) } + .format("isHappy").with { + background(if (it as Boolean) green else red) + } + .format("weight").notNull().with { linearBg(it as Int, 50 to blue, 90 to red) } + .format("age").perRowCol { row, col -> + col as DataColumn + textColor( + linear(value = col[row], from = ageMin to blue, to = ageMax to green), + ) + } + // SampleEnd + .saveDfHtmlSample() + } + + @Test + fun formatExample_properties() { + // SampleStart + val ageMin = df.age.min() + val ageMax = df.age.max() + + df + .format().with { bold and textColor(black) and background(white) } + .format { isHappy }.with { background(if (it) green else red) } + .format { weight }.notNull().linearBg(50 to FormattingDsl.blue, 90 to FormattingDsl.red) + .format { age }.perRowCol { row, col -> + textColor( + linear(value = col[row], from = ageMin to blue, to = ageMax to green), + ) + } + // SampleEnd + .saveDfHtmlSample() + } + + @Test + fun formatExampleNumbers() { + // SampleStart + df2.format().perRowCol { row, col -> + val rowIndex = row.index() + val colIndex = row.df().getColumnIndex(col) + if ((rowIndex - colIndex) % 3 == 0) { + background(darkGray) and textColor(white) + } else { + background(white) and textColor(black) + } + } + // SampleEnd + .saveDfHtmlSample() + } +}