使用 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])
})
包装器的另一个问题是投影仅部分实现,您现在无法真正为自己的链接创建投影(参见 TODO
s: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")
}
)
}
}
请注意,这不是一个普通的 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])
})
包装器的另一个问题是投影仅部分实现,您现在无法真正为自己的链接创建投影(参见 TODO
s: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")
}
)
}
}