Skip to content

Commit c26dcff

Browse files
committed
Add hierarchies visualization API
1 parent ce0bf03 commit c26dcff

File tree

25 files changed

+411
-14
lines changed

25 files changed

+411
-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,34 @@
1+
package org.jetbrains.kotlinx.jupyter.api.graphs.labels
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.Label
4+
import kotlin.reflect.KClass
5+
6+
/**
7+
* Label representing [kClass] with all members in HTML table
8+
*/
9+
class KClassLabel(private val kClass: KClass<*>) : Label {
10+
override val text: String get() {
11+
val members = kClass.members
12+
val nMembers = members.size
13+
14+
fun inTable(builderAction: StringBuilder.() -> Unit) = buildString {
15+
append("<<table>")
16+
builderAction()
17+
append("</table>>")
18+
}
19+
20+
if (nMembers == 0) return inTable { append("<tr><td>${kClass.simpleName}</td></tr>") }
21+
return inTable {
22+
members.forEachIndexed { i, member ->
23+
append("<tr>")
24+
if (i == 0) {
25+
append("""<td rowspan="$nMembers">${kClass.simpleName}</td>""")
26+
}
27+
append("""<td>${member.name}</td><td>${member.returnType}</td>""")
28+
appendLine("</tr>")
29+
}
30+
}
31+
}
32+
33+
override val shape get() = "plaintext"
34+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.visualization
2+
3+
import guru.nidi.graphviz.engine.Engine
4+
import guru.nidi.graphviz.engine.Format
5+
import guru.nidi.graphviz.engine.Graphviz
6+
import guru.nidi.graphviz.parse.Parser
7+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
8+
import org.jetbrains.kotlinx.jupyter.ext.Image
9+
import org.jetbrains.kotlinx.jupyter.ext.graph.structure.MultiGraph
10+
import java.io.ByteArrayOutputStream
11+
12+
fun <T> MultiGraph<T>.dotText(): String {
13+
val nodesNumbers = nodes.mapIndexed { index, hierarchyElement -> hierarchyElement to index }.toMap()
14+
fun id(el: GraphNode<T>) = "n${nodesNumbers[el]}"
15+
16+
return buildString {
17+
appendLine("""digraph "" { """)
18+
for (node in nodes) {
19+
val nodeId = id(node)
20+
appendLine("$nodeId ;")
21+
append("$nodeId [")
22+
with(node.label) {
23+
append("label=$text ")
24+
shape?.let { append("shape=$it ") }
25+
}
26+
appendLine("] ;")
27+
}
28+
29+
for ((n1, n2) in directedEdges) {
30+
appendLine("${id(n1)} -> ${id(n2)} ;")
31+
}
32+
for ((n1, n2) in undirectedEdges) {
33+
appendLine("${id(n1)} -> ${id(n2)} [dir=none] ;")
34+
}
35+
appendLine("}")
36+
}
37+
}
38+
39+
fun renderDotText(text: String): Image {
40+
val graph = Parser().read(text)
41+
val stream = ByteArrayOutputStream()
42+
Graphviz
43+
.fromGraph(graph)
44+
.engine(Engine.DOT)
45+
.render(Format.SVG)
46+
.toOutputStream(stream)
47+
return Image(stream.toByteArray(), "svg")
48+
}
49+
50+
fun <T> MultiGraph<T>.render(): Image {
51+
return renderDotText(dotText())
52+
}
53+
54+
fun <T> MultiGraph<T>.toHTML() = render().toHTML()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.wrappers
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
4+
import org.jetbrains.kotlinx.jupyter.api.graphs.NodeWrapper
5+
import org.jetbrains.kotlinx.jupyter.api.graphs.labels.TextLabel
6+
import java.net.URLClassLoader
7+
import kotlin.reflect.KClass
8+
9+
class ClassLoaderNode(node: ClassLoader) : NodeWrapper<ClassLoader>(node) {
10+
override val inNodes by lazy {
11+
node.parent?.let { listOf(ClassLoaderNode(it)) } ?: emptyList()
12+
}
13+
override val label = TextLabel(
14+
when (node) {
15+
is URLClassLoader -> node.urLs.joinToString("\\n", "URL ClassLoader:\\n") {
16+
it.toString()
17+
}
18+
else -> node.toString()
19+
}
20+
)
21+
}
22+
23+
fun GraphNode.Companion.fromClassLoader(classLoader: ClassLoader) = ClassLoaderNode(classLoader)
24+
fun GraphNode.Companion.fromClassLoader(kClass: KClass<*>) = fromClassLoader(kClass.java.classLoader)
25+
inline fun <reified T> GraphNode.Companion.fromClassLoader() = fromClassLoader(T::class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.jetbrains.kotlinx.jupyter.ext.graph.wrappers
2+
3+
import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode
4+
import org.jetbrains.kotlinx.jupyter.api.graphs.Label
5+
import org.jetbrains.kotlinx.jupyter.api.graphs.NodeWrapper
6+
import org.jetbrains.kotlinx.jupyter.api.graphs.labels.KClassLabel
7+
import kotlin.reflect.KClass
8+
import kotlin.reflect.full.superclasses
9+
10+
class KClassNode(node: KClass<*>) : NodeWrapper<KClass<*>>(node) {
11+
override val label: Label get() = KClassLabel(value)
12+
13+
override val inNodes by lazy {
14+
node.superclasses.map { KClassNode(it) }
15+
}
16+
}
17+
18+
fun GraphNode.Companion.fromClass(kClass: KClass<*>) = KClassNode(kClass)
19+
inline fun <reified T> GraphNode.Companion.fromClass() = fromClass(T::class)

0 commit comments

Comments
 (0)