Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hierarchies visualization API to lib-ext #220

Merged
merged 1 commit into from
May 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m -Xmx2048m
# Turn off README check when running check task
skipReadmeCheck=false

jupyterApiVersion=0.9.1-42
jupyterApiVersion=0.9.1-45
kotlin.jupyter.add.api=false
kotlin.jupyter.add.scanner=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.jetbrains.kotlinx.jupyter.api.graphs

/**
* Graph node which represents the object as a part of some hierarchy
*
* Classes implementing this interface should take care of [equals] and [hashCode]
* because they are used for testing the nodes for equality, and wrong implementation
* of these methods may lead to the wrong graph rendering, StackOverflow / OutOfMemory
* errors and so on. See example in [NodeWrapper]
*
* @param T Underlying object type
*/
interface GraphNode<T> {
/**
* Node label with all required information
*/
val label: Label

/**
* Nodes which are connected with the ingoing edges to this one:
* {this} <- {inNode}
*/
val inNodes: List<GraphNode<T>>

/**
* Nodes which are connected with the outgoing edges to this one:
* {this} -> {outNode}
*/
val outNodes: List<GraphNode<T>>

/**
* Nodes which are connected with the undirected edges to this one:
* {this} -- {biNode}
*/
val biNodes: List<GraphNode<T>>

companion object
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.jetbrains.kotlinx.jupyter.api.graphs

/**
* [Label] contains all information related to the node itself
*/
interface Label {
/**
* Node text. May be simple simple text or HTML
*/
val text: String

/**
* Shape of this node. The full list of shapes is given
* [here](https://graphviz.org/doc/info/shapes.html)
*/
val shape: String? get() = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.jetbrains.kotlinx.jupyter.api.graphs

import org.jetbrains.kotlinx.jupyter.api.graphs.labels.TextLabel

/**
* Use [NodeWrapper] if [T] cannot implement [GraphNode] itself for some reason
*/
abstract class NodeWrapper<T>(val value: T) : GraphNode<T> {
override val label: Label get() = TextLabel(value.toString())

override val inNodes get() = listOf<GraphNode<T>>()
override val outNodes get() = listOf<GraphNode<T>>()
override val biNodes get() = listOf<GraphNode<T>>()

override fun equals(other: Any?): Boolean {
return other is NodeWrapper<*> && other.value == this.value
}

override fun hashCode(): Int {
return value.hashCode()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.jetbrains.kotlinx.jupyter.api.graphs.labels

import kotlin.reflect.KProperty1

/**
* Convenience class for creating [PropObjectLabel] if only fixed subset
* of properties [propertiesToRender] should be rendered
*/
class FilteringPropObjectLabel<T : Any>(
value: T,
override val mainText: String = value.toString(),
private val propertiesToRender: Collection<String> = emptyList(),
) : PropObjectLabel<T>(value) {
override fun shouldRenderProperty(prop: KProperty1<out T, *>): Boolean {
return prop.name in propertiesToRender
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.jetbrains.kotlinx.jupyter.api.graphs.labels

import kotlin.reflect.KClass

/**
* Label representing [kClass] with all members in HTML table
*/
class KClassLabel(private val kClass: KClass<*>) : RecordTableLabel() {
override val mainText get() = kClass.simpleName.toString()

override val properties: Collection<Iterable<String>>
get() = kClass.members.map { listOf(it.name, it.returnType.toString()) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.jetbrains.kotlinx.jupyter.api.graphs.labels

import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible

/**
* Renders [value] object with its properties for
* those [shouldRenderProperty] returns `true`
*/
open class PropObjectLabel<T : Any>(val value: T) : RecordTableLabel() {
override val mainText get() = value.toString()

override val properties: Collection<Iterable<String>> get() {
val kClass = value::class

return kClass.memberProperties
.filter(::shouldRenderProperty)
.map { prop ->
@Suppress("UNCHECKED_CAST")
prop as KProperty1<T, *>
prop.isAccessible = true
listOf(prop.name, prop.invoke(value).toString())
}
}

open fun shouldRenderProperty(prop: KProperty1<out T, *>) = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.jetbrains.kotlinx.jupyter.api.graphs.labels

import org.jetbrains.kotlinx.jupyter.api.graphs.Label

/**
* Renders as n-column table
* First column consists of one cell containing [mainText].
* Next `(n-1)` columns contain values from [properties]. It is
* supposed that all element in [properties] collection would
* have `(n-1)` elements.
*/
abstract class RecordTableLabel : Label {
override val text: String get() {
val nProperties = properties.size

fun inTable(builderAction: StringBuilder.() -> Unit) = buildString {
append("<<table>")
builderAction()
append("</table>>")
}

if (nProperties == 0) return inTable { append("<tr><td>$mainText</td></tr>") }
return inTable {
properties.forEachIndexed { i, prop ->
append("<tr>")
if (i == 0 && mainText != null) {
append("""<td rowspan="$nProperties">$mainText</td>""")
}
prop.forEach { value ->
append("""<td>$value</td>""")
}
appendLine("</tr>")
}
}
}

override val shape: String? get() = "plaintext"

abstract val mainText: String?

abstract val properties: Collection<Iterable<String>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.jetbrains.kotlinx.jupyter.api.graphs.labels

import org.jetbrains.kotlinx.jupyter.api.graphs.Label

/**
* Label representing a plain text inside a given [shape]
*/
class TextLabel(value: String, override val shape: String? = "ellipse") : Label {
override val text: String = "\"${value.replace("\"", "\\\"")}\""
}
2 changes: 2 additions & 0 deletions jupyter-lib/lib-ext/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ dependencies {
implementation("org.apache.xmlgraphics:fop:2.6")
implementation("org.apache.xmlgraphics:batik-codec:1.14")
implementation("org.apache.xmlgraphics:xmlgraphics-commons:2.6")

implementation("guru.nidi:graphviz-java:0.18.1")
}

tasks.test {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.jetbrains.kotlinx.jupyter.ext.graph.structure

import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode

data class DirectedEdge<T>(
val fromNode: GraphNode<T>,
val toNode: GraphNode<T>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.jetbrains.kotlinx.jupyter.ext.graph.structure

import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode

interface Graph<T> : MultiGraph<T> {
override val directedEdges: Set<DirectedEdge<T>>
override val undirectedEdges: Set<UndirectedEdge<T>>

companion object {
fun <T> of(elements: Iterable<GraphNode<T>>): Graph<T> {
val nodes = mutableSetOf<GraphNode<T>>()
val directedEdges = mutableSetOf<DirectedEdge<T>>()
val undirectedEdges = mutableSetOf<UndirectedEdge<T>>()

for (element in elements) element.populate(nodes, directedEdges, undirectedEdges)

return GraphImpl(nodes, directedEdges, undirectedEdges)
}

fun <T> of(vararg elements: GraphNode<T>): Graph<T> = of(elements.toList())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.jetbrains.kotlinx.jupyter.ext.graph.structure

import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode

class GraphImpl<T>(
override val nodes: Set<GraphNode<T>>,
override val directedEdges: Set<DirectedEdge<T>>,
override val undirectedEdges: Set<UndirectedEdge<T>>,
) : Graph<T>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.jetbrains.kotlinx.jupyter.ext.graph.structure

import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode

interface MultiGraph<T> {
val nodes: Set<GraphNode<T>>
val directedEdges: Collection<DirectedEdge<T>>
val undirectedEdges: Collection<UndirectedEdge<T>>

companion object {
fun <T> of(elements: Iterable<GraphNode<T>>): MultiGraph<T> {
val nodes = mutableSetOf<GraphNode<T>>()
val directedEdges = mutableListOf<DirectedEdge<T>>()
val undirectedEdges = mutableListOf<UndirectedEdge<T>>()

for (element in elements) element.populate(nodes, directedEdges, undirectedEdges)

return MultiGraphImpl(nodes, directedEdges, undirectedEdges)
}

fun <T> of(vararg elements: GraphNode<T>): MultiGraph<T> = of(elements.toList())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.jetbrains.kotlinx.jupyter.ext.graph.structure

import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode

class MultiGraphImpl<T>(
override val nodes: Set<GraphNode<T>>,
override val directedEdges: List<DirectedEdge<T>>,
override val undirectedEdges: List<UndirectedEdge<T>>,
) : MultiGraph<T>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.jetbrains.kotlinx.jupyter.ext.graph.structure

import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode

data class UndirectedEdge<T>(
val fromNode: GraphNode<T>,
val toNode: GraphNode<T>,
) {
override fun equals(other: Any?): Boolean {
return other is UndirectedEdge<*> && (
(fromNode == other.fromNode) && (toNode == other.toNode) ||
(fromNode == other.toNode) && (toNode == other.fromNode)
)
}

override fun hashCode(): Int {
var h1 = fromNode.hashCode()
var h2 = toNode.hashCode()
if (h1 > h2) { val t = h2; h2 = h1; h1 = t }
return 31 * h1 + h2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.jetbrains.kotlinx.jupyter.ext.graph.structure

import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode

val <T> GraphNode<T>.allParents: Iterable<GraphNode<T>> get() {
return IterablesView(listOf(inNodes, outNodes, biNodes))
}

private class IterablesView<T>(private val iterables: Iterable<Iterable<T>>) : Iterable<T> {
override fun iterator(): Iterator<T> {
return MyIterator(iterables)
}

class MyIterator<T>(iterables: Iterable<Iterable<T>>) : Iterator<T> {
private val outerIterator = iterables.iterator()
private var innerIterator: Iterator<T>? = null

override fun hasNext(): Boolean {
while (innerIterator?.hasNext() != true) {
if (!outerIterator.hasNext()) return false
innerIterator = outerIterator.next().iterator()
}
return true
}

override fun next(): T {
if (!hasNext()) throw IndexOutOfBoundsException()
return innerIterator!!.next()
}
}
}

fun <T> GraphNode<T>.populate(
nodes: MutableSet<GraphNode<T>>,
directedEdges: MutableCollection<DirectedEdge<T>>,
undirectedEdges: MutableCollection<UndirectedEdge<T>>,
) {
nodes.add(this)
for (parent in inNodes) {
directedEdges.add(DirectedEdge(parent, this))
}
for (parent in outNodes) {
directedEdges.add(DirectedEdge(this, parent))
}
for (parent in this.biNodes) {
undirectedEdges.add(UndirectedEdge(this, parent))
}
for (parent in allParents) {
if (parent !in nodes) parent.populate(nodes, directedEdges, undirectedEdges)
}
}
Loading