Chart not Graph

As you probably know, we people at Neo4j really want to discern graphs from charts. It’s too easy to mix the two up in spoken language and "graph paper" doesn’t help. If it’s a chart like a bar-, pie- or line-chart then it’s not a graph but a visualisation of data across multiple axes (mostly 2 - x and y).

charts vs graphs

Fortunately the creators of chart.js didn’t call their impressive Javascript not graph.js :)

How did we get here

Some of you might still remember webadmin our first web based Neo4j interface which was pretty cool for its time and had a number of cool features. One feature many people loved but left everyone confused was the chart at the beginning showing the growth of data volume (nodes, relationships and properties) in your graph over time.

That’s especially nice, to see progress when your importing data or have your production app running.

webadmin charts

But it was also confusing because it didn’t actually show the counts of entites but the total storage file size, which can contain empty segments if you delete data. So people were wondering, why does this thing grow or not go down, when I actually delete data. Or why doesn’t it grow when I create data.

With Neo4j 3.0 we finally removed webadmin (RIP) but there are still a few things missing in Neo4j browser.

New Features

As there are some cool new features which come together, I thought it would be nice to give people the charting of graph data volume back while also combining those nice new features.

  • Fast Entity Counts

  • User Defined and Built-In Procedures

  • Binary Protocol

  • Official Drivers (incl. JavaScript)

Fast counting of entities

With Neo4j 3.0 we provide counts of entites from Cypher no longer by actually counting them but by inspecting our transactional database statistics. Many databases have database statistics, but few keep them transactionally up to date, to have always accurate and reliable counts.

So now when you issue Cypher statements like the following, you’ll get the results back in milliseconds.

MATCH () RETURN count(*);
MATCH (n) RETURN count(*);
MATCH (:Person) RETURN count(*);

MATCH ()-->() RETURN count(*);
MATCH ()-[:FOLLOWS]->() RETURN count(*);
MATCH (n)-[:LIKES]->(m) RETURN count(*);

Built-in and User Defined Procedures

Calling procedures from Cypher is another really cool feature in 3.0. You can use the built in procedures to get information about procedures (how meta), indexes, constraints, and entity meta information like lablels, relationship types and property-keys.

CALL db.relationshipTypes();

// integrated call
CALL db.labels() YIELD label
RETURN label ORDER BY label;

CALL dbms.procedures();

// complex call, list procedures by package
CALL dbms.procedures() YIELD name
WITH split(name,".") as parts
RETURN parts[0..-1] as package, count(*), collect(parts[-1]) as procedures
ORDER BY count(*) DESC;

It’s much more fun to write your own procedures or actually to contribute to the large collection of procedures we created in the APOC procedures community project.

But I digress. So we can use the built in procedures to get information about labels and relationship types. Nice! Combined with the counting queries, we can not only get total counts but also fast counts by label and relationship type.

Binary Protocol and Official Drivers

The next big thing with Neo4j 3.0 is our binary protocol "bolt", which is based on PackStream a extension of MessagePack.

With the binary protocol, we also provide officially supported drivers for .Net, Python, Java and JavaScript (detailed documentation) which are all Apache v2 licensed and can be found on GitHub.

Our awesome contributor community also provided bolt drivers for PHP (thanks Christophe) and is working on versions for Ruby and Go.

To integrate with chart.js, I just needed the JavaScript driver, which works both with node.js and in the browser, it’s available via Bower and NPM.


I want to provide an auto-updating chart of all the nodes in total and by label as well as the relationships in total and by relationship type.

Also the user should be able to run free form queries to be charted.

The source code is available under the MIT license.

Implementation Database Statistics

I haven’t used chart.js before and my JS skills are pretty bad, so bear with me. Here are the basics of what I did.

I pulled all Javascript libraries and CSS files from their CDNs. For the Neo4j Javascript Driver I used the CDN of rawgit. So I actually don’t need Bower or any other Hipster JavaScript build setup that downloads a Gigabyte of stuff onto my disk.

These are my dependencies:

  • neo4j-driver

  • Chart.js

  • JQuery

  • Bootstrap

I started with a basic bootstrap template and added a bunch of form fields and a button for the database connection, refresh frequency and to toggle the auto-updating.

I just followed the tutorial for chart.js starting with a <canvas> element that’s turned into a line-chart. For the statistics-view I wanted to use a logarithmic scale for the counts and a time scale for the measurements.

Here are the steps that I do:

  1. button is pressed

  2. get the URL and password from the form fields

  3. instantiate a driver with those credentials and get a session

  4. use the session to query for the total counts with MATCH () RETURN count(*) and use a promise to receive the results and update the chart with the new information

  5. the chart updater is a separate function which I reuse for all the named updates of information that happens at the same time

  6. query for all label names via the db.labels() procedure

  7. create a second session and get the count for each label

  8. do the same for relationships

  9. the chart update also includes picking a color and making sure we only display 25 results in total, moving them to the left

neo charts components

Here is the code:

Statistics Queries
        function stats() {
            var neo = neo4j.v1;
            var driver = neo.driver($("#url").val(), neo.auth.basic("neo4j", $("#password").val()));
            var session = driver.session();
            var session2 = driver.session();
            var d =;
  "MATCH () RETURN count(*)").then(function (result) {
                update(d, "nodes", result.records[0]._fields[0].toNumber());
  "MATCH ()-->() RETURN count(*)").then(function (result) {
                update(d, "rels", result.records[0]._fields[0].toNumber());

  "CALL db.labels()").then(function (result) {
                result.records.forEach(function (r) {
                    var l = r._fields[0];
                    var stmt = "MATCH (:`" + l + "`) RETURN count(*)";
           (result2) {
                        update(d, l, result2.records[0]._fields[0].toNumber());

  "CALL db.relationshipTypes()").then(function (result) {
                result.records.forEach(function (r) {
                    var l = r._fields[0];
                    var stmt = "MATCH ()-[:`" + l + "`]->() RETURN count(*)";
           (result2) {
                        update(d, l, result2.records[0]._fields[0].toNumber());
Chart Update
        // from via
        var colors = ['#d53e4f', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#e6f598', '#abdda4', '#66c2a5', '#3288bd'];
        function update(time, label, value) {
            var labels =;
            var datasets =;

            var title = "# of " + label;
            var insert = labels.indexOf(time);
            // new timestamp
            if (insert == -1) {
                // too many samples, remove first entry
                if (labels.length > samples) {
                    datasets.forEach(function (ds) {; });
                insert = labels.length;
                labels[insert] = time;
            // find dataset
            var idx = datasets.findIndex(function(ds) { return ds.label == title});
            // add new dataset
            if (idx == -1) {
                idx = datasets.length;
                datasets.push({ label: title, data:{return 1;}), borderColor: colors[idx % colors.length], fill: false});
            datasets[idx].data[insert] = value;


The chart update code is a bit awkward because of the datastructures chart.js uses and the way it handles updates. I spent 2 days figuring out a bug, that was caused by initializing a new data array with zeros. (1’s work).

Implementation Free Query

As a bonus feature I wanted to allow free querying, i.e. you enter a Cypher query that returns a stream of values where the first column is the X-Axis value and all subsequent columns are Y-axis values.

So I added that in a similar fashion.


As it is only a single html page, I was not sure it was worth a repository, so just put it in a GitHub Gist, which I can host with which is a pretty neat service to host html/css/js/json files with their correct mime-types. They even have a CDN.

As the driver connects via websockets, there are also no CORS issues. Just provide the password and URL of the Neo4j installation you want to query and you’re ready to go.

Visit to see it live and in action. The results below stem from importing a subset of the Panama Papers :)

neo4j charts