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