Skip to content

Commit 0dc5720

Browse files
committed
Add hierarchies visualization API
1 parent ce0bf03 commit 0dc5720

File tree

28 files changed

+477
-14
lines changed

28 files changed

+477
-14
lines changed

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m -Xmx2048m
2828
# Turn off README check when running check task
2929
skipReadmeCheck=false
3030

31-
jupyterApiVersion=0.9.1-42
31+
jupyterApiVersion=0.9.1-45
3232
kotlin.jupyter.add.api=false
3333
kotlin.jupyter.add.scanner=false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.jetbrains.kotlinx.jupyter.api.graphs
2+
3+
/**
4+
* Graph node which represents the object as a part of some hierarchy
5+
*
6+
* Classes implementing this interface should take care of [equals] and [hashCode]
7+
* because they are used for testing the nodes for equality, and wrong implementation
8+
* of these methods may lead to the wrong graph rendering, StackOverflow / OutOfMemory
9+
* errors and so on. See example in [NodeWrapper]
10+
*
11+
* @param T Underlying object type
12+
*/
13+
interface GraphNode<T> {
14+
/**
15+
* Node label with all required information
16+
*/
17+
val label: Label
18+
19+
/**
20+
* Nodes which are connected with the ingoing edges to this one:
21+
* {this} <- {inNode}
22+
*/
23+
val inNodes: List<GraphNode<T>>
24+
25+
/**
26+
* Nodes which are connected with the outgoing edges to this one:
27+
* {this} -> {outNode}
28+
*/
29+
val outNodes: List<GraphNode<T>>
30+
31+
/**
32+
* Nodes which are connected with the undirected edges to this one:
33+
* {this} -- {biNode}
34+
*/
35+
val biNodes: List<GraphNode<T>>
36+
37+
companion object
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.jetbrains.kotlinx.jupyter.api.graphs
2+
3+
/**
4+
* [Label] contains all information related to the node itself
5+
*/
6+
interface Label {
7+
/**
8+
* Node text. May be simple simple text or HTML
9+
*/
10+
val text: String
11+
12+
/**
13+
* Shape of this node. The full list of shapes is given
14+
* [here](https://graphviz.org/doc/info/shapes.html)
15+
*/
16+
val shape: String? get() = null
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.jetbrains.kotlinx.jupyter.api.graphs
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.labels.TextLabel
4+
5+
/**
6+
* Use [NodeWrapper] if [T] cannot implement [GraphNode] itself for some reason
7+
*/
8+
abstract class NodeWrapper<T>(val value: T) : GraphNode<T> {
9+
override val label: Label get() = TextLabel(value.toString())
10+
11+
override val inNodes get() = listOf<GraphNode<T>>()
12+
override val outNodes get() = listOf<GraphNode<T>>()
13+
override val biNodes get() = listOf<GraphNode<T>>()
14+
15+
override fun equals(other: Any?): Boolean {
16+
return other is NodeWrapper<*> && other.value == this.value
17+
}
18+
19+
override fun hashCode(): Int {
20+
return value.hashCode()
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.jetbrains.kotlinx.jupyter.api.graphs.labels
2+
3+
import kotlin.reflect.KProperty1
4+
5+
/**
6+
* Convenience class for creating [PropObjectLabel] if only fixed subset
7+
* of properties [propertiesToRender] should be rendered
8+
*/
9+
class FilteringPropObjectLabel<T : Any>(
10+
value: T,
11+
override val mainText: String = value.toString(),
12+
private val propertiesToRender: Collection<String> = emptyList(),
13+
) : PropObjectLabel<T>(value) {
14+
override fun shouldRenderProperty(prop: KProperty1<out T, *>): Boolean {
15+
return prop.name in propertiesToRender
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.jetbrains.kotlinx.jupyter.api.graphs.labels
2+
3+
import kotlin.reflect.KClass
4+
5+
/**
6+
* Label representing [kClass] with all members in HTML table
7+
*/
8+
class KClassLabel(private val kClass: KClass<*>) : RecordTableLabel() {
9+
override val mainText get() = kClass.simpleName.toString()
10+
11+
override val properties: Collection<Iterable<String>>
12+
get() = kClass.members.map { listOf(it.name, it.returnType.toString()) }
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.jetbrains.kotlinx.jupyter.api.graphs.labels
2+
3+
import kotlin.reflect.KProperty1
4+
import kotlin.reflect.full.memberProperties
5+
import kotlin.reflect.jvm.isAccessible
6+
7+
/**
8+
* Renders [value] object with its properties for
9+
* those [shouldRenderProperty] returns `true`
10+
*/
11+
open class PropObjectLabel<T : Any>(val value: T) : RecordTableLabel() {
12+
override val mainText get() = value.toString()
13+
14+
override val properties: Collection<Iterable<String>> get() {
15+
val kClass = value::class
16+
17+
return kClass.memberProperties
18+
.filter(::shouldRenderProperty)
19+
.map { prop ->
20+
@Suppress("UNCHECKED_CAST")
21+
prop as KProperty1<T, *>
22+
prop.isAccessible = true
23+
listOf(prop.name, prop.invoke(value).toString())
24+
}
25+
}
26+
27+
open fun shouldRenderProperty(prop: KProperty1<out T, *>) = true
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.jetbrains.kotlinx.jupyter.api.graphs.labels
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.Label
4+
5+
/**
6+
* Renders as n-column table
7+
* First column consists of one cell containing [mainText].
8+
* Next `(n-1)` columns contain values from [properties]. It is
9+
* supposed that all element in [properties] collection would
10+
* have `(n-1)` elements.
11+
*/
12+
abstract class RecordTableLabel : Label {
13+
override val text: String get() {
14+
val nProperties = properties.size
15+
16+
fun inTable(builderAction: StringBuilder.() -> Unit) = buildString {
17+
append("<<table>")
18+
builderAction()
19+
append("</table>>")
20+
}
21+
22+
if (nProperties == 0) return inTable { append("<tr><td>$mainText</td></tr>") }
23+
return inTable {
24+
properties.forEachIndexed { i, prop ->
25+
append("<tr>")
26+
if (i == 0 && mainText != null) {
27+
append("""<td rowspan="$nProperties">$mainText</td>""")
28+
}
29+
prop.forEach { value ->
30+
append("""<td>$value</td>""")
31+
}
32+
appendLine("</tr>")
33+
}
34+
}
35+
}
36+
37+
override val shape: String? get() = "plaintext"
38+
39+
abstract val mainText: String?
40+
41+
abstract val properties: Collection<Iterable<String>>
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.jetbrains.kotlinx.jupyter.api.graphs.labels
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.Label
4+
5+
/**
6+
* Label representing a plain text inside a given [shape]
7+
*/
8+
class TextLabel(value: String, override val shape: String? = "ellipse") : Label {
9+
override val text: String = "\"${value.replace("\"", "\\\"")}\""
10+
}

jupyter-lib/lib-ext/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ dependencies {
3232
implementation("org.apache.xmlgraphics:fop:2.6")
3333
implementation("org.apache.xmlgraphics:batik-codec:1.14")
3434
implementation("org.apache.xmlgraphics:xmlgraphics-commons:2.6")
35+
36+
implementation("guru.nidi:graphviz-java:0.18.1")
3537
}
3638

3739
tasks.test {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.structure
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
4+
5+
data class DirectedEdge<T>(
6+
val fromNode: GraphNode<T>,
7+
val toNode: GraphNode<T>,
8+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.structure
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
4+
5+
interface Graph<T> : MultiGraph<T> {
6+
override val directedEdges: Set<DirectedEdge<T>>
7+
override val undirectedEdges: Set<UndirectedEdge<T>>
8+
9+
companion object {
10+
fun <T> of(elements: Iterable<GraphNode<T>>): Graph<T> {
11+
val nodes = mutableSetOf<GraphNode<T>>()
12+
val directedEdges = mutableSetOf<DirectedEdge<T>>()
13+
val undirectedEdges = mutableSetOf<UndirectedEdge<T>>()
14+
15+
for (element in elements) element.populate(nodes, directedEdges, undirectedEdges)
16+
17+
return GraphImpl(nodes, directedEdges, undirectedEdges)
18+
}
19+
20+
fun <T> of(vararg elements: GraphNode<T>): Graph<T> = of(elements.toList())
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.structure
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
4+
5+
class GraphImpl<T>(
6+
override val nodes: Set<GraphNode<T>>,
7+
override val directedEdges: Set<DirectedEdge<T>>,
8+
override val undirectedEdges: Set<UndirectedEdge<T>>,
9+
) : Graph<T>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.structure
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
4+
5+
interface MultiGraph<T> {
6+
val nodes: Set<GraphNode<T>>
7+
val directedEdges: Collection<DirectedEdge<T>>
8+
val undirectedEdges: Collection<UndirectedEdge<T>>
9+
10+
companion object {
11+
fun <T> of(elements: Iterable<GraphNode<T>>): MultiGraph<T> {
12+
val nodes = mutableSetOf<GraphNode<T>>()
13+
val directedEdges = mutableListOf<DirectedEdge<T>>()
14+
val undirectedEdges = mutableListOf<UndirectedEdge<T>>()
15+
16+
for (element in elements) element.populate(nodes, directedEdges, undirectedEdges)
17+
18+
return MultiGraphImpl(nodes, directedEdges, undirectedEdges)
19+
}
20+
21+
fun <T> of(vararg elements: GraphNode<T>): MultiGraph<T> = of(elements.toList())
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.structure
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
4+
5+
class MultiGraphImpl<T>(
6+
override val nodes: Set<GraphNode<T>>,
7+
override val directedEdges: List<DirectedEdge<T>>,
8+
override val undirectedEdges: List<UndirectedEdge<T>>,
9+
) : MultiGraph<T>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.structure
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
4+
5+
data class UndirectedEdge<T>(
6+
val fromNode: GraphNode<T>,
7+
val toNode: GraphNode<T>,
8+
) {
9+
override fun equals(other: Any?): Boolean {
10+
return other is UndirectedEdge<*> && (
11+
(fromNode == other.fromNode) && (toNode == other.toNode) ||
12+
(fromNode == other.toNode) && (toNode == other.fromNode)
13+
)
14+
}
15+
16+
override fun hashCode(): Int {
17+
var h1 = fromNode.hashCode()
18+
var h2 = toNode.hashCode()
19+
if (h1 > h2) { val t = h2; h2 = h1; h1 = t }
20+
return 31 * h1 + h2
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.structure
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
4+
5+
val <T> GraphNode<T>.allParents: Iterable<GraphNode<T>> get() {
6+
return IterablesView(listOf(inNodes, outNodes, biNodes))
7+
}
8+
9+
private class IterablesView<T>(private val iterables: Iterable<Iterable<T>>) : Iterable<T> {
10+
override fun iterator(): Iterator<T> {
11+
return MyIterator(iterables)
12+
}
13+
14+
class MyIterator<T>(iterables: Iterable<Iterable<T>>) : Iterator<T> {
15+
private val outerIterator = iterables.iterator()
16+
private var innerIterator: Iterator<T>? = null
17+
18+
override fun hasNext(): Boolean {
19+
while (innerIterator?.hasNext() != true) {
20+
if (!outerIterator.hasNext()) return false
21+
innerIterator = outerIterator.next().iterator()
22+
}
23+
return true
24+
}
25+
26+
override fun next(): T {
27+
if (!hasNext()) throw IndexOutOfBoundsException()
28+
return innerIterator!!.next()
29+
}
30+
}
31+
}
32+
33+
fun <T> GraphNode<T>.populate(
34+
nodes: MutableSet<GraphNode<T>>,
35+
directedEdges: MutableCollection<DirectedEdge<T>>,
36+
undirectedEdges: MutableCollection<UndirectedEdge<T>>,
37+
) {
38+
nodes.add(this)
39+
for (parent in inNodes) {
40+
directedEdges.add(DirectedEdge(parent, this))
41+
}
42+
for (parent in outNodes) {
43+
directedEdges.add(DirectedEdge(this, parent))
44+
}
45+
for (parent in this.biNodes) {
46+
undirectedEdges.add(UndirectedEdge(this, parent))
47+
}
48+
for (parent in allParents) {
49+
if (parent !in nodes) parent.populate(nodes, directedEdges, undirectedEdges)
50+
}
51+
}

0 commit comments

Comments
 (0)