commonMain.Data.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of selection Show documentation
Show all versions of selection Show documentation
A collection of drawing/charting utilities
The newest version!
package com.juul.krayon.selection
import com.juul.krayon.element.Element
/** See analogous [d3 function](https://github.com/d3/d3-selection#selection_data). */
public fun Selection.data(
value: List,
): UpdateSelection = data { _, _ -> value }
/** See analogous [d3 function](https://github.com/d3/d3-selection#selection_data). */
public fun Selection.data(
value: (index: Int, group: Group) -> List,
): UpdateSelection = dataImpl(value, null)
/**
* See analogous [d3 function](https://github.com/d3/d3-selection#selection_data).
*
* TODO: Figure out a way to strongly type [key], even though it's called with
* two different sets of argument types.
*/
public fun Selection.keyedData(
value: List,
key: Element?.(Arguments) -> Any?,
): UpdateSelection =
keyedData(value = { _, _ -> value }, key)
/**
* See analogous [d3 function](https://github.com/d3/d3-selection#selection_data).
*
* TODO: Figure out a way to strongly type [key], even though it's called with
* two different sets of argument types.
*/
public fun Selection.keyedData(
value: (index: Int, group: Group) -> List,
key: Element?.(Arguments) -> Any?,
): UpdateSelection = dataImpl(value, key)
private fun Selection.dataImpl(
value: (index: Int, group: Group) -> List,
key: (Element?.(Arguments) -> Any?)?,
): UpdateSelection {
val update = ArrayList>(groups.size)
val enter = ArrayList>(groups.size)
val exit = ArrayList>(groups.size)
for ((index, group) in groups.withIndex()) {
val data = value(index, group)
val (updateNodes, enterNodes, exitNodes) = when (key) {
null -> bindIndex(group, data)
else -> bindKey(group, data, key)
}
update += Group(group.parent, updateNodes)
enter += Group(group.parent, enterNodes)
exit += Group(group.parent, exitNodes)
// Associate enter elements with their next update element
var updateIndex = 0
for ((enterIndex, node) in enterNodes.withIndex()) {
if (node != null) {
if (enterIndex >= updateIndex) {
updateIndex = enterIndex + 1
}
node.next = updateNodes.subList(updateIndex, updateNodes.size)
.onEach { if (it == null) updateIndex += 1 }
.filterNotNull()
.firstOrNull()
}
}
}
return UpdateSelection(update, EnterSelection(enter), ExitSelection(exit))
}
private fun bindIndex(
group: Group,
data: List,
): Triple, List, List> {
val update = ArrayList(data.size)
val enter = ArrayList(data.size)
val exit = ArrayList(maxOf(data.size, group.nodes.size))
data.forEachIndexed { index, value ->
val node = group.nodes.getOrNull(index)
if (node != null) {
update.add(node.also { it.data = value })
enter.add(null)
} else {
update.add(null)
enter.add(
EnterElement().also {
it.data = value
it.parent = group.parent
},
)
}
exit.add(null)
}
if (group.nodes.size > data.size) {
group.nodes
.subList(data.size, group.nodes.size)
.forEach { node ->
exit.add(node)
}
}
return Triple(update, enter, exit)
}
private fun bindKey(
group: Group,
data: List,
key: Element?.(Arguments) -> Any?,
): Triple, List, List> {
val update = ArrayList(data.size)
val enter = ArrayList(data.size)
val exit = ArrayList(group.nodes.size)
val nodeByKeyValue = HashMap()
val keys = ArrayList(group.nodes.size)
// Compute keys for existing nodes.
// If multiple nodes share a key, nodes after the first are added to exit.
val arguments = Arguments.Buffer()
group.nodes.forEachIndexed { index, node ->
if (node == null) {
keys.add(null)
exit.add(null)
} else {
val keyValue = node.key(arguments(node.data, index, group.nodes))
keys.add(keyValue)
if (keyValue in nodeByKeyValue) {
exit.add(node)
} else {
exit.add(null)
nodeByKeyValue[keyValue] = node
}
}
}
// Compute keys for data.
// Matching nodes are added to update and removed from the key map.
// If no node matches, add it to enter.
// If multiple datums share a key, datums after the first are added to enter.
data.forEachIndexed { index, value ->
val keyValue = group.parent.key(arguments(value, index, data))
val node = nodeByKeyValue[keyValue]
if (node == null) {
enter.add(
EnterElement().also {
it.parent = group.parent
it.data = value
},
)
update.add(null)
nodeByKeyValue.remove(keyValue)
} else {
enter.add(null)
update.add(
node.also {
it.data = value
},
)
}
}
// Nodes that still exist in the key map are inserted into exit.
group.nodes.forEachIndexed { index, node ->
if (node != null && nodeByKeyValue[keys[index]] === node) {
exit[index] = node
}
}
return Triple(update, enter, exit)
}