@@ -15,13 +15,10 @@ import org.jetbrains.kotlinx.dataframe.api.isEmpty
15
15
import org.jetbrains.kotlinx.dataframe.api.isNumber
16
16
import org.jetbrains.kotlinx.dataframe.api.isSubtypeOf
17
17
import org.jetbrains.kotlinx.dataframe.api.rows
18
- import org.jetbrains.kotlinx.dataframe.api.sumOf
19
- import org.jetbrains.kotlinx.dataframe.api.take
20
18
import org.jetbrains.kotlinx.dataframe.columns.BaseColumn
21
19
import org.jetbrains.kotlinx.dataframe.columns.ColumnGroup
22
20
import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath
23
21
import org.jetbrains.kotlinx.dataframe.columns.FrameColumn
24
- import org.jetbrains.kotlinx.dataframe.columns.depth
25
22
import org.jetbrains.kotlinx.dataframe.impl.DataFrameSize
26
23
import org.jetbrains.kotlinx.dataframe.impl.columns.addPath
27
24
import org.jetbrains.kotlinx.dataframe.impl.renderType
@@ -42,6 +39,7 @@ import java.net.URL
42
39
import java.nio.file.Path
43
40
import java.util.*
44
41
import kotlin.io.path.writeText
42
+ import kotlin.math.ceil
45
43
46
44
internal val tooltipLimit = 1000
47
45
@@ -163,10 +161,10 @@ internal fun AnyFrame.toHtmlData(
163
161
}
164
162
}
165
163
return ColumnDataForJs (
166
- col,
167
- if (col is ColumnGroup <* >) col.columns().map { col.columnToJs(it, rowsLimit) } else emptyList(),
168
- col.isSubtypeOf<Number ?>(),
169
- contents
164
+ column = col,
165
+ nested = if (col is ColumnGroup <* >) col.columns().map { col.columnToJs(it, rowsLimit) } else emptyList(),
166
+ rightAlign = col.isSubtypeOf<Number ?>(),
167
+ values = contents,
170
168
)
171
169
}
172
170
@@ -181,12 +179,19 @@ internal fun AnyFrame.toHtmlData(
181
179
}
182
180
val body = getResourceText(" /table.html" , " ID" to rootId)
183
181
val script = scripts.joinToString(" \n " ) + " \n " + getResourceText(" /renderTable.js" , " ___ID___" to rootId)
184
- return DataFrameHtmlData (" " , body, script)
182
+ return DataFrameHtmlData (style = " " , body = body, script = script)
185
183
}
186
184
187
185
/* *
188
186
* Renders [this] [DataFrame] as static HTML (meaning no JS is used).
189
187
* CSS rendering is enabled by default but can be turned off using [includeCss]
188
+ *
189
+ * @param configuration optional configuration for rendering
190
+ * @param cellRenderer optional cell renderer for rendering
191
+ * @param includeCss whether to include CSS in the output. This is `true` by default but it can be set
192
+ * to `false` to emulate what happens in environments where custom CSS is not allowed.
193
+ * @param openNestedDfs whether to open nested dataframes. This is `false` by default byt ut can be set
194
+ * to `true` to emulate what happens in environments where `<details>` tags are not supported.
190
195
*/
191
196
public fun AnyFrame.toStaticHtml (
192
197
configuration : DisplayConfiguration = DisplayConfiguration .DEFAULT ,
@@ -206,6 +211,7 @@ public fun AnyFrame.toStaticHtml(
206
211
207
212
// Limit for number of rows in dataframes inside frame columns
208
213
val nestedRowsLimit = configuration.nestedRowsLimit
214
+ val rowsLimit = configuration.rowsLimit
209
215
210
216
// Adds the given tag to the html, with the given attributes and contents
211
217
fun StringBuilder.emitTag (tag : String , attributes : String = "", tagContents : StringBuilder .() -> Unit ) {
@@ -239,34 +245,42 @@ public fun AnyFrame.toStaticHtml(
239
245
}
240
246
241
247
// Adds a single cell to the html. DataRows from column groups already need to be split up into separate cells.
242
- fun StringBuilder.emitCell (cellValue : Any? , borders : Set <Border >): Unit =
248
+ fun StringBuilder.emitCell (cellValue : Any? , row : AnyRow , col : ColumnWithPath < * >, borders : Set <Border >): Unit =
243
249
emitTag(" td" , " ${borders.toClass()} style=\" vertical-align:top\" " ) {
244
- when (cellValue ) {
250
+ when (col ) {
245
251
// uses the <details> and <summary> to create a collapsible cell for dataframes
246
- is AnyFrame ->
252
+ is FrameColumn <* > -> {
253
+ cellValue as AnyFrame
247
254
emitTag(" details" , if (openNestedDfs) " open" else " " ) {
248
255
emitTag(" summary" ) {
249
256
append(" DataFrame [${cellValue.size} ]" )
250
257
}
258
+
259
+ // nestedRowsLimit becomes the rowsLimit for nested DFs
260
+ // while the new nested rows limit is halved, keeping at least 1
261
+ val newRowsLimit = nestedRowsLimit
262
+ val newNestedRowsLimit = nestedRowsLimit?.let { ceil(it / 2.0 ).toInt() }
263
+
251
264
// add the dataframe as a nested table limiting the number of rows if needed
252
265
// CSS will not be included here, as it is already included in the main table
253
- append (
254
- cellValue.take(nestedRowsLimit ? : Int . MAX_VALUE )
255
- .toStaticHtml(
256
- configuration ,
257
- cellRenderer ,
258
- includeCss = false ,
259
- openNestedDfs = openNestedDfs
260
- )
261
- .body
262
- )
266
+ cellValue.toStaticHtml (
267
+ configuration = configuration.copy(
268
+ rowsLimit = newRowsLimit,
269
+ nestedRowsLimit = newNestedRowsLimit ,
270
+ ) ,
271
+ cellRenderer = cellRenderer ,
272
+ includeCss = false ,
273
+ openNestedDfs = openNestedDfs,
274
+ ). let { append(it .body) }
275
+
263
276
val size = cellValue.rowsCount()
264
- if (size > (nestedRowsLimit ? : Int .MAX_VALUE ) ) {
277
+ if (size > newRowsLimit ? : Int .MAX_VALUE ) {
265
278
emitTag(" p" ) {
266
- append(" ... showing only top $nestedRowsLimit of $size rows" )
279
+ append(" ... showing only top $newRowsLimit of $size rows" )
267
280
}
268
281
}
269
282
}
283
+ }
270
284
271
285
// Else use the default cell renderer
272
286
else ->
@@ -283,13 +297,13 @@ public fun AnyFrame.toStaticHtml(
283
297
border + = Border .RIGHT
284
298
}
285
299
val cell = row[col.path()]
286
- emitCell(cell, border)
300
+ emitCell(cell, row, col, border)
287
301
}
288
302
}
289
303
290
304
// Adds the body of the html. This body contains all the cols and rows of the dataframe.
291
305
fun StringBuilder.emitBody () = emitTag(" tbody" ) {
292
- val rowsCountToRender = minOf(rowsCount(), configuration. rowsLimit ? : Int .MAX_VALUE )
306
+ val rowsCountToRender = minOf(rowsCount(), rowsLimit ? : Int .MAX_VALUE )
293
307
for (rowIndex in 0 .. < rowsCountToRender) {
294
308
emitRow(df[rowIndex])
295
309
}
@@ -349,15 +363,6 @@ internal fun BaseColumn<*>.maxWidth(): Int =
349
363
if (this is ColumnGroup <* >) columns().sumOf { it.maxWidth() }.coerceAtLeast(1 )
350
364
else 1
351
365
352
- internal fun BaseColumn <* >.maxNumberOfRows (): Int =
353
- if (this is ColumnGroup <* >) {
354
- columns().maxOfOrNull { it.maxNumberOfRows() } ? : 0
355
- } else if (this is FrameColumn <* >) {
356
- values().sumOf { it.asColumnGroup(" " ).maxNumberOfRows() }
357
- } else {
358
- this .size()
359
- }
360
-
361
366
/* *
362
367
* Given a [DataFrame], this function returns a depth-first "matrix" containing all columns
363
368
* laid out in such a way that they can be used to render the header of a table. The
0 commit comments