This is an example to show you how easy it is nowadays to display server-side data in a web-client (mobile or not). There are various ways to manipulate standard vector graphics (SVG) but in this sample we’ll use the amazing d3.js library which quickly produces great results and is easy to learn. On the server side we’ll use a standard ASP.Net MVC (razor) project and add an extra method in a controller which returns some random data in the form of a JSON blob;
1 2 3 4 5 6 7 8 9 10 |
[HttpGet] public ActionResult DataBlob() { var list = new List<object>(); for (var i = 0; i < 50; i++) { list.Add(new{x=i,y=Rand.NextDouble()}); } return Json(list,JsonRequestBehavior.AllowGet); } |
Note that you don’t need to define explicitly a class since the C# class structure would get lost in the translation to JSON anyway. Inside the JSON caller/parser of d3 things will be converted to a JavaScript object automatically.
Now the charting part. We’ll update the chart on a regular interval and this can be done with the setInterval method; it calls a method every now and then (you specify the interval). The method being called accesses the JSON data on the server and then executes a callback;
1 2 3 4 5 6 7 |
setInterval(update, 5000); var update = function () { d3.json('http://localhost:2603/Home/DataBlob', //the callback ); }; |
Much like F#, you should be careful with some vars since they can be either functions or parameters. Here the update is actually a function. Now, the callback of the d3.json method should be a function with a parameter which will hold the parsed data (nope, you don’t need to explicitly parse it since it’s done internally in D3). So, after each call to the data we’ll refresh the canvas and redraw the lines representing the chart;
1 2 3 4 5 6 7 8 9 10 |
setInterval(update, 5000); var update = function () { d3.json('http://localhost:2603/Home/DataBlob', function (thedata) { //clear the canvas //draw a series of lines correpsonding to the chart/data //adorn with some more labels and widgets, animation... } ); }; |
Set up the canvas and define a group which will be refreshed on each pass:
1 2 3 4 5 6 7 8 9 10 11 12 |
var c = d3.select('#chart'); var svg = c.append('svg:svg').attr('width', 1024).attr('height', 400); var g; var update = function () { d3.json('http://localhost:2603/Home/DataBlob', function (d) { if (g) g.remove(); g = svg.append('svg:g') .attr('stroke', 'red') .attr('fill', 'transparent'); }}; |
Drawing a continuous curve is done through a path, very similar to the path object in Silverlight and the shorthand notation for paths is in fact the same.
1 2 3 4 5 |
var makeline = d3.svg.line() .x(function (d, i) { return d.x * 20; }) .y(function (d) { return 300 - d.y * 200; }) .interpolate('cardinal') .tension(.6) |
Here you should observe that this is again a functional and that the coordinates can be either values or functions. In our case, we use the (x,y) tuple handed over by the JSON service.
And so, now you can plug in this line-drawing function in the callback:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var c = d3.select('#chart'); var svg = c.append('svg:svg').attr('width', 1024).attr('height', 400); var g; var update = function () { d3.json('http://localhost:2603/Home/DataBlob', function (d) { if (g) g.remove(); g = svg.append('svg:g') .attr('stroke', 'red') .attr('fill', 'transparent'); g.selectAll("path") .data(d) .enter().append("svg:path") .attr("d", makeline(d)) }}; |
This is the basic plot, which now can be adorned with additional fun. In particular, you can add some transition/animation fx to the build-up of the chart as can be seen in the full source code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>D3 line chart</title> <script src="/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/d3.js")" type="text/javascript"></script> <script src="/Scripts/d3.js" type="text/javascript"></script> <script src="/Scripts/d3.layout.js" type="text/javascript"></script> <style type="text/css"> #chart { border: solid 3px #234057; -moz-border-radius-topleft: 19px; -moz-border-radius-topright: 5px; -moz-border-radius-bottomleft: 0px; -moz-border-radius-bottomright: 29px; -webkit-border-top-left-radius: 19px; -webkit-border-top-right-radius: 5px; -webkit-border-bottom-left-radius: 0px; -webkit-border-bottom-right-radius: 29px; border-top-left-radius: 19px; border-top-right-radius: 5px; border-bottom-left-radius: 0px; border-bottom-right-radius: 29px; width: 1050px; background-color: steelblue; background: -moz-linear-gradient(10% 20% 90deg, silver, dimgray 100%); } </style> </head> <body> <div id="chart"> </div> <script type="text/javascript"> var w = 400; var h = 200; var margin = 20; var dataset = []; d3.range(10).forEach(function (d) { dataset.push({ x: d, y: Math.random() }) }); var c = d3.select('#chart'); var svg = c.append('svg:svg').attr('width', 1024).attr('height', 400); var g; var update = function () { d3.json('http://localhost:2603/Home/DataBlob', function (d) { var mapy = d3.scale.linear().domain([0, d3.max(d)]).range([0 + margin, h - margin]); var mapx = d3.scale.linear().domain([0, d.length]).range([0 + margin, w - margin]) var makeline = d3.svg.line() .x(function (d, i) { return d.x * 20; }) .y(function (d) { return 300 - d.y * 200; }) .interpolate('cardinal') .tension(.6) //.interpolate('step-before'); if (g) g.remove(); g = svg.append('svg:g') .attr('stroke', 'red') .attr('fill', 'transparent'); g.selectAll('lines') .data(d).enter() .append('svg:line') .attr('stroke', 'black') .attr('y1', 300) .attr('x1', function (d) { return d.x * 20; }) .attr('x2', function (d) { return d.x * 20; }) .attr('y2', function (d) { return 300 - d.y * 200; }); g.selectAll('dots') .data(d).enter() .append('svg:circle') .attr('cx', function (d) { return d.x * 20; }) .attr('cy', function (d) { return 300 - d.y * 200; }) .attr('r', 5) .attr('fill', 'red'); g.selectAll('labels') .data(d).enter() .append('svg:text') .text(function (d) { return Math.round(d.y * 100) / 100; }) .attr('x', function (d) { return d.x * 20 + 10; }) .attr('y', 0) .attr('stroke', 'white') .attr('font-size', 10) .attr("stroke-opacity", 0.6) .transition() .attr('y', function (d) { return 305 - d.y * 200; }) .delay(function () { return Math.round(2500 * Math.random()); }) .duration(2000) .ease("cubic", 13, 3.945) g.selectAll("labels").transition() .attr("visibility", "visible") //.delay(Math.random())) .duration(500) g.selectAll("path") .data(d) .enter().append("svg:path") .attr("d", makeline(d)) }); } //update(); setInterval(update, 5000); </script> </body> </html> |
A JSON object with a custom event. Hard to find.
Read more