RadarChart + nvd3 scala.js façade
authorTomas Zeman <tzeman@volny.cz>
Thu, 05 Nov 2015 11:59:22 +0100
changeset 15 149647cd3801
parent 14 82489d99b232
child 16 38c341a46f5a
RadarChart + nvd3 scala.js façade
build.sbt
js/src/main/resources/RadarChart.js
js/src/main/scala/Nvd3Chart.scala
--- a/build.sbt	Thu Oct 08 15:18:33 2015 +0200
+++ b/build.sbt	Thu Nov 05 11:59:22 2015 +0100
@@ -27,7 +27,9 @@
   buildInfoPackage := "ngtags",
   buildInfoOptions ++= Seq(BuildInfoOption.ToMap, BuildInfoOption.ToJson)
 
-).jvmSettings().jsSettings()
+).jvmSettings().jsSettings(
+  jsDependencies += ProvidedJS / "RadarChart.js"
+)
 
 lazy val ngtagsJS = ngtags.js
 lazy val ngtagsJVM = ngtags.jvm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/js/src/main/resources/RadarChart.js	Thu Nov 05 11:59:22 2015 +0100
@@ -0,0 +1,245 @@
+/*
+ * Cloned from https://gist.github.com/nbremer/6506614
+ */
+
+//Practically all this code comes from https://github.com/alangrafu/radar-chart-d3
+//I only made some additions and aesthetic adjustments to make the chart look better 
+//(of course, that is only my point of view)
+//Such as a better placement of the titles at each line end, 
+//adding numbers that reflect what each circular level stands for
+//Not placing the last level and slight differences in color
+//
+//For a bit of extra information check the blog about it:
+//http://nbremer.blogspot.nl/2013/09/making-d3-radar-chart-look-bit-better.html
+
+var RadarChart = {
+  draw: function(id, d, options){
+  var cfg = {
+	 radius: 5,
+	 w: 600,
+	 h: 600,
+	 factor: 1,
+	 factorLegend: .85,
+	 levels: 3,
+	 maxValue: 0,
+	 radians: 2 * Math.PI,
+	 opacityArea: 0.5,
+	 ToRight: 5,
+	 TranslateX: 80,
+	 TranslateY: 30,
+	 ExtraWidthX: 100,
+	 ExtraWidthY: 100,
+	 color: d3.scale.category10(),
+	 offset: 0.0,
+	 formatAxis: d3.format('%'),
+	 formatValue: d3.format('%'),
+	 drawMaxLevel: false
+	};
+
+	if (d.length == 0)
+	  return;
+
+	if('undefined' !== typeof options){
+	  for(var i in options){
+		if('undefined' !== typeof options[i]){
+		  cfg[i] = options[i];
+		}
+	  }
+	}
+	cfg.maxValue = Math.max(cfg.maxValue, d3.max(d, function(i){return d3.max(i.map(function(o){return o.value;}))})) + cfg.offset;
+	var allAxis = (d[0].map(function(i, j){return i}));
+	var total = allAxis.length;
+	var radius = cfg.factor*Math.min(cfg.w/2, cfg.h/2);
+	d3.select(id).select("svg").remove();
+	
+	var g = d3.select(id)
+			.append("svg")
+			.attr("width", cfg.w+cfg.ExtraWidthX)
+			.attr("height", cfg.h+cfg.ExtraWidthY)
+			.append("g")
+			.attr("transform", "translate(" + cfg.TranslateX + "," + cfg.TranslateY + ")");
+			;
+
+	var tooltip;
+	
+	//Circular segments
+	var maxLevel = cfg.drawMaxLevel ? cfg.levels : cfg.levels - 1;
+	for(var j=0; j<maxLevel; j++){
+	  var levelFactor = cfg.factor*radius*((j+1)/cfg.levels);
+	  g.selectAll(".levels")
+	   .data(allAxis)
+	   .enter()
+	   .append("svg:line")
+	   .attr("x1", function(d, i){return levelFactor*(1-cfg.factor*Math.sin(i*cfg.radians/total));})
+	   .attr("y1", function(d, i){return levelFactor*(1-cfg.factor*Math.cos(i*cfg.radians/total));})
+	   .attr("x2", function(d, i){return levelFactor*(1-cfg.factor*Math.sin((i+1)*cfg.radians/total));})
+	   .attr("y2", function(d, i){return levelFactor*(1-cfg.factor*Math.cos((i+1)*cfg.radians/total));})
+	   .attr("class", "level level-" + j + " level-val-" +
+	     cfg.formatAxis((j+1)*cfg.maxValue/cfg.levels - cfg.offset))
+	   .attr("transform", "translate(" + (cfg.w/2-levelFactor) + ", " + (cfg.h/2-levelFactor) + ")");
+	}
+
+	//Text indicating at what % each level is
+	for(var j=0; j<cfg.levels; j++){
+	  var levelFactor = cfg.factor*radius*((j+1)/cfg.levels);
+	  g.selectAll(".levels")
+	   .data([1]) //dummy data
+	   .enter()
+	   .append("svg:text")
+	   .attr("x", function(d){return levelFactor*(1-cfg.factor*Math.sin(0));})
+	   .attr("y", function(d){return levelFactor*(1-cfg.factor*Math.cos(0));})
+	   .attr("class", "legend")
+	   .attr("transform", "translate(" + (cfg.w/2-levelFactor + cfg.ToRight) + ", " + (cfg.h/2-levelFactor) + ")")
+	   .text(cfg.formatAxis((j+1)*cfg.maxValue/cfg.levels - cfg.offset));
+	}
+	
+	series = 0;
+
+	var axis = g.selectAll(".axis")
+			.data(allAxis)
+			.enter()
+			.append("g")
+			.attr("class", "axis");
+
+	axis.append("line")
+		.attr("x1", cfg.w/2)
+		.attr("y1", cfg.h/2)
+		.attr("x2", function(d, i){return cfg.w/2*(1-cfg.factor*Math.sin(i*cfg.radians/total));})
+		.attr("y2", function(d, i){return cfg.h/2*(1-cfg.factor*Math.cos(i*cfg.radians/total));})
+		.attr("class", "line")
+		.style("stroke", "grey")
+		.style("stroke-width", "1px");
+
+	axis.append("text")
+		.attr("class", "legend")
+		.text(function(d){return d.axis})
+		.style("font-family", "sans-serif")
+		.style("font-size", "11px")
+		.attr("text-anchor", "middle")
+		.attr("dy", "1em")
+		.attr("transform", function(d, i){return "translate(0, -10)"})
+		.attr("x", function(d, i){return cfg.w/2*(1-cfg.factorLegend*Math.sin(i*cfg.radians/total))-60*Math.sin(i*cfg.radians/total);})
+		.attr("y", function(d, i){return cfg.h/2*(1-Math.cos(i*cfg.radians/total))-20*Math.cos(i*cfg.radians/total);})
+		.on('mouseover', function(d) {
+	          tooltip.text('');
+		  var a = d.tooltip().split("\n");
+		  for (var i = 0; i < a.length; i++) {
+		    tooltip
+		      .attr('x', 0)
+		      .attr('y', 0)
+		      .style('opacity', 1)
+		      .append('tspan')
+		      .attr('x', 0)
+		      .attr('y', cfg.h + cfg.ExtraWidthY - cfg.TranslateY + 12*(i-a.length)+9)
+		      .text(a[i])
+                      .transition(100);
+		  }
+		})
+		.on('mouseout', function(d) {
+		  tooltip.transition(100).style('opacity', 0);
+		});
+
+ 
+	d.forEach(function(y, x){
+	  dataValues = [];
+	  g.selectAll(".nodes")
+		.data(y, function(j, i){
+		  dataValues.push([
+			cfg.w/2*(1-(parseFloat(Math.max(j.value + cfg.offset, 0))/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)), 
+			cfg.h/2*(1-(parseFloat(Math.max(j.value + cfg.offset, 0))/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total))
+		  ]);
+		});
+	  dataValues.push(dataValues[0]);
+	  g.selectAll(".area")
+					 .data([dataValues])
+					 .enter()
+					 .append("polygon")
+					 .attr("class", "radar-chart-serie"+series)
+					 .style("stroke-width", "2px")
+					 .style("stroke", cfg.color(series))
+					 .attr("points",function(d) {
+						 var str="";
+						 for(var pti=0;pti<d.length;pti++){
+							 str=str+d[pti][0]+","+d[pti][1]+" ";
+						 }
+						 return str;
+					  })
+					 .style("fill", function(j, i){return cfg.color(series)})
+					 .style("fill-opacity", cfg.opacityArea)
+					 .on('mouseover', function (d){
+										z = "polygon."+d3.select(this).attr("class");
+										g.selectAll("polygon")
+										 .transition(200)
+										 .style("fill-opacity", 0.1); 
+										g.selectAll(z)
+										 .transition(200)
+										 .style("fill-opacity", .7);
+									  })
+					 .on('mouseout', function(){
+										g.selectAll("polygon")
+										 .transition(200)
+										 .style("fill-opacity", cfg.opacityArea);
+					 });
+	  series++;
+	});
+	series=0;
+
+
+	d.forEach(function(y, x){
+	  g.selectAll(".nodes")
+		.data(y).enter()
+		.append("svg:circle")
+		.attr("class", "radar-chart-serie"+series)
+		.attr('r', cfg.radius)
+		.attr("alt", function(j){return cfg.formatValue(j.value)})
+		.attr("cx", function(j, i){
+		  dataValues.push([
+			cfg.w/2*(1-(parseFloat(Math.max(j.value + cfg.offset, 0))/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)), 
+			cfg.h/2*(1-(parseFloat(Math.max(j.value + cfg.offset, 0))/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total))
+		]);
+		return cfg.w/2*(1-(Math.max(j.value + cfg.offset, 0)/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total));
+		})
+		.attr("cy", function(j, i){
+		  return cfg.h/2*(1-(Math.max(j.value + cfg.offset, 0)/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total));
+		})
+		.attr("data-id", function(j){return j.axis})
+		.style("fill", cfg.color(series)).style("fill-opacity", .9)
+		.on('mouseover', function (d){
+					newX =  parseFloat(d3.select(this).attr('cx')) - 10;
+					newY =  parseFloat(d3.select(this).attr('cy')) - 5;
+					
+					tooltip
+						.attr('x', newX)
+						.attr('y', newY)
+						.text(cfg.formatValue(d.value))
+						.transition(200)
+						.style('opacity', 1);
+						
+					z = "polygon."+d3.select(this).attr("class");
+					g.selectAll("polygon")
+						.transition(200)
+						.style("fill-opacity", 0.1); 
+					g.selectAll(z)
+						.transition(200)
+						.style("fill-opacity", .7);
+				  })
+		.on('mouseout', function(){
+					tooltip
+						.transition(200)
+						.style('opacity', 0);
+					g.selectAll("polygon")
+						.transition(200)
+						.style("fill-opacity", cfg.opacityArea);
+				  })
+		.append("svg:title")
+		.text(function(j){return cfg.formatValue(j.value)});
+
+	  series++;
+	});
+	//Tooltip
+	tooltip = g.append('text')
+			   .style('opacity', 0)
+			   .style('font-family', 'sans-serif')
+			   .style('font-size', '13px');
+  }
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/js/src/main/scala/Nvd3Chart.scala	Thu Nov 05 11:59:22 2015 +0100
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2015 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.tz.nvd3
+
+import scala.scalajs.js
+import scala.scalajs.js.annotation.{JSExport, JSExportAll}
+import scala.scalajs.js.Dynamic.{global => g}
+import scala.scalajs.js.JSConverters._
+
+private[nvd3] abstract class Constructable[C <: Constructable[C]](
+  b: Map[String, Any]
+, cf: Map[String, Any] => C) {
+  protected def v[T](n: Symbol, v: T): C = cf(b + (n.name -> v))
+  def toJs = b.toJSDictionary
+}
+
+class Axis(b: Map[String, Any])
+  extends Constructable[Axis](b, m => new Axis(m)) {
+  def axisLabel(l: String) = v('axisLabel, l)
+  def showMaxMin(b: Boolean) = v('showMaxMin, b)
+  def staggerLabels(b: Boolean) = v('staggerLabels, b)
+  def labelDistance(d: Int) = v('axisLabelDistance, d)
+  def tickFormat(f: js.Function1[Double, js.Any]) = v('tickFormat, f)
+  def rotateLabels(deg: Int) = v('rotateLabels, deg)
+}
+
+class Margin(b: Map[String, Any])
+  extends Constructable[Margin](b, m => new Margin(m)) {
+  def top(i: Int) = v('top, i)
+  def bottom(i: Int) = v('bottom, i)
+  def left(i: Int) = v('left, i)
+  def right(i: Int) = v('right, i)
+}
+
+class Legend(b: Map[String, Any])
+  extends Constructable[Legend](b, m => new Legend(m)) {
+  def margin(m: Margin) = v('margin, m)
+}
+
+class Chart(b: Map[String, Any])
+  extends Constructable[Chart](b, m => new Chart(m)) {
+
+  def transitionDuration(ms: Int) = v('transitionDuration, ms)
+  def height(h: Int) = v('height, h)
+  def width(w: Int) = v('width, w)
+  def x[R](f: js.Function1[js.Dynamic, R]) = v('x, f)
+  def y[R](f: js.Function1[js.Dynamic, R]) = v('y, f)
+  def average(f: js.Function1[js.Dynamic, Any]) = v('average, f)
+  def forceY(min: Double, max: Double) = v('forceY, js.Array(min, max))
+  def color(colors: js.Array[String]) = v('color, colors)
+  def color(colors: js.Any) = v('color, colors)
+  def useInteractiveGuideline(b: Boolean) = v('useInteractiveGuideline, b)
+  def clipVoronoi(b: Boolean) = v('clipVoronoi, b)
+  def useVoronoi(b: Boolean) = v('useVoronoi, b)
+  def xAxis(a: Axis) = v('xAxis, a.toJs)
+  def yAxis(a: Axis) = v('yAxis, a.toJs)
+  def margin(m: Margin) = v('margin, m.toJs)
+  def reduceXTicks(b: Boolean) = v('reduceXTicks, b)
+  def showValues(b: Boolean) = v('showValues, b)
+  def showControls(b: Boolean) = v('showControls, b)
+  def showLabels(b: Boolean) = v('showLabels, b)
+  def showLegend(b: Boolean) = v('showLegend, b)
+  def staggerLabels(b: Boolean) = v('staggerLabels, b)
+  def valueFormat[F, T](f: js.Function1[F, T]) = v('valueFormat, f)
+  def donut(b: Boolean) = v('donut, b)
+  def legend(l: Legend) = v('legend, l)
+  def labelThreshold(t: Double) = v('labelThreshold, t)
+  def clipEdge(b: Boolean) = v('clipEdge, b)
+}
+
+object Chart {
+  private def as(t: Symbol) = new Chart(Map("type" -> t.name))
+
+  def bulletChart = as('bulletChart)
+  def cumulativeLineChart = as('cumulativeLineChart)
+  def discreteBarChart = as('discreteBarChart)
+  def pieChart = as('pieChart)
+  def historicalBarChart = as('historicalBarChart)
+  def multiBarChart = as('multiBarChart)
+  def multiBarHorizontalChart = as('multiBarHorizontalChart)
+  def stackedAreaChart = as('stackedAreaChart)
+
+  def axis(l: String) = new Axis(Map()) axisLabel(l)
+  def margin = new Margin(Map())
+  def legend = new Legend(Map())
+}
+
+@JSExportAll
+case class Options(var chart: js.Dictionary[Any])
+
+class RadarChartConfig(b: Map[String, Any])
+  extends Constructable[RadarChartConfig](b, m => new RadarChartConfig(m)) {
+
+  def radius(r: Int) = v('radius, r)
+  def height(h: Int) = v('h, h)
+  def width(w: Int) = v('w, w)
+  def factor(f: Double) = v('factor, f)
+  def factorLegend(f: Double) = v('factorLegend, f)
+  def levels(l: Int) = v('levels, l)
+  def maxValue(m: Double) = v('maxValue, m)
+  def opacityArea(o: Double) = v('opacityArea, o)
+  def toRight(r: Int) = v('ToRight, r)
+  def translateX(t: Int) = v('TranslateX, t)
+  def translateY(t: Int) = v('TranslateY, t)
+  def extraWidthX(w: Int) = v('ExtraWidthX, w)
+  def extraWidthY(w: Int) = v('ExtraWidthY, w)
+  def color(c: js.Function1[Int, String]) = v('color, c)
+  def offset(d: Double) = v('offset, d)
+  def formatValue(f: js.Function1[Double, String]) = v('formatValue, f)
+  def formatAxis(f: js.Function1[Double, String]) = v('formatAxis, f)
+  def drawMaxLevel(b: Boolean) = v('drawMaxLevel, b)
+}
+
+object RadarChartConfig {
+  def default = new RadarChartConfig(Map())
+}
+
+object RadarChart {
+  case class V(val n: String, val v: Double, val t: Option[() => String] = None) {
+    @JSExport
+    def axis = n
+    @JSExport
+    def value = v
+    @JSExport
+    def tooltip() = t.map(_()).getOrElse("")
+  }
+
+  def apply(id: String, data: Seq[Seq[V]], cfg: RadarChartConfig) =
+    g.RadarChart.draw(id, data.map(_.toJSArray).toJSArray, cfg.toJs)
+}
+
+object MultiBarChart {
+  case class V[X](_x: X, _y: Double) {
+    @JSExport
+    def x = _x
+    @JSExport
+    def y = _y
+  }
+
+  case class Series[X](_key: String, _values: Seq[V[X]]) {
+    @JSExport
+    def key = _key
+    @JSExport
+    def values = _values.toJSArray
+  }
+}
+
+// vim: set ts=2 sw=2 et: