Создание дерева D3 из JSON с помощью ScalaJS


Пожалуйста, обратите внимание, что это не простой вопрос JS. Мне очень нужна помощь Скалайса.

Я застрял, пытаясь нарисовать простой, связный древовидный график уже несколько дней. Он может быть вложен сколь угодно глубоко. Я читаю в этом файле:
{
  "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

1 2

1 ответ:

Скала.библиотека JS wrapper немного глючная и неполная, я боюсь. Его ошибка в .attr("d", (myJson: Link[Node], x: Int, y: js.UndefOr[Int]) => { ... }, потому что типы времени выполнения ссылок на самом деле не соответствуют сигнатурам оболочки, поскольку они создаются d3 через функцию js.native. A 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).

Может быть, строки достаточно хороши для вашего usecase, я адаптировал пример из 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")
    }
    )
  }


}