使用 ScalaJS 从 JSON 创建 D3 树

Create D3 Tree from JSON using ScalaJS

请注意,这不是一个普通的 JS 问题。我真的需要 ScalaJS 的帮助。

几天来,我一直在尝试绘制一个简单的连接树图。它可以嵌套任意深度。我在这个文件中读到:

{
  "name": "Animal",
  "children": [
    {
      "name": "Vertebrates",
      "children": [
        {
          "name": "Mammals"
        },
        {
          "name": "Birds"
        }
      ]
    },
    {
      "name": "Invertebrates"
    }
  ]
}

当我运行这个程序时:

package example
import scala.scalajs.js
import org.singlespaced.d3js.{Link, Tree, d3}

@js.native
trait AnimalNode extends js.Object {
  val name: String = js.native
  val children: js.Array[AnimalNode] = js.native
}

object ScalaJSExample extends js.JSApp {
  def main(): Unit =
    d3.json("json-example.json", (error: js.Any, json: js.Any) => {
      val jsonTypedFromFile = json.asInstanceOf[AnimalNode]

      val width = 960.0
      val height = 500.0

      val tree: Tree[AnimalNode] = d3.layout.tree().size((width, height))
      val nodes = tree.nodes(jsonTypedFromFile)
      val links = tree.links(nodes)

      val svg = d3.select("#tree").append("svg")
        .attr("width", width).attr("height", height).append("g")
      val diagonal = d3.svg.diagonal() //Want to draw Diagonals across all links.

      svg.data(links)
        .append("path")
        .attr("class", "link")
        .style("stroke-width", 5)
        .attr("d", (myJson: Link[AnimalNode], x: Int, y: js.UndefOr[Int]) => {
          ??? // TODO: Draw Diagonal between source & target. Never reached.
        })
      println("Finished drawing paths.")
    })
}

我在 Firebug 中收到此错误:

uncaught exception: 
scala.scalajs.runtime.UndefinedBehaviorError:
An undefined behavior was detected: 
    [object Object] is not an instance of org.singlespaced.d3js.Link

我可能需要定位的替代签名是:

.attr("d", (myJson: Link[Node], x: Int, y: js.UndefOr[Int]) => { ... }

我的代码是 ScalaJSD3 示例应用程序的分支,可在此处获得:https://github.com/swoogles/scala-js-d3-example-app

它的灵感来自这里的 Javascript 代码:http://bl.ocks.org/d3noob/8375092

Scala.js 包装器库有点问题而且不完整,恐怕。它在 .attr("d", (myJson: Link[Node], x: Int, y: js.UndefOr[Int]) => { ... } 处失败,因为链接的运行时类型实际上并不映射到包装器的签名,因为它们是由 d3 通过 js.native 函数创建的。然后抛出 ClassCastException,因为 Scala.js 无法将普通 JS 对象转换为链接。

您可以解决这个问题:

val untypedLinks: js.Array[_ <: Any] = animalNodeTree.links(animalNodes)
val animalNodeLinks = untypedLinks.map(link => {
  val linkObj = link.asInstanceOf[js.Dynamic]
  SimpleLink(linkObj.source.asInstanceOf[AnimalNode], linkObj.target.asInstanceOf[AnimalNode])
})

包装器的另一个问题是投影仅部分实现,您现在无法真正为自己的链接创建投影(参见 TODOs:https://github.com/spaced/scala-js-d3/blob/master/src/main/scala/org/singlespaced/d3js/svg.scala)。

也许 Lines 足以满足您的用例,我将 http://www.d3noob.org/2014/01/tree-diagrams-in-d3js_11.html 中的示例改编为您的示例:

package example

import bill.d3.TreeData
import scala.scalajs.js
import scala.scalajs.js.Dynamic
import org.singlespaced.d3js.{Link, Tree, d3, SimpleLink}
import org.singlespaced.d3js.d3.Primitive
import scala.util.Try
import scala.collection.mutable
import js.JSConverters._

@js.native
trait AnimalNode extends js.Object {
  var id: js.UndefOr[Int] = js.native
  var x: js.UndefOr[Int] = js.native
  var y: js.UndefOr[Int] = js.native
  var depth: Int = js.native
  val parent: String = js.native
  val name: String = js.native
  val children: js.Array[AnimalNode] = js.native
}

object ScalaJSExample extends js.JSApp with TreeData {

  def main(): Unit = {
    println(Try {
      drawTree
    })
  }

  def drawTree = {
    d3.json("json-example.json", (error: js.Any, json: js.Any) => {

      val jsonTypedFromFile = json.asInstanceOf[AnimalNode]

      val width = 960.0
      val height = 650.0
      val marginLeft = 0.0
      val marginTop = 30.0

      val svg = d3.select("#tree").append("svg")
        .attr("width", width)
        .attr("height", height)
        .append("g")
        .attr("transform", "translate(" + marginLeft + "," + marginTop + ")")

      val tupledDimensions = (width, height)

      val animalNodeTree: Tree[AnimalNode] = d3.layout.tree().size(tupledDimensions)
      val animalNodes: js.Array[AnimalNode] = animalNodeTree.nodes(jsonTypedFromFile)
      val untypedLinks: js.Array[_ <: Any] = animalNodeTree.links(animalNodes)

      val animalNodeLinks = untypedLinks.map(link => {
        val linkObj = link.asInstanceOf[js.Dynamic]
        SimpleLink(linkObj.source.asInstanceOf[AnimalNode], linkObj.target.asInstanceOf[AnimalNode])
      })

      // Normalize for fixed-depth.
      animalNodes.foreach((node: AnimalNode) => {
        node.y = node.depth * 180
        println(node.y)
      })

      var nodeCount: Int = 0

      val node: org.singlespaced.d3js.selection.Update[AnimalNode] = svg.selectAll("g.node").data(animalNodes, (node: AnimalNode, index: Int) => {
          nodeCount += 1
          node.id = nodeCount
          node.id.toString
      })

      val nodeEnter: org.singlespaced.d3js.selection.Enter[AnimalNode] = node.enter()

      val nodeWithPosition = nodeEnter.append("g")
       .attr("class", "node")
       .attr("transform", (animalNode: AnimalNode, x: Int, y: js.UndefOr[Int]) => {
         println(animalNode.id)
         "translate(" + animalNode.x + "," + animalNode.y + ")": Primitive
        })

      nodeWithPosition.append("circle")
       .attr("r", 10)
       .style("fill", "#fff")

      nodeWithPosition.append("text")
       .attr("x", 13)
       .attr("dy", ".35em")
       .attr("text-anchor", "start")
       .text((node: AnimalNode, x: Int, y: js.UndefOr[Int]) => {
          node.name
       })
       .style("fill-opacity", 1)

       val link = svg.selectAll("g.link")
       .data(animalNodeLinks, (link: SimpleLink[AnimalNode], index: Int) => { "" + link.target.id })

      link.enter().insert("line", "g")
       .attr("class", "link")
       .attr("x1", (node: SimpleLink[AnimalNode], x: Int, y: js.UndefOr[Int]) => {
          "" + node.source.x.getOrElse(0.0) : Primitive
        })
       .attr("y1", (node: SimpleLink[AnimalNode], x: Int, y: js.UndefOr[Int]) => {
          "" + node.source.y.getOrElse(0.0) : Primitive
        })
       .attr("x2", (node: SimpleLink[AnimalNode], x: Int, y: js.UndefOr[Int]) => {
          "" + node.target.x.getOrElse(0.0) : Primitive
        })
       .attr("y2", (node: SimpleLink[AnimalNode], x: Int, y: js.UndefOr[Int]) => {
          "" + node.target.y.getOrElse(0.0) : Primitive
        })

      println("Done")
    }
    )
  }


}