Server-side charts with ASP.NET Core, Node services and D3.js

ASP.NET Core introduces Node services that allow applications to run Node scripts on server. We can send data from controller action to Node script and get back some output we can use in view. This blog post shows how to use Node services to render d3.js charts on server-side to PNG files.

Why Node services

Node has tons of modules available and ready for use. Before Node services there were no good way in ASP.NET Core to run Node scripts on such a generalized way than now. We can install node modules, write our own ones and call these from controllers to get back some data or to get something important done.

Related closely to Node services there are some other services in ASP.NET Core that may interest reader who finds this post useful:

  • SPA services
  • Angular services (in beta)
  • React services (in beta)

For good overview I suggest MSDB blog post Building Single Page Applications on ASP.NET Core with JavaScriptServices.

Adding Node services

To use Node services we have to add reference to NuGet package Microsoft.AspNetCore.NodeServices. After this we need to register Node services so we can use them in controllers through framework-level dependency injection. This is done in ConfigureServices() method of Startup class.

public void ConfigureServices(IServiceCollection services)
{
    services.AddNodeServices();
    services.AddMvc();
}

Now Node services are ready for use.

Initializing chart

Let’s initialize chart from MVC controller. For this let’s add new action called Chart to Home controller. Here is the code.

public async Task<IActionResult> Chart([FromServices] INodeServices nodeServices)
{
    var options = new { width = 400, height = 200 };

    var data = new[] {
        new { label = "Abulia", count = 10 },
        new { label = "Betelgeuse", count = 20 },
        new { label = "Cantaloupe", count = 30 },
        new { label = "Dijkstra", count = 40 }
    };

    ViewData["ChartImage"] = await nodeServices.InvokeAsync<string>("NodeChart.js", options, data);

    return View();
}

We create anonymous objects for options and chart data. We can use options defined here to be dynamic and configurable by user. Chart data here is static in sample purposes but in real application there will be query to some data source where chart data is stored. To render chart we use instance of INodeServices to call out Node script. Notice how options and data are given to Node script.

Displaying chart on view

Before jumping to JavaScript let’s define simple view to display our chart.

@{
    ViewData["Title"] = "Chart";
}
<h2>@ViewData["Title"]</h2>

<p><img src="@Html.Raw(ViewData["ChartImage"])" /></p>

One hint can be read out from view: chart image will be inline image.

Installing Node modules

In this point I expect that Node is installed to box where web application runs and it is configured correctly. Before we can write our Node script we have to install all modules we use.

  1. Open command line and move to web application root folder (not under wwwroot)
  2. Run npm install –save svg2png
  3. Run npm install –save jsdom
  4. Run npm install –save d3

Ignore errors and warning about packages.json file as things work still well.

Building chart and converting SVG to PNG

Now let’s write script that generates chart and transforms it to PNG image. For this let’s add file called NodeChart.js to root folder of our web application.

Here is the script with some code comments to clarify what it does. In short, we create disconnected HTML DOM, attach it to D3, generate chart and then convert SVG to PNG. After this we return PNG as inline image back to controller.

// Include all modules we need
const svg2png = require("svg2png");
const { JSDOM } = require("jsdom");
const d3 = require('d3');

// Define module
// callback - function to return data to caller
// options - chart options defined in controller
// data - chart data coming from controller
module.exports = function(callback, options, data) {

    // Create disconnected HTML DOM and attach it to D3
    var dom = new JSDOM('<html><body><div id="chart"></div></html>');
    dom.window.d3 = d3.select(dom.window.document);

    // Build D3 chart
    var width = options.width || 360;
    var height = options.height || 360;
    var radius = Math.min(width, height) / 2;

    var color = d3.scaleOrdinal(d3.schemeCategory20b);

    var svg = dom.window.d3.select('#chart')
        .append('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate(' + (width / 2) +
        ',' + (height / 2) + ')');

    var arc = d3.arc()
        .innerRadius(0)
        .outerRadius(radius);

    var pie = d3.pie()
        .value(function(d) { return d.count; })
        .sort(null);

    var path = svg.selectAll('path')
        .data(pie(data))
        .enter()
        .append('path')
        .attr('d', arc)
        .attr('fill', function(d) {
            return color(d.data.label);
        });

    // Convert SVG to PNG and return it to controller
    var svgText = dom.window.d3.select('#chart').html();
    svg2png(Buffer.from(svgText), { width: width, height: height })
        .then(buffer => 'data:image/png;base64,' + buffer.toString('base64'))
        .then(buffer => callback(null, buffer));
};

Here is the chart view with simple pie chart that was built in Node on server.

ASP.NET Core server-side chart using Node services and D3.js

The code above can be used also as a starting point for some more complex server-side chart.

There are also other ways to get SVG to image (SVG => Canvas => data URI) but the method used here is easy and powerful enough. It doesn’t need any additional software to be installed on server and therefore it is less demanding about technical environment.

References

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    12 thoughts on “Server-side charts with ASP.NET Core, Node services and D3.js

    • Pingback:The Morning Brew - Chris Alcock » The Morning Brew #2451

    • May 22, 2018 at 8:50 am
      Permalink

      I get black pie. Any idea why?

    • September 25, 2018 at 10:43 pm
      Permalink

      Replace the var color line with
      var color = d3.scaleOrdinal().range([‘#A60F2B’, ‘#648C85’, ‘#B3F2C9’, ‘#528C18’, ‘#C3F25C’]);

    • March 1, 2019 at 3:56 pm
      Permalink

      I followed the instructions but get an

      NodeInvocationException: Cannot find module ‘G:\Finanzen_Betrieb\Informatik\Dokumentation\03-Applikationen\Phoenix\sourcen\TransportWesen\NodeChart.js’ Error: Cannot find module ‘G:\Finanzen_Betrieb\Informatik\Dokumentation\03-Applikationen\Phoenix\sourcen\TransportWesen\NodeChart.js’at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15)at Function.Module._load (internal/modules/cjs/loader.js:507:25)at Module.require (internal/modules/cjs/loader.js:637:17)at require (internal/modules/cjs/helpers.js:22:18)at C:\Users\hoffmannp\AppData\Local\Temp\4ptswx05.jbc:161:33at IncomingMessage. (C:\Users\hoffmannp\AppData\Local\Temp\4ptswx05.jbc:186:37)at IncomingMessage.emit (events.js:194:15)at endReadableNT (_stream_readable.js:1103:12)at process._tickCallback (internal/process/next_tick.js:63:19)

      Any idea ?

    • March 1, 2019 at 6:36 pm
      Permalink

      It seems like there’s no NodeChart.js file in root folder of web application. Do you have this file with script given above before chart image?

    • March 4, 2019 at 9:49 am
      Permalink

      Hi Gunnar,

      thanks for your support. Some minutes later I solved the issue on my own, at a first glance I didn’t see that the script is mandatory. ;-) But now I get the next one:

      NodeInvocationException: Cannot convert undefined or null to object TypeError: Cannot convert undefined or null to objectat slice ()at Function.scale.range (G:\Finanzen_Betrieb\Informatik\Dokumentation\03-Applikationen\Phoenix\sourcen\Transportwesen\node_modules\d3-scale\dist\d3-scale.js:57:46)at Function.initRange (G:\Finanzen_Betrieb\Informatik\Dokumentation\03-Applikationen\Phoenix\sourcen\Transportwesen\node_modules\d3-scale\dist\d3-scale.js:11:18)at Object.ordinal [as scaleOrdinal] (G:\Finanzen_Betrieb\Informatik\Dokumentation\03-Applikationen\Phoenix\sourcen\Transportwesen\node_modules\d3-scale\dist\d3-scale.js:68:13)at module.exports (G:\Finanzen_Betrieb\Informatik\Dokumentation\03-Applikationen\Phoenix\sourcen\Transportwesen\NodeChart.js:22:17)at C:\Users\hoffmannp\AppData\Local\Temp\jh0epaas.lcx:166:18at IncomingMessage. (C:\Users\hoffmannp\AppData\Local\Temp\jh0epaas.lcx:186:37)at IncomingMessage.emit (events.js:194:15)at endReadableNT (_stream_readable.js:1103:12)at process._tickCallback (internal/process/next_tick.js:63:19)

      in ViewData[“ChartImage”] = await nodeServices.InvokeAsync(“NodeChart.js”, options, data);
      ???
      Any idea ? otherwise I could send you the whole project

    • March 4, 2019 at 11:27 am
      Permalink

      Something in input data seems to be null. NodeChart.js line 22 seems to be it by stack trace.

    • August 27, 2019 at 10:06 am
      Permalink

      Hello, can i use that technique to build API that return image created from d3 and nodejs?

    • August 31, 2019 at 7:40 am
      Permalink

      Yes, you can. If this is all you need then I would consider using some nodejs web server instead. I mean you have nodejs there anyway.

    • October 5, 2019 at 3:15 am
      Permalink

      Hi Gunnar, any examples of how to use nodejs charts with GenericHost of dotnet.core 2.1?
      I don’t have an MVC in my services and we don’t want to have it.
      this project should be running as background script from active batch.

    • October 16, 2019 at 12:31 pm
      Permalink

      Hi Gunnar,

      Thank you for this great article. I have setup everything and it works beautifully. However, what is not entirely clear to me is how would this work in a production environment? Do I deploy the node_modules folder or can I combine this with webpack?

      Could you please give a short and quick rundown on how this would be handled? Thanks.

    • October 1, 2020 at 9:52 pm
      Permalink

      Great article! Unfortunately INodeServices is depreciated now, mentions using Microsoft.AspNetCore.SpaServices.Extensions. Do you have an example using that? Thank you!

    Leave a Reply

    Your email address will not be published. Required fields are marked *