Skip to content

Commit

Permalink
Add hierarchies visualization API
Browse files Browse the repository at this point in the history
  • Loading branch information
ileasile committed May 5, 2021
1 parent ce0bf03 commit 4de1719
Show file tree
Hide file tree
Showing 28 changed files with 477 additions and 14 deletions.
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) {
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

0 comments on commit 4de1719

Please sign in to comment.