Search code examples
jquerytemplatesd3.jsjsrenderjsviews

D3.js Graphs in JavaScript Templates (jsRender/jsViews, jquery tmpl, etc.)


I've been trying to create a page that uses JavaScript Templates which incorporate graphs made using d3.js to display information about each inventory item (in this example, I have an array of JSON blobs containing transistor data.) For example, I need boilerplate "Absolute Max Ratings" table followed by graphs like the impedance curve for a range of Gate Voltages. Here's what I have so far:

var partData = [
	{
		name: "IRF10",
		maxRatings:
		{
			vds: 200,
			vgs: 20,
			id: 2.1,
			pd: 36
		},
		impedanceGraph:
		[
			[1, 0.7],
			[2, 1.6],
			[3, 2.0],
			[4, 2.5]
		]
	},
	{
		name: "2n7000",
		maxRatings:
		{
			vds: 60,
			vgs: 20,
			id: 0.2,
			pd: 0.4
		},
		impedanceGraph:
		[
			[1, 0.6],
			[2, 1.5],
			[3, 1.9],
			[4, 2.4]
		]
	}
]

$('#container').html($('#partTemplate').render(partData));
<script id="partTemplate" type="text/x-jsrender">
    <div style="background-color:mintcream">
        <h2>{{>name}}</h2>
        Max Ratings
        {{props maxRatings}}
            <li>{{>key}}: {{>prop}}</li>
        {{/props}}
        <br />
        Impedance Graph: <br />
        PLACE_D3_GRAPH_HERE
    </div>
</script>
<div id="container"></div>

Here is my jsfiddle since jsRender doesn't quite work: http://jsfiddle.net/xaty70b6/

Since D3.js appears to work by appending an SVG element to the body and inserting data to the selection, I've been led to believe that I need to somehow select the instance of a template with jquery and feed that to D3.js to build the graph, but I have no idea how to get the template instance while building the rows. Really, All I want to do is have a D3.js graph appear in my template instance using the data provided inside the JSON blob for that instance. Is there perhaps another way to insert the SVG to be passed into D3.js with the pertinent data array?


Solution

  • I have not worked with D3.js, but on looking into it, it is clear the D3 works directly on the DOM - whereas JsRender rendering is independent of the DOM - and produces an HTML string that you then insert into the DOM yourself.

    However JsViews is completely 'DOM aware' and does binding between the template instances, the data and the DOM. So I would suggest using JsViews.

    In addition that opens the possibility of using JsViews data-binding to update the data interactively. JsView data-binding is of course different than D3's data-binding, but it is interesting to explore integrating them.

    So here (in the code snippet below, and also here: http://jsfiddle.net/BorisMoore/Lbfn5aog/1/) is one approach to that.

    I have created a custom {{barChart}} tag, to integrate the D3 chart:

    $.views.tags("barChart", {
      init: function(tagCtx, linkCtx) {
        this.data = tagCtx.args[0];
        this.svg = initChart(linkCtx.elem, this.data);
      },
      onAfterLink: function(tagCtx, linkCtx) {
        var tag = this;
        $.observable(tag.data).observeAll(function() {
          updateChart(tag.data, tag.svg);
        });
      }
    });
    

    and included it in the template like so:

    <div class="chart" data-link="{barChart impedanceGraph}"></div>
    

    (One could also create a version with the div rendered by the tag, so you could write {^{barChart impedanceGraph /}} instead...)

    If you don't want the dynamic data-binding, you can omit the onAfterLink method.

    But if you include it, then now you can modify the values in the text boxes and the data will be updated by JsViews. The data-linked barChart tag will pick up the change and pass it to D3 which will recalculate the columns, with a new scaling factor as appropriate.

    var margin = {top: 20, right: 20, bottom: 30, left: 40},
      width = 180 - margin.left - margin.right,
      height = 200 - margin.top - margin.bottom;
    
    function scaleChart(data) {
      var x = d3.scale.ordinal()
        .rangeRoundBands([0, width], .1);
    
      var y = d3.scale.linear()
        .range([height, 0]);
    
      var xAxis = d3.svg.axis()
        .scale(x)
        .orient("bottom");
    
      var yAxis = d3.svg.axis()
        .scale(y)
        .orient("left")
        .ticks(10, "%");
    
      x.domain(data.map(function(d) { 
        return d.name; }));
      y.domain([0, d3.max(data, function(d) {
        return d.amt; 
      })]);
    
      return {x:x, y:y, xAxis: xAxis, yAxis: yAxis};
    }
    
    function initChart(element, data) {
      var chart = scaleChart(data);
      var svg = d3.select(element).append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
      .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
      svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(chart.xAxis);
    
      svg.selectAll(".bar")
        .data(data)
        .enter().append("rect")
          .attr("class", "bar")
          .attr("x", function(d) {
            return chart.x(d.name);
          })
          .attr("width", chart.x.rangeBand())
          .attr("y", function(d) { return chart.y(d.amt); })
          .attr("height", function(d) {
            return height - chart.y(d.amt);
          });
      return svg;
    }
    
    function updateChart(data, svg) {
      var chart = scaleChart(data);
        svg.selectAll(".bar")
          .data(data)
          .attr("y", function(d) { return chart.y(d.amt); })
          .attr("height", function(d) {
            return height - chart.y(d.amt);
          });
    }
    
    var partData = [
      {
        name: "IRF10",
        maxRatings:
        {
          vds: 200,
          vgs: 20,
          id: 2.1,
          pd: 36
        },
        impedanceGraph:
        [
          {name: 1, amt: 0.7},
          {name: 2, amt: 1.6},
          {name: 3, amt: 2.0},
          {name: 4, amt: 2.0},
          {name: 5, amt: 0.5}
        ]
      },
      {
        name: "2n7000",
        maxRatings:
        {
          vds: 60,
          vgs: 20,
          id: 0.2,
          pd: 0.4
        },
        impedanceGraph:
        [
          {name: 1, amt: 0.6},
          {name: 2, amt: 1.5},
          {name: 3, amt: 1.9},
          {name: 4, amt: 2.4}
        ]
      }
    ]
    
    $.views.tags("barChart", {
      init: function(tagCtx, linkCtx) {
        this.data = tagCtx.args[0];
        this.svg = initChart(linkCtx.elem, this.data);
      },
      onAfterLink: function(tagCtx, linkCtx) {
        var tag = this;
        $.observable(tag.data).observeAll(function() {
          updateChart(tag.data, tag.svg);
        });
      }
    });
    
    var tmpl = $.templates("#partTemplate");
    
    tmpl.link('#container', partData);
    li {
      list-style:none
    }
    .bar {
      fill: steelblue;
    }
    
    .bar:hover {
      fill: brown;
    }
    
    .axis {
      font: 10px sans-serif;
    }
    
    .axis path,
    .axis line {
      fill: none;
      stroke: #000;
      shape-rendering: crispEdges;
    }
    
    .x.axis path {
      display: none;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="//www.jsviews.com/download/jsviews.js"></script>
    
    <script id="partTemplate" type="text/x-jsrender">
        <div style="background-color:mintcream">
            <h2>{{>name}}</h2>
            Max Ratings
            <ul>
              {{props maxRatings}}
                <li>{{>key}}: {{>prop}}</li>
              {{/props}}
            </ul>
            <br />
            Impedance Graph: <br />
            <ul>
              {{for impedanceGraph}}
                <li><input data-link="amt" /></li>
              {{/for}}
            </ul>
            <div class="chart" data-link="{barChart impedanceGraph}"></div>
        </div>
    </script>
    
    <div id="container"></div>